21#include <CustomLookAndFeel.h>
38 auto font = juce::Font(juce::FontOptions(m_subCircleRadius * 1.1f, juce::Font::plain));
46 g.fillPath(m_upmixIndicator);
50 for (
const auto&
rcp : m_renderedFloorPositions)
52 auto labelBounds = juce::Rectangle<float>(m_subCircleRadius * 2.0f, m_subCircleRadius * 2.0f)
53 .withCentre(
rcp.screenPos);
54 g.drawFittedText(
rcp.label,
labelBounds.toNearestInt(), juce::Justification::centred, 1);
60 g.fillPath(m_upmixHeightIndicator);
64 for (
const auto&
rcp : m_renderedHeightPositions)
66 auto labelBounds = juce::Rectangle<float>(m_subCircleRadius * 2.0f, m_subCircleRadius * 2.0f)
67 .withCentre(
rcp.screenPos);
68 g.drawFittedText(
rcp.label,
labelBounds.toNearestInt(), juce::Justification::centred, 1);
72 if (!m_renderedFloorPositions.empty())
83 if (!m_renderedHeightPositions.empty())
89 + (m_speakersRealBoundingCube[5] - m_speakersRealBoundingCube[2]) * m_boundingFitFactor;
90 g.drawFittedText(juce::String(
"Height: ") + juce::String(
effectiveHeightZ, 2) + juce::String(
" m"),
91 heightLine.toNearestInt(), juce::Justification::bottomRight, 1);
94 g.drawFittedText(juce::String(
"Normal: 1.20 m"),
95 floorLine.toNearestInt(), juce::Justification::bottomRight, 1);
102 g.drawFittedText(juce::String(
"Normal: 1.20 m"),
103 floorLine.toNearestInt(), juce::Justification::bottomRight, 1);
110 g.fillPath(m_centerHandlePath);
111 g.fillPath(m_stretchHandlePath);
121 g.drawFittedText(
"Re-fit to\nbounding cube",
refitBounds.reduced(4), juce::Justification::centred, 2);
127 g.setFont(juce::Font(juce::FontOptions(16.0f, juce::Font::plain)));
131 ? juce::String(
"External position changes detected. Double-click the upmix indicator to apply its current positions.")
132 : juce::String(
"Double-click the upmix indicator to change sound object positions to match it.");
136 juce::Justification::centred,
143 PrerenderUpmixIndicatorInBounds();
150 PrerenderUpmixIndicatorInBounds();
157 PrerenderUpmixIndicatorInBounds();
162 m_sourcePositions[sourceId] =
position;
163 if (m_inhibitFlashCount > 0)
164 --m_inhibitFlashCount;
175 if (!m_renderedFloorPositions.empty() && m_stretchHandleTangent != juce::Point<float>{})
177 float dx =
float(
x) - m_stretchHandlePos.x;
178 float dy =
float(
y) - m_stretchHandlePos.y;
179 float tx = m_stretchHandleTangent.x;
180 float ty = m_stretchHandleTangent.y;
184 && std::abs(
localRadial) <= m_subCircleRadius * 0.4f)
190 float dx =
float(
x) - m_upmixCenter.x;
191 float dy =
float(
y) - m_upmixCenter.y;
192 float halfLen = m_subCircleRadius * 1.0f;
197 return getRefitButtonBounds().contains(
x,
y)
198 || m_upmixIndicator.contains(
float(
x),
float(
y))
199 || m_upmixHeightIndicator.contains(
float(
x),
float(
y));
207 if (!m_renderedFloorPositions.empty() && m_stretchHandleTangent != juce::Point<float>{})
209 float dx =
e.position.x - m_stretchHandlePos.x;
210 float dy =
e.position.y - m_stretchHandlePos.y;
211 float tx = m_stretchHandleTangent.x;
212 float ty = m_stretchHandleTangent.y;
216 && std::abs(
localRadial) <= m_subCircleRadius * 0.4f)
218 m_draggingStretchHandle =
true;
219 m_draggingHeightRing =
false;
220 m_dragStartStretch = m_upmixAngleStretch;
221 m_dragStartAngle = std::atan2(
e.position.x - m_upmixCenter.x,
222 -(
e.position.y - m_upmixCenter.y)) - m_upmixRot;
226 m_draggingStretchHandle =
false;
230 float dx =
e.position.x - m_upmixCenter.x;
231 float dy =
e.position.y - m_upmixCenter.y;
232 float halfLen = m_subCircleRadius * 1.0f;
235 m_draggingCenterHandle =
true;
236 m_draggingHeightRing =
false;
237 m_dragStartOffsetX = m_upmixOffsetX;
238 m_dragStartOffsetY = m_upmixOffsetY;
239 m_dragStartMousePos =
e.position;
243 m_draggingCenterHandle =
false;
245 if (getRefitButtonBounds().
contains(
e.getPosition()))
249 m_upmixHeightTrans = 0.6f;
250 m_upmixAngleStretch = 1.0f;
251 m_upmixOffsetX = 0.0f;
252 m_upmixOffsetY = 0.0f;
253 PrerenderUpmixIndicatorInBounds();
256 for (
auto const&
rcp : m_renderedFloorPositions)
258 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
262 for (
auto const&
rcp : m_renderedHeightPositions)
264 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
276 auto dx =
e.position.x - m_upmixCenter.x;
277 auto dy =
e.position.y - m_upmixCenter.y;
280 m_dragStartAngle = std::atan2(
dx, -
dy);
281 m_dragStartDist = std::sqrt(
dx *
dx +
dy *
dy);
282 m_dragStartRot = m_upmixRot;
283 m_dragStartTrans = m_upmixTrans;
284 m_dragStartHeightTrans = m_upmixHeightTrans;
287 m_draggingHeightRing = m_upmixHeightIndicator.contains(
e.position.x,
e.position.y);
290 <<
" trans:" << m_upmixTrans <<
" heightTrans:" << m_upmixHeightTrans
291 <<
" heightRing:" << (
int)m_draggingHeightRing);
298 if (m_draggingStretchHandle)
300 auto dx =
e.position.x - m_upmixCenter.x;
301 auto dy =
e.position.y - m_upmixCenter.y;
307 if (
deltaAngle > juce::MathConstants<float>::pi)
deltaAngle -= juce::MathConstants<float>::twoPi;
308 if (
deltaAngle < -juce::MathConstants<float>::pi)
deltaAngle += juce::MathConstants<float>::twoPi;
309 if (m_naturalFloorMaxAngleDeg > 0.0f)
310 m_upmixAngleStretch = juce::jlimit(0.05f,
311 180.0f / m_naturalFloorMaxAngleDeg,
312 m_dragStartStretch +
deltaAngle / juce::degreesToRadians(m_naturalFloorMaxAngleDeg));
314 PrerenderUpmixIndicatorInBounds();
318 for (
auto const&
rcp : m_renderedFloorPositions)
320 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
324 for (
auto const&
rcp : m_renderedHeightPositions)
326 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
337 if (m_draggingCenterHandle)
339 if (m_baseRadius > 0.0f)
341 m_upmixOffsetX = juce::jlimit(-2.0f, 2.0f,
342 m_dragStartOffsetX + (
e.position.x - m_dragStartMousePos.x) / m_baseRadius);
343 m_upmixOffsetY = juce::jlimit(-2.0f, 2.0f,
344 m_dragStartOffsetY + (
e.position.y - m_dragStartMousePos.y) / m_baseRadius);
347 PrerenderUpmixIndicatorInBounds();
351 for (
auto const&
rcp : m_renderedFloorPositions)
353 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
357 for (
auto const&
rcp : m_renderedHeightPositions)
359 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
372 if (getRefitButtonBounds().
contains(
e.getMouseDownPosition()))
375 auto dx =
e.position.x - m_upmixCenter.x;
376 auto dy =
e.position.y - m_upmixCenter.y;
379 m_upmixRot = m_dragStartRot + std::atan2(
dx, -
dy) - m_dragStartAngle;
382 if (m_dragStartDist > 0.0f)
385 if (m_draggingHeightRing)
386 m_upmixHeightTrans = juce::jlimit(0.1f, 10.0f, m_dragStartHeightTrans *
scaleFactor);
388 m_upmixTrans = juce::jlimit(0.1f, 10.0f, m_dragStartTrans *
scaleFactor);
392 <<
" trans:" << m_upmixTrans <<
" heightTrans:" << m_upmixHeightTrans);
394 PrerenderUpmixIndicatorInBounds();
398 for (
auto const&
rcp : m_renderedFloorPositions)
400 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
404 for (
auto const&
rcp : m_renderedHeightPositions)
406 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
426 if (m_upmixIndicator.contains(
e.position.x,
e.position.y)
427 || m_upmixHeightIndicator.contains(
e.position.x,
e.position.y))
430 for (
auto const&
rcp : m_renderedFloorPositions)
432 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
436 for (
auto const&
rcp : m_renderedHeightPositions)
438 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
454 m_flashState = !m_flashState;
458void UmsciUpmixIndicatorPaintNControlComponent::onZoomChanged()
460 PrerenderUpmixIndicatorInBounds();
464void UmsciUpmixIndicatorPaintNControlComponent::PrerenderUpmixIndicatorInBounds()
466 m_upmixIndicator.clear();
467 m_upmixHeightIndicator.clear();
468 m_renderedFloorPositions.clear();
469 m_renderedHeightPositions.clear();
471 auto speakersRealBoundingTopLeft = std::array<float, 3>{ m_speakersRealBoundingCube.at(0), m_speakersRealBoundingCube.at(1), m_speakersRealBoundingCube.at(2) };
472 auto speakersRealBoundingBottomRight = std::array<float, 3>{ m_speakersRealBoundingCube.at(3), m_speakersRealBoundingCube.at(4), m_speakersRealBoundingCube.at(5) };
513 m_upmixCenter.x += m_upmixOffsetX * m_baseRadius;
514 m_upmixCenter.y += m_upmixOffsetY * m_baseRadius;
515 auto cx = m_upmixCenter.x;
516 auto cy = m_upmixCenter.y;
521 auto cosRot = std::cos(m_upmixRot);
522 auto sinRot = std::sin(m_upmixRot);
540 return d <= 0.0f ?
d + 360.0f :
d;
546 auto dx = std::sin(
rad);
547 auto dy = -std::cos(
rad);
548 auto t =
r / std::max(std::abs(
dx), std::abs(
dy));
549 return {
t *
dx,
t *
dy };
553 auto toScreen = [&](juce::Point<float>
local) -> juce::Point<float> {
565 for (
int k = 0;
k < 4; ++
k)
573 for (
int j = 0;
j < 4; ++
j)
593 juce::PathStrokeType(
arcStrokeWidth).createStrokedPath(m_upmixIndicator,
618 auto t =
radius / std::max(std::abs(
dx), std::abs(
dy));
630 m_upmixIndicator.addEllipse(
px - m_subCircleRadius,
py - m_subCircleRadius,
631 m_subCircleRadius * 2.0f, m_subCircleRadius * 2.0f);
635 RenderedChannelPosition
rcp;
636 rcp.sourceId =
static_cast<std::int16_t
>(
638 rcp.screenPos = juce::Point<float>(
px,
py);
640 rcp.realPos[2] = 1.2f;
642 m_renderedFloorPositions.push_back(
rcp);
658 auto t =
radius / std::max(std::abs(
dx), std::abs(
dy));
665 float utx = (std::abs(
dx) >= std::abs(
dy)) ? 0.0f : 1.0f;
666 float uty = (std::abs(
dx) >= std::abs(
dy)) ? 1.0f : 0.0f;
683 m_stretchHandlePos = {
anchorX +
ux * m_subCircleRadius * 1.5f,
684 anchorY +
uy * m_subCircleRadius * 1.5f };
690 m_stretchHandleTangent = {
uy, -
ux };
737 m_upmixHeightIndicator.addEllipse(
px - m_subCircleRadius,
py - m_subCircleRadius,
738 m_subCircleRadius * 2.0f, m_subCircleRadius * 2.0f);
742 RenderedChannelPosition
rcp;
743 rcp.sourceId =
static_cast<std::int16_t
>(
745 rcp.screenPos = juce::Point<float>(
px,
py);
747 rcp.realPos[2] = m_speakersRealBoundingCube[5]
748 + (m_speakersRealBoundingCube[5] - m_speakersRealBoundingCube[2]) * m_boundingFitFactor;
750 m_renderedHeightPositions.push_back(
rcp);
766 juce::PathStrokeType(
lineWidth, juce::PathStrokeType::beveled,
767 juce::PathStrokeType::square).createStrokedPath(
result,
shaft);
789 m_centerHandlePath.clear();
791 float halfLen = m_subCircleRadius * 1.0f;
792 float headLen = m_subCircleRadius * 0.45f;
793 float headWidth = m_subCircleRadius * 0.35f;
794 float lineWidth = m_subCircleRadius * 0.18f;
800 m_stretchHandlePath.clear();
803 float halfLen = m_subCircleRadius * 1.2f;
804 float headLen = m_subCircleRadius * 0.55f;
805 float headWidth = m_subCircleRadius * 0.4f;
806 float lineWidth = m_subCircleRadius * 0.18f;
808 m_stretchHandlePos.x, m_stretchHandlePos.y,
809 m_stretchHandleTangent.x, m_stretchHandleTangent.y,
818 m_sourceStartId = juce::jmax(1,
startId);
819 PrerenderUpmixIndicatorInBounds();
825 return m_sourceStartId;
841 PrerenderUpmixIndicatorInBounds();
847 PrerenderUpmixIndicatorInBounds();
859 m_upmixTrans =
trans;
861 m_upmixAngleStretch = m_naturalFloorMaxAngleDeg > 0.0f
862 ? juce::jlimit(0.05f, 180.0f / m_naturalFloorMaxAngleDeg, angleStretch)
864 PrerenderUpmixIndicatorInBounds();
877 PrerenderUpmixIndicatorInBounds();
888 m_inhibitFlashCount +=
static_cast<int>(m_renderedFloorPositions.size() + m_renderedHeightPositions.size());
890 m_flashState =
false;
893 for (
auto const&
rcp : m_renderedFloorPositions)
895 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
899 for (
auto const&
rcp : m_renderedHeightPositions)
901 m_sourcePositions[
rcp.sourceId] =
rcp.realPos;
919 PrerenderUpmixIndicatorInBounds();
926juce::Rectangle<int> UmsciUpmixIndicatorPaintNControlComponent::getRefitButtonBounds()
const
935void UmsciUpmixIndicatorPaintNControlComponent::updateFlashState()
940 auto checkPos = [&](
const RenderedChannelPosition&
rcp)
942 auto it = m_sourcePositions.find(
rcp.sourceId);
943 if (
it == m_sourcePositions.end())
948 auto const&
sp =
it->second;
955 for (
auto const&
rcp : m_renderedFloorPositions)
957 for (
auto const&
rcp : m_renderedHeightPositions)
968 m_flashState =
false;
Abstract base class for all three overlaid visualisation layers in UmsciControlComponent.
ControlsSize
Visual size of source/speaker icons. Multiplier accessible via getControlsSizeMultiplier().
void mouseDoubleClick(const juce::MouseEvent &) override
Double-click resets zoom to 1.0 via resetZoom().
bool processPinchGesture(const juce::MouseEvent &e, bool isDown, bool isUp)
JUCE-level two-touch pinch-zoom fallback for platforms where neither mouseMagnify nor a native gestur...
std::array< float, 3 > GetRealCoordinateForPoint(const juce::Point< float > &screenPoint)
Inverse of GetPointForRealCoordinate — converts a screen pixel point back to a 3D real-world coordina...
juce::Point< float > GetPointForRealCoordinate(const std::array< float, 3 > &realCoordinate)
Converts a 3D real-world coordinate to a 2D screen pixel point.
virtual void setControlsSize(ControlsSize size)
Updates the icon size; derived classes may override to re-prerender.
float getControlsSizeMultiplier() const
Returns a multiplier (e.g. 0.5 / 1.0 / 1.5) for S/M/L icon sizes.
void mouseDrag(const juce::MouseEvent &) override
IndicatorShape
The geometric shape used to draw the upmix speaker ring.
IndicatorShape getShape() const
void notifyTransformChanged()
Fires live-mode position callbacks and onTransformChanged after a programmatic transform change (e....
bool hitTest(int x, int y) override
void mouseDoubleClick(const juce::MouseEvent &) override
void setUpmixTransform(float rot, float trans, float heightTrans, float angleStretch=1.0f)
Applies all four transform parameters and triggers a prerender + repaint.
UmsciUpmixIndicatorPaintNControlComponent()
void triggerFlashCheck()
Checks whether the ideal ring positions diverge from the stored DS100 positions and starts the flash ...
std::function< void()> onTransformChanged
Fired whenever any transform parameter changes via an interactive drag, so UmsciControlComponent can ...
float getUpmixAngleStretch() const
Front/rear angular compression factor.
~UmsciUpmixIndicatorPaintNControlComponent() override
float getUpmixRot() const
Ring rotation (normalised 0–1 = 0–360°).
void setLiveMode(bool liveMode)
When true, actual DS100 positions for the upmix channels are overlaid on the ideal indicator ring so ...
int getSourceStartId() const
void timerCallback() override
float getUpmixOffsetY() const
void paint(Graphics &) override
void mouseUp(const juce::MouseEvent &) override
void setSourcePosition(std::int16_t sourceId, const std::array< std::float_t, 3 > &position)
Updates a single source position (called on each OCP.1 notification).
void setControlsSize(ControlsSize size) override
Updates the icon size; derived classes may override to re-prerender.
void setSpeakersRealBoundingCube(const std::array< float, 6 > &speakersRealBoundingCube)
Provides the axis-aligned bounding cube of all loudspeaker positions.
void mouseDown(const juce::MouseEvent &) override
float getUpmixHeightTrans() const
Height ring radius as a fraction of floor radius.
bool setChannelConfiguration(const juce::AudioChannelSet &channelLayout) override
void setSourcePositions(const std::map< std::int16_t, std::array< std::float_t, 3 > > &sourcePositions)
Provides live DS100 source positions for all upmix channels. Only rendered when m_liveMode is true.
void setSourceStartId(int startId)
Sets the first DS100 input channel (1-based) assigned to the upmix renderer.
void setUpmixOffset(float x, float y)
Sets the ring centre offset in units of base radius.
float getUpmixTrans() const
Radial scale factor.
float getUpmixOffsetX() const
void setShape(IndicatorShape shape)
Sets the indicator ring geometry (circle or rectangle).
std::function< void(std::int16_t, std::array< std::float_t, 3 >)> onSourcePositionChanged
Fired when the user drags a source circle in live mode (pass-through from this component,...