1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Updated BlocksMonitor example to display correct topology with connections and rotations

This commit is contained in:
ed 2016-11-21 10:54:16 +00:00
parent 8a93ddc818
commit 6641b9d844
4 changed files with 422 additions and 53 deletions

View file

@ -35,6 +35,9 @@ public:
// If this is a Lightpad then redraw it at 25Hz
if (block->getType() == Block::lightPadBlock)
startTimerHz (25);
// Make sure the component can't go offscreen if it is draggable
constrainer.setMinimumOnscreenAmounts (50, 50, 50, 50);
}
~BlockComponent()
@ -109,6 +112,76 @@ public:
return -1;
}
Point<float> getOffsetForPort (Block::ConnectionPort port)
{
using e = Block::ConnectionPort::DeviceEdge;
switch (rotation)
{
case 0:
{
switch (port.edge)
{
case e::north:
return { static_cast<float> (port.index), 0.0f };
case e::east:
return { static_cast<float> (block->getWidth()), static_cast<float> (port.index) };
case e::south:
return { static_cast<float> (port.index), static_cast<float> (block->getHeight()) };
case e::west:
return { 0.0f, static_cast<float> (port.index) };
}
}
case 90:
{
switch (port.edge)
{
case e::north:
return { 0.0f, static_cast<float> (port.index) };
case e::east:
return { static_cast<float> (-1.0f - port.index), static_cast<float> (block->getWidth()) };
case e::south:
return { static_cast<float> (0.0f - block->getHeight()), static_cast<float> (port.index) };
case e::west:
return { static_cast<float> (-1.0f - port.index), 0.0f };
}
}
case 180:
{
switch (port.edge)
{
case e::north:
return { static_cast<float> (-1.0f - port.index), 0.0f };
case e::east:
return { static_cast<float> (0.0f - block->getWidth()), static_cast<float> (-1.0f - port.index) };
case e::south:
return { static_cast<float> (-1.0f - port.index), static_cast<float> (0.0f - block->getHeight()) };
case e::west:
return { 0.0f, static_cast<float> (-1.0f - port.index) };
}
}
case 270:
{
switch (port.edge)
{
case e::north:
return { 0.0f, static_cast<float> (-1.0f - port.index) };
case e::east:
return { static_cast<float> (port.index), static_cast<float> (0 - block->getWidth()) };
case e::south:
return { static_cast<float> (block->getHeight()), static_cast<float> (-1.0f - port.index) };
case e::west:
return { static_cast<float> (port.index), 0.0f };
}
}
}
return Point<float>();
}
int rotation = 0;
Point<float> topLeft = { 0.0f, 0.0f };
private:
/** Used to call repaint() periodically */
void timerCallback() override { repaint(); }
@ -122,6 +195,26 @@ private:
/** Overridden from ControlButton::Listener */
void buttonReleased (ControlButton& b, Block::Timestamp t) override { handleButtonReleased (b.getType(), t); }
/** Overridden from MouseListener. Prepares the master Block component for dragging. */
void mouseDown (const MouseEvent& e) override
{
if (block->isMasterBlock())
componentDragger.startDraggingComponent (this, e);
}
/** Overridden from MouseListener. Drags the master Block component */
void mouseDrag (const MouseEvent& e) override
{
if (block->isMasterBlock())
{
componentDragger.dragComponent (this, e, &constrainer);
getParentComponent()->resized();
}
}
ComponentDragger componentDragger;
ComponentBoundsConstrainer constrainer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlockComponent)
};

View file

@ -10,7 +10,8 @@
*/
class MainComponent : public Component,
public TopologySource::Listener,
private Timer
private Timer,
private Button::Listener
{
public:
MainComponent()
@ -19,12 +20,23 @@ public:
noBlocksLabel.setText ("No BLOCKS connected...", dontSendNotification);
noBlocksLabel.setJustificationType (Justification::centred);
addAndMakeVisible (noBlocksLabel);
zoomOutButton.setButtonText ("+");
zoomOutButton.addListener (this);
zoomOutButton.setAlwaysOnTop (true);
zoomInButton.setButtonText ("-");
zoomInButton.addListener (this);
zoomInButton.setAlwaysOnTop (true);
// Register MainComponent as a listener to the PhysicalTopologySource object
topologySource.addListener (this);
startTimer (10000);
addAndMakeVisible (noBlocksLabel);
addAndMakeVisible (zoomOutButton);
addAndMakeVisible (zoomInButton);
}
void paint (Graphics& g) override
@ -45,56 +57,84 @@ public:
return;
}
// Work out the maximum diplay area for each Block
auto bounds = getLocalBounds().reduced (20);
zoomOutButton.setBounds (10, getHeight() - 40, 40, 30);
zoomInButton.setBounds (zoomOutButton.getRight(), zoomOutButton.getY(), 40, 30);
auto squareRoot = std::sqrt (numBlockComponents);
int gridSize = (int) squareRoot;
if (squareRoot - gridSize > 0)
gridSize++;
int sideLength = bounds.getWidth() / gridSize;
int xCounter = 0;
int yCounter = 0;
bool hasSpaceForControlBlock = false;
Rectangle<int> lastControlBlockBounds;
for (auto block : blockComponents)
if (isInitialResized)
{
Rectangle<int> blockBounds;
auto type = block->block->getType();
// Work out the area needed in terms of Block units
Rectangle<float> maxArea;
for (auto blockComponent : blockComponents)
{
auto topLeft = blockComponent->topLeft;
int rotation = blockComponent->rotation;
int blockSize = 0;
// Can fit 2 ControlBlockComponents in the space of one LightpadBlockComponent
if (type == Block::liveBlock || type == Block::loopBlock)
{
if (hasSpaceForControlBlock)
{
blockBounds = lastControlBlockBounds.withY (lastControlBlockBounds.getY() + (int)(sideLength * 0.5));
hasSpaceForControlBlock = false;
}
else
{
blockBounds = Rectangle<int> (bounds.getX() + (xCounter * sideLength), bounds.getY() + (yCounter * sideLength),
sideLength, (int)(sideLength * 0.5));
hasSpaceForControlBlock = true;
lastControlBlockBounds = blockBounds;
}
}
else
{
blockBounds = Rectangle<int> (bounds.getX() + (xCounter * sideLength), bounds.getY() + (yCounter * sideLength),
sideLength, sideLength);
if (rotation == 180)
blockSize = blockComponent->block->getWidth();
else if (rotation == 90)
blockSize = blockComponent->block->getHeight();
if (topLeft.x - blockSize < maxArea.getX())
maxArea.setX (topLeft.x - blockSize);
blockSize = 0;
if (rotation == 0)
blockSize = blockComponent->block->getWidth();
else if (rotation == 270)
blockSize = blockComponent->block->getHeight();
if (topLeft.x + blockSize > maxArea.getRight())
maxArea.setWidth (topLeft.x + blockSize);
blockSize = 0;
if (rotation == 180)
blockSize = blockComponent->block->getHeight();
else if (rotation == 270)
blockSize = blockComponent->block->getWidth();
if (topLeft.y - blockSize < maxArea.getY())
maxArea.setY (topLeft.y - blockSize);
blockSize = 0;
if (rotation == 0)
blockSize = blockComponent->block->getHeight();
else if (rotation == 90)
blockSize = blockComponent->block->getWidth();
if (topLeft.y + blockSize > maxArea.getBottom())
maxArea.setHeight (topLeft.y + blockSize);
}
block->setBounds (blockBounds.reduced (5));
float totalWidth = std::abs (maxArea.getX()) + maxArea.getWidth();
float totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight();
if (++xCounter >= gridSize)
{
yCounter++;
xCounter = 0;
}
blockUnitInPixels = static_cast<int> (jmin ((getHeight() / totalHeight) - 50, (getWidth() / totalWidth) - 50));
masterBlockComponent->centreWithSize (masterBlockComponent->block->getWidth() * blockUnitInPixels,
masterBlockComponent->block->getHeight() * blockUnitInPixels);
isInitialResized = false;
}
else
{
masterBlockComponent->setSize (masterBlockComponent->block->getWidth() * blockUnitInPixels, masterBlockComponent->block->getHeight() * blockUnitInPixels);
}
for (auto blockComponent : blockComponents)
{
if (blockComponent == masterBlockComponent)
continue;
blockComponent->setBounds (masterBlockComponent->getX() + static_cast<int> (blockComponent->topLeft.x * blockUnitInPixels),
masterBlockComponent->getY() + static_cast<int> (blockComponent->topLeft.y * blockUnitInPixels),
blockComponent->block->getWidth() * blockUnitInPixels,
blockComponent->block->getHeight() * blockUnitInPixels);
if (blockComponent->rotation != 0)
blockComponent->setTransform (AffineTransform::rotation (blockComponent->rotation * (static_cast<float> (double_Pi) / 180.0f),
static_cast<float> (blockComponent->getX()),
static_cast<float> (blockComponent->getY())));
}
}
@ -103,16 +143,32 @@ public:
{
// Clear the array of Block components
blockComponents.clear();
masterBlockComponent = nullptr;
// Get the array of currently connected Block objects from the PhysicalTopologySource
auto blocksArray = topologySource.getCurrentTopology().blocks;
// Get the current topology
auto topology = topologySource.getCurrentTopology();
// Create a BlockComponent object for each Block object
for (auto& block : blocksArray)
// Create a BlockComponent object for each Block object and store a pointer to the master
for (auto& block : topology.blocks)
{
if (auto* blockComponent = createBlockComponent (block))
{
addAndMakeVisible (blockComponents.add (blockComponent));
if (blockComponent->block->isMasterBlock())
masterBlockComponent = blockComponent;
}
}
// Must have a master Block!
if (topology.blocks.size() > 0)
jassert (masterBlockComponent != nullptr);
// Calculate the relative position and rotation for each Block
positionBlocks (topology);
// Update the display
isInitialResized = true;
resized();
}
@ -128,7 +184,7 @@ private:
if (type == Block::loopBlock || type == Block::liveBlock)
return new ControlBlockComponent (newBlock);
// should only be connecting a Lightpad or Control Block!
// Should only be connecting a Lightpad or Control Block!
jassertfalse;
return nullptr;
}
@ -140,12 +196,225 @@ private:
c->updateStatsAndTooltip();
}
/** Zooms the display in or out */
void buttonClicked (Button* button) override
{
blockUnitInPixels *= (button == &zoomOutButton ? 1.05f : (button == &zoomInButton ? 0.95f : 1.0f));
resized();
}
/** Calculates the position and rotation of each connected Block relative to the master Block */
void positionBlocks (BlockTopology topology)
{
Array<BlockComponent*> blocksConnectedToMaster;
float maxDelta = std::numeric_limits<float>::max();
int maxLoops = 50;
// Store all the connections to the master Block
Array<BlockDeviceConnection> masterBlockConnections;
for (auto connection : topology.connections)
if (connection.device1 == masterBlockComponent->block->uid || connection.device2 == masterBlockComponent->block->uid)
masterBlockConnections.add (connection);
// Position all the Blocks that are connected to the master Block
while (maxDelta > 0.001f && --maxLoops)
{
maxDelta = 0.0f;
// Loop through each connection on the master Block
for (auto connection : masterBlockConnections)
{
// Work out whether the master Block is device 1 or device 2 in the BlockDeviceConnection struct
bool isDevice1 = true;
if (masterBlockComponent->block->uid == connection.device2)
isDevice1 = false;
// Get the connected ports
auto masterPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
for (auto otherBlockComponent : blockComponents)
{
// Get the other block
if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
{
blocksConnectedToMaster.addIfNotAlreadyThere (otherBlockComponent);
// Get the rotation of the other Block relative to the master Block
otherBlockComponent->rotation = getRotation (masterPort.edge, otherPort.edge);
// Get the offsets for the connected ports
auto masterBlockOffset = masterBlockComponent->getOffsetForPort (masterPort);
auto otherBlockOffset = otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort);
// Work out the distance between the two connected ports
auto delta = masterBlockOffset - otherBlockOffset;
// Move the other block half the distance to the connection
otherBlockComponent->topLeft += delta / 2.0f;
// Work out whether we are close enough for the loop to end
maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
}
}
}
}
// Check if there are any Blocks that have not been positioned yet
Array<BlockComponent*> unpositionedBlocks;
for (auto blockComponent : blockComponents)
if (blockComponent != masterBlockComponent && ! blocksConnectedToMaster.contains (blockComponent))
unpositionedBlocks.add (blockComponent);
if (unpositionedBlocks.size() > 0)
{
// Reset the loop conditions
maxDelta = std::numeric_limits<float>::max();
maxLoops = 50;
// Position all the remaining Blocks
while (maxDelta > 0.001f && --maxLoops)
{
maxDelta = 0.0f;
// Loop through each unpositioned Block
for (auto blockComponent : unpositionedBlocks)
{
// Store all the connections to this Block
Array<BlockDeviceConnection> blockConnections;
for (auto connection : topology.connections)
if (connection.device1 == blockComponent->block->uid || connection.device2 == blockComponent->block->uid)
blockConnections.add (connection);
// Loop through each connection on this Block
for (auto connection : blockConnections)
{
// Work out whether this Block is device 1 or device 2 in the BlockDeviceConnection struct
bool isDevice1 = true;
if (blockComponent->block->uid == connection.device2)
isDevice1 = false;
// Get the connected ports
auto thisPort = isDevice1 ? connection.connectionPortOnDevice1 : connection.connectionPortOnDevice2;
auto otherPort = isDevice1 ? connection.connectionPortOnDevice2 : connection.connectionPortOnDevice1;
// Get the other Block
for (auto otherBlockComponent : blockComponents)
{
if (otherBlockComponent->block->uid == (isDevice1 ? connection.device2 : connection.device1))
{
// Get the rotation
int rotation = getRotation (otherPort.edge, thisPort.edge) + otherBlockComponent->rotation;
if (rotation > 360)
rotation -= 360;
blockComponent->rotation = rotation;
// Get the offsets for the connected ports
auto otherBlockOffset = (otherBlockComponent->topLeft + otherBlockComponent->getOffsetForPort (otherPort));
auto thisBlockOffset = (blockComponent->topLeft + blockComponent->getOffsetForPort (thisPort));
// Work out the distance between the two connected ports
auto delta = otherBlockOffset - thisBlockOffset;
// Move this block half the distance to the connection
blockComponent->topLeft += delta / 2.0f;
// Work out whether we are close enough for the loop to end
maxDelta = jmax (maxDelta, std::abs (delta.x), std::abs (delta.y));
}
}
}
}
}
}
}
/** Returns a rotation in degrees based on the connected edges of two blocks */
int getRotation (Block::ConnectionPort::DeviceEdge staticEdge, Block::ConnectionPort::DeviceEdge rotatedEdge)
{
using edge = Block::ConnectionPort::DeviceEdge;
switch (staticEdge)
{
case edge::north:
{
switch (rotatedEdge)
{
case edge::north:
return 180;
case edge::south:
return 0;
case edge::east:
return 90;
case edge::west:
return 270;
}
}
case edge::south:
{
switch (rotatedEdge)
{
case edge::north:
return 0;
case edge::south:
return 180;
case edge::east:
return 270;
case edge::west:
return 90;
}
}
case edge::east:
{
switch (rotatedEdge)
{
case edge::north:
return 270;
case edge::south:
return 90;
case edge::east:
return 180;
case edge::west:
return 0;
}
}
case edge::west:
{
switch (rotatedEdge)
{
case edge::north:
return 90;
case edge::south:
return 270;
case edge::east:
return 0;
case edge::west:
return 180;
}
}
}
return 0;
}
//==============================================================================
PhysicalTopologySource topologySource;
OwnedArray<BlockComponent> blockComponents;
BlockComponent* masterBlockComponent = nullptr;
Label noBlocksLabel;
TextButton zoomOutButton;
TextButton zoomInButton;
int blockUnitInPixels;
bool isInitialResized;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Before After
Before After

View file

@ -9,7 +9,7 @@ BlocksMonitor is a simple JUCE application that shows currently connected Lightp
Navigate to the <tt>JUCE/examples/BLOCKS/BlocksMonitor/Builds/</tt> directory and open the code project in your IDE of choice. Run the application and connect your Blocks (if you do not know how to do this, see @ref connecting_blocks). Any devices that you have connected should now show up in the application window and this display will be updated as you add and remove Blocks. Lightpads are represented as a black square and will display the current touches as coloured circles, the size of which depend on the touch pressure, and Control Blocks are shown as rectangles containing the LED row and clickable buttons on the hardware. If you hover the mouse cursor over a %Block, a tooltip will appear displaying the name, UID, serial number and current battery level.
\image html BlocksMonitor.png "The BlocksMonitor application with a Lightpad and Control Block connected"
\image html BlocksMonitor.png "The BlocksMonitor application with a Lightpad and 3 Control Blocks connected"
@section blocks_monitor_topology Topology
@ -19,7 +19,7 @@ To access the current topology, <code>MainComponent</code> inherits from the Top
@section blocks_monitor_block_object The Block Object
The array of %Block objects contained in the %BlockTopology struct can be used to access individual %Block objects and determine their type using the Block::getType() method. The application uses this information to construct an on-screen representation of the currently connected Blocks by creating either a <code>LightpadComponent</code> or <code>ControlBlockComponent</code> object for each %Block in the current topology. Both of these classes derive from <code>BlockComponent</code>, a relatively simple base class that contains some virtual functions for painting the %Block on screen and handling callbacks from the touch surface and/or buttons on the %Block. In its constructor, <code>BlockComponent</code> takes a reference to the %Block object that it represents and adds itself as a listener to the touch surface (if it is a Lightpad) and buttons using the Block::getTouchSurface() and Block::getButtons() methods, respectively. It inherits from the TouchSurface::Listener and ControlButton::Listener classes and overrides the TouchSurface::Listener::touchChanged(), ControlButton::Listener::buttonPressed() and ControlButton::Listener::buttonReleased() methods to call its own virtual methods, which are implemented by the <code>LightpadComponent</code> and <code>ControlBlockComponent</code> classes to update the on-screen components.
The array of %Block objects contained in the %BlockTopology struct can be used to access individual %Block objects and determine their type using the Block::getType() method. The application uses this information to construct an on-screen representation of the currently connected Blocks by creating either a <code>LightpadComponent</code> or <code>ControlBlockComponent</code> object for each %Block in the current topology. Both of these classes derive from <code>BlockComponent</code>, a relatively simple base class that contains some virtual functions for painting the %Block on screen and handling callbacks from the touch surface and/or buttons on the %Block. In its constructor, <code>BlockComponent</code> takes a pointer to the %Block object that it represents and adds itself as a listener to the touch surface (if it is a Lightpad) and buttons using the Block::getTouchSurface() and Block::getButtons() methods, respectively. It inherits from the TouchSurface::Listener and ControlButton::Listener classes and overrides the TouchSurface::Listener::touchChanged(), ControlButton::Listener::buttonPressed() and ControlButton::Listener::buttonReleased() methods to call its own virtual methods, which are implemented by the <code>LightpadComponent</code> and <code>ControlBlockComponent</code> classes to update the on-screen components.
To visualise touches on the Lightpad, <code>LightpadComponent</code> contains an instance of the TouchList class called <code>touches</code> and calls the TouchList::updateTouch() method whenever it receives a touch surface listener callback in the <code>LightpadComponent::handleTouchChange()</code> method. The <code>LightpadBlock::paint()</code> method then iterates over the current TouchSurface::Touch objects in the %TouchList and visualises them on the component at 25Hz.
@ -27,6 +27,13 @@ The <code>ControlBlockComponent</code> class represents a generic Control %Block
These callback methods are a simple and powerful way to get user input from the Blocks and use this data to drive some process in your application. In this example, the input is simply mirrored on the screen but it could be used to control any number of things such as audio synthesis (see the @ref example_blocks_synth example) and graphics (see the @ref example_blocks_drawing example).
@section blocks_monitor_connections Blocks Connections
The %BlockTopology struct returned by the <code>%PhysicalTopologySource::getCurrentTopology()</code> method also contains an array of BlockDeviceConnection objects representing all the current DNA port connections between Blocks in the topology. A single %BlockDeviceConnection struct describes a physical connection between two ports on two Blocks and contains a Block::UID and Block::ConnectionPort object for each of the two devices.
This information is used to calculate the position and rotation of each connected %Block and update the corresponding <code>topLeft</code> and <code>rotation</code> member variables of its <code>BlockComponent</code> so that the correct topology is displayed on the screen. The <code>topLeft</code> variable is a Point that describes the position of the top left of the <code>BlockComponent</code> in terms of logical device units relative to the top left of the master %Block at Point (0, 0). Initially, all <code>BlockComponent</code> instances have the <code>topLeft</code> position (0, 0) and the <code>MainComponent::positionBlocks()</code> method iterates first over all of the Blocks connected to the master %Block and then any remaining Blocks and calculates the correct <code>topLeft</code> %Point and <code>rotation</code> for each using the array of %BlockDeviceConnection objects. Then, in the <code>MainComponent::resized()</code> method these attributes are used to correctly position the components.
@section blocks_monitor_summary Summary
This tutorial and the accompanying code has introduced the %BlockTopology and %Block objects, and demonstrated how to receive callbacks from connected Blocks when the touch surface or buttons are pressed, allowing you to use this input in your own applications.