Generating clusters manually

Hello all,

I have a question regarding generating clusters in SC.
In the code from a past thread (Exploring sound corpora in SC), the clusters are created by seeding with kmeans, giving the algorithm the suggestions by clicking on certain areas of a plotter.
This is the section where that happens (skipping the first steps of reading the Datasets), for further usage in buffers:

(

var counter = 0;
var xybuf = Buffer.alloc(s,2);
Window.closeAll;
//~ds = FluidDataSet(s).read("/Users/jan/Sound/Recordings/ynzhi/yy/ds_umap.json".resolveRelative);
~ds =~ds_umap;
FluidNormalize(s).fitTransform(~ds,~ds);
~mean_seeds = FluidDataSet(s);
~ds.dump({
	arg dict;
	fork({
		var win = Window(bounds:Rect(0,50,800,800));
		var uv, means = List.new;
		~fp = FluidPlotter(win,Rect(0,0,win.bounds.width,win.bounds.height),dict:dict,mouseMoveAction:{
			arg view, x, y, modifiers, butnum, clickcount;
			if(butnum.notNil,{// don't add a point when the mouse is released
				means.add([x,y]);
				xybuf.setn(0,[x,y]);
				~mean_seeds.addPoint(counter,xybuf,{
				~mean_seeds.print;
					{win.refresh}.defer;
				});
				counter = counter + 1;
			});
		});
		uv = UserView(win,Rect(0,0,win.bounds.width,win.bounds.height))
		.acceptsMouse_(false)
		.drawFunc_{
			means.do{
				arg xy;
				var width = 12;
				var half_width = width / 2;
				var x = xy[0].linlin(0,1,0,win.bounds.width);
				var y = xy[1].linlin(0,1,win.bounds.height,0);
				Pen.addOval(Rect(x - half_width,y - half_width,width,width));
				Pen.color_(Color.red);
				Pen.fill;
			}
		};
		win.front;
	},AppClock);
});
)


// run a KMeans with these seeds
(
~labels = FluidLabelSet(s);
~mean_seeds.size({
	arg sz;
	~kmeans = FluidKMeans(s,sz).setMeans(~mean_seeds);
	~kmeans.fitPredict(~ds,~labels,{
		~labels.dump({
			arg labels;
			var colors = {Color.rand} ! sz;
			labels["data"].keysValuesDo{
				arg id, cluster;
				cluster = cluster[0].asInteger;
				~fp.pointColor_(id,colors[cluster]);
			};
		});
	});
});
)


(
~labels.dump({
	arg dict;
	~clusters = Dictionary.new;
	dict["data"].keysValuesDo{
		arg id, cluster;
		cluster = cluster[0].asInteger;
		if(~clusters[cluster].isNil,{~clusters[cluster] = List.new});
		~clusters[cluster].add(id);
	};
	~clusters.keysValuesDo{
		arg k, v;
		"points in cluster %: %".format(k,v).postln;
	};
});
)


// 7. Put the "play info" from all the slices of each in cluster into a buffer

(
fork{
	var tmpbuf = Buffer(s);
	~play_info_clusters = {Buffer(s)} ! ~kmeans.numClusters;
	~clusters.keysValuesDo{
		arg cluster_i, list;
		list.do{
			arg id, i;
			"cluster % / %\tid %".format(cluster_i,~kmeans.numClusters,id).postln;
			"\tnumber of buffers in ~play_info_clusters: %".format(~play_info_clusters.size).postln;
			~ds_play_info.getPoint(id,tmpbuf);
			s.sync;
			// write the first frame to channel 0, frame "i"
			FluidBufCompose.processBlocking(s,tmpbuf,startFrame:0,numFrames:1,destination:~play_info_clusters[cluster_i.asInteger],destStartFrame:i,destStartChan:0);

			// write the second frame to channel 1, frame "i"
			FluidBufCompose.processBlocking(s,tmpbuf,startFrame:1,numFrames:1,destination:~play_info_clusters[cluster_i.asInteger],destStartFrame:i,destStartChan:1);
		}
	};
	s.sync;
	"completed".postln;
	// {~play_info_clust_5.plot}.defer;
};
)

My question is: would it be possible to manually encircle/ draw around certain sections in the plotter, to give them colours which then become the clusters?
(All the non selected points could be discarded, or a become a “bin” cluster).

Thanks!

Jan

Hi @jan,

From a computational standpoint this is possible. Unfortunately there’s not an idiomatic way to do it in FluCoMa with the plotter.

You’d have to record a series of mouse positions and make a polygon from that. Then for each point, check to see if it is inside the polygon (there are algorithms that do this, I don’t remember the names or keywords for searching off the top of my head).

If you get something working (or just started) I’d be curious to see it!

T

Hi @tedmoore,

thanks for getting back!
Ok i see, i’d imagine that to be a really straightforward way of using these corpus visualizations.
Id love to try something to get started… Are you referring to algorithms that are part of the flucoma toolkit or even a function/method of the plotter?

Thank you,

Jan

The functionality I describe is not part of the plotter or FluCoMa. You can use the Plotter’s callback function to get the mouse positions and construct the polygon with them. Then you’d need to write the algorithms yourself to do the rest. You can probably find some clear implementations of these ideas (“is point in polygon”) elsewhere and port them to SC!

T

Thanks, Ill see if i can find some more ways on how this could be achieved!

1 Like

Hi @tedmoore,

with some Ai assistance i’ve managed to come up with some code that creates polygons on the plotter! (i dont have the programming skills for that myself unfortunately)
While the polygon creation does work, the identification & coloring of the right points within the shape seems to be the more challenging part. This is the code so far:

(
// Accurate Point-in-Polygon Detection
// Uses robust algorithm for finding points in polygons

// Clear variables
~polygons = List[]; // Store each completed polygon
~polygonColors = List[]; // Color for each polygon
~currentPoints = List[]; // Points in current polygon
~claimedPoints = Set[]; // Track which points are already assigned
~pointsInPolygons = Dictionary(); // Map of polygon index to points
~isDrawing = false;

// Create window
w = Window("Polygon Selection Tool", Rect(50, 50, 800, 800));

// Setup visualization
~setupPlotter = {
    // Load and normalize dataset
    ~ds_norm = FluidDataSet(s);
    
    FluidNormalize(s).fitTransform(~ds_umap, ~ds_norm, {
        "Dataset normalized".postln;
        
        ~ds_norm.dump({ |dict|
            defer {
                // Store dataset 
                ~dataDict = dict;
                
                // Create FluidPlotter
                ~fp = FluidPlotter(w, Rect(0, 0, 800, 800), dict: dict);
                
                // Create overlay for drawing
                ~overlay = UserView(w, Rect(0, 0, 800, 800));
                ~overlay.background_(Color.clear);
                
                // Draw function for polygons
                ~overlay.drawFunc = {
                    // Draw each completed polygon
                    ~polygons.do { |polygon, i|
                        var color = ~polygonColors[i];
                        
                        // Each polygon is independently drawn
                        Pen.width = 2;
                        Pen.strokeColor = color;
                        Pen.fillColor = color.alpha_(0.2);
                        
                        // Start fresh path
                        Pen.beginPath;
                        
                        if(polygon.size > 0) {
                            // Draw the polygon
                            Pen.moveTo(Point(polygon[0][0], polygon[0][1]));
                            polygon[1..].do { |pt|
                                Pen.lineTo(Point(pt[0], pt[1]));
                            };
                            Pen.lineTo(Point(polygon[0][0], polygon[0][1]));
                            Pen.fillStroke;
                            
                            // Draw the polygon points
                            polygon.do { |pt|
                                Pen.fillColor = color;
                                Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                                Pen.fill;
                            };
                        };
                    };
                    
                    // Draw current polygon (red outline)
                    if(~isDrawing && (~currentPoints.size > 0)) {
                        // Start fresh path
                        Pen.beginPath;
                        
                        Pen.width = 2;
                        Pen.strokeColor = Color.red;
                        
                        // Draw the lines
                        Pen.moveTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
                        ~currentPoints[1..].do { |pt|
                            Pen.lineTo(Point(pt[0], pt[1]));
                        };
                        
                        // Close the shape if enough points
                        if(~currentPoints.size > 2) {
                            Pen.lineTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
                        };
                        
                        Pen.stroke;
                        
                        // Draw the points
                        ~currentPoints.do { |pt|
                            Pen.fillColor = Color.red;
                            Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                            Pen.fill;
                        };
                    };
                };
                
                // Handle mouse clicks
                ~overlay.mouseDownAction = { |view, x, y|
                    if(~isDrawing) {
                        ~currentPoints = ~currentPoints.add([x, y]);
                        "Added point at %, %".format(x, y).postln;
                        ~overlay.refresh;
                    };
                };
                
                ~overlay.acceptsMouse = true;
                
                // ALTERNATIVE APPROACH: Check if point is in polygon by testing if it's
                // on the same side of all edges (simplified but effective)
                ~pointInConvexPolygon = { |pt, poly|
                    var allOnSameSide = true;
                    var sign = nil;
                    
                    // For each edge in the polygon
                    poly.size.do { |i|
                        var j = (i + 1) % poly.size;
                        var edge1 = poly[i];
                        var edge2 = poly[j];
                        
                        // Get edge vector
                        var edgeX = edge2[0] - edge1[0];
                        var edgeY = edge2[1] - edge1[1];
                        
                        // Get vector from edge1 to point
                        var toPointX = pt[0] - edge1[0];
                        var toPointY = pt[1] - edge1[1];
                        
                        // Cross product (2D)
                        var cross = (edgeX * toPointY) - (edgeY * toPointX);
                        
                        // First edge determines sign to check
                        if(sign.isNil) {
                            sign = cross > 0;
                        };
                        
                        // If point is on different side of any edge, it's outside
                        if((cross > 0) != sign) {
                            allOnSameSide = false;
                        };
                    };
                    
                    allOnSameSide;
                };
                
                // Loop through all data points and try them explicitly against the polygon
                ~testAndColorPoints = { |polygonIndex|
                    var polygon = ~polygons[polygonIndex];
                    var color = ~polygonColors[polygonIndex];
                    var pointsDetected = List.new;
                    
                    // Test each point in the dataset
                    ~dataDict["data"].keysValuesDo { |id, values|
                        // Skip already claimed points
                        if(~claimedPoints.includes(id).not) {
                            // Convert data coordinates (0-1) to window coordinates
                            var wx = values[0] * 800;
                            var wy = values[1] * 800;
                            
                            // Test if point is inside polygon
                            if(~pointInConvexPolygon.([wx, wy], polygon)) {
                                // Found a point in this polygon
                                pointsDetected.add(id);
                                ~fp.pointColor_(id, color);
                                ~claimedPoints.add(id);
                                "Point % added to cluster %".format(id, polygonIndex).postln;
                            };
                        };
                    };
                    
                    // Store all points found in this polygon
                    ~pointsInPolygons[polygonIndex] = pointsDetected;
                    "Polygon % contains % points".format(polygonIndex, pointsDetected.size).postln;
                };
                
                // Complete current polygon and find points inside
                ~finishPolygon = {
                    if(~currentPoints.size > 2) {
                        var newPolygonIndex = ~polygons.size;
                        var color = Color.rand;
                        
                        // Store new polygon
                        ~polygons = ~polygons.add(~currentPoints.copy);
                        ~polygonColors = ~polygonColors.add(color);
                        
                        // Process this polygon
                        ~testAndColorPoints.(newPolygonIndex);
                        
                        // Reset for next polygon
                        ~currentPoints = List.new;
                        ~overlay.refresh;
                    };
                };
                
                // Keyboard handler - with shift+key support
                w.view.keyDownAction = { |view, char, mod, unicode, keycode|
                    var isShiftDown = (mod & 131072) == 131072; // Check if Shift is held
                    
                    "Key pressed: % (Unicode: %, Shift: %)".format(char, unicode, isShiftDown).postln;
                    
                    // Shift+D to start drawing mode
                    if(isShiftDown && (unicode == 100 || unicode == 68)) { // Shift+d or Shift+D
                        ~isDrawing = true;
                        ~currentPoints = List[];
                        "Drawing mode ON - Click to add points".postln;
                        ~overlay.refresh;
                    };
                    
                    // Shift+Enter to complete polygon
                    if(isShiftDown && (unicode == 13)) {
                        if(~isDrawing && (~currentPoints.size > 2)) {
                            ~finishPolygon.();
                        } {
                            "Can't complete polygon - need at least 3 points".postln;
                        };
                    };
                    
                    // Shift+Esc to cancel
                    if(isShiftDown && (unicode == 27)) {
                        if(~currentPoints.size > 0) {
                            ~currentPoints = List[];
                            "Current polygon canceled".postln;
                        } {
                            ~isDrawing = false;
                            "Drawing mode OFF".postln;
                        };
                        ~overlay.refresh;
                    };
                    
                    // Shift+Space to finalize
                    if(isShiftDown && (unicode == 32)) {
                        if(~polygons.size > 0) {
                            var labels = FluidLabelSet(s);
                            var restPoints = List[];
                            
                            "Finalizing all clusters".postln;
                            
                            // Add labeled points to clusters
                            ~pointsInPolygons.keysValuesDo { |cluster, points|
                                points.do { |id|
                                    labels.addPoint(id, cluster);
                                };
                                "Cluster % has % points".format(cluster, points.size).postln;
                            };
                            
                            // Add unclaimed points to rest cluster
                            ~dataDict["data"].keysValuesDo { |id, values|
                                if(~claimedPoints.includes(id).not) {
                                    restPoints.add(id);
                                    ~fp.pointColor_(id, Color.gray);
                                };
                            };
                            
                            // Add rest cluster if needed
                            if(restPoints.size > 0) {
                                var restIdx = ~polygons.size;
                                ~pointsInPolygons[restIdx] = restPoints;
                                "Rest cluster %: % points".format(restIdx, restPoints.size).postln;
                            };
                            
                            // Store cluster data
                            ~polygonClusterLabels = labels;
                            ~polygonClusters = ~pointsInPolygons;
                            
                            "All clusters finalized - run ~processPolygonClusters.() to process".postln;
                        } {
                            "No polygons defined yet".postln;
                        };
                    };
                };
                
                // Help text
                StaticText(w, Rect(10, 10, 780, 30))
                    .string_("Press Shift+D to draw, Click to add points, Shift+Enter to complete, Shift+Esc to cancel, Shift+Space to finalize")
                    .align_(\center)
                    .background_(Color.white.alpha_(0.7));
                
                w.front;
                "Polygon selector ready. Press Shift+D to start drawing.".postln;
            };
        });
    });
};

// Initialize and run
~polygons = List[];
~polygonColors = List[];
~currentPoints = List[];
~claimedPoints = Set[];
~pointsInPolygons = Dictionary();
~setupPlotter.();
)

Do you think this is a good way to go & potentially feasible for implemention in the cluster creation?

Thanks,

Jan

I tried to run this but got an error pretty quickly. Do you have a working version?

t

Hi Ted,

apologies, that must have been running with some data from an earlier version that somehow avoided the error.
I have another version now that runs. It creates some arbitrary data points and the polygons, but fails to properly identify & color them inside the shape boundaries:

(
// Polygon Selection Tool for FluidPlotter
// Creates shapes to select points in a dataset

// Clear any previous state
Window.closeAll;

// Variables
~polygons = List[];          // Store completed polygons
~polygonColors = List[];     // Color for each polygon
~currentPoints = List[];     // Points in the polygon being drawn
~claimedPoints = Set[];      // Points already assigned to a polygon
~pointsInPolygons = Dictionary(); // Map polygons to their points
~isDrawing = false;          // Drawing mode flag

// Create test dataset
~createTestDataset = {
    var numPoints = 1000;
    var dict;
    
    "Creating test dataset with % points...".format(numPoints).postln;
    
    dict = Dictionary.newFrom([
        "cols", 2,
        "data", Dictionary.newFrom(Array.fill(numPoints * 2, { |i|
            var return;
            if((i % 2) == 0, {
                // IDs in format "slice-0", "slice-1", etc.
                return = "slice-%".format((i/2).asInteger);
            }, {
                // Create clusters of points
                return = [1.0.rand, 1.0.rand];
            });
            return;
        }))
    ]);
    
    dict;
};

// Create the main window
w = Window("Polygon Selection Tool", Rect(50, 50, 800, 800));

// Set up the UI
~setupUI = {
    var dataDict;
    
    // Create the test dataset
    dataDict = ~createTestDataset.();
    
    // Create the FluidPlotter
    ~fp = FluidPlotter(w, Rect(0, 0, 800, 800), dict: dataDict);
    
    // Create overlay for drawing polygons
    ~overlay = UserView(w, Rect(0, 0, 800, 800));
    ~overlay.background_(Color.clear);
    
    // Draw function for polygons
    ~overlay.drawFunc = {
        // Draw completed polygons
        ~polygons.do { |polygon, i|
            var color = ~polygonColors[i];
            
            // Each polygon is independently drawn
            Pen.width = 2;
            Pen.strokeColor = color;
            Pen.fillColor = color.alpha_(0.2);
            
            if(polygon.size > 0) {
                // Draw polygon fill
                Pen.beginPath;
                Pen.moveTo(Point(polygon[0][0], polygon[0][1]));
                polygon[1..].do { |pt|
                    Pen.lineTo(Point(pt[0], pt[1]));
                };
                Pen.lineTo(Point(polygon[0][0], polygon[0][1]));
                Pen.fillStroke;
                
                // Draw polygon vertices
                polygon.do { |pt|
                    Pen.fillColor = color;
                    Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                    Pen.fill;
                };
            };
        };
        
        // Draw the polygon currently being created (red outline)
        if(~isDrawing && (~currentPoints.size > 0)) {
            // Start a new path
            Pen.beginPath;
            
            Pen.width = 2;
            Pen.strokeColor = Color.red;
            
            // Draw lines connecting points
            Pen.moveTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
            ~currentPoints[1..].do { |pt|
                Pen.lineTo(Point(pt[0], pt[1]));
            };
            
            // Close the shape if enough points
            if(~currentPoints.size > 2) {
                Pen.lineTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
            };
            
            Pen.stroke;
            
            // Draw the points
            ~currentPoints.do { |pt|
                Pen.fillColor = Color.red;
                Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                Pen.fill;
            };
        };
    };
    
    // Handle mouse clicks
    ~overlay.mouseDownAction = { |view, x, y|
        if(~isDrawing) {
            ~currentPoints = ~currentPoints.add([x, y]);
            "Added point at %, %".format(x, y).postln;
            ~overlay.refresh;
        };
    };
    
    ~overlay.acceptsMouse = true;
    
    // Point-in-polygon detection algorithm
    ~pointInPolygon = { |pt, poly|
        var allOnSameSide = true;
        var sign = nil;
        
        // For each edge in the polygon
        poly.size.do { |i|
            var j = (i + 1) % poly.size;
            var edge1 = poly[i];
            var edge2 = poly[j];
            
            // Get edge vector
            var edgeX = edge2[0] - edge1[0];
            var edgeY = edge2[1] - edge1[1];
            
            // Get vector from edge1 to point
            var toPointX = pt[0] - edge1[0];
            var toPointY = pt[1] - edge1[1];
            
            // Cross product (2D)
            var cross = (edgeX * toPointY) - (edgeY * toPointX);
            
            // First edge determines sign to check
            if(sign.isNil) {
                sign = cross > 0;
            };
            
            // If point is on different side of any edge, it's outside
            if((cross > 0) != sign) {
                allOnSameSide = false;
            };
        };
        
        allOnSameSide;
    };
    
    // Get point coordinates in screen space
    ~dataToScreenCoords = { |dataX, dataY|
        // Default behavior: convert normalized 0-1 coordinates to screen pixels
        var screenX = dataX * 800;
        var screenY = dataY * 800;
        
        [screenX, screenY];
    };
    
    // Test and color points within polygon
    ~testAndColorPoints = { |polygonIndex|
        var polygon = ~polygons[polygonIndex];
        var color = ~polygonColors[polygonIndex];
        var pointsDetected = List.new;
        
        "Testing polygon % for points...".format(polygonIndex).postln;
        
        // Test each point in the dataset
        dataDict["data"].keysValuesDo { |id, values|
            // Skip already claimed points
            if(~claimedPoints.includes(id).not) {
                var screenCoords = ~dataToScreenCoords.(values[0], values[1]);
                
                // Test if point is inside polygon
                if(~pointInPolygon.(screenCoords, polygon)) {
                    // Found a point in this polygon
                    pointsDetected.add(id);
                    
                    // Color the point
                    defer {
                        ~fp.pointColor_(id, color);
                    };
                    
                    ~claimedPoints.add(id);
                    "Point % inside polygon %".format(id, polygonIndex).postln;
                };
            };
        };
        
        // Store points for this polygon
        ~pointsInPolygons[polygonIndex] = pointsDetected;
        "Polygon % contains % points".format(polygonIndex, pointsDetected.size).postln;
        
        // Make sure UI is refreshed
        defer {
            ~fp.refresh;
        };
    };
    
    // Complete current polygon and find points inside
    ~finishPolygon = {
        if(~currentPoints.size > 2) {
            var newPolygonIndex = ~polygons.size;
            var color = Color.rand;
            
            "Completing polygon %".format(newPolygonIndex).postln;
            
            // Store new polygon
            ~polygons = ~polygons.add(~currentPoints.copy);
            ~polygonColors = ~polygonColors.add(color);
            
            // Process this polygon
            ~testAndColorPoints.(newPolygonIndex);
            
            // Reset for next polygon
            ~currentPoints = List.new;
            ~overlay.refresh;
            
            "Polygon % completed".format(newPolygonIndex).postln;
        } {
            "Can't complete polygon - need at least 3 points".postln;
        };
    };
    
    // Keyboard handler
    w.view.keyDownAction = { |view, char, mod, unicode, keycode|
        var isShiftDown = (mod & 131072) == 131072; // Check if Shift is held
        
        // Shift+D to start drawing mode
        if(isShiftDown && (unicode == 100 || unicode == 68)) { // Shift+d or Shift+D
            ~isDrawing = true;
            ~currentPoints = List[];
            "Drawing mode ON - Click to add points".postln;
            ~overlay.refresh;
        };
        
        // Shift+Enter to complete polygon
        if(isShiftDown && (unicode == 13)) {
            if(~isDrawing) {
                ~finishPolygon.();
            };
        };
        
        // Shift+Esc to cancel
        if(isShiftDown && (unicode == 27)) {
            if(~currentPoints.size > 0) {
                ~currentPoints = List[];
                "Current polygon canceled".postln;
            } {
                ~isDrawing = false;
                "Drawing mode OFF".postln;
            };
            ~overlay.refresh;
        };
        
        // Shift+Space to finalize
        if(isShiftDown && (unicode == 32)) {
            if(~polygons.size > 0) {
                var restPoints = List[];
                
                "Finalizing all clusters".postln;
                
                // Clusters summary
                ~pointsInPolygons.keysValuesDo { |cluster, points|
                    "Cluster % has % points".format(cluster, points.size).postln;
                };
                
                // Add unclaimed points to rest cluster
                dataDict["data"].keysValuesDo { |id, values|
                    if(~claimedPoints.includes(id).not) {
                        restPoints.add(id);
                        
                        // Color unclaimed points gray
                        defer {
                            ~fp.pointColor_(id, Color.gray);
                        };
                    };
                };
                
                // Add rest cluster if needed
                if(restPoints.size > 0) {
                    var restIdx = ~polygons.size;
                    ~pointsInPolygons[restIdx] = restPoints;
                    "Rest cluster %: % points".format(restIdx, restPoints.size).postln;
                    
                    defer {
                        ~fp.refresh;
                    };
                };
                
                "All clusters finalized".postln;
            } {
                "No polygons defined yet".postln;
            };
        };
    };
    
    // Help text
    StaticText(w, Rect(10, 10, 780, 30))
        .string_("Shift+D: draw, Click: add point, Shift+Enter: complete, Shift+Esc: cancel, Shift+Space: finalize")
        .align_(\center)
        .background_(Color.white.alpha_(0.7));
    
    w.front;
    "Polygon selector ready. Press Shift+D to start drawing.".postln;
};

// Run the UI setup
~setupUI.();

// Print instructions
"Polygon Selection Tool Initialized".postln;
"------------------------------".postln;
"• Shift+D: Start drawing mode".postln;
"• Click: Add points to polygon".postln;
"• Shift+Enter: Complete current polygon".postln;
"• Shift+Esc: Cancel current polygon or exit drawing mode".postln;
"• Shift+Space: Finalize all polygons/clusters".postln;
)

UserView’s y axis has the “low” values on the top and the high values on the bottom, whereas the FluidPlotter has the more “plot-like” orientation of the y axis having the low values on the bottom and the high values on the top. You’ll need to invert the y values you’re getting from UserView (and the values you use to draw the points and polygons). Then it should work.

This is cool! It seems super useful and it’s something that I think users have asked about in the past. I wonder if @rodrigo.constanzo would be intrigued!

Thanks for the suggestion, this seems to work now!! :slight_smile:

(
// Polygon Selection Tool for FluidPlotter
// Creates shapes to select points in a dataset

// Clear any previous state
Window.closeAll;

// Variables
~polygons = List[];          // Store completed polygons
~currentPoints = List[];     // Points in the polygon being drawn
~claimedPoints = Set[];      // Points already assigned to a polygon
~pointsInPolygons = Dictionary(); // Map polygons to their points
~isDrawing = false;          // Drawing mode flag

// Predefined colors to ensure full opacity and consistency
~polygonColors = [
    Color.new255(255, 0, 0),     // Red
    Color.new255(0, 0, 255),     // Blue
    Color.new255(0, 255, 0),     // Green
    Color.new255(255, 165, 0),   // Orange
    Color.new255(128, 0, 128),   // Purple
    Color.new255(255, 192, 203), // Pink
    Color.new255(165, 42, 42),   // Brown
    Color.new255(64, 224, 208),  // Turquoise
    Color.new255(255, 255, 0)    // Yellow
];

// Create test dataset
~createTestDataset = {
    var numPoints = 1000;
    var dict = Dictionary.newFrom([
        "cols", 2,
        "data", Dictionary.newFrom(Array.fill(numPoints * 2, { |i|
            var return;
            if((i % 2) == 0, {
                return = "slice-%".format((i/2).asInteger);
            }, {
                return = [1.0.rand, 1.0.rand];
            });
            return;
        }))
    ]);
    dict;
};

// Create the main window
w = Window("Polygon Selection Tool", Rect(50, 50, 800, 800));

// Set up the UI
~setupUI = {
    var dataDict = ~createTestDataset.();
    
    // Create the FluidPlotter
    ~fp = FluidPlotter(w, Rect(0, 0, 800, 800), dict: dataDict);
    
    // Create overlay for drawing polygons
    ~overlay = UserView(w, Rect(0, 0, 800, 800));
    ~overlay.background_(Color.clear);
    
    // Draw function for polygons
    ~overlay.drawFunc = {
        // Draw completed polygons
        ~polygons.do { |polygon, i|
            var color = ~polygonColors[i % ~polygonColors.size];
            
            // Each polygon is independently drawn
            Pen.width = 2;
            Pen.strokeColor = color;
            Pen.fillColor = color.alpha_(0.2);
            
            if(polygon.size > 0) {
                // Draw polygon fill
                Pen.beginPath;
                Pen.moveTo(Point(polygon[0][0], polygon[0][1]));
                polygon[1..].do { |pt|
                    Pen.lineTo(Point(pt[0], pt[1]));
                };
                Pen.lineTo(Point(polygon[0][0], polygon[0][1]));
                Pen.fillStroke;
                
                // Draw polygon vertices
                polygon.do { |pt|
                    Pen.fillColor = color;
                    Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                    Pen.fill;
                };
            };
        };
        
        // Draw the polygon currently being created (red outline)
        if(~isDrawing && (~currentPoints.size > 0)) {
            // Start a new path
            Pen.beginPath;
            
            Pen.width = 2;
            Pen.strokeColor = Color.red;
            
            // Draw lines connecting points
            Pen.moveTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
            ~currentPoints[1..].do { |pt|
                Pen.lineTo(Point(pt[0], pt[1]));
            };
            
            // Close the shape if enough points
            if(~currentPoints.size > 2) {
                Pen.lineTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
            };
            
            Pen.stroke;
            
            // Draw the points
            ~currentPoints.do { |pt|
                Pen.fillColor = Color.red;
                Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                Pen.fill;
            };
        };
    };
    
    // Handle mouse clicks
    ~overlay.mouseDownAction = { |view, x, y|
        if(~isDrawing) {
            ~currentPoints = ~currentPoints.add([x, y]);
            ~overlay.refresh;
        };
    };
    
    ~overlay.acceptsMouse = true;
    
    // Point-in-polygon detection algorithm
    ~pointInPolygon = { |pt, poly|
        var allOnSameSide = true;
        var sign = nil;
        
        // For each edge in the polygon
        poly.size.do { |i|
            var j = (i + 1) % poly.size;
            var edge1 = poly[i];
            var edge2 = poly[j];
            
            // Get edge vector
            var edgeX = edge2[0] - edge1[0];
            var edgeY = edge2[1] - edge1[1];
            
            // Get vector from edge1 to point
            var toPointX = pt[0] - edge1[0];
            var toPointY = pt[1] - edge1[1];
            
            // Cross product (2D)
            var cross = (edgeX * toPointY) - (edgeY * toPointX);
            
            // First edge determines sign to check
            if(sign.isNil) {
                sign = cross > 0;
            };
            
            // If point is on different side of any edge, it's outside
            if((cross > 0) != sign) {
                allOnSameSide = false;
            };
        };
        
        allOnSameSide;
    };
    
    // Convert data coordinates to screen coordinates
    ~dataToScreenCoords = { |dataX, dataY|
        // Convert normalized 0-1 coordinates to screen pixels
        // FluidPlotter has y=0 at bottom, UserView has y=0 at top
        var screenX = dataX * 800;
        var screenY = 800 - (dataY * 800); // Invert the y-coordinate
        
        [screenX, screenY];
    };
    
    // Find points inside a polygon without coloring them
    ~findPointsInPolygon = { |polygon|
        var pointList = List[];
        
        // Only check unclaimed points
        dataDict["data"].keysValuesDo { |id, values|
            if(~claimedPoints.includes(id).not) {
                var screenCoords = ~dataToScreenCoords.(values[0], values[1]);
                
                if(~pointInPolygon.(screenCoords, polygon)) {
                    pointList.add(id);
                };
            };
        };
        
        pointList;
    };
    
    // Apply a solid color to a list of points
    ~colorPoints = { |points, polygonIndex|
        var color = ~polygonColors[polygonIndex % ~polygonColors.size];
        
        // Use AppClock to ensure GUI operations happen in the right thread
        Routine({
            points.do { |id|
                var solidColor = color.copy.alpha_(1.0);
                ~fp.pointColor_(id, solidColor);
            };
            ~fp.refresh;
        }).play(AppClock);
    };
    
    // Process polygon - find points and assign them
    ~processPolygon = { |polygonIndex|
        var polygon = ~polygons[polygonIndex];
        var pointsInside = ~findPointsInPolygon.(polygon);
        
        // Mark these points as claimed
        pointsInside.do { |id|
            ~claimedPoints.add(id);
        };
        
        // Store point list for this polygon
        ~pointsInPolygons[polygonIndex] = pointsInside;
        
        // Color these points
        ~colorPoints.(pointsInside, polygonIndex);
    };
    
    // Complete current polygon
    ~finishPolygon = {
        if(~currentPoints.size > 2) {
            var newPolygonIndex = ~polygons.size;
            
            // Add polygon to the list
            ~polygons = ~polygons.add(~currentPoints.copy);
            
            // Process the polygon (find and color points)
            ~processPolygon.(newPolygonIndex);
            
            // Clear the current points
            ~currentPoints = List[];
            ~overlay.refresh;
        } {
            "Cannot complete polygon - need at least 3 points".postln;
        };
    };
    
    // Keyboard handler
    w.view.keyDownAction = { |view, char, mod, unicode, keycode|
        var isShiftDown = (mod & 131072) == 131072; // Check if Shift is held
        
        // Shift+D to start drawing mode
        if(isShiftDown && (unicode == 100 || unicode == 68)) { // Shift+d or Shift+D
            ~isDrawing = true;
            ~currentPoints = List[];
            "Drawing mode ON - Click to add points".postln;
            ~overlay.refresh;
        };
        
        // Shift+Enter to complete polygon
        if(isShiftDown && (unicode == 13)) {
            if(~isDrawing) {
                ~finishPolygon.();
            };
        };
        
        // Shift+Esc to cancel
        if(isShiftDown && (unicode == 27)) {
            if(~currentPoints.size > 0) {
                ~currentPoints = List[];
                "Current polygon canceled".postln;
            } {
                ~isDrawing = false;
                "Drawing mode OFF".postln;
            };
            ~overlay.refresh;
        };
        
        // Shift+Space to finalize
        if(isShiftDown && (unicode == 32)) {
            if(~polygons.size > 0) {
                var restPoints = List[];
                var restIdx = ~polygons.size;
                
                // Find all unclaimed points for rest cluster
                dataDict["data"].keysValuesDo { |id, values|
                    if(~claimedPoints.includes(id).not) {
                        restPoints.add(id);
                        ~claimedPoints.add(id);
                    };
                };
                
                // Store and color rest cluster if needed
                if(restPoints.size > 0) {
                    ~pointsInPolygons[restIdx] = restPoints;
                    
                    // Color rest points gray with full opacity
                    Routine({
                        restPoints.do { |id|
                            ~fp.pointColor_(id, Color.gray.alpha_(1.0));
                        };
                        ~fp.refresh;
                    }).play(AppClock);
                };
                
                "All clusters finalized".postln;
            } {
                "No polygons defined yet".postln;
            };
        };
    };
    
    // Help text
    StaticText(w, Rect(10, 10, 780, 30))
        .string_("Shift+D: draw, Click: add point, Shift+Enter: complete, Shift+Esc: cancel, Shift+Space: finalize")
        .align_(\center)
        .background_(Color.white.alpha_(0.7));
    
    w.front;
};

// Run the UI setup
~setupUI.();

// Print instructions
"Polygon Selection Tool Initialized".postln;
"------------------------------".postln;
"• Shift+D: Start drawing mode".postln;
"• Click: Add points to polygon".postln;
"• Shift+Enter: Complete current polygon".postln;
"• Shift+Esc: Cancel current polygon or exit drawing mode".postln;
"• Shift+Space: Finalize all polygons/clusters".postln;
)
1 Like

I don’t know how much further you’re planning to go with this. I have two more thoughts. I think in either case it would be cool to add to some general FluCoMa materials because I think some users will find it useful!

  1. Currently I think it only works with convex polygons. Non-convex polygons are harder to determine “is point in polygon” but would be excellent to account for.
  2. Currently each point can only be assigned to one “cluster” (i.e., polygon). I think because this is user-defined and it is possible to draw overlapping shapes or shapes that create or supersets/subsets of points, it should be possible to ask for and get multiple shapes that might not be mutually exclusive.

Hi @tedmoore,

basically the possibility of choosing these clusters by one’s own listening is what i was after!
In a next step maybe optimizing the possibilities of further altering the shape, e.g. moving the boundaries after the polygon is completed could be useful…
My impression is that it works with all sorts of polygons, or maybe i’m misunderstanding what you mean?

Jan

Here’s what I mean:

Ah yes, indeed, now i see what you mean…
Surely that’s quite a limitation to have…I’ll see if i find some way around it and report back!

1 Like

Awesome. Thanks for investigating!

Now it seems to work for non-convex polygons as well!

(EDIT: now with dummy data to test right away)


(
// Polygon Selection Tool for FluidPlotter
// Creates shapes to select points in a dataset with evenly distributed points

// Clear any previous state
Window.closeAll;

// Variables
~polygons = List[];          // Store completed polygons
~currentPoints = List[];     // Points in the polygon being drawn
~claimedPoints = Set[];      // Points already assigned to a polygon
~pointsInPolygons = Dictionary(); // Map polygons to their points
~isDrawing = false;          // Drawing mode flag
~drawingSuspended = false;   // For pausing drawing to explore data

// Predefined colors to ensure full opacity and consistency
~polygonColors = [
    Color.new255(255, 0, 0),     // Red
    Color.new255(0, 0, 255),     // Blue
    Color.new255(0, 255, 0),     // Green
    Color.new255(255, 165, 0),   // Orange
    Color.new255(128, 0, 128),   // Purple
    Color.new255(255, 192, 203), // Pink
    Color.new255(165, 42, 42),   // Brown
    Color.new255(64, 224, 208),  // Turquoise
    Color.new255(255, 255, 0)    // Yellow
];

// Create test dataset with more evenly distributed points
~createTestDataset = {
    var numPoints = 1000;
    var dict = Dictionary.newFrom([
        "cols", 2,
        "data", Dictionary.new
    ]);
    
    // Create evenly distributed points
    numPoints.do { |i|
        var id = "slice-%".format(i);
        var coords;
        
        // More even distribution across the entire plotter
        coords = [0.9.rand + 0.05, 0.9.rand + 0.05];
        
        dict["data"].put(id, coords);
    };
    
    dict;
};

// Create the main window
w = Window("Polygon Selection Tool", Rect(50, 50, 800, 850));

// Set up the UI
~setupUI = {
    var dataDict = ~createTestDataset.();
    
    // Create the FluidPlotter
    ~fp = FluidPlotter(w, Rect(0, 0, 800, 800), dict: dataDict);
    
    // Create overlay for drawing polygons
    ~overlay = UserView(w, Rect(0, 0, 800, 800));
    ~overlay.background_(Color.clear);
    
    // Draw function for polygons
    ~overlay.drawFunc = {
        // Draw completed polygons
        ~polygons.do { |polygon, i|
            var color = ~polygonColors[i % ~polygonColors.size];
            
            // Each polygon is independently drawn
            Pen.width = 2;
            Pen.strokeColor = color;
            Pen.fillColor = color.alpha_(0.2);
            
            if(polygon.size > 0) {
                // Draw polygon fill
                Pen.beginPath;
                Pen.moveTo(Point(polygon[0][0], polygon[0][1]));
                polygon[1..].do { |pt|
                    Pen.lineTo(Point(pt[0], pt[1]));
                };
                Pen.lineTo(Point(polygon[0][0], polygon[0][1]));
                Pen.fillStroke;
                
                // Draw polygon vertices
                polygon.do { |pt|
                    Pen.fillColor = color;
                    Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                    Pen.fill;
                };
            };
        };
        
        // Draw the polygon currently being created
        if(~isDrawing && (~currentPoints.size > 0)) {
            // Start a new path
            Pen.beginPath;
            
            // Use different color when drawing is suspended
            Pen.width = 2;
            Pen.strokeColor = if(~drawingSuspended, {Color.gray}, {Color.red});
            
            // Draw lines connecting points
            Pen.moveTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
            ~currentPoints[1..].do { |pt|
                Pen.lineTo(Point(pt[0], pt[1]));
            };
            
            // Close the shape if enough points
            if(~currentPoints.size > 2) {
                Pen.lineTo(Point(~currentPoints[0][0], ~currentPoints[0][1]));
            };
            
            Pen.stroke;
            
            // Draw the points
            ~currentPoints.do { |pt|
                Pen.fillColor = if(~drawingSuspended, {Color.gray}, {Color.red});
                Pen.addOval(Rect(pt[0]-5, pt[1]-5, 10, 10));
                Pen.fill;
            };
        };
        
        // Draw drawing mode status
        if(~isDrawing) {
            var statusText = if(~drawingSuspended,
                {"DRAWING PAUSED - Shift+S to resume"},
                {"DRAWING ACTIVE - Shift+S to pause"});
            
            Pen.fillColor = Color.black.alpha_(0.7);
            Pen.fillRect(Rect(10, 40, 300, 25));
            
            Pen.fillColor = if(~drawingSuspended, {Color.yellow}, {Color.green});
            Pen.stringAtPoint(statusText, Point(15, 45), Font("Helvetica", 12));
        };
    };
    
    // Handle mouse clicks
    ~overlay.mouseDownAction = { |view, x, y|
        if(~isDrawing && ~drawingSuspended.not) {
            ~currentPoints = ~currentPoints.add([x, y]);
            ~overlay.refresh;
        };
    };
    
    // Track mouse state
    ~mousePressed = false;
    
    // Handle mouse down event
    ~overlay.mouseDownAction = { |view, x, y, mod, buttonNumber|
        ~mousePressed = true;
        
        // If in drawing mode AND drawing is not suspended, add point to current polygon
        if(~isDrawing && ~drawingSuspended.not) {
            ~currentPoints = ~currentPoints.add([x, y]);
            ~overlay.refresh;
        };
    };
    
    // Handle mouse up event
    ~overlay.mouseUpAction = { |view, x, y, mod|
        ~mousePressed = false;
    };
    
    // Note: Sound feedback has been removed
    
    ~overlay.acceptsMouse = true;
    
    // Point-in-polygon detection algorithm
    ~pointInPolygon = { |pt, poly|
        var px, py, i, j, vertX1, vertY1, vertX2, vertY2;
        var intersect, inside;
        
        // Initialize variables
        px = pt[0];
        py = pt[1];
        inside = false;
        j = poly.size - 1;
        
        // Process each edge of the polygon
        i = 0;
        while { i < poly.size } {
            // Get edge vertex coordinates
            vertX1 = poly[i][0];
            vertY1 = poly[i][1];
            vertX2 = poly[j][0];
            vertY2 = poly[j][1];
            
            // Check for ray intersections
            intersect = false;
            
            // If y coordinates straddle horizontal line at py
            if (((vertY1 > py) && (vertY2 <= py)) || 
                ((vertY2 > py) && (vertY1 <= py))) {
                
                // Calculate x intersection with horizontal ray
                if (vertY1 != vertY2) { // Avoid division by zero
                    var t = (py - vertY1) / (vertY2 - vertY1);
                    var xIntersect = vertX1 + (t * (vertX2 - vertX1));
                    
                    // If intersection is to the right of the test point
                    if (xIntersect > px) {
                        intersect = true;
                    };
                };
            };
            
            // If ray intersects edge, toggle inside/outside
            if (intersect) {
                inside = inside.not;
            };
            
            // Move to next edge
            j = i;
            i = i + 1;
        };
        
        inside;
    };
    
    // Convert data coordinates to screen coordinates
    ~dataToScreenCoords = { |dataX, dataY|
        // Convert normalized 0-1 coordinates to screen pixels
        // FluidPlotter has y=0 at bottom, UserView has y=0 at top
        var screenX = dataX * 800;
        var screenY = 800 - (dataY * 800); // Invert the y-coordinate
        
        [screenX, screenY];
    };
    
    // Find points inside a polygon without coloring them
    ~findPointsInPolygon = { |polygon|
        var pointList = List[];
        
        // Only check unclaimed points
        dataDict["data"].keysValuesDo { |id, values|
            if(~claimedPoints.includes(id).not) {
                var screenCoords = ~dataToScreenCoords.(values[0], values[1]);
                
                if(~pointInPolygon.(screenCoords, polygon)) {
                    pointList.add(id);
                };
            };
        };
        
        pointList;
    };
    
    // Apply a solid color to a list of points
    ~colorPoints = { |points, polygonIndex|
        var color = ~polygonColors[polygonIndex % ~polygonColors.size];
        
        // Use AppClock to ensure GUI operations happen in the right thread
        Routine({
            points.do { |id|
                var solidColor = color.copy.alpha_(1.0);
                ~fp.pointColor_(id, solidColor);
            };
            ~fp.refresh;
        }).play(AppClock);
    };
    
    // Process polygon - find points and assign them
    ~processPolygon = { |polygonIndex|
        var polygon = ~polygons[polygonIndex];
        var pointsInside = ~findPointsInPolygon.(polygon);
        
        // Mark these points as claimed
        pointsInside.do { |id|
            ~claimedPoints.add(id);
        };
        
        // Store point list for this polygon
        ~pointsInPolygons[polygonIndex] = pointsInside;
        
        // Color these points
        ~colorPoints.(pointsInside, polygonIndex);
        
        "Polygon % created with % points".format(polygonIndex, pointsInside.size).postln;
    };
    
    // Complete current polygon
    ~finishPolygon = {
        if(~currentPoints.size > 2) {
            var newPolygonIndex = ~polygons.size;
            
            // Add polygon to the list
            ~polygons = ~polygons.add(~currentPoints.copy);
            
            // Process the polygon (find and color points)
            ~processPolygon.(newPolygonIndex);
            
            // Clear the current points
            ~currentPoints = List[];
            ~overlay.refresh;
        } {
            "Cannot complete polygon - need at least 3 points".postln;
        };
    };
    
    // Function to undo the last point in current polygon
    ~undoLastPoint = {
        if(~isDrawing && (~currentPoints.size > 0)) {
            ~currentPoints.pop;
            "Point removed - % points in current polygon".format(~currentPoints.size).postln;
            ~overlay.refresh;
        };
    };
    
    // Function to undo the last polygon
    ~undoLastPolygon = {
        if(~polygons.size > 0) {
            var lastPolygonIndex = ~polygons.size - 1;
            var pointsToUnmark;
            
            // Get points from the last polygon
            pointsToUnmark = ~pointsInPolygons[lastPolygonIndex];
            
            // Remove these points from the claimed set
            pointsToUnmark.do { |id|
                ~claimedPoints.remove(id);
            };
            
            // Reset the colors of these points
            Routine({
                pointsToUnmark.do { |id|
                    ~fp.pointColor_(id, Color.black.alpha_(1.0));
                };
                ~fp.refresh;
            }).play(AppClock);
            
            // Remove the polygon and its points
            ~polygons.pop;
            ~pointsInPolygons.removeAt(lastPolygonIndex);
            
            "Last polygon removed - % polygons remaining".format(~polygons.size).postln;
            ~overlay.refresh;
        } {
            "No polygons to undo".postln;
        };
    };
    
    // Function to clear all polygons
    ~clearAllPolygons = {
        if(~polygons.size > 0) {
            var allPoints = List[];
            
            // Collect all points from all polygons
            ~pointsInPolygons.do { |points|
                allPoints = allPoints.addAll(points);
            };
            
            // Reset the colors of all points
            Routine({
                allPoints.do { |id|
                    ~fp.pointColor_(id, Color.black.alpha_(1.0));
                };
                ~fp.refresh;
            }).play(AppClock);
            
            // Clear all polygon data
            ~polygons = List[];
            ~claimedPoints = Set[];
            ~pointsInPolygons = Dictionary();
            
            "All polygons cleared".postln;
            ~overlay.refresh;
        } {
            "No polygons to clear".postln;
        };
    };
    
    // Finalize all clusters and create the LabelSet
    ~finalizeClusters = {
        if(~polygons.size > 0) {
            var restPoints = List[];
            var restIdx = ~polygons.size;
            
            // Find all unclaimed points for rest cluster
            dataDict["data"].keysValuesDo { |id, values|
                if(~claimedPoints.includes(id).not) {
                    restPoints.add(id);
                    ~claimedPoints.add(id);
                };
            };
            
            // Store and color rest cluster if needed
            if(restPoints.size > 0) {
                ~pointsInPolygons[restIdx] = restPoints;
                
                // Color rest points gray with full opacity
                Routine({
                    restPoints.do { |id|
                        ~fp.pointColor_(id, Color.gray.alpha_(1.0));
                    };
                    ~fp.refresh;
                }).play(AppClock);
                
                "Uncategorized points assigned to cluster %: % points".format(restIdx, restPoints.size).postln;
            };
            
            // Report cluster sizes
            ~pointsInPolygons.keysValuesDo { |clusterIdx, points|
                "Cluster %: % points".format(clusterIdx, points.size).postln;
            };
            
            "All clusters finalized - % total clusters".format(~pointsInPolygons.size).postln;
        } {
            "No polygons defined yet".postln;
        };
    };
    
    // Keyboard handler
    w.view.keyDownAction = { |view, char, mod, unicode, keycode|
        var isShiftDown = (mod & 131072) == 131072; // Check if Shift is held
        
        // Shift+D to start drawing mode
        if(isShiftDown && (unicode == 100 || unicode == 68)) { // Shift+d or Shift+D
            ~isDrawing = true;
            ~drawingSuspended = false;
            ~currentPoints = List[];
            "Drawing mode ON - Click to add points".postln;
            ~overlay.refresh;
        };
        
        // Shift+Enter to complete polygon
        if(isShiftDown && (unicode == 13)) {
            if(~isDrawing) {
                ~finishPolygon.();
            };
        };
        
        // Shift+Esc to cancel
        if(isShiftDown && (unicode == 27)) {
            if(~currentPoints.size > 0) {
                ~currentPoints = List[];
                "Current polygon canceled".postln;
            } {
                ~isDrawing = false;
                "Drawing mode OFF".postln;
            };
            ~overlay.refresh;
        };
        
        // Shift+Space to finalize
        if(isShiftDown && (unicode == 32)) {
            ~finalizeClusters.();
        };
        
        // Shift+S to toggle drawing suspension
        if(isShiftDown && (unicode == 115 || unicode == 83)) { // Shift+s or Shift+S
            if(~isDrawing) {
                ~drawingSuspended = ~drawingSuspended.not;
                if(~drawingSuspended) {
                    "Drawing PAUSED - Exploration mode".postln;
                } {
                    "Drawing RESUMED - Clicks will now add points to polygon".postln;
                };
                ~overlay.refresh;
            };
        };
        
        // Shift+Z to undo last polygon
        if(isShiftDown && (unicode == 122 || unicode == 90)) { // Shift+z or Shift+Z
            ~undoLastPolygon.();
        };
        
        // Shift+C to clear all polygons
        if(isShiftDown && (unicode == 99 || unicode == 67)) { // Shift+c or Shift+C
            ~clearAllPolygons.();
        };
        
        // Backspace or Delete to undo last point
        if(unicode == 8 || unicode == 127 || keycode == 51) {
            ~undoLastPoint.();
        };
    };
    
    // Add UI buttons at the bottom
    
    // Add a dedicated button for undoing points
    Button(w, Rect(20, 810, 120, 30))
    .states_([["Undo Last Point", Color.white, Color.red]])
    .action_({ ~undoLastPoint.() });
    
    // Add a button for undoing a whole polygon
    Button(w, Rect(150, 810, 120, 30))
    .states_([["Undo Polygon", Color.white, Color.blue]])
    .action_({ ~undoLastPolygon.() });
    
    // Add a button for clearing all polygons
    Button(w, Rect(280, 810, 120, 30))
    .states_([["Clear All", Color.white, Color.black]])
    .action_({ ~clearAllPolygons.() });
    
    // Add drawing controls status
    Button(w, Rect(410, 810, 120, 30))
    .states_([
        ["Start Drawing", Color.white, Color.green],
        ["Pause Drawing", Color.white, Color.new255(255, 165, 0)], // Using RGB for orange
        ["Resume Drawing", Color.white, Color.green]
    ])
    .action_({ |button|
        if(button.value == 0) { // Start Drawing
            ~isDrawing = true;
            ~drawingSuspended = false;
            ~currentPoints = List[];
            "Drawing mode ON - Click to add polygon points".postln;
            button.value = 1;
        } {
            if(button.value == 1) { // Pause Drawing
                ~drawingSuspended = true;
                "Drawing PAUSED - Exploration mode".postln;
                button.value = 2;
            } {
                // Resume Drawing
                ~drawingSuspended = false;
                "Drawing RESUMED - Clicks will now add points to polygon".postln;
                button.value = 1;
            };
        };
        ~overlay.refresh;
    });
    
    // Add complete polygon button
    Button(w, Rect(540, 810, 120, 30))
    .states_([["Complete Polygon", Color.white, Color.green]])
    .action_({ ~finishPolygon.() });
    
    // Add finalize all button
    Button(w, Rect(670, 810, 120, 30))
    .states_([["Finalize All", Color.white, Color.red]])
    .action_({ ~finalizeClusters.() });
    
    // Add help text at the top
    StaticText(w, Rect(10, 10, 780, 30))
    .string_("Shift+D: draw mode, Click: add point, Shift+S: pause/resume, Shift+Enter: complete, Shift+Esc: cancel")
    .align_(\center)
    .background_(Color.white.alpha_(0.7));
    
    w.front;
};

// Run the UI setup
~setupUI.();

// Print instructions
"Polygon Selection Tool Initialized".postln;
"------------------------------".postln;
"• Shift+D: Start drawing mode".postln;
"• Click: Add points to polygon".postln;
"• Shift+S: Toggle between drawing and exploring".postln;
"• Shift+Enter: Complete current polygon".postln;
"• Shift+Esc: Cancel current polygon or exit drawing mode".postln;
"• Shift+Z: Undo last polygon".postln;
"• Shift+C: Clear all polygons".postln;
"• Backspace: Remove last point".postln;
"• Shift+Space: Finalize all polygons/clusters".postln;
"• Use buttons at bottom for all actions".postln;
)
1 Like

this is a fantastic UI job, congratulations!

1 Like