/* ============================================================================== This file is part of the JUCE examples. Copyright (c) 2020 - Raw Material Software Limited 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(*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 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 getOffsetForPort (Block::ConnectionPort port) { using e = Block::ConnectionPort::DeviceEdge; switch (rotation) { case 0: { switch (port.edge) { case e::north: return { static_cast (port.index), 0.0f }; case e::east: return { static_cast (block->getWidth()), static_cast (port.index) }; case e::south: return { static_cast (port.index), static_cast (block->getHeight()) }; case e::west: return { 0.0f, static_cast (port.index) }; default: break; } break; } case 90: { switch (port.edge) { case e::north: return { 0.0f, static_cast (port.index) }; case e::east: return { static_cast (-1.0f - (float) port.index), static_cast (block->getWidth()) }; case e::south: return { static_cast (0.0f - (float) block->getHeight()), static_cast (port.index) }; case e::west: return { static_cast (-1.0f - (float) port.index), 0.0f }; default: break; } break; } case 180: { switch (port.edge) { case e::north: return { static_cast (-1.0f - (float) port.index), 0.0f }; case e::east: return { static_cast (0.0f - (float) block->getWidth()), static_cast (-1.0f - (float) port.index) }; case e::south: return { static_cast (-1.0f - (float) port.index), static_cast (0.0f - (float) block->getHeight()) }; case e::west: return { 0.0f, static_cast (-1.0f - (float) port.index) }; default: break; } break; } case 270: { switch (port.edge) { case e::north: return { 0.0f, static_cast (-1.0f - (float) port.index) }; case e::east: return { static_cast (port.index), static_cast (0 - (float) block->getWidth()) }; case e::south: return { static_cast (block->getHeight()), static_cast (-1.0f - (float) port.index) }; case e::west: return { static_cast (port.index), 0.0f }; default: break; } break; } default: break; } return {}; } int rotation = 0; Point 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 ratio (r.getWidth() / (float) block->getWidth(), r.getHeight() / (float) block->getHeight()); auto maxCircleSize = (float) 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 touchPosition (touch.touch.x, touch.touch.y); auto blob = Rectangle (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 colourArray = { Colours::red, Colours::blue, Colours::green, Colours::yellow, Colours::white, Colours::hotpink, Colours::mediumpurple }; /** A list of current Touch events */ TouchList 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 ((float) 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 ((float) 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 (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 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) ((float) blockUnitInPixels * 1.05f); resized(); }; zoomOutButton.setAlwaysOnTop (true); zoomInButton.setButtonText ("-"); zoomInButton.onClick = [this] { blockUnitInPixels = (int) ((float) 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 (masterBlockComponent == nullptr || 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 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 - (float) blockSize < maxArea.getX()) maxArea.setX (topLeft.x - (float) blockSize); blockSize = 0; if (rotation == 0) blockSize = blockComponent->block->getWidth(); else if (rotation == 270) blockSize = blockComponent->block->getHeight(); if (topLeft.x + (float) blockSize > maxArea.getRight()) maxArea.setWidth (topLeft.x + (float) blockSize); blockSize = 0; if (rotation == 180) blockSize = blockComponent->block->getHeight(); else if (rotation == 270) blockSize = blockComponent->block->getWidth(); if (topLeft.y - (float) blockSize < maxArea.getY()) maxArea.setY (topLeft.y - (float) blockSize); blockSize = 0; if (rotation == 0) blockSize = blockComponent->block->getHeight(); else if (rotation == 90) blockSize = blockComponent->block->getWidth(); if (topLeft.y + (float) blockSize > maxArea.getBottom()) maxArea.setHeight (topLeft.y + (float) blockSize); } auto totalWidth = std::abs (maxArea.getX()) + maxArea.getWidth(); auto totalHeight = std::abs (maxArea.getY()) + maxArea.getHeight(); blockUnitInPixels = static_cast (jmin (((float) getHeight() / totalHeight) - 50, ((float) 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 (blockComponent->topLeft.x * (float) blockUnitInPixels), masterBlockComponent->getY() + static_cast (blockComponent->topLeft.y * (float) blockUnitInPixels), blockComponent->block->getWidth() * blockUnitInPixels, blockComponent->block->getHeight() * blockUnitInPixels); if (blockComponent->rotation != 0) blockComponent->setTransform (AffineTransform::rotation (static_cast (degreesToRadians (blockComponent->rotation)), static_cast (blockComponent->getX()), static_cast (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) { if (masterBlockComponent == nullptr) return; Array blocksConnectedToMaster; auto maxDelta = std::numeric_limits::max(); auto maxLoops = 50; // Store all the connections to the master Block Array 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 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::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 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 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) };