Umsci
Upmix Spatial Control Interface — OCA/OCP.1 spatial audio utility
Loading...
Searching...
No Matches
UmsciPaintNControlComponentBase.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
26
30
32{
33 m_controlsSize = size;
34 repaint();
35}
36
41
43{
44 switch (m_controlsSize)
45 {
46 case ControlsSize::M:
47 return 1.5f;
48 case ControlsSize::L:
49 return 2.0f;
50 case ControlsSize::S:
51 default:
52 return 1.0f;
53 }
54}
55
56void UmsciPaintNControlComponentBase::setBoundsRealRef(const juce::Rectangle<float>& boundsRealRef)
57{
58 m_boundsRealRef = boundsRealRef;
59}
60
61juce::Rectangle<float> UmsciPaintNControlComponentBase::computeBaseContentBounds() const
62{
63 auto bounds = getLocalBounds().toFloat();
64 if (m_boundsRealRef.isEmpty())
65 return bounds;
66
67 if (m_boundsRealRef.getAspectRatio() > bounds.getAspectRatio())
68 return bounds.withSizeKeepingCentre(bounds.getWidth(), bounds.getWidth() / m_boundsRealRef.getAspectRatio());
69 else
70 return bounds.withSizeKeepingCentre(bounds.getHeight() * m_boundsRealRef.getAspectRatio(), bounds.getHeight());
71}
72
73juce::Rectangle<float> UmsciPaintNControlComponentBase::getContentBounds() const
74{
75 auto base = computeBaseContentBounds();
76 if (m_zoomFactor == 1.0f && m_zoomPanOffset.isOrigin())
77 return base;
78
79 // Centre of the zoomed view in component-pixel space (normalised offset * base size).
80 auto cx = base.getCentreX() + m_zoomPanOffset.x * base.getWidth();
81 auto cy = base.getCentreY() + m_zoomPanOffset.y * base.getHeight();
82 auto hw = base.getWidth() * m_zoomFactor * 0.5f;
83 auto hh = base.getHeight() * m_zoomFactor * 0.5f;
84 return { cx - hw, cy - hh, hw * 2.0f, hh * 2.0f };
85}
86
87void UmsciPaintNControlComponentBase::applyZoomAtScreenPoint(float newFactor, juce::Point<float> screenFocus)
88{
89 newFactor = juce::jlimit(0.1f, 10.0f, newFactor);
90 auto rz = newFactor / m_zoomFactor;
91
92 auto base = computeBaseContentBounds();
93 if (!base.isEmpty())
94 {
95 // Express screenFocus as a normalised offset from the base content centre.
96 auto Pnorm = juce::Point<float>((screenFocus.x - base.getCentreX()) / base.getWidth(),
97 (screenFocus.y - base.getCentreY()) / base.getHeight());
98 // Scale the existing pan offset toward/away from the focus point.
99 m_zoomPanOffset = Pnorm + (m_zoomPanOffset - Pnorm) * rz;
100 }
101
102 m_zoomFactor = newFactor;
103}
104
106{
107 resetZoom();
108}
109
110void UmsciPaintNControlComponentBase::mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)
111{
112 const float zoomSpeed = 0.5f;
113 auto newFactor = m_zoomFactor * std::exp(wheel.deltaY * zoomSpeed);
114 applyZoomAtScreenPoint(newFactor, e.position);
115
117
119 onViewportZoomChanged(m_zoomFactor, m_zoomPanOffset);
120}
121
122void UmsciPaintNControlComponentBase::mouseMagnify(const juce::MouseEvent& e, float scaleFactor)
123{
124 applyZoomAtScreenPoint(m_zoomFactor * scaleFactor, e.position);
125
127
129 onViewportZoomChanged(m_zoomFactor, m_zoomPanOffset);
130}
131
132bool UmsciPaintNControlComponentBase::processPinchGesture(const juce::MouseEvent& e, bool isDown, bool isUp)
133{
134 const int idx = e.source.getIndex();
135
136 if (idx < 0 || idx > 1)
137 return false; // only track the first two simultaneous touches
138
139 if (isDown)
140 {
141 m_pinchDown[idx] = true;
142 m_pinchPos[idx] = e.position;
143
144 if (m_pinchDown[0] && m_pinchDown[1])
145 {
146 auto d = m_pinchPos[1] - m_pinchPos[0];
147 m_pinchStartDistance = std::hypot(d.x, d.y);
148 m_pinchStartZoom = m_zoomFactor;
149 if (m_pinchStartDistance > 1.0f)
150 m_pinchActive = true;
151 }
152 // Suppress non-primary touches from triggering element interactions.
153 return idx > 0;
154 }
155 else if (isUp)
156 {
157 const bool wasActive = m_pinchActive;
158 m_pinchDown[idx] = false;
159 if (!m_pinchDown[0] || !m_pinchDown[1])
160 m_pinchActive = false;
161 // Suppress the up event for non-primary touches, or whenever a pinch just ended.
162 return idx > 0 || wasActive;
163 }
164 else // drag
165 {
166 m_pinchPos[idx] = e.position;
167
168 if (m_pinchActive && m_pinchDown[0] && m_pinchDown[1])
169 {
170 auto d = m_pinchPos[1] - m_pinchPos[0];
171 const float currentDist = std::hypot(d.x, d.y);
172 if (currentDist > 1.0f && m_pinchStartDistance > 1.0f)
173 {
174 auto midpoint = (m_pinchPos[0] + m_pinchPos[1]) * 0.5f;
175 applyZoomAtScreenPoint(m_pinchStartZoom * currentDist / m_pinchStartDistance, midpoint);
178 onViewportZoomChanged(m_zoomFactor, m_zoomPanOffset);
179 }
180 return true;
181 }
182 return m_pinchActive;
183 }
184}
185
186void UmsciPaintNControlComponentBase::simulatePinchZoom(float scaleFactor, juce::Point<float> centre)
187{
188 applyZoomAtScreenPoint(m_zoomFactor * scaleFactor, centre);
191 onViewportZoomChanged(m_zoomFactor, m_zoomPanOffset);
192}
193
198
199void UmsciPaintNControlComponentBase::setZoom(float factor, juce::Point<float> normalizedPanOffset)
200{
201 auto clamped = juce::jlimit(0.1f, 10.0f, factor);
202 if (m_zoomFactor == clamped && m_zoomPanOffset == normalizedPanOffset)
203 return;
204
205 m_zoomFactor = clamped;
206 m_zoomPanOffset = normalizedPanOffset;
208}
209
211{
212 return m_zoomFactor;
213}
214
216{
217 if (m_zoomFactor == 1.0f && m_zoomPanOffset.isOrigin())
218 return;
219
220 m_zoomFactor = 1.0f;
221 m_zoomPanOffset = {};
223
225 onViewportZoomChanged(m_zoomFactor, m_zoomPanOffset);
226}
227
228std::array<float, 3> UmsciPaintNControlComponentBase::GetRealCoordinateForPoint(const juce::Point<float>& screenPoint)
229{
230 auto contentBounds = getContentBounds();
231 if (contentBounds.getWidth() == 0.0f || contentBounds.getHeight() == 0.0f)
232 return { 0.0f, 0.0f, 0.0f };
233
234 auto relativeX = (screenPoint.getX() - contentBounds.getX()) / contentBounds.getWidth();
235 auto relativeY = (screenPoint.getY() - contentBounds.getY()) / contentBounds.getHeight();
236
237 // m_boundsRealRef is screen-space-aligned: getX()/getWidth() = d&b y, getY()/getHeight() = d&b x
238 auto yReal = relativeX * m_boundsRealRef.getWidth() + m_boundsRealRef.getX();
239 auto xReal = relativeY * m_boundsRealRef.getHeight() + m_boundsRealRef.getY();
240
241 return { xReal, yReal, 0.0f };
242}
243
244juce::Point<float> UmsciPaintNControlComponentBase::GetPointForRealCoordinate(const std::array<float, 3>& realCoordinate)
245{
246 auto& xReal = realCoordinate.at(0);
247 auto& yReal = realCoordinate.at(1);
248 //auto& zReal = realCoordinate.at(2);
249
250 if (m_boundsRealRef.getWidth() == 0.0f || m_boundsRealRef.getHeight() == 0.0f)
251 return { 0.0f, 0.0f };
252
253 // m_boundsRealRef is screen-space-aligned: getX()/getWidth() = d&b y, getY()/getHeight() = d&b x
254 auto relativeX = (yReal - m_boundsRealRef.getX()) / m_boundsRealRef.getWidth();
255 auto relativeY = (xReal - m_boundsRealRef.getY()) / m_boundsRealRef.getHeight();
256
257 return getContentBounds().getRelativePoint(relativeX, relativeY);
258}
259
void mouseMagnify(const juce::MouseEvent &, float scaleFactor) override
Trackpad pinch-to-zoom (macOS).
ControlsSize
Visual size of source/speaker icons. Multiplier accessible via getControlsSizeMultiplier().
void setZoom(float factor, juce::Point< float > normalizedPanOffset={})
Silently applies zoom without firing onViewportZoomChanged. Used by UmsciControlComponent to synchron...
virtual void onZoomChanged()
Called after any zoom state change.
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...
void mouseWheelMove(const juce::MouseEvent &, const juce::MouseWheelDetails &) override
Mouse-wheel zooms about the cursor position.
std::function< void(float, juce::Point< float >)> onViewportZoomChanged
Fired after every user-initiated zoom/pan change (wheel, pinch, double-click). Parameters: (newFactor...
void simulatePinchZoom(float scaleFactor, juce::Point< float > centre)
Applies an incremental pinch-zoom step, as if the user performed a native pinch gesture centred at ce...
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.
void resetZoom()
Resets zoom to 1.0 / no pan and fires onViewportZoomChanged.
void setBoundsRealRef(const juce::Rectangle< float > &boundsRealRef)
Sets the real-world rectangle that the component's pixel bounds map to.
float getControlsSizeMultiplier() const
Returns a multiplier (e.g. 0.5 / 1.0 / 1.5) for S/M/L icon sizes.