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;
)