KiCad PCB EDA Suite
Loading...
Searching...
No Matches
3d_spheres_gizmo.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright 2025, Damjan Prerad <[email protected]>
5 * Copyright (C) The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <kicad_gl/kiglu.h> // Must be included first
22#include <glm/geometric.hpp>
23#include <limits>
24
25#include "3d_spheres_gizmo.h"
26
27
28namespace
29{
30
31// View-matrix Z translation: camera sits this many arrow-lengths from the gizmo origin.
32constexpr float GIZMO_CAMERA_DISTANCE_FACTOR = 2.75f;
33
34// Nudge axis labels toward the camera to reduce z-fighting on the sphere surface.
35constexpr float GIZMO_LABEL_CAMERA_OFFSET = 0.02f;
36
37// Full width/height of the X/Y/Z letter strokes in gizmo units.
38constexpr float GIZMO_LABEL_SIZE = 0.30f;
39
40} // namespace
41
42
44{
45 if( m_quadric )
46 {
47 gluDeleteQuadric( m_quadric );
48 m_quadric = nullptr;
49 }
50}
51
52
53SPHERES_GIZMO::SPHERES_GIZMO( int aGizmoPosX, int aGizmoPosY )
54{
55 m_gizmoPosX = aGizmoPosX;
56 m_gizmoPosY = aGizmoPosY;
57
58 m_quadric = gluNewQuadric();
59 gluQuadricNormals( m_quadric, GLU_SMOOTH );
60}
61
62
63void SPHERES_GIZMO::setViewport( int ax, int ay, int aWidth, int aHeight )
64{
65 m_viewportX = ax;
66 m_viewportY = ay;
67 m_viewportW = aWidth;
68 m_viewportH = aHeight;
69}
70
71
72std::tuple<int, int, int, int> SPHERES_GIZMO::getViewport() const
73{
74 return std::make_tuple( m_viewportX, m_viewportY, m_viewportW, m_viewportH );
75}
76
77
78void SPHERES_GIZMO::setGizmoPosition( int ax, int ay )
79{
80 m_gizmoPosX = ax;
81 m_gizmoPosY = ay;
82}
83
84
86{
87 glEnable( GL_COLOR_MATERIAL );
88 glColorMaterial( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE );
89
90 const SFVEC4F ambient = SFVEC4F( 0.0f, 0.0f, 0.0f, 1.0f );
91 const SFVEC4F diffuse = SFVEC4F( 0.0f, 0.0f, 0.0f, 1.0f );
92 const SFVEC4F emissive = SFVEC4F( 0.0f, 0.0f, 0.0f, 1.0f );
93 const SFVEC4F specular = SFVEC4F( 0.1f, 0.1f, 0.1f, 1.0f );
94
95 glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, &specular.r );
96 glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, 96.0f );
97
98 glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, &ambient.r );
99 glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, &diffuse.r );
100 glMaterialfv( GL_FRONT_AND_BACK, GL_EMISSION, &emissive.r );
101}
102
103void SPHERES_GIZMO::handleMouseInput( int aMouseX, int aMouseY )
104{
105 int smallViewportW = m_viewportH / 8;
106 int smallViewportH = m_viewportH / 8;
107
108 bool inside = ( aMouseX >= m_gizmoPosX && aMouseX <= m_gizmoPosX + smallViewportW && aMouseY >= m_gizmoPosY
109 && aMouseY <= m_gizmoPosY + smallViewportH );
110
111 if( inside )
112 {
113 m_ndcX = 2.0f * static_cast<float>( aMouseX - m_gizmoPosX ) / smallViewportW - 1.0f;
114 m_ndcY = 2.0f * static_cast<float>( aMouseY - m_gizmoPosY ) / smallViewportH - 1.0f;
115 }
116 else
117 {
118 m_ndcX = -1.0f;
119 m_ndcY = -1.0f;
120 }
121}
122
123
124void SPHERES_GIZMO::render3dSpheresGizmo( glm::mat4 aCameraRotationMatrix )
125{
126 float fov = 60.0f;
127
128 glDisable( GL_CULL_FACE );
129
130 // Set up a square viewport (Y x Y)
131 glViewport( m_gizmoPosX, m_gizmoPosY, m_viewportH / 8, m_viewportH / 8 );
132 glClear( GL_DEPTH_BUFFER_BIT );
133
134 glMatrixMode( GL_PROJECTION );
135 glLoadIdentity();
136
137 const float camDist = m_arrowSize * GIZMO_CAMERA_DISTANCE_FACTOR;
138 const float clipRadius = m_arrowSize + m_sphereRadius;
139 gluPerspective( fov, 1.0f, camDist - clipRadius, camDist + clipRadius );
140
141 glMatrixMode( GL_MODELVIEW );
142 glLoadIdentity();
143
144 glm::mat4 TranslationMatrix = glm::translate( glm::mat4( 1.0f ), SFVEC3F( 0.0f, 0.0f, -( m_arrowSize * GIZMO_CAMERA_DISTANCE_FACTOR ) ) );
145 glm::mat4 ViewMatrix = TranslationMatrix * aCameraRotationMatrix;
146 glLoadMatrixf( glm::value_ptr( ViewMatrix ) );
147
149
150 auto drawBillboardCircle =
151 []( const glm::vec3& aCenter, float aRadius, const glm::vec3& aColor,
152 const glm::vec3& aCamRight, const glm::vec3& aCamUp, int aSegments = 64 )
153 {
154 float thickness = aRadius * 0.4f;
155 glColor3f( aColor.r, aColor.g, aColor.b );
156
157 glBegin( GL_TRIANGLE_STRIP );
158 for( int i = 0; i <= aSegments; ++i )
159 {
160 float angle = 2.0f * glm::pi<float>() * i / aSegments;
161 glm::vec3 dir = cos( angle ) * aCamRight + sin( angle ) * aCamUp;
162
163 glm::vec3 outer = aCenter + dir * ( aRadius + thickness * 0.5f );
164 glm::vec3 inner = aCenter + dir * ( aRadius - thickness * 0.5f );
165
166 glVertex3f( outer.x, outer.y, outer.z );
167 glVertex3f( inner.x, inner.y, inner.z );
168 }
169 glEnd();
170 };
171
172 glm::vec3 camRight( aCameraRotationMatrix[0][0], aCameraRotationMatrix[1][0], aCameraRotationMatrix[2][0] );
173 glm::vec3 camUp( aCameraRotationMatrix[0][1], aCameraRotationMatrix[1][1], aCameraRotationMatrix[2][1] );
174
175 glEnable( GL_BLEND );
176 glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
177
178 for( const auto& sphere : m_spheres )
179 {
180 glColor4f( sphere.m_color.r, sphere.m_color.g, sphere.m_color.b, 0.3f );
181 glPushMatrix();
182 glTranslatef( sphere.m_position.x, sphere.m_position.y, sphere.m_position.z );
183 if( m_quadric )
184 {
185 gluSphere( m_quadric, sphere.m_radius, 32, 32 );
186 }
187 glPopMatrix();
188
189 drawBillboardCircle( sphere.m_position, sphere.m_radius, sphere.m_color, camRight, camUp );
190 }
191
192 // Draw sphere labels
193
194 glDisable( GL_DEPTH_TEST );
195 glDisable( GL_LIGHTING );
196
197 std::array<std::string, 6> labels = { "X", "", "Y", "", "Z", "" };
198
199 // View direction (camera looks along negative Z in view space)
200 // So we offset a little toward the camera to avoid z-fighting
201 glm::vec3 camForward( m_cameraRotationMatrix[0][2], m_cameraRotationMatrix[1][2], m_cameraRotationMatrix[2][2] );
202 glm::vec3 offset = camForward * GIZMO_LABEL_CAMERA_OFFSET;
203
204 glColor4f( 0.0f, 0.0f, 0.0f, 1.0f );
205
206 auto drawX =
207 []( const glm::vec3& aPos, float aSize, const glm::vec3& aColor, const glm::vec3& aCamRight,
208 const glm::vec3& aCamUp )
209 {
210 glColor3f( aColor.r, aColor.g, aColor.b );
211 glLineWidth( 3.0f );
212
213 float h = aSize * 0.5f;
214
215 // Define two diagonal line directions in camera-facing plane
216 glm::vec3 dir1 = ( -aCamRight + aCamUp ) * h; // one diagonal
217 glm::vec3 dir2 = ( -aCamRight - aCamUp ) * h; // other diagonal
218
219 glBegin( GL_LINES );
220 glVertex3f( ( aPos - dir1 ).x, ( aPos - dir1 ).y, ( aPos - dir1 ).z );
221 glVertex3f( ( aPos + dir1 ).x, ( aPos + dir1 ).y, ( aPos + dir1 ).z );
222
223 glVertex3f( ( aPos - dir2 ).x, ( aPos - dir2 ).y, ( aPos - dir2 ).z );
224 glVertex3f( ( aPos + dir2 ).x, ( aPos + dir2 ).y, ( aPos + dir2 ).z );
225 glEnd();
226 };
227
228 auto drawY =
229 []( const glm::vec3& aPos, float aSize, const glm::vec3& aColor, const glm::vec3& aCamRight,
230 const glm::vec3& aCamUp )
231 {
232 glColor3f( aColor.r, aColor.g, aColor.b );
233 glLineWidth( 3.0f );
234
235 float h = aSize * 0.5f;
236
237 // Top-left and top-right in screen plane
238 glm::vec3 topLeft = aPos + aCamUp * h - aCamRight * h;
239 glm::vec3 topRight = aPos + aCamUp * h + aCamRight * h;
240 glm::vec3 bottom = aPos - aCamUp * h;
241
242 glBegin( GL_LINES );
243 glVertex3f( topLeft.x, topLeft.y, topLeft.z );
244 glVertex3f( aPos.x, aPos.y, aPos.z );
245
246 glVertex3f( topRight.x, topRight.y, topRight.z );
247 glVertex3f( aPos.x, aPos.y, aPos.z );
248
249 glVertex3f( aPos.x, aPos.y, aPos.z );
250 glVertex3f( bottom.x, bottom.y, bottom.z );
251 glEnd();
252 };
253
254 auto drawZ =
255 []( const glm::vec3& aPos, float aSize, const glm::vec3& aColor, const glm::vec3& aCamRight,
256 const glm::vec3& aCamUp )
257 {
258 glColor3f( aColor.r, aColor.g, aColor.b );
259 glLineWidth( 3.0f );
260
261 float h = aSize * 0.5f;
262
263 // Define corners in screen plane relative to camera
264 glm::vec3 topLeft = aPos + aCamUp * h - aCamRight * h;
265 glm::vec3 topRight = aPos + aCamUp * h + aCamRight * h;
266 glm::vec3 bottomLeft = aPos - aCamUp * h - aCamRight * h;
267 glm::vec3 bottomRight = aPos - aCamUp * h + aCamRight * h;
268
269 glBegin( GL_LINE_STRIP );
270 glVertex3f( topLeft.x, topLeft.y, topLeft.z );
271 glVertex3f( topRight.x, topRight.y, topRight.z );
272 glVertex3f( bottomLeft.x, bottomLeft.y, bottomLeft.z );
273 glVertex3f( bottomRight.x, bottomRight.y, bottomRight.z );
274 glEnd();
275 };
276
277 for( size_t i = 0; i < m_spheres.size(); ++i )
278 {
279 if( labels[i].empty() )
280 continue;
281
282 glm::vec3 textPos = m_spheres[i].m_position + offset;
283 const std::string& label = labels[i];
284
285 if( label == "X" )
286 {
287 drawX( textPos, GIZMO_LABEL_SIZE, glm::vec3( 0.0f ), camRight, camUp );
288 }
289 else if( label == "Y" )
290 {
291 drawY( textPos, GIZMO_LABEL_SIZE, glm::vec3( 0.0f ), camRight, camUp );
292 }
293 else if( label == "Z" )
294 {
295 drawZ( textPos, GIZMO_LABEL_SIZE, glm::vec3( 0.0f ), camRight, camUp );
296 }
297 }
298
299 glEnable( GL_LIGHTING );
300 glEnable( GL_DEPTH_TEST );
301
302 // Draw lines only to the positive axis spheres
303 glLineWidth( 2.0f );
304 glBegin( GL_LINES );
305
306 glColor3f( 0.9f, 0.0f, 0.0f ); // X+
307 glVertex3f( 0.0f, 0.0f, 0.0f );
308 glVertex3f( m_arrowSize, 0.0f, 0.0f );
309
310 glColor3f( 0.0f, 0.9f, 0.0f ); // Y+
311 glVertex3f( 0.0f, 0.0f, 0.0f );
312 glVertex3f( 0.0f, m_arrowSize, 0.0f );
313
314 glColor3f( 0.0f, 0.0f, 0.9f ); // Z+
315 glVertex3f( 0.0f, 0.0f, 0.0f );
316 glVertex3f( 0.0f, 0.0f, m_arrowSize );
317
318 glEnd();
319
320 glEnable( GL_CULL_FACE );
321}
322
327
334
335
336void SPHERES_GIZMO::updateSelection( glm::mat4 aCameraRotationMatrix )
337{
338 m_cameraRotationMatrix = aCameraRotationMatrix;
339
340 float fov = 60.0f;
341 glm::mat4 TranslationMatrix = glm::translate( glm::mat4( 1.0f ), SFVEC3F( 0.0f, 0.0f, -( m_arrowSize * GIZMO_CAMERA_DISTANCE_FACTOR ) ) );
342 glm::mat4 ViewMatrix = TranslationMatrix * aCameraRotationMatrix;
343
344 const float camDist = m_arrowSize * GIZMO_CAMERA_DISTANCE_FACTOR;
345 const float clipRadius = m_arrowSize + m_sphereRadius;
346 glm::mat4 proj = glm::perspective( glm::radians( fov ), 1.0f, camDist - clipRadius, camDist + clipRadius );
347 glm::mat4 invVP = glm::inverse( proj * ViewMatrix );
348
349 glm::vec4 rayStartNDC( m_ndcX, m_ndcY, -1.0f, 1.0f );
350 glm::vec4 rayEndNDC( m_ndcX, m_ndcY, 1.0f, 1.0f );
351
352 glm::vec4 rayStartWorld = invVP * rayStartNDC;
353 rayStartWorld /= rayStartWorld.w;
354
355 glm::vec4 rayEndWorld = invVP * rayEndNDC;
356 rayEndWorld /= rayEndWorld.w;
357
358 glm::vec3 rayOrigin = glm::vec3( rayStartWorld );
359 glm::vec3 rayDirection = glm::normalize( glm::vec3( rayEndWorld - rayStartWorld ) );
360
361 auto intersectDist = []( const glm::vec3& aRayOrigin, const glm::vec3& aRayDir, const glm::vec3& aSphereCenter,
362 float aRadius ) -> float
363 {
364 glm::vec3 L = aSphereCenter - aRayOrigin;
365 float tca = glm::dot( L, aRayDir );
366 float d2 = glm::dot( L, L ) - tca * tca;
367
368 if( d2 > aRadius * aRadius )
369 return -1.0f;
370
371 return tca;
372 };
373
374 int clickedIndex = -1;
375 float closestDist = std::numeric_limits<float>::max();
377
378 for( size_t i = 0; i < m_spheres.size(); ++i )
379 {
380 const auto& sphere = m_spheres[i];
381 float dist = intersectDist( rayOrigin, rayDirection, sphere.m_position, sphere.m_radius );
382
383 if( dist >= 0.0f && dist < closestDist )
384 {
385 closestDist = dist;
386 clickedIndex = static_cast<int>( i );
388 }
389 }
390
391 for( size_t i = 0; i < m_spheres.size(); ++i )
392 {
393 if( static_cast<int>( i ) == clickedIndex )
394 m_spheres[i].m_color = { 1.0f, 1.0f, 1.0f };
395 else
396 m_spheres[i].m_color = m_spheres[i].m_originalColor;
397 }
398}
GizmoSphereSelection
Enum to indicate which sphere (direction) is selected.
void updateSelection(glm::mat4 aCameraRotationMatrix)
void handleMouseInput(int aMouseX, int aMouseY)
void render3dSpheresGizmo(glm::mat4 aCameraRotationMatrix)
GLUquadric * m_quadric
const float m_arrowSize
std::array< GizmoSphere, 6 > m_spheres
List of all directional gizmo spheres.
GizmoSphereSelection getSelectedGizmoSphere() const
std::tuple< int, int, int, int > getViewport() const
void setViewport(int ax, int ay, int aWidth, int aHeight)
glm::mat4 m_cameraRotationMatrix
GizmoSphereSelection m_selectedGizmoSphere
SPHERES_GIZMO(int aGizmoPosX, int aGizmoPosY)
void resetSelectedGizmoSphere()
const float m_sphereRadius
void setGizmoPosition(int ax, int ay)
static bool empty(const wxTextEntryBase *aCtrl)
glm::vec3 SFVEC3F
Definition xv3d_types.h:40
glm::vec4 SFVEC4F
Definition xv3d_types.h:42