Umsci
Upmix Spatial Control Interface — OCA/OCP.1 spatial audio utility
Loading...
Searching...
No Matches
UmsciSoundobjectsPaintComponent.cpp
Go to the documentation of this file.
1/* Copyright (c) 2026, Christian Ahrens
2 *
3 * This file is part of Umsci <https://github.com/ChristianAhrens/Umsci>
4 *
5 * This tool is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU Lesser General Public License version 3.0 as published
7 * by the Free Software Foundation.
8 *
9 * This tool is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 * details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this tool; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
20
21#include <CustomLookAndFeel.h>
22
23
28
32
34{
35 auto knobSize = 14.0f * getControlsSizeMultiplier();
36 auto knobThickness = 4.0f * getControlsSizeMultiplier();
37
38 g.setColour(getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId));
39 g.setOpacity(1.0f);
40
41 auto font = juce::Font(juce::FontOptions(knobSize, juce::Font::plain));
42 g.setFont(font);
43
44 for (auto const sourceScreenPositionKV : m_sourceScreenPositions)
45 {
46 auto& sourceId = sourceScreenPositionKV.first;
47 if (!m_sourceIdFilter.empty() && m_sourceIdFilter.count(sourceId) == 0)
48 continue;
49 auto sourceScreenPos = sourceScreenPositionKV.second.toFloat();
50
51 // Paint source thumb
52 g.drawEllipse(juce::Rectangle<float>(sourceScreenPos.getX() - (knobSize / 2.0f), sourceScreenPos.getY() - (knobSize / 2.0f), knobSize, knobSize), knobThickness);
53
54 // Paint source number
55 juce::String textLabel = juce::String(sourceId);
56 auto fontDependantWidth = float(juce::GlyphArrangement::getStringWidthInt(font, textLabel));
57 g.drawText(textLabel, juce::Rectangle<float>(sourceScreenPos.getX() - (0.5f * fontDependantWidth), sourceScreenPos.getY() + 3, fontDependantWidth, knobSize * 2.0f), Justification::centred, true);
58 }
59
60 // Paint crosshair for the currently dragged source
61 if (m_draggedSourceId != -1 && m_sourceScreenPositions.count(m_draggedSourceId))
62 {
63 auto dragPos = m_sourceScreenPositions.at(m_draggedSourceId).toFloat();
64 g.drawLine(0.0f, dragPos.getY(), float(getWidth()), dragPos.getY(), 1.0f);
65 g.drawLine(dragPos.getX(), 0.0f, dragPos.getX(), float(getHeight()), 1.0f);
66 }
67}
68
70{
71 PrerenderSourcesInBounds();
72}
73
74void UmsciSoundobjectsPaintComponent::setSourcePositions(const std::map<std::int16_t, std::array<std::float_t, 3>>& sourcePositions)
75{
76 if (sourcePositions.empty())
77 {
78 m_sourcePositions.clear();
79 m_sourceScreenPositions.clear();
80 }
81 else
82 {
83 m_sourcePositions = sourcePositions;
84
85 PrerenderSourcesInBounds();
86 }
87
88 repaint();
89}
90
92{
93 auto const hitRadius = 14.0f * getControlsSizeMultiplier(); // matches mouseDown
94 auto point = juce::Point<int>(x, y);
95 for (auto const& kv : m_sourceScreenPositions)
96 {
97 if (!m_sourceIdFilter.empty() && m_sourceIdFilter.count(kv.first) == 0)
98 continue;
99 if (point.getDistanceFrom(kv.second) <= hitRadius)
100 return true;
101 }
102 return false;
103}
104
105void UmsciSoundobjectsPaintComponent::setSourceIdFilter(const std::set<std::int16_t>& allowedIds)
106{
107 m_sourceIdFilter = allowedIds;
108 repaint();
109}
110
111void UmsciSoundobjectsPaintComponent::setSourcePosition(std::int16_t sourceId, const std::array<std::float_t, 3>& position)
112{
113 m_sourcePositions[sourceId] = position;
114 m_sourceScreenPositions[sourceId] = GetPointForRealCoordinate({ position.at(0), position.at(1), position.at(2) }).toInt();
115 repaint();
116}
117
118void UmsciSoundobjectsPaintComponent::mouseDown(const juce::MouseEvent& e)
119{
120 if (processPinchGesture(e, true, false)) return;
121
122 auto const hitRadius = 14.0f * getControlsSizeMultiplier(); // matches knobSize in paint()
123 auto clickPos = e.getPosition();
124
125 m_draggedSourceId = -1;
126 for (auto const& kv : m_sourceScreenPositions)
127 {
128 if (!m_sourceIdFilter.empty() && m_sourceIdFilter.count(kv.first) == 0)
129 continue;
130 if (clickPos.getDistanceFrom(kv.second) <= hitRadius)
131 {
132 m_draggedSourceId = kv.first;
133 break;
134 }
135 }
136}
137
138void UmsciSoundobjectsPaintComponent::mouseDrag(const juce::MouseEvent& e)
139{
140 if (processPinchGesture(e, false, false)) return;
141
142 if (m_draggedSourceId == -1)
143 return;
144
145 auto dragPos = e.getPosition().toFloat();
146 dragPos.setX(juce::jlimit(0.0f, float(getWidth()), dragPos.getX()));
147 dragPos.setY(juce::jlimit(0.0f, float(getHeight()), dragPos.getY()));
148
149 m_sourceScreenPositions[m_draggedSourceId] = dragPos.toInt();
150
151 auto realCoord = GetRealCoordinateForPoint(dragPos);
152 m_sourcePositions[m_draggedSourceId] = realCoord;
153
155 onSourcePositionChanged(m_draggedSourceId, realCoord);
156
157 repaint();
158}
159
160void UmsciSoundobjectsPaintComponent::mouseUp(const juce::MouseEvent& e)
161{
162 if (processPinchGesture(e, false, true)) return;
163
164 if (m_draggedSourceId != -1)
165 {
166 m_draggedSourceId = -1;
167 repaint();
168 }
169}
170
171void UmsciSoundobjectsPaintComponent::onZoomChanged()
172{
173 PrerenderSourcesInBounds();
174 repaint();
175}
176
177void UmsciSoundobjectsPaintComponent::PrerenderSourcesInBounds()
178{
179 // Speaker positions
180 for (auto const sourcePositionKV : m_sourcePositions)
181 {
182 auto& sourceId = sourcePositionKV.first;
183 auto& spourcePos = sourcePositionKV.second;
184 auto& x = spourcePos.at(0);
185 auto& y = spourcePos.at(1);
186 auto& z = spourcePos.at(2);
187 m_sourceScreenPositions[sourceId] = GetPointForRealCoordinate({ x, y, z }).toInt();
188 }
189}
190
Abstract base class for all three overlaid visualisation layers in UmsciControlComponent.
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.
float getControlsSizeMultiplier() const
Returns a multiplier (e.g. 0.5 / 1.0 / 1.5) for S/M/L icon sizes.
void setSourcePosition(std::int16_t sourceId, const std::array< std::float_t, 3 > &position)
Updates a single source position.
void setSourcePositions(const std::map< std::int16_t, std::array< std::float_t, 3 > > &sourcePositions)
Replaces all source positions at once (e.g. on reconnect or initial query).
void setSourceIdFilter(const std::set< std::int16_t > &allowedIds)
Restricts rendering and interaction to the given set of source IDs. An empty set means no sources are...
void mouseDrag(const juce::MouseEvent &e) override
Converts the drag position to world coordinates and fires onSourcePositionChanged.
std::function< void(std::int16_t, std::array< std::float_t, 3 >)> onSourcePositionChanged
Fired during a drag with the new world position. UmsciControlComponent receives this and calls Device...
void mouseDown(const juce::MouseEvent &e) override
Identifies which source is under the cursor and begins a drag.
void mouseUp(const juce::MouseEvent &e) override
Clears the active drag state.
bool hitTest(int x, int y) override
Returns true only when the point falls within a rendered source circle.