1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00
JUCE/examples/BLOCKS/BlocksMonitorDemo.h

1033 lines
38 KiB
C++

/*
==============================================================================
This file is part of the JUCE examples.
Copyright (c) 2017 - ROLI Ltd.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
/*******************************************************************************
The block below describes the properties of this PIP. A PIP is a short snippet
of code that can be read by the Projucer and used to generate a JUCE project.
BEGIN_JUCE_PIP_METADATA
name: BlocksMonitorDemo
version: 1.0.0
vendor: JUCE
website: http://juce.com
description: Application to monitor Blocks devices.
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
juce_audio_processors, juce_audio_utils, juce_blocks_basics,
juce_core, juce_data_structures, juce_events, juce_graphics,
juce_gui_basics, juce_gui_extra
exporters: xcode_mac, vs2019, linux_make, xcode_iphone
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
type: Component
mainClass: BlocksMonitorDemo
useLocalCopy: 1
END_JUCE_PIP_METADATA
*******************************************************************************/
#pragma once
//==============================================================================
/**
Base class that renders a Block on the screen
*/
class BlockComponent : public Component,
public SettableTooltipClient,
private TouchSurface::Listener,
private ControlButton::Listener,
private Timer
{
public:
BlockComponent (Block::Ptr blockToUse)
: block (blockToUse)
{
updateStatsAndTooltip();
// Register BlockComponent as a listener to the touch surface
if (auto touchSurface = block->getTouchSurface())
touchSurface->addListener (this);
// Register BlockComponent as a listener to any buttons
for (auto button : block->getButtons())
button->addListener (this);
// If this is a Lightpad then set the grid program to be blank
if (block->getLEDGrid() != nullptr)
block->setProgram (std::make_unique<BitmapLEDProgram>(*block));
// 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() override
{
// Remove any listeners
if (auto touchSurface = block->getTouchSurface())
touchSurface->removeListener (this);
for (auto button : block->getButtons())
button->removeListener (this);
}
/** Called periodically to update the tooltip with information about the Block */
void updateStatsAndTooltip()
{
// Get the battery level of this Block and inform any subclasses
auto batteryLevel = block->getBatteryLevel();
handleBatteryLevelUpdate (batteryLevel);
// Update the tooltip
setTooltip ("Name = " + block->getDeviceDescription() + "\n"
+ "UID = " + String (block->uid) + "\n"
+ "Serial number = " + block->serialNumber + "\n"
+ "Battery level = " + String ((int) (batteryLevel * 100)) + "%"
+ (block->isBatteryCharging() ? "++"
: "--"));
}
/** Subclasses should override this to paint the Block object on the screen */
void paint (Graphics&) override = 0;
/** Subclasses can override this to receive button down events from the Block */
virtual void handleButtonPressed (ControlButton::ButtonFunction, uint32) {}
/** Subclasses can override this to receive button up events from the Block */
virtual void handleButtonReleased (ControlButton::ButtonFunction, uint32) {}
/** Subclasses can override this to receive touch events from the Block */
virtual void handleTouchChange (TouchSurface::Touch) {}
/** Subclasses can override this to battery level updates from the Block */
virtual void handleBatteryLevelUpdate (float) {}
/** The Block object that this class represents */
Block::Ptr block;
//==============================================================================
/** Returns an integer index corresponding to a physical position on the hardware
for each type of Control Block. */
static int controlButtonFunctionToIndex (ControlButton::ButtonFunction f)
{
using CB = ControlButton;
static Array<ControlButton::ButtonFunction> map[] =
{
{ CB::mode, CB::button0, CB::velocitySensitivity },
{ CB::volume, CB::button1, CB::glideSensitivity },
{ CB::scale, CB::button2, CB::slideSensitivity, CB::click },
{ CB::chord, CB::button3, CB::pressSensitivity, CB::snap },
{ CB::arp, CB::button4, CB::liftSensitivity, CB::back },
{ CB::sustain, CB::button5, CB::fixedVelocity, CB::playOrPause },
{ CB::octave, CB::button6, CB::glideLock, CB::record },
{ CB::love, CB::button7, CB::pianoMode, CB::learn },
{ CB::up },
{ CB::down }
};
for (int i = 0; i < numElementsInArray (map); ++i)
if (map[i].contains (f))
return i;
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) };
default:
break;
}
break;
}
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 };
default:
break;
}
break;
}
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) };
default:
break;
}
break;
}
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 };
default:
break;
}
break;
}
default:
break;
}
return {};
}
int rotation = 0;
Point<float> topLeft = { 0.0f, 0.0f };
private:
/** Used to call repaint() periodically */
void timerCallback() override { repaint(); }
/** Overridden from TouchSurface::Listener */
void touchChanged (TouchSurface&, const TouchSurface::Touch& t) override { handleTouchChange (t); }
/** Overridden from ControlButton::Listener */
void buttonPressed (ControlButton& b, Block::Timestamp t) override { handleButtonPressed (b.getType(), t); }
/** 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)
};
//==============================================================================
/**
Class that renders a Lightpad on the screen
*/
class LightpadComponent : public BlockComponent
{
public:
LightpadComponent (Block::Ptr blockToUse)
: BlockComponent (blockToUse)
{}
void paint (Graphics& g) override
{
auto r = getLocalBounds().toFloat();
// clip the drawing area to only draw in the block area
{
Path clipArea;
clipArea.addRoundedRectangle (r, r.getWidth() / 20.0f);
g.reduceClipRegion (clipArea);
}
// Fill a black square for the Lightpad
g.fillAll (Colours::black);
// size ration between physical and on-screen blocks
Point<float> ratio (r.getWidth() / block->getWidth(),
r.getHeight() / block->getHeight());
auto maxCircleSize = block->getWidth() / 3.0f;
// iterate over the list of current touches and draw them on the onscreen Block
for (auto touch : touches)
{
auto circleSize = touch.touch.z * maxCircleSize;
Point<float> touchPosition (touch.touch.x,
touch.touch.y);
auto blob = Rectangle<float> (circleSize, circleSize)
.withCentre (touchPosition) * ratio;
ColourGradient cg (colourArray[touch.touch.index], blob.getCentreX(), blob.getCentreY(),
Colours::transparentBlack, blob.getRight(), blob.getBottom(),
true);
g.setGradientFill (cg);
g.fillEllipse (blob);
}
}
void handleTouchChange (TouchSurface::Touch touch) override { touches.updateTouch (touch); }
private:
/** An Array of colours to use for touches */
Array<Colour> colourArray = { Colours::red,
Colours::blue,
Colours::green,
Colours::yellow,
Colours::white,
Colours::hotpink,
Colours::mediumpurple };
/** A list of current Touch events */
TouchList<TouchSurface::Touch> touches;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LightpadComponent)
};
//==============================================================================
/**
Class that renders a Control Block on the screen
*/
class ControlBlockComponent : public BlockComponent
{
public:
ControlBlockComponent (Block::Ptr blockToUse)
: BlockComponent (blockToUse),
numLeds (block->getLEDRow()->getNumLEDs())
{
addAndMakeVisible (roundedRectangleButton);
// Display the battery level on the LEDRow
auto numLedsToTurnOn = static_cast<int> (numLeds * block->getBatteryLevel());
// add LEDs
for (auto i = 0; i < numLeds; ++i)
{
auto ledComponent = new LEDComponent();
ledComponent->setOnState (i < numLedsToTurnOn);
addAndMakeVisible (leds.add (ledComponent));
}
previousNumLedsOn = numLedsToTurnOn;
// add buttons
for (auto i = 0; i < 8; ++i)
addAndMakeVisible (circleButtons[i]);
}
void resized() override
{
auto r = getLocalBounds().reduced (10);
auto rowHeight = r.getHeight() / 5;
auto ledWidth = (r.getWidth() - 70) / numLeds;
auto buttonWidth = (r.getWidth() - 40) / 5;
auto row = r;
auto ledRow = row.removeFromTop (rowHeight) .withSizeKeepingCentre (r.getWidth(), ledWidth);
auto buttonRow1 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
auto buttonRow2 = row.removeFromTop (rowHeight * 2).withSizeKeepingCentre (r.getWidth(), buttonWidth);
for (auto* led : leds)
{
led->setBounds (ledRow.removeFromLeft (ledWidth).reduced (2));
ledRow.removeFromLeft (5);
}
for (auto i = 0; i < 5; ++i)
{
circleButtons[i].setBounds (buttonRow1.removeFromLeft (buttonWidth).reduced (2));
buttonRow1.removeFromLeft (10);
}
for (auto i = 5; i < 8; ++i)
{
circleButtons[i].setBounds (buttonRow2.removeFromLeft (buttonWidth).reduced (2));
buttonRow2.removeFromLeft (10);
}
roundedRectangleButton.setBounds (buttonRow2);
}
void paint (Graphics& g) override
{
auto r = getLocalBounds().toFloat();
// Fill a black rectangle for the Control Block
g.setColour (Colours::black);
g.fillRoundedRectangle (r, r.getWidth() / 20.0f);
}
void handleButtonPressed (ControlButton::ButtonFunction function, uint32) override
{
displayButtonInteraction (controlButtonFunctionToIndex (function), true);
}
void handleButtonReleased (ControlButton::ButtonFunction function, uint32) override
{
displayButtonInteraction (controlButtonFunctionToIndex (function), false);
}
void handleBatteryLevelUpdate (float batteryLevel) override
{
// Update the number of LEDs that are on to represent the battery level
auto numLedsOn = static_cast<int> (numLeds * batteryLevel);
if (numLedsOn != previousNumLedsOn)
for (auto i = 0; i < numLeds; ++i)
leds.getUnchecked (i)->setOnState (i < numLedsOn);
previousNumLedsOn = numLedsOn;
repaint();
}
private:
//==============================================================================
/**
Base class that renders a Control Block button
*/
struct ControlBlockSubComponent : public Component,
public TooltipClient
{
ControlBlockSubComponent (Colour componentColourToUse)
: componentColour (componentColourToUse)
{}
/** Subclasses should override this to paint the button on the screen */
void paint (Graphics&) override = 0;
/** Sets the colour of the button */
void setColour (Colour c) { componentColour = c; }
/** Sets the on state of the button */
void setOnState (bool isOn)
{
onState = isOn;
repaint();
}
/** Returns the Control Block tooltip */
String getTooltip() override
{
for (Component* comp = this; comp != nullptr; comp = comp->getParentComponent())
if (auto* sttc = dynamic_cast<SettableTooltipClient*> (comp))
return sttc->getTooltip();
return {};
}
//==============================================================================
Colour componentColour;
bool onState = false;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockSubComponent)
};
/**
Class that renders a Control Block LED on the screen
*/
struct LEDComponent : public ControlBlockSubComponent
{
LEDComponent() : ControlBlockSubComponent (Colours::green) {}
void paint (Graphics& g) override
{
g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
g.fillEllipse (getLocalBounds().toFloat());
}
};
/**
Class that renders a Control Block single circular button on the screen
*/
struct CircleButtonComponent : public ControlBlockSubComponent
{
CircleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
void paint (Graphics& g) override
{
g.setColour (componentColour.withAlpha (onState ? 1.0f : 0.2f));
g.fillEllipse (getLocalBounds().toFloat());
}
};
/**
Class that renders a Control Block rounded rectangular button containing two buttons
on the screen
*/
struct RoundedRectangleButtonComponent : public ControlBlockSubComponent
{
RoundedRectangleButtonComponent() : ControlBlockSubComponent (Colours::blue) {}
void paint (Graphics& g) override
{
auto r = getLocalBounds().toFloat();
g.setColour (componentColour.withAlpha (0.2f));
g.fillRoundedRectangle (r.toFloat(), 20.0f);
g.setColour (componentColour.withAlpha (1.0f));
// is a button pressed?
if (doubleButtonOnState[0] || doubleButtonOnState[1])
{
auto semiButtonWidth = r.getWidth() / 2.0f;
auto semiButtonBounds = r.withWidth (semiButtonWidth)
.withX (doubleButtonOnState[1] ? semiButtonWidth : 0)
.reduced (5.0f, 2.0f);
g.fillEllipse (semiButtonBounds);
}
}
void setPressedState (bool isPressed, int button)
{
doubleButtonOnState[button] = isPressed;
repaint();
}
private:
bool doubleButtonOnState[2] = { false, false };
};
/** Displays a button press or release interaction for a button at a given index */
void displayButtonInteraction (int buttonIndex, bool isPressed)
{
if (! isPositiveAndBelow (buttonIndex, 10))
return;
if (buttonIndex >= 8)
roundedRectangleButton.setPressedState (isPressed, buttonIndex == 8);
else
circleButtons[buttonIndex].setOnState (isPressed);
}
//==============================================================================
int numLeds;
OwnedArray<LEDComponent> leds;
CircleButtonComponent circleButtons[8];
RoundedRectangleButtonComponent roundedRectangleButton;
int previousNumLedsOn;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlBlockComponent)
};
//==============================================================================
/**
The main component where the Block components will be displayed
*/
class BlocksMonitorDemo : public Component,
public TopologySource::Listener,
private Timer
{
public:
BlocksMonitorDemo()
{
noBlocksLabel.setText ("No BLOCKS connected...", dontSendNotification);
noBlocksLabel.setJustificationType (Justification::centred);
zoomOutButton.setButtonText ("+");
zoomOutButton.onClick = [this] { blockUnitInPixels = (int) (blockUnitInPixels * 1.05f); resized(); };
zoomOutButton.setAlwaysOnTop (true);
zoomInButton.setButtonText ("-");
zoomInButton.onClick = [this] { blockUnitInPixels = (int) (blockUnitInPixels * 0.95f); resized(); };
zoomInButton.setAlwaysOnTop (true);
// Register BlocksMonitorDemo as a listener to the PhysicalTopologySource object
topologySource.addListener (this);
startTimer (10000);
addAndMakeVisible (noBlocksLabel);
addAndMakeVisible (zoomOutButton);
addAndMakeVisible (zoomInButton);
#if JUCE_IOS
connectButton.setButtonText ("Connect");
connectButton.onClick = [] { BluetoothMidiDevicePairingDialogue::open(); };
connectButton.setAlwaysOnTop (true);
addAndMakeVisible (connectButton);
#endif
setSize (600, 600);
topologyChanged();
}
~BlocksMonitorDemo() override
{
topologySource.removeListener (this);
}
void paint (Graphics&) override {}
void resized() override
{
#if JUCE_IOS
connectButton.setBounds (getRight() - 100, 20, 80, 30);
#endif
noBlocksLabel.setVisible (false);
auto numBlockComponents = blockComponents.size();
// If there are no currently connected Blocks then display some text on the screen
if (numBlockComponents == 0)
{
noBlocksLabel.setVisible (true);
noBlocksLabel.setBounds (0, (getHeight() / 2) - 50, getWidth(), 100);
return;
}
zoomOutButton.setBounds (10, getHeight() - 40, 40, 30);
zoomInButton.setBounds (zoomOutButton.getRight(), zoomOutButton.getY(), 40, 30);
if (isInitialResized)
{
// Work out the area needed in terms of Block units
Rectangle<float> maxArea;
for (auto blockComponent : blockComponents)
{
auto topLeft = blockComponent->topLeft;
auto rotation = blockComponent->rotation;
auto blockSize = 0;
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);
}
auto totalWidth = std::abs (maxArea.getX()) + maxArea.getWidth();
auto totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight();
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 (static_cast<float> (degreesToRadians (blockComponent->rotation)),
static_cast<float> (blockComponent->getX()),
static_cast<float> (blockComponent->getY())));
}
}
/** Overridden from TopologySource::Listener, called when the topology changes */
void topologyChanged() override
{
// Clear the array of Block components
blockComponents.clear();
masterBlockComponent = nullptr;
// Get the current topology
auto topology = topologySource.getCurrentTopology();
// 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();
}
private:
/** Creates a BlockComponent object for a new Block and adds it to the content component */
BlockComponent* createBlockComponent (Block::Ptr newBlock)
{
auto type = newBlock->getType();
if (type == Block::lightPadBlock)
return new LightpadComponent (newBlock);
if (type == Block::loopBlock || type == Block::liveBlock
|| type == Block::touchBlock || type == Block::developerControlBlock)
return new ControlBlockComponent (newBlock);
// Should only be connecting a Lightpad or Control Block!
jassertfalse;
return nullptr;
}
/** Periodically updates the displayed BlockComponent tooltips */
void timerCallback() override
{
for (auto c : blockComponents)
c->updateStatsAndTooltip();
}
/** Calculates the position and rotation of each connected Block relative to the master Block */
void positionBlocks (BlockTopology topology)
{
Array<BlockComponent*> blocksConnectedToMaster;
auto maxDelta = std::numeric_limits<float>::max();
auto 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
auto 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
auto 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;
default:
break;
}
break;
}
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;
default:
break;
}
break;
}
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;
default:
break;
}
break;
}
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;
default:
break;
}
break;
}
default:
break;
}
return 0;
}
//==============================================================================
TooltipWindow tooltipWindow;
PhysicalTopologySource topologySource;
OwnedArray<BlockComponent> blockComponents;
BlockComponent* masterBlockComponent = nullptr;
Label noBlocksLabel;
TextButton zoomOutButton;
TextButton zoomInButton;
int blockUnitInPixels;
bool isInitialResized;
#if JUCE_IOS
TextButton connectButton;
#endif
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BlocksMonitorDemo)
};