v2.0.0
Loading...
Searching...
No Matches
electrodeobject.cpp
Go to the documentation of this file.
1//=============================================================================================================
34
35//=============================================================================================================
36// INCLUDES
37//=============================================================================================================
38
39#include "electrodeobject.h"
40
41#include <rhi/qrhi.h>
42
43#include <QtMath>
44
45#include <limits>
46
47//=============================================================================================================
48// USED NAMESPACES
49//=============================================================================================================
50
51using namespace DISP3DLIB;
52
53//=============================================================================================================
54// PIMPL
55//=============================================================================================================
56
58{
59 std::unique_ptr<QRhiBuffer> vertexBuffer; // shaft geometry (pos+normal)
60 std::unique_ptr<QRhiBuffer> indexBuffer; // shaft triangle indices
61 std::unique_ptr<QRhiBuffer> instanceBuffer; // per-contact instance data
62 uint32_t shaftIndexCount = 0;
63 uint32_t instanceCount = 0;
64 bool dirty = true;
65};
66
67//=============================================================================================================
68// DEFINE MEMBER METHODS
69//=============================================================================================================
70
72 : m_bbMin(std::numeric_limits<float>::max(),
73 std::numeric_limits<float>::max(),
74 std::numeric_limits<float>::max())
75 , m_bbMax(std::numeric_limits<float>::lowest(),
76 std::numeric_limits<float>::lowest(),
77 std::numeric_limits<float>::lowest())
78 , m_gpu(std::make_unique<GpuBuffers>())
79{
80}
81
82//=============================================================================================================
83
85
86//=============================================================================================================
87
88void ElectrodeObject::setShafts(const QVector<ElectrodeShaft>& shafts)
89{
90 m_shafts = shafts;
91 m_selectedContact.clear();
92 computeBoundingBox();
93 m_gpu->dirty = true;
94}
95
96//=============================================================================================================
97
98const QVector<ElectrodeShaft>& ElectrodeObject::shafts() const
99{
100 return m_shafts;
101}
102
103//=============================================================================================================
104
106{
107 int count = 0;
108 for (const auto& shaft : m_shafts)
109 count += shaft.contacts.size();
110 return count;
111}
112
113//=============================================================================================================
114
115void ElectrodeObject::setContactValues(const QMap<QString, float>& values,
116 const QColor& minColor,
117 const QColor& maxColor)
118{
119 if (values.isEmpty())
120 return;
121
122 // Find global min/max across provided values
123 float minVal = std::numeric_limits<float>::max();
124 float maxVal = std::numeric_limits<float>::lowest();
125 for (auto it = values.cbegin(); it != values.cend(); ++it) {
126 if (it.value() < minVal) minVal = it.value();
127 if (it.value() > maxVal) maxVal = it.value();
128 }
129
130 // Apply to contacts
131 for (auto& shaft : m_shafts) {
132 for (auto& contact : shaft.contacts) {
133 auto it = values.find(contact.name);
134 if (it != values.end()) {
135 contact.value = it.value();
136 contact.color = interpolateColor(it.value(), minVal, maxVal,
137 minColor, maxColor);
138 }
139 }
140 }
141 m_gpu->dirty = true;
142}
143
144//=============================================================================================================
145
146void ElectrodeObject::selectContact(const QString& name)
147{
148 // Clear previous selection
150
151 // Set new selection
152 m_selectedContact = name;
153 for (auto& shaft : m_shafts) {
154 for (auto& contact : shaft.contacts) {
155 if (contact.name == name) {
156 contact.selected = true;
157 m_gpu->dirty = true;
158 return;
159 }
160 }
161 }
162
163 // Not found — clear name
164 m_selectedContact.clear();
165}
166
167//=============================================================================================================
168
170{
171 m_selectedContact.clear();
172 for (auto& shaft : m_shafts) {
173 for (auto& contact : shaft.contacts)
174 contact.selected = false;
175 }
176 m_gpu->dirty = true;
177}
178
179//=============================================================================================================
180
182{
183 return m_selectedContact;
184}
185
186//=============================================================================================================
187
188void ElectrodeObject::generateShaftGeometry(QVector<float>& vertices,
189 QVector<unsigned int>& indices,
190 int cylinderSides) const
191{
192 vertices.clear();
193 indices.clear();
194
195 if (cylinderSides < 3)
196 cylinderSides = 3;
197
198 for (const auto& shaft : m_shafts) {
199 if (shaft.contacts.size() < 2)
200 continue;
201
202 const QVector3D& tipPos = shaft.contacts.first().position;
203 const QVector3D& tailPos = shaft.contacts.last().position;
204 const QVector3D axis = tailPos - tipPos;
205 const float length = axis.length();
206 if (length < 1e-6f)
207 continue;
208
209 const QVector3D axisNorm = axis.normalized();
210
211 // Build a perpendicular basis
212 QVector3D perp;
213 if (qAbs(QVector3D::dotProduct(axisNorm, QVector3D(0, 1, 0))) < 0.99f)
214 perp = QVector3D::crossProduct(axisNorm, QVector3D(0, 1, 0)).normalized();
215 else
216 perp = QVector3D::crossProduct(axisNorm, QVector3D(1, 0, 0)).normalized();
217
218 const QVector3D biperp = QVector3D::crossProduct(axisNorm, perp).normalized();
219
220 const float r = shaft.shaftRadius;
221 const unsigned int baseIdx = static_cast<unsigned int>(vertices.size() / 6);
222
223 // Generate circle vertices at tip and tail
224 for (int ring = 0; ring < 2; ++ring) {
225 const QVector3D center = (ring == 0) ? tipPos : tailPos;
226 for (int i = 0; i < cylinderSides; ++i) {
227 const float angle = 2.0f * float(M_PI) * float(i) / float(cylinderSides);
228 const float cs = cosf(angle);
229 const float sn = sinf(angle);
230
231 const QVector3D normal = (perp * cs + biperp * sn).normalized();
232 const QVector3D pos = center + normal * r;
233
234 // position
235 vertices.append(pos.x());
236 vertices.append(pos.y());
237 vertices.append(pos.z());
238 // normal
239 vertices.append(normal.x());
240 vertices.append(normal.y());
241 vertices.append(normal.z());
242 }
243 }
244
245 // Side triangles (connect ring 0 to ring 1)
246 for (int i = 0; i < cylinderSides; ++i) {
247 const unsigned int i0 = baseIdx + static_cast<unsigned int>(i);
248 const unsigned int i1 = baseIdx + static_cast<unsigned int>((i + 1) % cylinderSides);
249 const unsigned int i2 = i0 + static_cast<unsigned int>(cylinderSides);
250 const unsigned int i3 = i1 + static_cast<unsigned int>(cylinderSides);
251
252 // Two triangles per quad
253 indices.append(i0); indices.append(i2); indices.append(i1);
254 indices.append(i1); indices.append(i2); indices.append(i3);
255 }
256
257 // Tip endcap (ring 0, center = tipPos)
258 {
259 const QVector3D normal = -axisNorm;
260 const unsigned int centerIdx = static_cast<unsigned int>(vertices.size() / 6);
261 vertices.append(tipPos.x());
262 vertices.append(tipPos.y());
263 vertices.append(tipPos.z());
264 vertices.append(normal.x());
265 vertices.append(normal.y());
266 vertices.append(normal.z());
267
268 for (int i = 0; i < cylinderSides; ++i) {
269 const unsigned int i0 = baseIdx + static_cast<unsigned int>(i);
270 const unsigned int i1 = baseIdx + static_cast<unsigned int>((i + 1) % cylinderSides);
271 indices.append(centerIdx); indices.append(i1); indices.append(i0);
272 }
273 }
274
275 // Tail endcap (ring 1, center = tailPos)
276 {
277 const QVector3D normal = axisNorm;
278 const unsigned int centerIdx = static_cast<unsigned int>(vertices.size() / 6);
279 vertices.append(tailPos.x());
280 vertices.append(tailPos.y());
281 vertices.append(tailPos.z());
282 vertices.append(normal.x());
283 vertices.append(normal.y());
284 vertices.append(normal.z());
285
286 const unsigned int ring1Base = baseIdx + static_cast<unsigned int>(cylinderSides);
287 for (int i = 0; i < cylinderSides; ++i) {
288 const unsigned int i0 = ring1Base + static_cast<unsigned int>(i);
289 const unsigned int i1 = ring1Base + static_cast<unsigned int>((i + 1) % cylinderSides);
290 indices.append(centerIdx); indices.append(i0); indices.append(i1);
291 }
292 }
293 }
294}
295
296//=============================================================================================================
297
298void ElectrodeObject::generateContactInstances(QVector<float>& instanceData) const
299{
300 instanceData.clear();
301 const int floatsPerInstance = 9;
302 instanceData.reserve(totalContactCount() * floatsPerInstance);
303
304 for (const auto& shaft : m_shafts) {
305 for (const auto& contact : shaft.contacts) {
306 // position (3)
307 instanceData.append(contact.position.x());
308 instanceData.append(contact.position.y());
309 instanceData.append(contact.position.z());
310 // radius (1)
311 instanceData.append(contact.radius);
312 // color RGBA (4)
313 instanceData.append(static_cast<float>(contact.color.redF()));
314 instanceData.append(static_cast<float>(contact.color.greenF()));
315 instanceData.append(static_cast<float>(contact.color.blueF()));
316 instanceData.append(static_cast<float>(contact.color.alphaF()));
317 // selected flag (1)
318 instanceData.append(contact.selected ? 1.0f : 0.0f);
319 }
320 }
321}
322
323//=============================================================================================================
324
326{
327 return m_bbMin;
328}
329
330//=============================================================================================================
331
333{
334 return m_bbMax;
335}
336
337//=============================================================================================================
338
339void ElectrodeObject::computeBoundingBox()
340{
341 m_bbMin = QVector3D(std::numeric_limits<float>::max(),
342 std::numeric_limits<float>::max(),
343 std::numeric_limits<float>::max());
344 m_bbMax = QVector3D(std::numeric_limits<float>::lowest(),
345 std::numeric_limits<float>::lowest(),
346 std::numeric_limits<float>::lowest());
347
348 for (const auto& shaft : m_shafts) {
349 for (const auto& contact : shaft.contacts) {
350 const float pad = contact.radius;
351 const QVector3D& p = contact.position;
352
353 if (p.x() - pad < m_bbMin.x()) m_bbMin.setX(p.x() - pad);
354 if (p.y() - pad < m_bbMin.y()) m_bbMin.setY(p.y() - pad);
355 if (p.z() - pad < m_bbMin.z()) m_bbMin.setZ(p.z() - pad);
356
357 if (p.x() + pad > m_bbMax.x()) m_bbMax.setX(p.x() + pad);
358 if (p.y() + pad > m_bbMax.y()) m_bbMax.setY(p.y() + pad);
359 if (p.z() + pad > m_bbMax.z()) m_bbMax.setZ(p.z() + pad);
360 }
361 }
362}
363
364//=============================================================================================================
365
366QColor ElectrodeObject::interpolateColor(float value, float minVal, float maxVal,
367 const QColor& minColor, const QColor& maxColor)
368{
369 if (maxVal <= minVal)
370 return minColor;
371
372 float t = (value - minVal) / (maxVal - minVal);
373 t = qBound(0.0f, t, 1.0f);
374
375 const float r = static_cast<float>(minColor.redF()) * (1.0f - t) + static_cast<float>(maxColor.redF()) * t;
376 const float g = static_cast<float>(minColor.greenF()) * (1.0f - t) + static_cast<float>(maxColor.greenF()) * t;
377 const float b = static_cast<float>(minColor.blueF()) * (1.0f - t) + static_cast<float>(maxColor.blueF()) * t;
378 const float a = static_cast<float>(minColor.alphaF()) * (1.0f - t) + static_cast<float>(maxColor.alphaF()) * t;
379
380 QColor result;
381 result.setRedF(static_cast<qreal>(r));
382 result.setGreenF(static_cast<qreal>(g));
383 result.setBlueF(static_cast<qreal>(b));
384 result.setAlphaF(static_cast<qreal>(a));
385 return result;
386}
387
388//=============================================================================================================
389
390void ElectrodeObject::updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
391{
392 const bool needsCreate = !m_gpu->vertexBuffer || !m_gpu->indexBuffer || !m_gpu->instanceBuffer;
393
394#ifdef __EMSCRIPTEN__
395 if (!needsCreate && !m_gpu->dirty) {
396 // WASM: always re-upload to avoid VAO cache staleness
397 if (m_gpu->shaftIndexCount > 0) {
398 QVector<float> verts;
399 QVector<unsigned int> idx;
400 generateShaftGeometry(verts, idx);
401 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), verts.constData());
402 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), idx.constData());
403 }
404 if (m_gpu->instanceCount > 0) {
405 QVector<float> inst;
407 u->uploadStaticBuffer(m_gpu->instanceBuffer.get(), inst.constData());
408 }
409 return;
410 }
411#else
412 if (!m_gpu->dirty && !needsCreate) return;
413#endif
414
415 // Generate CPU-side data
416 QVector<float> shaftVerts;
417 QVector<unsigned int> shaftIdx;
418 generateShaftGeometry(shaftVerts, shaftIdx);
419
420 QVector<float> instData;
421 generateContactInstances(instData);
422
423 m_gpu->shaftIndexCount = static_cast<uint32_t>(shaftIdx.size());
424 m_gpu->instanceCount = static_cast<uint32_t>(instData.size() / 9);
425
426 // Shaft vertex buffer
427 const quint32 vbufSize = static_cast<quint32>(shaftVerts.size() * sizeof(float));
428 if (vbufSize > 0) {
429 if (!m_gpu->vertexBuffer) {
430 m_gpu->vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, vbufSize));
431 m_gpu->vertexBuffer->create();
432 }
433 u->uploadStaticBuffer(m_gpu->vertexBuffer.get(), shaftVerts.constData());
434 }
435
436 // Shaft index buffer
437 const quint32 ibufSize = static_cast<quint32>(shaftIdx.size() * sizeof(unsigned int));
438 if (ibufSize > 0) {
439 if (!m_gpu->indexBuffer) {
440 m_gpu->indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, ibufSize));
441 m_gpu->indexBuffer->create();
442 }
443 u->uploadStaticBuffer(m_gpu->indexBuffer.get(), shaftIdx.constData());
444 }
445
446 // Contact instance buffer
447 const quint32 instBufSize = static_cast<quint32>(instData.size() * sizeof(float));
448 if (instBufSize > 0) {
449 if (!m_gpu->instanceBuffer) {
450 m_gpu->instanceBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, instBufSize));
451 m_gpu->instanceBuffer->create();
452 }
453 u->uploadStaticBuffer(m_gpu->instanceBuffer.get(), instData.constData());
454 }
455
456 m_gpu->dirty = false;
457}
458
459//=============================================================================================================
460
462{
463 return m_gpu->vertexBuffer.get();
464}
465
466//=============================================================================================================
467
469{
470 return m_gpu->indexBuffer.get();
471}
472
473//=============================================================================================================
474
476{
477 return m_gpu->instanceBuffer.get();
478}
479
480//=============================================================================================================
481
483{
484 return m_gpu->shaftIndexCount;
485}
486
487//=============================================================================================================
488
490{
491 return m_gpu->instanceCount;
492}
ElectrodeObject class declaration.
#define M_PI
3-D brain visualisation using the Qt RHI rendering backend.
std::unique_ptr< QRhiBuffer > indexBuffer
std::unique_ptr< QRhiBuffer > vertexBuffer
std::unique_ptr< QRhiBuffer > instanceBuffer
void generateShaftGeometry(QVector< float > &vertices, QVector< unsigned int > &indices, int cylinderSides=16) const
QRhiBuffer * instanceBuffer() const
void setShafts(const QVector< ElectrodeShaft > &shafts)
void generateContactInstances(QVector< float > &instanceData) const
QRhiBuffer * indexBuffer() const
void updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
const QVector< ElectrodeShaft > & shafts() const
QRhiBuffer * vertexBuffer() const
void setContactValues(const QMap< QString, float > &values, const QColor &minColor=Qt::blue, const QColor &maxColor=Qt::red)
void selectContact(const QString &name)
uint32_t contactInstanceCount() const