Struggling with Pen

Hi all,

in the code below I’m trying to prototype a simple patchbay. I.e. when you click one of the green dots you should be able to draw a line to one of the green dots on the opposite side. However, if I draw a cable and release the mouse (in mouseUpAction) all existing cables are redrawn again in a funky way.
I guess I still don’t quite understand how Pen works. As I understood it a drawing in a UserView is performed by one ‘drawFunc’, a Function to which I incrementally add more Functions for each new cable. Basically that approach seems to work. E.g. in mouseMoveAction I’m drawing lines from a defined origin to my current mouse position by first removing the function for the previously drawn temporary line and then adding a function for a line from the origin to my new mouse position. Only when it comes to drawing the final cable in mouseUpAction add a function of which I assumed it would only affect my final cable. However, it has an impact on all previously drawn cables too.

Can anyone explain what’s going on here? (sorry for the comprehensive code - you should be able to see what I mean when you paste and execute my code in SuperCollider).

Thanks, Stefan

(
var win, u, points, leftRects, rightRects,
leftPlugs, rightPlugs,
leftHitTest, rightHitTest, leftHit, rightHit,
startPoint, drawFunc, finalCables;

points = List.new;
leftRects = List.new;
rightRects = List.new;
leftPlugs = List.new;
rightPlugs = List.new;
finalCables = List.new;

12.do { |i|
	points.add(Point(10, 10 + (i * 50) ))
};
win = Window(\patchbay, Rect(
	Window.screenBounds.width/2-400,
	Window.screenBounds.height/2-350,
	800, 700
));
u = UserView(win, win.view.bounds);
u.resize = 5;
u.background_(Color(alpha: 0.7));
win.acceptsMouseOver_(true);
u.drawFunc_({ |uv|
	uv.clearDrawing;
	[leftRects, rightRects, leftPlugs, rightPlugs].do(_.clear);
	points.do { |p, i|
		Pen.strokeColor_(Color.white);
		Pen.joinStyle_(1);
		leftRects.add(Rect(p.x, p.y, 200, 40));
		Pen.strokeRect(leftRects[i]);

		rightRects.add(Rect(uv.bounds.width - 210, p.y, 200, 40));
		Pen.strokeRect(rightRects[i]);

		Pen.fillColor_(Color.green);
		leftPlugs.add(Rect(p.x + 190, p.y + 10, 20, 20));
		Pen.addOval(leftPlugs[i]);
		Pen.fill;

		rightPlugs.add(Rect(uv.bounds.width - 220, p.y + 10, 20, 20));
		Pen.addOval(rightPlugs[i]);
		Pen.fill;
	}
});
u.mouseDownAction_({ |v, x, y, mod, bn, ccount|
	"mouse down".postln;
        // only draw if mouse is over one of the green dots
	leftHit = leftHitTest.(x, y);
	rightHit = rightHitTest.(x, y);
	leftHit !? {
		startPoint = Point(
			leftPlugs[leftHit].width.div(2) + leftPlugs[leftHit].left,
			leftPlugs[leftHit].height.div(2) + leftPlugs[leftHit].top
		)
	};
	rightHit !? {
		startPoint = Point(
			rightPlugs[rightHit].width.div(2) + rightPlugs[rightHit].left,
			rightPlugs[rightHit].height.div(2) + rightPlugs[rightHit].top
		)
	};
	if (startPoint.notNil) {
		v.drawFunc_(v.drawFunc.addFunc({ |uv|
			Pen.moveTo(startPoint);
		}))
	};
});
u.mouseMoveAction_({ |v, x, y, mods|
        // remove drawFunc for temporary line before
        // drawing a new one 
	v.drawFunc.removeFunc(drawFunc);
	drawFunc = { |uv|
		Pen.width_(5);
		Pen.joinStyle_(1);
		Pen.moveTo(startPoint);
		Pen.lineTo(Point(x, y));
		Pen.stroke;
	};
       // draw a new temporary line
	v.drawFunc.addFunc(drawFunc);
	v.refresh;
});
u.mouseUpAction_({ |v, x, y, mods, bn|
	var endPoint;
	"mouse up".postln;
        // remove drawFunc for temporary cable, drawn in mouseMoveAction
	v.drawFunc.removeFunc(drawFunc);
	case
        // drawing a cable from the left side
	{ leftHit.notNil } {
		rightHit = rightHitTest.(x, y);
		rightHit !? {
			endPoint = Point(
				rightPlugs[rightHit].width.div(2) + rightPlugs[rightHit].left,
				rightPlugs[rightHit].height.div(2) + rightPlugs[rightHit].top
			);
			finalCables.add((
				start: startPoint,
				end: endPoint
			));

                        // this function should only be added once for the last cable
                        // but gets applied for aleady existing ones as well, it seems
			v.drawFunc.addFunc({ |uv|
				Pen.width_(5);
				Pen.joinStyle_(1);
                                // see, this function gets repeated for every existing cable
				Pen.moveTo(finalCables[finalCables.size-1].start.postln);
				Pen.lineTo(endPoint.postln);
				Pen.stroke;
			});
		}
	}
        // drawing a cable from the right side
	{ rightHit.notNil } {
		"drawn from the right".postln;
		leftHit = leftHitTest.(x, y);
		leftHit !? {
			endPoint = Point(
				leftPlugs[leftHit].width.div(2) + leftPlugs[leftHit].left,
				leftPlugs[leftHit].height.div(2) + leftPlugs[leftHit].top
			);
			finalCables.add((
				start: endPoint,
				end: startPoint
			));

			v.drawFunc.addFunc({ |uv|
				Pen.width_(5);
				Pen.joinStyle_(1);
                                // see, this function gets repeated for every existing cable
				Pen.moveTo(finalCables[finalCables.size-1].end.postln);
				Pen.lineTo(endPoint.postln);
				Pen.stroke;
			});
		}
	};
	v.refresh;
	"finalCables: %\n".postf(finalCables);
});

leftHitTest = { |x, y|
	var ret;
	leftPlugs.do { |p, i|
		if (x > p.left and: { x < p.right and: { y > p.top and: { y < p.bottom }}}) {
			ret = i;
		}
	};
	ret;
};
rightHitTest = { |x, y|
	var ret;
	rightPlugs.do { |p, i|
		if (x > p.left and: { x < p.right and: { y > p.top and: { y < p.bottom }}}) {
			ret = i;
		}
	};
	ret;
};

win.front;
win.view.onResize_({ |v|
	u.resizeToBounds(v.bounds);
})
)

… sorry for the noise. Just after writing my first post found a solution. The problem was how I addressed the start and end point in mouseUpAction:

Instead of directly adding an (anonymous) Event to finalCables I first create the Event in a variable which I then add to finalCables. Then I determine the index of this Event through indexOf and keep it in a variable local to mouseUpAction instead of addressing it in the drawFunc by finalCables.size-1:

var cableEvent, index;
...
cableEvent = (start: startPoint, end: endPoint);
finalCables.add(cableEvent);
index = finalCables.indexOf(cableEvent);
...
Pen.moveTo(finalCables[index].start);
Pen.lineTo(finalCables[index].end);

Here’s the full code for anyone who’s interested:

(
var win, u, points, leftRects, rightRects,
leftPlugs, rightPlugs,
leftHitTest, rightHitTest, leftHit, rightHit,
startPoint, drawFunc, finalCables;

points = List.new;
leftRects = List.new;
rightRects = List.new;
leftPlugs = List.new;
rightPlugs = List.new;
finalCables = List.new;

12.do { |i|
	points.add(Point(10, 10 + (i * 50) ))
};
win = Window(\patchbay, Rect(
	Window.screenBounds.width/2-400,
	Window.screenBounds.height/2-350,
	800, 700
));
u = UserView(win, win.view.bounds);
u.resize = 5;
u.background_(Color(alpha: 0.7));
win.acceptsMouseOver_(true);
u.drawFunc_({ |uv|
	uv.clearDrawing;
	[leftRects, rightRects, leftPlugs, rightPlugs].do(_.clear);
	points.do { |p, i|
		Pen.strokeColor_(Color.white);
		Pen.joinStyle_(1);
		leftRects.add(Rect(p.x, p.y, 200, 40));
		Pen.strokeRect(leftRects[i]);

		rightRects.add(Rect(uv.bounds.width - 210, p.y, 200, 40));
		Pen.strokeRect(rightRects[i]);

		Pen.fillColor_(Color.green);
		leftPlugs.add(Rect(p.x + 190, p.y + 10, 20, 20));
		Pen.addOval(leftPlugs[i]);
		Pen.fill;

		rightPlugs.add(Rect(uv.bounds.width - 220, p.y + 10, 20, 20));
		Pen.addOval(rightPlugs[i]);
		Pen.fill;
	}
});
u.mouseDownAction_({ |v, x, y, mod, bn, ccount|
	"mouse down".postln;
	// only draw if mouse is over one of the green dots
	leftHit = leftHitTest.(x, y);
	rightHit = rightHitTest.(x, y);
	leftHit !? {
		startPoint = Point(
			leftPlugs[leftHit].width.div(2) + leftPlugs[leftHit].left,
			leftPlugs[leftHit].height.div(2) + leftPlugs[leftHit].top
		)
	};
	rightHit !? {
		startPoint = Point(
			rightPlugs[rightHit].width.div(2) + rightPlugs[rightHit].left,
			rightPlugs[rightHit].height.div(2) + rightPlugs[rightHit].top
		)
	};
	if (startPoint.notNil) {
		v.drawFunc_(v.drawFunc.addFunc({ |uv|
			Pen.moveTo(startPoint);
		}))
	};
});
u.mouseMoveAction_({ |v, x, y, mods|
	// remove drawFunc for temporary line before
	// drawing a new one
	v.drawFunc.removeFunc(drawFunc);
	drawFunc = { |uv|
		Pen.width_(5);
		Pen.joinStyle_(1);
		Pen.moveTo(startPoint);
		Pen.lineTo(Point(x, y));
		Pen.stroke;
	};
	// draw a new temporary line
	v.drawFunc.addFunc(drawFunc);
	v.refresh;
});
u.mouseUpAction_({ |v, x, y, mods, bn|
	var endPoint, cableEvent, index;
	"mouse up".postln;
	// remove drawFunc for temporary cable, drawn in mouseMoveAction
	v.drawFunc.removeFunc(drawFunc);
	case
	// drawing a cable from the left side
	{ leftHit.notNil } {
		rightHit = rightHitTest.(x, y);
		rightHit !? {
			endPoint = Point(
				rightPlugs[rightHit].width.div(2) + rightPlugs[rightHit].left,
				rightPlugs[rightHit].height.div(2) + rightPlugs[rightHit].top
			);
			cableEvent = (start: startPoint, end: endPoint);
			finalCables.add(cableEvent);
			index = finalCables.indexOf(cableEvent);

			v.drawFunc.addFunc({ |uv|
				Pen.width_(5);
				Pen.joinStyle_(1);
				Pen.moveTo(finalCables[index].start);
				Pen.lineTo(finalCables[index].end);
				Pen.stroke;
			});
		}
	}
	// drawing a cable from the right side
	{ rightHit.notNil } {
		"drawn from the right".postln;
		leftHit = leftHitTest.(x, y);
		leftHit !? {
			endPoint = Point(
				leftPlugs[leftHit].width.div(2) + leftPlugs[leftHit].left,
				leftPlugs[leftHit].height.div(2) + leftPlugs[leftHit].top
			);
			cableEvent = (start: endPoint, end: startPoint);
			finalCables.add(cableEvent);
			index = finalCables.indexOf(cableEvent);

			v.drawFunc.addFunc({ |uv|
				Pen.width_(5);
				Pen.joinStyle_(1);
				Pen.moveTo(finalCables[index].end);
				Pen.lineTo(finalCables[index].start);
				Pen.stroke;
			});
		}
	};
	v.refresh;
	"finalCables: %\n".postf(finalCables);
});

leftHitTest = { |x, y|
	var ret;
	leftPlugs.do { |p, i|
		if (x > p.left and: { x < p.right and: { y > p.top and: { y < p.bottom }}}) {
			ret = i;
		}
	};
	ret;
};
rightHitTest = { |x, y|
	var ret;
	rightPlugs.do { |p, i|
		if (x > p.left and: { x < p.right and: { y > p.top and: { y < p.bottom }}}) {
			ret = i;
		}
	};
	ret;
};

win.front;
win.view.onResize_({ |v|
	u.resizeToBounds(v.bounds);
})
)

I wouldn’t necessarily recommend this for ALL use cases like what you’re building, BUT… given you’ve got a relatively simple patch bay with a finite number of points/connections, you might consider leveraging the UI systems own hit detection and drawing/positioning capabilities rather than rolling your own. For example:

  1. Implement each patch point as it’s own UserView with a fixed drawFunc. These can have their own mouse actions, and the UI system will detect clicks for you without requiring that you hit test yourself.
  2. Implement patch cables as a separate UserView, that simply draws from the top-left to lower-right of the view bounds (or bottom-left → top-right, depending).

I believe this should allow you to remove all of the custom hit test code, EXCEPT if you want to hit test your cables. But, in this case, the hit test will only need to check whether a mouse event occurs along the diagonal of the bounding box for that view.

I’m not 100% sure this is the right way to build this, but my intuition is that it could drastically simply your code.

Thanks for the comment!

Of course my code was a very rough sketch. It will be a complicated GUI with an infinite number of points and cables, dynamic updates, comprehensive bookkeeping, etc… I’m less concerned about that.
I’ve thought about using several UserViews and I think that’s a good idea. I’m certainly going to try that. Though it appears to me, that using Pen isn’t as painful as I had expected. It allows function composition and gives me all conveniences a FunctionList has to offer.
Mostly I care about performance. It seems feasible to separate connection points / widgets from cables in another UserView, not having to redraw everything, while dragging a cable. I.e. drawing in mouseMoveAction could be made optional. That’s likely the most CPU intensive phase.
My basic idea behind a patchbay was having a one view overview over a set of current OSC and MIDI connections. I’ve got an older interface that doesn’t allow that but has lots of moving sliders, which tend to be super-exhaustive CPU-vise.
Drawing a single cable (or even more) once will hit performance only shortly but permanent updates to drawings are expensive. Especially for live performances that’s not what I want…

Side note – I just made a ddwSpeedLim quark which could help with that.

hjh

Ah, thanks for this nice quark! Can certainly become useful in some situations. I like these tiny quarks that do such useful things.

Stefan