Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquickqmlgenerator.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6#include "utils_p.h"
7
8#include <private/qsgcurveprocessor_p.h>
9#include <private/qquickshape_p.h>
10#include <private/qquadpath_p.h>
11#include <private/qquickitem_p.h>
12#include <private/qquickimagebase_p_p.h>
13#include <private/qquicktext_p.h>
14#include <private/qquicktranslate_p.h>
15#include <private/qquickimage_p.h>
16
17#include <QtCore/qloggingcategory.h>
18#include <QtCore/qdir.h>
19
21
22using namespace Qt::StringLiterals;
23
24static QString sanitizeString(const QString &input)
25{
26 QString s = input;
27 s.replace(QLatin1Char('"'), QLatin1String("\\\""));
28 return s;
29}
30
31
32QQuickAnimatedProperty::PropertyAnimation QQuickAnimatedProperty::PropertyAnimation::simplified() const
33{
34 QQuickAnimatedProperty::PropertyAnimation res = *this;
35 int consecutiveEquals = 0;
36 int prevTimePoint = -1;
37 QVariant prevValue;
38 for (const auto &[timePoint, value] : frames.asKeyValueRange()) {
39 if (value != prevValue) {
40 consecutiveEquals = 1;
41 prevValue = value;
42 } else if (consecutiveEquals < 2) {
43 consecutiveEquals++;
44 } else {
45 // Third consecutive equal value found, remove the redundant middle one
46 res.frames.remove(prevTimePoint);
47 res.easingPerFrame.remove(prevTimePoint);
48 }
49 prevTimePoint = timePoint;
50 }
51
52 return res;
53}
54
55QQuickQmlGenerator::QQuickQmlGenerator(const QString fileName, QQuickVectorImageGenerator::GeneratorFlags flags, const QString &outFileName)
56 : QQuickGenerator(fileName, flags)
57 , outputFileName(outFileName)
58{
59 m_result.open(QIODevice::ReadWrite);
60 m_oldIndentLevels.push(0);
61}
62
63QQuickQmlGenerator::~QQuickQmlGenerator()
64{
65}
66
67bool QQuickQmlGenerator::save()
68{
69 if (Q_UNLIKELY(errorState()))
70 return false;
71
72 bool res = true;
73 if (!outputFileName.isEmpty()) {
74 QFileInfo fileInfo(outputFileName);
75 QDir dir(fileInfo.absolutePath());
76 if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
77 qCWarning(lcQuickVectorImage) << "Failed to create path" << dir.absolutePath();
78 res = false;
79 } else {
80 QFile outFile(outputFileName);
81 if (outFile.open(QIODevice::WriteOnly)) {
82 outFile.write(m_result.data());
83 outFile.close();
84 } else {
85 qCWarning(lcQuickVectorImage) << "Failed to write to file" << outFile.fileName();
86 res = false;
87 }
88 }
89 }
90
91 if (lcQuickVectorImage().isDebugEnabled())
92 qCDebug(lcQuickVectorImage).noquote() << m_result.data().left(300);
93
94 return res;
95}
96
97void QQuickQmlGenerator::setShapeTypeName(const QString &name)
98{
99 m_shapeTypeName = name.toLatin1();
100}
101
102QString QQuickQmlGenerator::shapeTypeName() const
103{
104 return QString::fromLatin1(m_shapeTypeName);
105}
106
107void QQuickQmlGenerator::setCommentString(const QString commentString)
108{
109 m_commentString = commentString;
110}
111
112QString QQuickQmlGenerator::commentString() const
113{
114 return m_commentString;
115}
116
117QString QQuickQmlGenerator::generateNodeBase(const NodeInfo &info, const QString &idSuffix)
118{
119 static qint64 maxNodes = qEnvironmentVariableIntegerValue("QT_QUICKVECTORIMAGE_MAX_NODES").value_or(10000);
120 if (Q_UNLIKELY(!checkSanityLimit(++m_nodeCounter, maxNodes, "nodes"_L1)))
121 return {};
122
123 if (!info.nodeId.isEmpty())
124 stream() << "objectName: \"" << info.nodeId << "\"";
125
126 if (!info.id.isEmpty())
127 stream() << "id: " << info.id << idSuffix;
128
129 if (!info.bounds.isNull() || !info.boundsReferenceId.isEmpty()) {
130 stream() << "property var originalBounds: ";
131 if (!info.bounds.isNull()) {
132 stream(SameLine) << "Qt.rect(" << info.bounds.x() << ", " << info.bounds.y() << ", "
133 << info.bounds.width() << ", " << info.bounds.height() << ")";
134 } else {
135 stream(SameLine) << info.boundsReferenceId << ".originalBounds";
136 }
137 stream() << "width: originalBounds.width";
138 stream() << "height: originalBounds.height";
139 }
140
141 stream() << "transformOrigin: Item.TopLeft";
142
143 if (info.filterId.isEmpty() && info.maskId.isEmpty()) {
144 if (!info.isDefaultOpacity)
145 stream() << "opacity: " << info.opacity.defaultValue().toReal();
146 generateItemAnimations(info.id, info);
147 }
148
149 return info.id;
150}
151
152void QQuickQmlGenerator::generateNodeEnd(const NodeInfo &info)
153{
154 if (Q_UNLIKELY(errorState()))
155 return;
156 m_indentLevel--;
157 stream() << "}";
158 generateShaderUse(info);
159}
160
161void QQuickQmlGenerator::generateItemAnimations(const QString &idString, const NodeInfo &info)
162{
163 const bool hasTransform = info.transform.isAnimated()
164 || !info.maskId.isEmpty()
165 || !info.filterId.isEmpty()
166 || !info.isDefaultTransform
167 || !info.transformReferenceId.isEmpty()
168 || info.motionPath.isAnimated();
169
170 if (hasTransform) {
171 stream() << "transform: TransformGroup {";
172 m_indentLevel++;
173
174 bool hasNonConstantTransform = false;
175 int earliestOverrideGroup = -1;
176
177 if (!idString.isEmpty()) {
178 stream() << "id: " << idString << "_transform_base_group";
179
180 if (!info.maskId.isEmpty() || !info.filterId.isEmpty())
181 stream() << "Translate { x: " << idString << ".sourceX; y: " << idString << ".sourceY }";
182
183 if (info.transform.isAnimated()) {
184 for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) {
185 stream() << "TransformGroup {";
186 m_indentLevel++;
187
188 if (!idString.isEmpty())
189 stream() << "id: " << idString << "_transform_group_" << groupIndex;
190
191 int animationStart = info.transform.animationGroup(groupIndex);
192 int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount()
193 ? info.transform.animationGroup(groupIndex + 1)
194 : info.transform.animationCount();
195
196 const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(animationStart);
197 const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations;
198 if (replace && earliestOverrideGroup < 0)
199 earliestOverrideGroup = groupIndex;
200
201 for (int i = nextAnimationStart - 1; i >= animationStart; --i) {
202 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i);
203 if (animation.frames.isEmpty())
204 continue;
205
206 const QVariantList &parameters = animation.frames.first().value<QVariantList>();
207 switch (animation.subtype) {
208 case QTransform::TxTranslate:
209 if (animation.isConstant()) {
210 const QPointF translation = parameters.value(0).value<QPointF>();
211 if (!translation.isNull())
212 stream() << "Translate { x: " << translation.x() << "; y: " << translation.y() << " }";
213 } else {
214 hasNonConstantTransform = true;
215 stream() << "Translate { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
216 }
217 break;
218 case QTransform::TxScale:
219 if (animation.isConstant()) {
220 const QPointF scale = parameters.value(0).value<QPointF>();
221 if (scale != QPointF(1, 1))
222 stream() << "Scale { xScale: " << scale.x() << "; yScale: " << scale.y() << " }";
223 } else {
224 hasNonConstantTransform = true;
225 stream() << "Scale { id: " << idString << "_transform_" << groupIndex << "_" << i << "}";
226 }
227 break;
228 case QTransform::TxRotate:
229 if (animation.isConstant()) {
230 const QPointF center = parameters.value(0).value<QPointF>();
231 const qreal angle = parameters.value(1).toReal();
232 if (!qFuzzyIsNull(angle))
233 stream() << "Rotation { angle: " << angle << "; origin.x: " << center.x() << "; origin.y: " << center.y() << " }"; //### center relative to what?
234 } else {
235 hasNonConstantTransform = true;
236 stream() << "Rotation { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
237 }
238 break;
239 case QTransform::TxShear:
240 if (animation.isConstant()) {
241 const QPointF skew = parameters.value(0).value<QPointF>();
242 if (!skew.isNull())
243 stream() << "Shear { xAngle: " << skew.x() << "; yAngle: " << skew.y() << " }";
244 } else {
245 hasNonConstantTransform = true;
246 stream() << "Shear { id: " << idString << "_transform_" << groupIndex << "_" << i << " }";
247 }
248 break;
249 default:
250 Q_UNREACHABLE();
251 }
252 }
253
254 m_indentLevel--;
255 stream() << "}";
256 }
257 }
258
259 if (info.motionPath.isAnimated()) {
260 QVariantList defaultProps = info.motionPath.defaultValue().value<QVariantList>();
261 const bool adaptAngle = defaultProps.value(1).toBool();
262 const qreal baseRotation = defaultProps.value(2).toReal();
263 QString interpolatorId = idString + QStringLiteral("_motion_interpolator");
264 if (adaptAngle || !qFuzzyIsNull(baseRotation)) {
265 stream() << "Rotation {";
266 m_indentLevel++;
267
268 if (adaptAngle) {
269 stream() << "angle: " << interpolatorId << ".angle";
270 if (!qFuzzyIsNull(baseRotation))
271 stream(SameLine) << " + " << baseRotation;
272 } else {
273 stream() << "angle: " << baseRotation;
274 }
275
276 m_indentLevel--;
277 stream() << "}";
278 }
279
280 stream() << "Translate {";
281 m_indentLevel++;
282
283 stream() << "x: " << interpolatorId << ".x";
284 stream() << "y: " << interpolatorId << ".y";
285
286 m_indentLevel--;
287 stream() << "}";
288 }
289 }
290
291 if (!info.isDefaultTransform) {
292 QTransform xf = info.transform.defaultValue().value<QTransform>();
293 if (xf.type() <= QTransform::TxTranslate) {
294 stream() << "Translate { x: " << xf.dx() << "; y: " << xf.dy() << "}";
295 } else {
296 stream() << "Matrix4x4 { matrix: ";
297 generateTransform(xf);
298 stream(SameLine) << "}";
299 }
300 }
301
302 if (!info.transformReferenceId.isEmpty())
303 stream() << "Matrix4x4 { matrix: " << info.transformReferenceId << ".transformMatrix }";
304
305 m_indentLevel--;
306 stream() << "}";
307
308 if (hasNonConstantTransform) {
309 generateAnimateTransform(idString, info);
310 } else if (info.transform.isAnimated() && earliestOverrideGroup >= 0) {
311 // We have animations, but they are all constant? Then we still need to respect the
312 // override flag of the animations
313 stream() << "Component.onCompleted: {";
314 m_indentLevel++;
315
316 stream() << idString << "_transform_base_group.activateOverride("
317 << idString << "_transform_group_" << earliestOverrideGroup << ")";
318
319 m_indentLevel--;
320 stream() << "}";
321 }
322 }
323
324 generateAnimateMotionPath(idString, info.motionPath);
325
326 generatePropertyAnimation(info.opacity, idString, QStringLiteral("opacity"));
327}
328
329void QQuickQmlGenerator::generateShaderUse(const NodeInfo &info)
330{
331 const bool hasMask = !info.maskId.isEmpty();
332 const bool hasFilters = !info.filterId.isEmpty();
333 if (!hasMask && !hasFilters)
334 return;
335
336 const QString effectId = hasFilters
337 ? info.filterId + QStringLiteral("_") + info.id + QStringLiteral("_effect")
338 : QString{};
339
340 QString animatedItemId;
341 if (hasFilters) {
342 stream() << "ShaderEffectSource {";
343 m_indentLevel++;
344
345 const QString seId = info.id + QStringLiteral("_se");
346 stream() << "id: " << seId;
347
348 stream() << "ItemSpy {";
349 m_indentLevel++;
350 stream() << "id: " << info.id << "_itemspy";
351 stream() << "anchors.fill: parent";
352 m_indentLevel--;
353 stream() << "}";
354
355 stream() << "hideSource: true";
356 stream() << "wrapMode: " << info.filterId << "_filterParameters.wrapMode";
357 stream() << "sourceItem: " << info.id;
358 stream() << "sourceRect: " << info.filterId
359 << "_filterParameters.adaptToFilterRect("
360 << info.id << ".originalBounds.x, "
361 << info.id << ".originalBounds.y, "
362 << info.id << ".originalBounds.width, "
363 << info.id << ".originalBounds.height)";
364 stream() << "textureSize: " << info.id << "_itemspy.requiredTextureSize";
365 stream() << "width: sourceRect.width";
366 stream() << "height: sourceRect.height";
367 stream() << "visible: false";
368
369 m_indentLevel--;
370 stream() << "}";
371
372 stream() << "Loader {";
373 m_indentLevel++;
374
375 animatedItemId = effectId;
376 stream() << "id: " << effectId;
377
378 stream() << "property var filterSourceItem: " << seId;
379 stream() << "sourceComponent: " << info.filterId << "_container";
380 stream() << "property real sourceX: " << info.id << ".originalBounds.x";
381 stream() << "property real sourceY: " << info.id << ".originalBounds.y";
382 stream() << "width: " << info.id << ".originalBounds.width";
383 stream() << "height: " << info.id << ".originalBounds.height";
384
385 if (hasMask) {
386 m_indentLevel--;
387 stream() << "}";
388 }
389 }
390
391 if (hasMask) {
392 // Shader effect source for the mask itself
393 stream() << "ShaderEffectSource {";
394 m_indentLevel++;
395
396 const QString maskId = info.maskId + QStringLiteral("_") + info.id + QStringLiteral("_mask");
397 stream() << "id: " << maskId;
398 stream() << "sourceItem: " << info.maskId;
399 stream() << "visible: false";
400 stream() << "hideSource: true";
401
402 stream() << "ItemSpy {";
403 m_indentLevel++;
404 stream() << "id: " << maskId << "_itemspy";
405 stream() << "anchors.fill: parent";
406 m_indentLevel--;
407 stream() << "}";
408 stream() << "textureSize: " << maskId << "_itemspy.requiredTextureSize";
409
410 stream() << "sourceRect: " << info.maskId << ".maskRect("
411 << info.id << ".originalBounds.x,"
412 << info.id << ".originalBounds.y,"
413 << info.id << ".originalBounds.width,"
414 << info.id << ".originalBounds.height)";
415
416 stream() << "width: sourceRect.width";
417 stream() << "height: sourceRect.height";
418
419 m_indentLevel--;
420 stream() << "}";
421
422 // Shader effect source of the masked item
423 stream() << "ShaderEffectSource {";
424 m_indentLevel++;
425
426 const QString seId = info.id + QStringLiteral("_masked_se");
427 stream() << "id: " << seId;
428
429 stream() << "ItemSpy {";
430 m_indentLevel++;
431 stream() << "id: " << info.id << "_masked_se_itemspy";
432 stream() << "anchors.fill: parent";
433 m_indentLevel--;
434 stream() << "}";
435
436 stream() << "hideSource: true";
437 if (hasFilters)
438 stream() << "sourceItem: " << effectId;
439 else
440 stream() << "sourceItem: " << info.id;
441 stream() << "textureSize: " << info.id << "_masked_se_itemspy.requiredTextureSize";
442 if (!hasFilters) {
443 stream() << "sourceRect: " << info.maskId << ".maskRect("
444 << info.id << ".originalBounds.x,"
445 << info.id << ".originalBounds.y,"
446 << info.id << ".originalBounds.width,"
447 << info.id << ".originalBounds.height)";
448 } else {
449 stream() << "sourceRect: " << info.maskId << ".maskRect(0, 0,"
450 << info.id << ".originalBounds.width,"
451 << info.id << ".originalBounds.height)";
452 }
453 stream() << "width: sourceRect.width";
454 stream() << "height: sourceRect.height";
455 stream() << "smooth: false";
456 stream() << "visible: false";
457
458 m_indentLevel--;
459 stream() << "}";
460
461 stream() << "ShaderEffect {";
462 m_indentLevel++;
463
464 const QString maskShaderId = maskId + QStringLiteral("_se");
465 animatedItemId = maskShaderId;
466
467 stream() << "id:" << maskShaderId;
468
469 stream() << "property real sourceX: " << maskId << ".sourceRect.x";
470 stream() << "property real sourceY: " << maskId << ".sourceRect.y";
471 stream() << "width: " << maskId << ".sourceRect.width";
472 stream() << "height: " << maskId << ".sourceRect.height";
473
474 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/genericmask.frag.qsb\"";
475 stream() << "property var source: " << seId;
476 stream() << "property var maskSource: " << maskId;
477 stream() << "property bool isAlpha: " << (info.isMaskAlpha ? "true" : "false");
478 stream() << "property bool isInverted: " << (info.isMaskInverted ? "true" : "false");
479 }
480
481 if (!info.isDefaultOpacity)
482 stream() << "opacity: " << info.opacity.defaultValue().toReal();
483
484 generateItemAnimations(animatedItemId, info);
485
486 m_indentLevel--;
487 stream() << "}";
488}
489
490bool QQuickQmlGenerator::generateDefsNode(const StructureNodeInfo &info)
491{
492 if (Q_UNLIKELY(errorState()))
493 return false;
494
495 if (info.stage == StructureNodeStage::Start) {
496 m_oldIndentLevels.push(m_indentLevel);
497
498 stream() << "Component {";
499 m_indentLevel++;
500
501 stream() << "id: " << info.id << "_container";
502
503 stream() << "Item {";
504 m_indentLevel++;
505
506 generateTimelineFields(info);
507 if (!info.transformReferenceChildId.isEmpty()) {
508 stream() << "property alias transformMatrix: "
509 << info.transformReferenceChildId << ".transformMatrix";
510 }
511
512 generateNodeBase(info, QStringLiteral("_defs"));
513 } else {
514 generateNodeEnd(info);
515
516 m_indentLevel--;
517 stream() << "}"; // Component
518
519 stream() << m_defsSuffix;
520 m_defsSuffix.clear();
521
522 m_indentLevel = m_oldIndentLevels.pop();
523 }
524
525 return true;
526}
527
528void QQuickQmlGenerator::generateImageNode(const ImageNodeInfo &info)
529{
530 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
531 return;
532
533 const QFileInfo outputFileInfo(outputFileName);
534 const QDir outputDir(outputFileInfo.absolutePath());
535
536 QString filePath;
537
538 if (!m_retainFilePaths || info.externalFileReference.isEmpty()) {
539 filePath = m_assetFileDirectory;
540 if (filePath.isEmpty())
541 filePath = outputDir.absolutePath();
542
543 if (!filePath.isEmpty() && !filePath.endsWith(u'/'))
544 filePath += u'/';
545
546 QDir fileDir(filePath);
547 if (!fileDir.exists()) {
548 if (!fileDir.mkpath(QStringLiteral(".")))
549 qCWarning(lcQuickVectorImage) << "Failed to create image resource directory:" << filePath;
550 }
551
552 filePath += QStringLiteral("%1%2.png").arg(m_assetFilePrefix.isEmpty()
553 ? QStringLiteral("svg_asset_")
554 : m_assetFilePrefix)
555 .arg(info.image.cacheKey());
556
557 if (!info.image.save(filePath))
558 qCWarning(lcQuickVectorImage) << "Unabled to save image resource" << filePath;
559 qCDebug(lcQuickVectorImage) << "Saving copy of IMAGE" << filePath;
560 } else {
561 filePath = info.externalFileReference;
562 }
563
564 const QFileInfo assetFileInfo(filePath);
565
566 stream() << "Image {";
567
568 m_indentLevel++;
569 generateNodeBase(info);
570 stream() << "x: " << info.rect.x();
571 stream() << "y: " << info.rect.y();
572 stream() << "width: " << info.rect.width();
573 stream() << "height: " << info.rect.height();
574 stream() << "source: \"" << m_urlPrefix << outputDir.relativeFilePath(assetFileInfo.absoluteFilePath()) <<"\"";
575 generateNodeEnd(info);
576}
577
578void QQuickQmlGenerator::generateMarkers(const PathNodeInfo &info)
579{
580 const QPainterPath path = info.path.defaultValue().value<QPainterPath>();
581 for (int i = 0; i < path.elementCount(); ++i) {
582 const QPainterPath::Element element = path.elementAt(i);
583 QString markerId;
584 qreal angle = 0;
585
586 // Copied from Qt SVG
587 auto getMeanAngle = [](QPointF p0, QPointF p1, QPointF p2) -> qreal {
588 QPointF t1 = p1 - p0;
589 QPointF t2 = p2 - p1;
590 qreal hyp1 = hypot(t1.x(), t1.y());
591 if (hyp1 > 0)
592 t1 /= hyp1;
593 else
594 return 0.;
595 qreal hyp2 = hypot(t2.x(), t2.y());
596 if (hyp2 > 0)
597 t2 /= hyp2;
598 else
599 return 0.;
600 QPointF tangent = t1 + t2;
601 return -atan2(tangent.y(), tangent.x()) / M_PI * 180.;
602 };
603
604 if (i == 0) {
605 markerId = info.markerStartId;
606 angle = path.angleAtPercent(0.0);
607 } else if (i == path.elementCount() - 1) {
608 markerId = info.markerEndId;
609 angle = path.angleAtPercent(1.0);
610 } else if (path.elementAt(i + 1).type != QPainterPath::CurveToDataElement) {
611 markerId = info.markerMidId;
612
613 const QPainterPath::Element prevElement = path.elementAt(i - 1);
614 const QPainterPath::Element nextElement = path.elementAt(i + 1);
615
616 QPointF p1(prevElement.x, prevElement.y);
617 QPointF p2(element.x, element.y);
618 QPointF p3(nextElement.x, nextElement.y);
619
620 angle = getMeanAngle(p1, p2, p3);
621 }
622
623 if (!markerId.isEmpty()) {
624 stream() << "Loader {";
625 m_indentLevel++;
626
627 //stream() << "clip: true";
628 stream() << "sourceComponent: " << markerId << "_container";
629 stream() << "property real strokeWidth: " << info.strokeStyle.width;
630 stream() << "transform: [";
631 m_indentLevel++;
632 if (i == 0) {
633 stream() << "Scale { "
634 << "xScale: " << markerId << "_markerParameters.startReversed ? -1 : 1; "
635 << "yScale: " << markerId << "_markerParameters.startReversed ? -1 : 1 },";
636 }
637 stream() << "Rotation { angle: " << markerId << "_markerParameters.autoAngle(" << -angle << ") },";
638 stream() << "Translate { x: " << element.x << "; y: " << element.y << "}";
639
640 m_indentLevel--;
641 stream() << "]";
642
643 m_indentLevel--;
644 stream() << "}";
645 }
646 }
647}
648
649void QQuickQmlGenerator::generatePath(const PathNodeInfo &info, const QRectF &overrideBoundingRect)
650{
651 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
652 return;
653
654 if (m_inShapeItemLevel > 0) {
655 if (!info.isDefaultTransform)
656 qWarning() << "Skipped transform for node" << info.nodeId << "type" << info.typeName << "(this is not supposed to happen)";
657 optimizePaths(info, overrideBoundingRect);
658 } else {
659 m_inShapeItemLevel++;
660 stream() << shapeName() << " {";
661
662 m_indentLevel++;
663 generateNodeBase(info);
664
665 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
666 stream() << "preferredRendererType: Shape.CurveRenderer";
667 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::AsyncShapes))
668 stream() << "asynchronous: true";
669 optimizePaths(info, overrideBoundingRect);
670 //qCDebug(lcQuickVectorGraphics) << *node->qpath();
671
672 if (!info.markerStartId.isEmpty()
673 || !info.markerMidId.isEmpty()
674 || !info.markerEndId.isEmpty()) {
675 generateMarkers(info);
676 }
677
678 generateNodeEnd(info);
679 m_inShapeItemLevel--;
680 }
681}
682
683void QQuickQmlGenerator::generateGradient(const QGradient *grad,
684 const QString &propertyName,
685 const QRectF &coordinateConversion)
686{
687 const QSizeF &scale = coordinateConversion.size();
688 const QPointF &translation = coordinateConversion.topLeft();
689
690 if (grad->type() == QGradient::LinearGradient) {
691 auto *linGrad = static_cast<const QLinearGradient *>(grad);
692 stream() << propertyName << ": LinearGradient {";
693 m_indentLevel++;
694
695 QRectF gradRect(linGrad->start(), linGrad->finalStop());
696
697 stream() << "x1: " << (gradRect.left() * scale.width()) + translation.x();
698 stream() << "y1: " << (gradRect.top() * scale.height()) + translation.y();
699 stream() << "x2: " << (gradRect.right() * scale.width()) + translation.x();
700 stream() << "y2: " << (gradRect.bottom() * scale.height()) + translation.y();
701 for (auto &stop : linGrad->stops())
702 stream() << "GradientStop { position: " << QString::number(stop.first, 'g', 7)
703 << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }";
704 } else if (grad->type() == QGradient::RadialGradient) {
705 auto *radGrad = static_cast<const QRadialGradient*>(grad);
706 stream() << propertyName << ": RadialGradient {";
707 m_indentLevel++;
708
709 stream() << "centerX: " << (radGrad->center().x() * scale.width()) + translation.x();
710 stream() << "centerY: " << (radGrad->center().y() * scale.height()) + translation.y();
711 stream() << "centerRadius: " << (radGrad->radius() * scale.width()); // ### ?
712 stream() << "focalX:" << (radGrad->focalPoint().x() * scale.width()) + translation.x();
713 stream() << "focalY:" << (radGrad->focalPoint().y() * scale.height()) + translation.y();
714 for (auto &stop : radGrad->stops())
715 stream() << "GradientStop { position: " << QString::number(stop.first, 'g', 7)
716 << "; color: \"" << stop.second.name(QColor::HexArgb) << "\" }";
717 }
718
719 stream() << "spread: ShapeGradient.";
720 switch (grad->spread()) {
721 case QGradient::PadSpread:
722 stream(SameLine) << "PadSpread";
723 break;
724 case QGradient::ReflectSpread:
725 stream(SameLine) << "ReflectSpread";
726 break;
727 case QGradient::RepeatSpread:
728 stream(SameLine) << "RepeatSpread";
729 break;
730 }
731
732 m_indentLevel--;
733 stream() << "}";
734}
735
736void QQuickQmlGenerator::generateAnimationBindings()
737{
738 QString prefix;
739 if (Q_UNLIKELY(!isRuntimeGenerator()))
740 prefix = QStringLiteral(".animations");
741
742 stream() << "loops: " << m_topLevelIdString << prefix << ".loops";
743 stream() << "paused: " << m_topLevelIdString << prefix << ".paused";
744 stream() << "running: true";
745
746 // We need to reset the animation when the loop count changes
747 stream() << "onLoopsChanged: { if (running) { restart() } }";
748}
749
750void QQuickQmlGenerator::generateEasing(const QQuickAnimatedProperty::PropertyAnimation &animation,
751 int time, int streamFlags)
752{
753 if (animation.easingPerFrame.contains(time)) {
754 QBezier bezier = animation.easingPerFrame.value(time);
755 QPointF c1 = bezier.pt2();
756 QPointF c2 = bezier.pt3();
757
758 bool isLinear = (c1 == c1.transposed() && c2 == c2.transposed());
759 if (!isLinear) {
760 int nextIdx = m_easings.size();
761 QString &id = m_easings[{c1.x(), c1.y(), c2.x(), c2.y()}];
762 if (id.isNull())
763 id = QString(QLatin1String("easing_%1")).arg(nextIdx, 2, 10, QLatin1Char('0'));
764 if (streamFlags & SameLine)
765 stream(streamFlags) << "; ";
766 stream(streamFlags) << "easing: " << m_topLevelIdString << "." << id;
767 }
768 }
769}
770
771static int processAnimationTime(int time)
772{
773 static qreal multiplier = qreal(qEnvironmentVariable("QT_QUICKVECTORIMAGE_TIME_DILATION", QStringLiteral("1.0"))
774 .toDouble());
775 return std::round(multiplier * time);
776}
777
778void QQuickQmlGenerator::generatePropertyAnimation(const QQuickAnimatedProperty &property,
779 const QString &targetName,
780 const QString &propertyName,
781 AnimationType animationType)
782{
783 if (!property.isAnimated())
784 return;
785
786 if (usingTimelineAnimation())
787 return generatePropertyTimeline(property, targetName, propertyName, animationType);
788
789 QString mainAnimationId = targetName
790 + QStringLiteral("_")
791 + propertyName
792 + QStringLiteral("_animation");
793 mainAnimationId.replace(QLatin1Char('.'), QLatin1Char('_'));
794
795 QString prefix;
796 if (Q_UNLIKELY(!isRuntimeGenerator()))
797 prefix = QStringLiteral(".animations");
798
799 stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }";
800
801 stream() << "ParallelAnimation {";
802 m_indentLevel++;
803
804 stream() << "id: " << mainAnimationId;
805
806 generateAnimationBindings();
807
808 for (int i = 0; i < property.animationCount(); ++i) {
809 const QQuickAnimatedProperty::PropertyAnimation &animation = property.animation(i);
810
811 stream() << "SequentialAnimation {";
812 m_indentLevel++;
813
814 const int startOffset = processAnimationTime(animation.startOffset);
815 if (startOffset > 0)
816 stream() << "PauseAnimation { duration: " << startOffset << " }";
817
818 stream() << "SequentialAnimation {";
819 m_indentLevel++;
820
821 const int repeatCount = animation.repeatCount;
822 if (repeatCount < 0)
823 stream() << "loops: Animation.Infinite";
824 else
825 stream() << "loops: " << repeatCount;
826
827 int previousTime = 0;
828 QVariant previousValue;
829 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
830 const int time = it.key();
831 const int frameTime = processAnimationTime(time - previousTime);
832 const QVariant &value = it.value();
833
834 if (previousValue.isValid() && previousValue == value) {
835 if (frameTime > 0)
836 stream() << "PauseAnimation { duration: " << frameTime << " }";
837 } else if (animationType == AnimationType::Auto && value.typeId() == QMetaType::Bool) {
838 // We special case bools, with PauseAnimation and then a setter at the end
839 if (frameTime > 0)
840 stream() << "PauseAnimation { duration: " << frameTime << " }";
841 stream() << "ScriptAction {";
842 m_indentLevel++;
843
844 stream() << "script:" << targetName << "." << propertyName << " = " << value.toString();
845
846 m_indentLevel--;
847 stream() << "}";
848 } else {
849 generateAnimatedPropertySetter(targetName,
850 propertyName,
851 value,
852 animation,
853 frameTime,
854 time,
855 animationType);
856 }
857
858 previousTime = time;
859 previousValue = value;
860 }
861
862 if (!(animation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd)) {
863 stream() << "ScriptAction {";
864 m_indentLevel++;
865 stream() << "script: ";
866
867 switch (animationType) {
868 case AnimationType::Auto:
869 stream(SameLine) << targetName << "." << propertyName << " = ";
870 break;
871 case AnimationType::ColorOpacity:
872 stream(SameLine) << targetName << "." << propertyName << ".a = ";
873 break;
874 };
875
876 QVariant value = property.defaultValue();
877 if (value.typeId() == QMetaType::QColor)
878 stream(SameLine) << "\"" << value.toString() << "\"";
879 else
880 stream(SameLine) << value.toReal();
881
882 m_indentLevel--;
883 stream() << "}";
884 }
885
886 m_indentLevel--;
887 stream() << "}";
888
889 m_indentLevel--;
890 stream() << "}";
891 }
892
893 m_indentLevel--;
894 stream() << "}";
895}
896
897void QQuickQmlGenerator::generateTimelinePropertySetter(
898 const QString &targetName,
899 const QString &propertyName,
900 const QQuickAnimatedProperty::PropertyAnimation &animation,
901 std::function<QVariant(const QVariant &)> const& extractValue,
902 int valueIndex)
903{
904 if (animation.repeatCount != 1 || animation.startOffset
905 || animation.flags != QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd) {
906 qCWarning(lcQuickVectorImage) << "Animation feature not implemented in timeline mode, for"
907 << targetName << propertyName;
908 }
909
910 stream() << "KeyframeGroup {";
911 m_indentLevel++;
912 stream() << "target: " << targetName;
913 stream() << "property: \"" << propertyName << "\"";
914
915 for (const auto &[frame, rawValue] : animation.frames.asKeyValueRange()) {
916 QVariant value;
917 if (rawValue.typeId() == QMetaType::QVariantList)
918 value = extractValue(rawValue.toList().value(valueIndex));
919 else
920 value = extractValue(rawValue);
921
922 stream() << "Keyframe { frame: " << frame << "; value: ";
923 if (value.typeId() == QMetaType::QVector3D) {
924 const QVector3D &v = value.value<QVector3D>();
925 stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
926 } else if (value.typeId() == QMetaType::QColor) {
927 stream(SameLine) << "\"" << value.toString() << "\"";
928 } else {
929 stream(SameLine) << value.toReal();
930 }
931 generateEasing(animation, frame, SameLine);
932 stream(SameLine) << " }";
933 }
934
935 m_indentLevel--;
936 stream() << "}";
937}
938
939void QQuickQmlGenerator::generatePropertyTimeline(const QQuickAnimatedProperty &property,
940 const QString &targetName,
941 const QString &propertyName,
942 AnimationType animationType)
943{
944 if (animationType == QQuickQmlGenerator::AnimationType::ColorOpacity) {
945 qCWarning(lcQuickVectorImage) << "ColorOpacity animation not available in timeline mode";
946 return;
947 }
948
949 if (property.animationGroupCount() > 1 || property.animationCount() > 1) {
950 qCWarning(lcQuickVectorImage) << "Property feature not implemented in timeline mode, for"
951 << targetName << propertyName;
952 }
953
954 stream() << "Timeline {";
955 m_indentLevel++;
956 stream() << "currentFrame: " << property.timelineReferenceId() << ".frameCounter";
957 stream() << "enabled: true";
958
959 auto extractor = [](const QVariant &value) { return value; };
960 generateTimelinePropertySetter(targetName, propertyName, property.animation(0), extractor);
961
962 m_indentLevel--;
963 stream() << "}";
964}
965
966void QQuickQmlGenerator::generateTransform(const QTransform &xf)
967{
968 if (xf.isAffine()) {
969 stream(SameLine) << "PlanarTransform.fromAffineMatrix("
970 << xf.m11() << ", " << xf.m12() << ", "
971 << xf.m21() << ", " << xf.m22() << ", "
972 << xf.dx() << ", " << xf.dy() << ")";
973 } else {
974 QMatrix4x4 m(xf);
975 stream(SameLine) << "Qt.matrix4x4(";
976 m_indentLevel += 3;
977 const auto *data = m.data();
978 for (int i = 0; i < 4; i++) {
979 stream() << data[i] << ", " << data[i+4] << ", " << data[i+8] << ", " << data[i+12];
980 if (i < 3)
981 stream(SameLine) << ", ";
982 }
983 stream(SameLine) << ")";
984 m_indentLevel -= 3;
985 }
986}
987
988void QQuickQmlGenerator::outputShapePath(const PathNodeInfo &info, const QPainterPath *painterPath, const QQuadPath *quadPath, QQuickVectorImageGenerator::PathSelector pathSelector, const QRectF &boundingRect)
989{
990 Q_UNUSED(pathSelector)
991 Q_ASSERT(painterPath || quadPath);
992
993 if (Q_UNLIKELY(errorState()))
994 return;
995
996 const bool invalidGradientBounds = info.strokeGrad.coordinateMode() == QGradient::ObjectMode
997 && (qFuzzyIsNull(boundingRect.width()) ||
998 qFuzzyIsNull(boundingRect.height()));
999
1000 const QColor strokeColor = info.strokeStyle.color.defaultValue().value<QColor>();
1001 const bool noPen = (strokeColor == QColorConstants::Transparent || !strokeColor.isValid())
1002 && !info.strokeStyle.color.isAnimated()
1003 && !info.strokeStyle.opacity.isAnimated()
1004 && (info.strokeGrad.type() == QGradient::NoGradient
1005 || invalidGradientBounds);
1006 if (pathSelector == QQuickVectorImageGenerator::StrokePath && noPen)
1007 return;
1008
1009 const QColor fillColor = info.fillColor.defaultValue().value<QColor>();
1010 const bool noFill = info.grad.type() == QGradient::NoGradient
1011 && fillColor == QColorConstants::Transparent
1012 && !info.fillColor.isAnimated()
1013 && !info.fillOpacity.isAnimated();
1014 if (pathSelector == QQuickVectorImageGenerator::FillPath && noFill)
1015 return;
1016
1017 if (noPen && noFill)
1018 return;
1019 auto fillRule = QQuickShapePath::FillRule(painterPath ? painterPath->fillRule() : quadPath->fillRule());
1020 stream() << "ShapePath {";
1021 m_indentLevel++;
1022
1023 QString shapePathId = info.id;
1024 if (pathSelector & QQuickVectorImageGenerator::FillPath)
1025 shapePathId += QStringLiteral("_fill");
1026 if (pathSelector & QQuickVectorImageGenerator::StrokePath)
1027 shapePathId += QStringLiteral("_stroke");
1028
1029 stream() << "id: " << shapePathId;
1030
1031 if (!info.nodeId.isEmpty()) {
1032 switch (pathSelector) {
1033 case QQuickVectorImageGenerator::FillPath:
1034 stream() << "objectName: \"svg_fill_path:" << info.nodeId << "\"";
1035 break;
1036 case QQuickVectorImageGenerator::StrokePath:
1037 stream() << "objectName: \"svg_stroke_path:" << info.nodeId << "\"";
1038 break;
1039 case QQuickVectorImageGenerator::FillAndStroke:
1040 stream() << "objectName: \"svg_path:" << info.nodeId << "\"";
1041 break;
1042 }
1043 }
1044
1045 if (noPen || !(pathSelector & QQuickVectorImageGenerator::StrokePath)) {
1046 stream() << "strokeColor: \"transparent\"";
1047 } else {
1048 if (info.strokeGrad.type() != QGradient::NoGradient && !invalidGradientBounds) {
1049 QRectF coordinateSys = info.strokeGrad.coordinateMode() == QGradient::ObjectMode
1050 ? boundingRect
1051 : QRectF(0.0, 0.0, 1.0, 1.0);
1052 generateGradient(&info.strokeGrad, QStringLiteral("strokeGradient"), coordinateSys);
1053 } else if (info.strokeStyle.opacity.isAnimated()) {
1054 stream() << "property color strokeBase: \"" << strokeColor.name(QColor::HexArgb) << "\"";
1055 stream() << "property real strokeOpacity: " << info.strokeStyle.opacity.defaultValue().toReal();
1056 stream() << "strokeColor: Qt.rgba(strokeBase.r, strokeBase.g, strokeBase.b, strokeOpacity)";
1057 } else {
1058 stream() << "strokeColor: \"" << strokeColor.name(QColor::HexArgb) << "\"";
1059 }
1060 stream() << "strokeWidth: " << info.strokeStyle.width;
1061 stream() << "capStyle: " << QQuickVectorImageGenerator::Utils::strokeCapStyleString(info.strokeStyle.lineCapStyle);
1062 stream() << "joinStyle: " << QQuickVectorImageGenerator::Utils::strokeJoinStyleString(info.strokeStyle.lineJoinStyle);
1063 stream() << "miterLimit: " << info.strokeStyle.miterLimit;
1064 if (info.strokeStyle.cosmetic)
1065 stream() << "cosmeticStroke: true";
1066 if (info.strokeStyle.dashArray.length() != 0) {
1067 stream() << "strokeStyle: " << "ShapePath.DashLine";
1068 stream() << "dashPattern: " << QQuickVectorImageGenerator::Utils::listString(info.strokeStyle.dashArray);
1069 stream() << "dashOffset: " << info.strokeStyle.dashOffset;
1070 }
1071 }
1072
1073 QTransform fillTransform = info.fillTransform;
1074 if (!(pathSelector & QQuickVectorImageGenerator::FillPath)) {
1075 stream() << "fillColor: \"transparent\"";
1076 } else if (info.grad.type() != QGradient::NoGradient) {
1077 generateGradient(&info.grad, QStringLiteral("fillGradient"));
1078
1079 // Scaling done via fillTransform to get correct order of operations
1080 if (info.grad.coordinateMode() == QGradient::ObjectMode) {
1081 QTransform objectToUserSpace;
1082 objectToUserSpace.translate(boundingRect.x(), boundingRect.y());
1083 objectToUserSpace.scale(boundingRect.width(), boundingRect.height());
1084 fillTransform *= objectToUserSpace;
1085 }
1086 } else {
1087 if (info.fillOpacity.isAnimated()) {
1088 stream() << "property color fillBase: \"" << fillColor.name(QColor::HexArgb) << "\"";
1089 stream() << "property real fillOpacity:" << info.fillOpacity.defaultValue().toReal();
1090 stream() << "fillColor: Qt.rgba(fillBase.r, fillBase.g, fillBase.b, fillOpacity)";
1091 } else {
1092 stream() << "fillColor: \"" << fillColor.name(QColor::HexArgb) << "\"";
1093 }
1094 }
1095
1096 if (!info.patternId.isEmpty()) {
1097 stream() << "fillItem: ShaderEffectSource {";
1098 m_indentLevel++;
1099
1100 stream() << "parent: " << info.id;
1101 stream() << "sourceItem: " << info.patternId;
1102 stream() << "hideSource: true";
1103 stream() << "visible: false";
1104 stream() << "width: " << info.patternId << ".width";
1105 stream() << "height: " << info.patternId << ".height";
1106 stream() << "wrapMode: ShaderEffectSource.Repeat";
1107 stream() << "textureSize: Qt.size(width * __qt_toplevel_scale_itemspy.requiredTextureSize.width, "
1108 << "height * __qt_toplevel_scale_itemspy.requiredTextureSize.height)";;
1109 stream() << "sourceRect: " << info.patternId << ".sourceRect("
1110 << info.id << ".width, "
1111 << info.id << ".height)";
1112
1113 m_indentLevel--;
1114 stream() << "}";
1115
1116 // Fill transform has to include the inverse of the scene scale, since the texture size
1117 // is scaled by this amount
1118 stream() << "function calculateFillTransform(xScale, yScale) {";
1119 m_indentLevel++;
1120
1121 stream() << "var m = ";
1122 generateTransform(fillTransform);
1123
1124 stream() << "m.translate(" << info.patternId << ".sourceOffset("
1125 << info.id << ".width, "
1126 << info.id << ".height))";
1127
1128 stream() << "m.scale(1.0 / xScale, 1.0 / yScale, 1.0)";
1129 stream() << "return m";
1130
1131 m_indentLevel--;
1132 stream() << "}";
1133
1134 stream() << "fillTransform: calculateFillTransform(__qt_toplevel_scale_itemspy.requiredTextureSize.width, "
1135 << "__qt_toplevel_scale_itemspy.requiredTextureSize.height)";
1136
1137 } else if (!fillTransform.isIdentity()) {
1138 const QTransform &xf = fillTransform;
1139 stream() << "fillTransform: ";
1140 if (info.fillTransform.type() == QTransform::TxTranslate)
1141 stream(SameLine) << "PlanarTransform.fromTranslate(" << xf.dx() << ", " << xf.dy() << ")";
1142 else if (info.fillTransform.type() == QTransform::TxScale && !xf.dx() && !xf.dy())
1143 stream(SameLine) << "PlanarTransform.fromScale(" << xf.m11() << ", " << xf.m22() << ")";
1144 else
1145 generateTransform(xf);
1146 }
1147
1148 if (info.trim.enabled) {
1149 stream() << "trim.start: " << info.trim.start.defaultValue().toReal();
1150 stream() << "trim.end: " << info.trim.end.defaultValue().toReal();
1151 stream() << "trim.offset: " << info.trim.offset.defaultValue().toReal();
1152
1153 }
1154
1155 if (fillRule == QQuickShapePath::WindingFill)
1156 stream() << "fillRule: ShapePath.WindingFill";
1157 else
1158 stream() << "fillRule: ShapePath.OddEvenFill";
1159
1160 QString hintStr;
1161 if (quadPath)
1162 hintStr = QQuickVectorImageGenerator::Utils::pathHintString(*quadPath);
1163 if (!hintStr.isEmpty())
1164 stream() << hintStr;
1165
1166 QQuickAnimatedProperty pathFactor(QVariant::fromValue(0));
1167 pathFactor.setTimelineReferenceId(info.path.timelineReferenceId());
1168 QString pathId = shapePathId + "_ip"_L1;
1169 if (!info.path.isAnimated() || (info.path.animation(0).startOffset == 0 && info.path.animation(0).isConstant())) {
1170 QString svgPathString = painterPath ? QQuickVectorImageGenerator::Utils::toSvgString(*painterPath) : QQuickVectorImageGenerator::Utils::toSvgString(*quadPath);
1171 stream() << "PathSvg { path: \"" << svgPathString << "\" }";
1172 } else {
1173 stream() << "PathInterpolated {";
1174 m_indentLevel++;
1175 stream() << "id: " << pathId;
1176 stream() << "svgPaths: [";
1177 m_indentLevel++;
1178 QQuickAnimatedProperty::PropertyAnimation pathFactorAnim = info.path.animation(0);
1179 auto &frames = pathFactorAnim.frames;
1180 int pathIdx = -1;
1181 QString lastSvg;
1182 for (auto it = frames.begin(); it != frames.end(); ++it) {
1183 QString svg = QQuickVectorImageGenerator::Utils::toSvgString(it->value<QPainterPath>());
1184 if (svg != lastSvg) {
1185 if (pathIdx >= 0)
1186 stream(SameLine) << ",";
1187 stream() << "\"" << svg << "\"";
1188 ++pathIdx;
1189 lastSvg = svg;
1190 }
1191 *it = QVariant::fromValue(pathIdx);
1192 }
1193 pathFactor.addAnimation(pathFactorAnim);
1194 m_indentLevel--;
1195 stream() << "]";
1196 m_indentLevel--;
1197 stream() << "}";
1198 }
1199
1200 m_indentLevel--;
1201 stream() << "}";
1202
1203 if (pathFactor.isAnimated())
1204 generatePropertyAnimation(pathFactor, pathId, "factor"_L1);
1205
1206 if (info.trim.enabled) {
1207 generatePropertyAnimation(info.trim.start, shapePathId + QStringLiteral(".trim"), QStringLiteral("start"));
1208 generatePropertyAnimation(info.trim.end, shapePathId + QStringLiteral(".trim"), QStringLiteral("end"));
1209 generatePropertyAnimation(info.trim.offset, shapePathId + QStringLiteral(".trim"), QStringLiteral("offset"));
1210 }
1211
1212 if (info.strokeStyle.opacity.isAnimated()) {
1213 generatePropertyAnimation(info.strokeStyle.color, shapePathId, QStringLiteral("strokeBase"));
1214 generatePropertyAnimation(info.strokeStyle.opacity, shapePathId, QStringLiteral("strokeOpacity"));
1215 } else {
1216 generatePropertyAnimation(info.strokeStyle.color, shapePathId, QStringLiteral("strokeColor"));
1217 }
1218 if (info.fillOpacity.isAnimated()) {
1219 generatePropertyAnimation(info.fillColor, shapePathId, QStringLiteral("fillBase"));
1220 generatePropertyAnimation(info.fillOpacity, shapePathId, QStringLiteral("fillOpacity"));
1221 } else {
1222 generatePropertyAnimation(info.fillColor, shapePathId, QStringLiteral("fillColor"));
1223 }
1224}
1225
1226void QQuickQmlGenerator::generateNode(const NodeInfo &info)
1227{
1228 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1229 return;
1230
1231 stream() << "// Missing Implementation for SVG Node: " << info.typeName;
1232 stream() << "// Adding an empty Item and skipping";
1233 stream() << "Item {";
1234 m_indentLevel++;
1235 generateNodeBase(info);
1236 generateNodeEnd(info);
1237}
1238
1239void QQuickQmlGenerator::generateTextNode(const TextNodeInfo &info)
1240{
1241 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1242 return;
1243
1244 stream() << "Item {";
1245 m_indentLevel++;
1246 generateNodeBase(info);
1247
1248 if (!info.isTextArea)
1249 stream() << "Item { id: textAlignItem_" << m_textNodeCounter << "; x: " << info.position.x() << "; y: " << info.position.y() << "}";
1250
1251 stream() << "Text {";
1252
1253 m_indentLevel++;
1254
1255 const QString textItemId = QStringLiteral("_qt_textItem_%1").arg(m_textNodeCounter);
1256 stream() << "id: " << textItemId;
1257
1258 generatePropertyAnimation(info.fillColor, textItemId, QStringLiteral("color"));
1259 generatePropertyAnimation(info.fillOpacity, textItemId, QStringLiteral("color"), AnimationType::ColorOpacity);
1260 generatePropertyAnimation(info.strokeColor, textItemId, QStringLiteral("styleColor"));
1261 generatePropertyAnimation(info.strokeOpacity, textItemId, QStringLiteral("styleColor"), AnimationType::ColorOpacity);
1262
1263 if (info.isTextArea) {
1264 stream() << "x: " << info.position.x();
1265 stream() << "y: " << info.position.y();
1266 if (info.size.width() > 0)
1267 stream() << "width: " << info.size.width();
1268 if (info.size.height() > 0)
1269 stream() << "height: " << info.size.height();
1270 stream() << "wrapMode: Text.Wrap"; // ### WordWrap? verify with SVG standard
1271 stream() << "clip: true"; //### Not exactly correct: should clip on the text level, not the pixel level
1272 } else {
1273 QString hAlign = QStringLiteral("left");
1274 stream() << "anchors.baseline: textAlignItem_" << m_textNodeCounter << ".top";
1275 switch (info.alignment) {
1276 case Qt::AlignHCenter:
1277 hAlign = QStringLiteral("horizontalCenter");
1278 break;
1279 case Qt::AlignRight:
1280 hAlign = QStringLiteral("right");
1281 break;
1282 default:
1283 qCDebug(lcQuickVectorImage) << "Unexpected text alignment" << info.alignment;
1284 Q_FALLTHROUGH();
1285 case Qt::AlignLeft:
1286 break;
1287 }
1288 stream() << "anchors." << hAlign << ": textAlignItem_" << m_textNodeCounter << ".left";
1289 }
1290 m_textNodeCounter++;
1291
1292 stream() << "color: \"" << info.fillColor.defaultValue().value<QColor>().name(QColor::HexArgb) << "\"";
1293 stream() << "textFormat:" << (info.needsRichText ? "Text.RichText" : "Text.StyledText");
1294
1295 stream() << "text: \"" << sanitizeString(info.text) << "\"";
1296 stream() << "font.family: \"" << sanitizeString(info.font.family()) << "\"";
1297 if (info.font.pixelSize() > 0)
1298 stream() << "font.pixelSize:" << info.font.pixelSize();
1299 else if (info.font.pointSize() > 0)
1300 stream() << "font.pixelSize:" << info.font.pointSizeF();
1301 if (info.font.underline())
1302 stream() << "font.underline: true";
1303 if (info.font.weight() != QFont::Normal)
1304 stream() << "font.weight: " << int(info.font.weight());
1305 if (info.font.italic())
1306 stream() << "font.italic: true";
1307 switch (info.font.hintingPreference()) {
1308 case QFont::PreferFullHinting:
1309 stream() << "font.hintingPreference: Font.PreferFullHinting";
1310 break;
1311 case QFont::PreferVerticalHinting:
1312 stream() << "font.hintingPreference: Font.PreferVerticalHinting";
1313 break;
1314 case QFont::PreferNoHinting:
1315 stream() << "font.hintingPreference: Font.PreferNoHinting";
1316 break;
1317 case QFont::PreferDefaultHinting:
1318 stream() << "font.hintingPreference: Font.PreferDefaultHinting";
1319 break;
1320 };
1321
1322 const QColor strokeColor = info.strokeColor.defaultValue().value<QColor>();
1323 if (strokeColor != QColorConstants::Transparent || info.strokeColor.isAnimated()) {
1324 stream() << "styleColor: \"" << strokeColor.name(QColor::HexArgb) << "\"";
1325 stream() << "style: Text.Outline";
1326 }
1327
1328 m_indentLevel--;
1329 stream() << "}";
1330
1331 generateNodeEnd(info);
1332}
1333
1334void QQuickQmlGenerator::generateUseNode(const UseNodeInfo &info)
1335{
1336 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1337 return;
1338
1339 if (info.stage == StructureNodeStage::Start) {
1340 stream() << "Item {";
1341 m_indentLevel++;
1342 generateNodeBase(info);
1343 } else {
1344 generateNodeEnd(info);
1345 }
1346}
1347
1348void QQuickQmlGenerator::generatePathContainer(const StructureNodeInfo &info)
1349{
1350 Q_UNUSED(info);
1351 stream() << shapeName() <<" {";
1352 m_indentLevel++;
1353 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::CurveRenderer))
1354 stream() << "preferredRendererType: Shape.CurveRenderer";
1355 if (m_flags.testFlag(QQuickVectorImageGenerator::GeneratorFlag::AsyncShapes))
1356 stream() << "asynchronous: true";
1357 m_indentLevel--;
1358
1359 m_inShapeItemLevel++;
1360}
1361
1362void QQuickQmlGenerator::generateAnimateMotionPath(const QString &targetName,
1363 const QQuickAnimatedProperty &property)
1364{
1365 if (!property.isAnimated())
1366 return;
1367
1368 QPainterPath path = property.defaultValue().value<QVariantList>().value(0).value<QPainterPath>();
1369 const QString mainAnimationId = targetName + QStringLiteral("_motion_interpolator");
1370 stream() << "PathInterpolator {";
1371 m_indentLevel++;
1372 stream() << "id: " << mainAnimationId;
1373 const QString svgPathString = QQuickVectorImageGenerator::Utils::toSvgString(path);
1374 stream() << "path: Path { PathSvg { path: \"" << svgPathString << "\" } }";
1375 m_indentLevel--;
1376 stream() << "}";
1377
1378 generatePropertyAnimation(property, mainAnimationId, QStringLiteral("progress"));
1379}
1380
1381void QQuickQmlGenerator::generateAnimatedPropertySetter(const QString &targetName,
1382 const QString &propertyName,
1383 const QVariant &value,
1384 const QQuickAnimatedProperty::PropertyAnimation &animation,
1385 int frameTime,
1386 int time,
1387 AnimationType animationType)
1388{
1389 if (frameTime > 0) {
1390 switch (animationType) {
1391 case AnimationType::Auto:
1392 if (value.typeId() == QMetaType::QColor)
1393 stream() << "ColorAnimation {";
1394 else
1395 stream() << "PropertyAnimation {";
1396 break;
1397 case AnimationType::ColorOpacity:
1398 stream() << "ColorOpacityAnimation {";
1399 break;
1400 };
1401 m_indentLevel++;
1402
1403 stream() << "duration: " << frameTime;
1404 stream() << "target: " << targetName;
1405 stream() << "property: \"" << propertyName << "\"";
1406 stream() << "to: ";
1407 if (value.typeId() == QMetaType::QVector3D) {
1408 const QVector3D &v = value.value<QVector3D>();
1409 stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
1410 } else if (value.typeId() == QMetaType::QColor) {
1411 stream(SameLine) << "\"" << value.toString() << "\"";
1412 } else {
1413 stream(SameLine) << value.toReal();
1414 }
1415 generateEasing(animation, time);
1416 m_indentLevel--;
1417 stream() << "}";
1418 } else {
1419 stream() << "ScriptAction {";
1420 m_indentLevel++;
1421 stream() << "script:" << targetName << "." << propertyName;
1422 if (animationType == AnimationType::ColorOpacity)
1423 stream(SameLine) << ".a";
1424
1425 stream(SameLine) << " = ";
1426 if (value.typeId() == QMetaType::QVector3D) {
1427 const QVector3D &v = value.value<QVector3D>();
1428 stream(SameLine) << "Qt.vector3d(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
1429 } else if (value.typeId() == QMetaType::QColor) {
1430 stream(SameLine) << "\"" << value.toString() << "\"";
1431 } else {
1432 stream(SameLine) << value.toReal();
1433 }
1434 m_indentLevel--;
1435 stream() << "}";
1436 }
1437}
1438
1439void QQuickQmlGenerator::generateAnimateTransform(const QString &targetName, const NodeInfo &info)
1440{
1441 if (!info.transform.isAnimated())
1442 return;
1443
1444 if (usingTimelineAnimation())
1445 return generateTransformTimeline(targetName, info);
1446
1447 const QString mainAnimationId = targetName
1448 + QStringLiteral("_transform_animation");
1449
1450 QString prefix;
1451 if (Q_UNLIKELY(!isRuntimeGenerator()))
1452 prefix = QStringLiteral(".animations");
1453 stream() << "Connections { target: " << m_topLevelIdString << prefix << "; function onRestart() {" << mainAnimationId << ".restart() } }";
1454
1455 stream() << "ParallelAnimation {";
1456 m_indentLevel++;
1457
1458 stream() << "id:" << mainAnimationId;
1459
1460 generateAnimationBindings();
1461 for (int groupIndex = 0; groupIndex < info.transform.animationGroupCount(); ++groupIndex) {
1462 int animationStart = info.transform.animationGroup(groupIndex);
1463 int nextAnimationStart = groupIndex + 1 < info.transform.animationGroupCount()
1464 ? info.transform.animationGroup(groupIndex + 1)
1465 : info.transform.animationCount();
1466
1467 // The first animation in the group holds the shared properties for the whole group
1468 const QQuickAnimatedProperty::PropertyAnimation &firstAnimation = info.transform.animation(animationStart);
1469 const bool freeze = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd;
1470 const bool replace = firstAnimation.flags & QQuickAnimatedProperty::PropertyAnimation::ReplacePreviousAnimations;
1471
1472 stream() << "SequentialAnimation {";
1473 m_indentLevel++;
1474
1475 const int startOffset = processAnimationTime(firstAnimation.startOffset);
1476 if (startOffset > 0)
1477 stream() << "PauseAnimation { duration: " << startOffset << " }";
1478
1479 const int repeatCount = firstAnimation.repeatCount;
1480 if (repeatCount < 0)
1481 stream() << "loops: Animation.Infinite";
1482 else
1483 stream() << "loops: " << repeatCount;
1484
1485 if (replace) {
1486 stream() << "ScriptAction {";
1487 m_indentLevel++;
1488
1489 stream() << "script: " << targetName << "_transform_base_group"
1490 << ".activateOverride(" << targetName << "_transform_group_" << groupIndex << ")";
1491
1492 m_indentLevel--;
1493 stream() << "}";
1494 }
1495
1496 stream() << "ParallelAnimation {";
1497 m_indentLevel++;
1498
1499 for (int i = animationStart; i < nextAnimationStart; ++i) {
1500 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i);
1501 if (animation.isConstant())
1502 continue;
1503 bool hasRotationCenter = false;
1504 if (animation.subtype == QTransform::TxRotate) {
1505 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
1506 const QPointF center = it->value<QVariantList>().value(0).value<QPointF>();
1507 if (!center.isNull()) {
1508 hasRotationCenter = true;
1509 break;
1510 }
1511 }
1512 }
1513
1514 stream() << "SequentialAnimation {";
1515 m_indentLevel++;
1516
1517 int previousTime = 0;
1518 QVariantList previousParameters;
1519 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
1520 const int time = it.key();
1521 const int frameTime = processAnimationTime(time - previousTime);
1522 const QVariantList &parameters = it.value().value<QVariantList>();
1523 if (parameters.isEmpty())
1524 continue;
1525
1526 if (parameters == previousParameters) {
1527 if (frameTime > 0)
1528 stream() << "PauseAnimation { duration: " << frameTime << " }";
1529 } else {
1530 stream() << "ParallelAnimation {";
1531 m_indentLevel++;
1532
1533 const QString propertyTargetName = targetName
1534 + QStringLiteral("_transform_")
1535 + QString::number(groupIndex)
1536 + QStringLiteral("_")
1537 + QString::number(i);
1538
1539 switch (animation.subtype) {
1540 case QTransform::TxTranslate:
1541 {
1542 const QPointF translation = parameters.first().value<QPointF>();
1543
1544 generateAnimatedPropertySetter(propertyTargetName,
1545 QStringLiteral("x"),
1546 translation.x(),
1547 animation,
1548 frameTime,
1549 time);
1550 generateAnimatedPropertySetter(propertyTargetName,
1551 QStringLiteral("y"),
1552 translation.y(),
1553 animation,
1554 frameTime,
1555 time);
1556 break;
1557 }
1558 case QTransform::TxScale:
1559 {
1560 const QPointF scale = parameters.first().value<QPointF>();
1561 generateAnimatedPropertySetter(propertyTargetName,
1562 QStringLiteral("xScale"),
1563 scale.x(),
1564 animation,
1565 frameTime,
1566 time);
1567 generateAnimatedPropertySetter(propertyTargetName,
1568 QStringLiteral("yScale"),
1569 scale.y(),
1570 animation,
1571 frameTime,
1572 time);
1573 break;
1574 }
1575 case QTransform::TxRotate:
1576 {
1577 Q_ASSERT(parameters.size() == 2);
1578 const qreal angle = parameters.value(1).toReal();
1579 if (hasRotationCenter) {
1580 const QPointF center = parameters.value(0).value<QPointF>();
1581 generateAnimatedPropertySetter(propertyTargetName,
1582 QStringLiteral("origin"),
1583 QVector3D(center.x(), center.y(), 0.0),
1584 animation,
1585 frameTime,
1586 time);
1587 }
1588 generateAnimatedPropertySetter(propertyTargetName,
1589 QStringLiteral("angle"),
1590 angle,
1591 animation,
1592 frameTime,
1593 time);
1594 break;
1595 }
1596 case QTransform::TxShear:
1597 {
1598 const QPointF skew = parameters.first().value<QPointF>();
1599
1600 generateAnimatedPropertySetter(propertyTargetName,
1601 QStringLiteral("xAngle"),
1602 skew.x(),
1603 animation,
1604 frameTime,
1605 time);
1606
1607 generateAnimatedPropertySetter(propertyTargetName,
1608 QStringLiteral("yAngle"),
1609 skew.y(),
1610 animation,
1611 frameTime,
1612 time);
1613 break;
1614 }
1615 default:
1616 Q_UNREACHABLE();
1617 }
1618
1619 m_indentLevel--;
1620 stream() << "}"; // Parallel key frame animation
1621 }
1622
1623 previousTime = time;
1624 previousParameters = parameters;
1625 }
1626
1627 m_indentLevel--;
1628 stream() << "}"; // Parallel key frame animation
1629 }
1630
1631 m_indentLevel--;
1632 stream() << "}"; // Parallel key frame animation
1633
1634 // If the animation ever finishes, then we add an action on the end that handles itsr
1635 // freeze state
1636 if (firstAnimation.repeatCount >= 0) {
1637 stream() << "ScriptAction {";
1638 m_indentLevel++;
1639
1640 stream() << "script: {";
1641 m_indentLevel++;
1642
1643 if (!freeze) {
1644 stream() << targetName << "_transform_base_group.deactivate("
1645 << targetName << "_transform_group_" << groupIndex << ")";
1646 } else if (!replace) {
1647 stream() << targetName << "_transform_base_group.deactivateOverride("
1648 << targetName << "_transform_group_" << groupIndex << ")";
1649 }
1650
1651 m_indentLevel--;
1652 stream() << "}";
1653
1654 m_indentLevel--;
1655 stream() << "}";
1656 }
1657
1658 m_indentLevel--;
1659 stream() << "}";
1660 }
1661
1662 m_indentLevel--;
1663 stream() << "}";
1664}
1665
1666void QQuickQmlGenerator::generateTransformTimeline(const QString &targetName, const NodeInfo &info)
1667{
1668 stream() << "Timeline {";
1669 m_indentLevel++;
1670 stream() << "currentFrame: " << info.transform.timelineReferenceId() << ".frameCounter";
1671 stream() << "enabled: true";
1672
1673 const int groupIndex = 0;
1674 for (int i = 0; i < info.transform.animationCount(); ++i) {
1675 const QQuickAnimatedProperty::PropertyAnimation &animation = info.transform.animation(i);
1676 if (animation.isConstant())
1677 continue;
1678 if (info.transform.animationGroupCount() > 1
1679 || animation.repeatCount != 1 || animation.startOffset
1680 || animation.flags != QQuickAnimatedProperty::PropertyAnimation::FreezeAtEnd) {
1681 qCWarning(lcQuickVectorImage) << "Feature not implemented in timeline xf animation mode, for"
1682 << targetName << "subtype" << animation.subtype;
1683 }
1684
1685 bool hasRotationCenter = false;
1686 if (animation.subtype == QTransform::TxRotate) {
1687 for (auto it = animation.frames.constBegin(); it != animation.frames.constEnd(); ++it) {
1688 const QPointF center = it->value<QVariantList>().value(0).value<QPointF>();
1689 if (!center.isNull()) {
1690 hasRotationCenter = true;
1691 break;
1692 }
1693 }
1694 }
1695
1696 auto pointFxExtractor = [](const QVariant &value) { return value.toPointF().x(); };
1697 auto pointFyExtractor = [](const QVariant &value) { return value.toPointF().y(); };
1698 auto realExtractor = [](const QVariant &value) { return value.toReal(); };
1699 auto pointFtoVector3dExtractor = [](const QVariant &v) { return QVector3D(v.toPointF()); };
1700
1701 const QString propertyTargetName = targetName
1702 + QStringLiteral("_transform_")
1703 + QString::number(groupIndex)
1704 + QStringLiteral("_")
1705 + QString::number(i);
1706
1707 switch (animation.subtype) {
1708 case QTransform::TxTranslate:
1709 generateTimelinePropertySetter(propertyTargetName, "x"_L1, animation, pointFxExtractor);
1710 generateTimelinePropertySetter(propertyTargetName, "y"_L1, animation, pointFyExtractor);
1711 break;
1712 case QTransform::TxScale:
1713 generateTimelinePropertySetter(propertyTargetName, "xScale"_L1, animation, pointFxExtractor);
1714 generateTimelinePropertySetter(propertyTargetName, "yScale"_L1, animation, pointFyExtractor);
1715 break;
1716 case QTransform::TxRotate:
1717 if (hasRotationCenter)
1718 generateTimelinePropertySetter(propertyTargetName, "origin"_L1, animation, pointFtoVector3dExtractor);
1719 generateTimelinePropertySetter(propertyTargetName, "angle"_L1, animation, realExtractor, 1);
1720 break;
1721 case QTransform::TxShear:
1722 generateTimelinePropertySetter(propertyTargetName, "xAngle"_L1, animation, pointFxExtractor);
1723 generateTimelinePropertySetter(propertyTargetName, "yAngle"_L1, animation, pointFyExtractor);
1724 break;
1725 }
1726 }
1727
1728 m_indentLevel--;
1729 stream() << "}";
1730}
1731
1732bool QQuickQmlGenerator::generateStructureNode(const StructureNodeInfo &info)
1733{
1734 if (Q_UNLIKELY(errorState() || !isNodeVisible(info)))
1735 return false;
1736
1737 const bool isPathContainer = !info.forceSeparatePaths && info.isPathContainer;
1738 if (info.stage == StructureNodeStage::Start) {
1739 if (!info.clipBox.isEmpty()) {
1740 stream() << "Item { // Clip";
1741
1742 m_indentLevel++;
1743 stream() << "width: " << info.clipBox.width();
1744 stream() << "height: " << info.clipBox.height();
1745 stream() << "clip: true";
1746 }
1747
1748 if (isPathContainer) {
1749 generatePathContainer(info);
1750 } else if (!info.customItemType.isEmpty()) {
1751 stream() << info.customItemType << " {";
1752 } else {
1753 stream() << "Item { // Structure node";
1754 }
1755 m_indentLevel++;
1756
1757 generateTimelineFields(info);
1758
1759 if (!info.viewBox.isEmpty()) {
1760 stream() << "transform: [";
1761 m_indentLevel++;
1762 bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y());
1763 if (translate)
1764 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
1765 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
1766 m_indentLevel--;
1767 stream() << "]";
1768 }
1769
1770 generateNodeBase(info);
1771 } else {
1772 generateNodeEnd(info);
1773 if (isPathContainer)
1774 m_inShapeItemLevel--;
1775
1776 if (!info.clipBox.isEmpty()) {
1777 m_indentLevel--;
1778 stream() << "}";
1779 }
1780 }
1781
1782 return true;
1783}
1784
1785bool QQuickQmlGenerator::generateMaskNode(const MaskNodeInfo &info)
1786{
1787 if (Q_UNLIKELY(errorState()))
1788 return false;
1789
1790 // Generate an invisible item subtree which can be used in ShaderEffectSource
1791 if (info.stage == StructureNodeStage::End) {
1792 // Generate code to add after defs block
1793 startDefsSuffixBlock();
1794 stream() << "Loader {";
1795 m_indentLevel++;
1796
1797 stream() << "id: " << info.id; // This is in a different scope, so we can reuse the ID
1798 stream() << "sourceComponent: " << info.id << "_container";
1799 stream() << "width: item !== null ? item.originalBounds.width : 0";
1800 stream() << "height: item !== null ? item.originalBounds.height : 0";
1801
1802 if (info.boundsReferenceId.isEmpty()) {
1803 stream() << "property real maskX: " << info.maskRect.left();
1804 stream() << "property real maskY: " << info.maskRect.top();
1805 stream() << "property real maskWidth: " << info.maskRect.width();
1806 stream() << "property real maskHeight: " << info.maskRect.height();
1807 }
1808
1809 stream() << "function maskRect(otherX, otherY, otherWidth, otherHeight) {";
1810 m_indentLevel++;
1811
1812 stream() << "return ";
1813 if (!info.boundsReferenceId.isEmpty()) {
1814 stream(SameLine) << info.boundsReferenceId << ".originalBounds";
1815 } else if (info.isMaskRectRelativeCoordinates) {
1816 stream(SameLine)
1817 << "Qt.rect("
1818 << info.id << ".maskX * otherWidth + otherX,"
1819 << info.id << ".maskY * otherHeight + otherY,"
1820 << info.id << ".maskWidth * otherWidth,"
1821 << info.id << ".maskHeight * otherHeight)";
1822 } else {
1823 stream(SameLine)
1824 << "Qt.rect("
1825 << info.id << ".maskX, "
1826 << info.id << ".maskY, "
1827 << info.id << ".maskWidth, "
1828 << info.id << ".maskHeight)";
1829 }
1830
1831 m_indentLevel--;
1832 stream() << "}";
1833
1834 m_indentLevel--;
1835 stream() << "}";
1836
1837 endDefsSuffixBlock();
1838 }
1839
1840 return true;
1841}
1842
1843void QQuickQmlGenerator::generateFilterNode(const FilterNodeInfo &info)
1844{
1845 if (Q_UNLIKELY(errorState()))
1846 return;
1847
1848 stream() << "Item {";
1849 m_indentLevel++;
1850
1851 generateNodeBase(info);
1852
1853 stream() << "property real originalWidth: filterSourceItem.sourceItem.originalBounds.width";
1854 stream() << "property real originalHeight: filterSourceItem.sourceItem.originalBounds.height";
1855 stream() << "property rect filterRect: " << info.id << "_filterParameters"
1856 << ".adaptToFilterRect(0, 0, originalWidth, originalHeight)";
1857
1858 for (qsizetype i = 0; i < info.steps.size();)
1859 i = generateFilterStep(info, i);
1860
1861 // Generate code to be added after defs block
1862 startDefsSuffixBlock();
1863 stream() << "QtObject {";
1864 m_indentLevel++;
1865
1866 stream() << "id: " << info.id << "_filterParameters";
1867 stream() << "property int wrapMode: ";
1868 if (info.wrapMode == QSGTexture::Repeat)
1869 stream(SameLine) << "ShaderEffectSource.Repeat";
1870 else
1871 stream(SameLine) << "ShaderEffectSource.ClampToEdge";
1872
1873 stream() << "property rect filterRect: Qt.rect("
1874 << info.filterRect.x() << ", "
1875 << info.filterRect.y() << ", "
1876 << info.filterRect.width() << ", "
1877 << info.filterRect.height() << ")";
1878
1879 stream() << "function adaptToFilterRect(sx, sy, sw, sh) {";
1880 m_indentLevel++;
1881
1882 if (info.csFilterRect == FilterNodeInfo::CoordinateSystem::Absolute) {
1883 stream() << "return Qt.rect(filterRect.x, filterRect.y, filterRect.width, filterRect.height)";
1884 } else {
1885 stream() << "return Qt.rect(sx + sw * filterRect.x, sy + sh * filterRect.y, sw * filterRect.width, sh * filterRect.height)";
1886 }
1887
1888 m_indentLevel--;
1889 stream() << "}";
1890
1891 m_indentLevel--;
1892 stream() << "}";
1893 endDefsSuffixBlock();
1894
1895 generateNodeEnd(info);
1896}
1897
1898qsizetype QQuickQmlGenerator::generateFilterStep(const FilterNodeInfo &info,
1899 qsizetype stepIndex)
1900{
1901 const FilterNodeInfo::FilterStep &step = info.steps.at(stepIndex);
1902 const QString primitiveId = info.id + QStringLiteral("_primitive") + QString::number(stepIndex);
1903
1904 stepIndex++;
1905
1906 QString inputId = step.input1 != FilterNodeInfo::FilterInput::SourceColor
1907 ? step.namedInput1
1908 : QStringLiteral("filterSourceItem");
1909
1910 bool isComposite = false;
1911 switch (step.filterType) {
1912 case FilterNodeInfo::Type::Merge:
1913 {
1914 const int maxNodeCount = 8;
1915
1916 // Find all nodes for this merge
1917 QList<std::pair<FilterNodeInfo::FilterInput, QString> > inputs;
1918 for (; stepIndex < info.steps.size(); ++stepIndex) {
1919 const FilterNodeInfo::FilterStep &nodeStep = info.steps.at(stepIndex);
1920 if (nodeStep.filterType != FilterNodeInfo::Type::MergeNode)
1921 break;
1922
1923 inputs.emplace_back(nodeStep.input1, nodeStep.namedInput1);
1924 }
1925
1926 if (inputs.size() > maxNodeCount) {
1927 qCWarning(lcQuickVectorImage) << "Maximum of" << maxNodeCount
1928 << "nodes exceeded in merge effect.";
1929 }
1930
1931 if (inputs.isEmpty()) {
1932 qCWarning(lcQuickVectorImage) << "Merge effect requires at least one node.";
1933 break;
1934 }
1935
1936 stream() << "ShaderEffect {";
1937 m_indentLevel++;
1938
1939 stream() << "id: " << primitiveId;
1940 stream() << "visible: false";
1941
1942 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/femerge.frag.qsb\"";
1943 stream() << "width: source1.width";
1944 stream() << "height: source1.height";
1945 stream() << "property int sourceCount: " << std::min(qsizetype(8), inputs.size());
1946
1947 for (int i = 0; i < maxNodeCount; ++i) {
1948 auto input = i < inputs.size()
1949 ? inputs.at(i)
1950 : std::pair(FilterNodeInfo::FilterInput::None, u"null"_s);
1951
1952 QString inputId = input.first != FilterNodeInfo::FilterInput::SourceColor
1953 ? input.second
1954 : QStringLiteral("filterSourceItem");
1955
1956 stream() << "property var source" << (i + 1) << ": " << inputId;
1957 }
1958
1959 m_indentLevel--;
1960 stream() << "}";
1961
1962 break;
1963 }
1964 case FilterNodeInfo::Type::CompositeOver:
1965 case FilterNodeInfo::Type::CompositeOut:
1966 case FilterNodeInfo::Type::CompositeIn:
1967 case FilterNodeInfo::Type::CompositeXor:
1968 case FilterNodeInfo::Type::CompositeAtop:
1969 case FilterNodeInfo::Type::CompositeArithmetic:
1970 case FilterNodeInfo::Type::CompositeLighter:
1971 isComposite = true;
1972 Q_FALLTHROUGH();
1973
1974 case FilterNodeInfo::Type::BlendNormal:
1975 case FilterNodeInfo::Type::BlendMultiply:
1976 case FilterNodeInfo::Type::BlendScreen:
1977 case FilterNodeInfo::Type::BlendDarken:
1978 case FilterNodeInfo::Type::BlendLighten:
1979 {
1980 stream() << "ShaderEffect {";
1981 m_indentLevel++;
1982
1983 QString input2Id = step.input2 != FilterNodeInfo::FilterInput::SourceColor
1984 ? step.namedInput2
1985 : QStringLiteral("filterSourceItem");
1986
1987 stream() << "id: " << primitiveId;
1988 stream() << "visible: false";
1989
1990 QString shader;
1991 switch (step.filterType) {
1992 case FilterNodeInfo::Type::CompositeOver:
1993 shader = QStringLiteral("fecompositeover");
1994 break;
1995 case FilterNodeInfo::Type::CompositeOut:
1996 shader = QStringLiteral("fecompositeout");
1997 break;
1998 case FilterNodeInfo::Type::CompositeIn:
1999 shader = QStringLiteral("fecompositein");
2000 break;
2001 case FilterNodeInfo::Type::CompositeXor:
2002 shader = QStringLiteral("fecompositexor");
2003 break;
2004 case FilterNodeInfo::Type::CompositeAtop:
2005 shader = QStringLiteral("fecompositeatop");
2006 break;
2007 case FilterNodeInfo::Type::CompositeArithmetic:
2008 shader = QStringLiteral("fecompositearithmetic");
2009 break;
2010 case FilterNodeInfo::Type::CompositeLighter:
2011 shader = QStringLiteral("fecompositelighter");
2012 break;
2013 case FilterNodeInfo::Type::BlendNormal:
2014 shader = QStringLiteral("feblendnormal");
2015 break;
2016 case FilterNodeInfo::Type::BlendMultiply:
2017 shader = QStringLiteral("feblendmultiply");
2018 break;
2019 case FilterNodeInfo::Type::BlendScreen:
2020 shader = QStringLiteral("feblendscreen");
2021 break;
2022 case FilterNodeInfo::Type::BlendDarken:
2023 shader = QStringLiteral("feblenddarken");
2024 break;
2025 case FilterNodeInfo::Type::BlendLighten:
2026 shader = QStringLiteral("feblendlighten");
2027 break;
2028 default:
2029 Q_UNREACHABLE();
2030 }
2031
2032 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/"
2033 << shader << ".frag.qsb\"";
2034 stream() << "property var source: " << inputId;
2035 stream() << "property var source2: " << input2Id;
2036 stream() << "width: source.width";
2037 stream() << "height: source.height";
2038
2039 if (isComposite) {
2040 QVector4D k = step.filterParameter.value<QVector4D>();
2041 stream() << "property var k: Qt.vector4d("
2042 << k.x() << ", "
2043 << k.y() << ", "
2044 << k.z() << ", "
2045 << k.w() << ")";
2046 }
2047
2048 m_indentLevel--;
2049 stream() << "}";
2050
2051 break;
2052
2053 }
2054 case FilterNodeInfo::Type::Flood:
2055 {
2056 stream() << "Rectangle {";
2057 m_indentLevel++;
2058
2059 stream() << "id: " << primitiveId;
2060 stream() << "visible: false";
2061
2062 stream() << "width: " << inputId << ".width";
2063 stream() << "height: " << inputId << ".height";
2064
2065 QColor floodColor = step.filterParameter.value<QColor>();
2066 stream() << "color: \"" << floodColor.name(QColor::HexArgb) << "\"";
2067
2068 m_indentLevel--;
2069 stream() << "}";
2070
2071 break;
2072 }
2073 case FilterNodeInfo::Type::ColorMatrix:
2074 {
2075 stream() << "ShaderEffect {";
2076 m_indentLevel++;
2077
2078 stream() << "id: " << primitiveId;
2079 stream() << "visible: false";
2080
2081 stream() << "fragmentShader: \"qrc:/qt-project.org/quickvectorimage/helpers/shaders_ng/fecolormatrix.frag.qsb\"";
2082 stream() << "property var source: " << inputId;
2083 stream() << "width: source.width";
2084 stream() << "height: source.height";
2085
2086 QGenericMatrix<5, 5, qreal> matrix = step.filterParameter.value<QGenericMatrix<5, 5, qreal> >();
2087 for (int row = 0; row < 4; ++row) { // Last row is ignored
2088
2089 // Qt SVG stores rows as columns, so we flip the coordinates
2090 for (int col = 0; col < 5; ++col)
2091 stream() << "property real m_" << row << "_" << col << ": " << matrix(col, row);
2092 }
2093
2094 m_indentLevel--;
2095 stream() << "}";
2096
2097 break;
2098 }
2099
2100 case FilterNodeInfo::Type::Offset:
2101 {
2102 stream() << "ShaderEffectSource {";
2103 m_indentLevel++;
2104
2105 stream() << "id: " << primitiveId;
2106 stream() << "visible: false";
2107 stream() << "sourceItem: " << inputId;
2108 stream() << "width: sourceItem.width + offset.x";
2109 stream() << "height: sourceItem.height + offset.y";
2110
2111 QVector2D offset = step.filterParameter.value<QVector2D>();
2112 stream() << "property vector2d offset: Qt.vector2d(";
2113 if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Absolute)
2114 stream(SameLine) << offset.x() << " / width, " << offset.y() << " / height)";
2115 else
2116 stream(SameLine) << offset.x() << ", " << offset.y() << ")";
2117
2118 stream() << "sourceRect: Qt.rect(-offset.x, -offset.y, width, height)";
2119
2120 stream() << "ItemSpy {";
2121 m_indentLevel++;
2122 stream() << "id: " << primitiveId << "_offset_itemspy";
2123 stream() << "anchors.fill: parent";
2124
2125 m_indentLevel--;
2126 stream() << "}";
2127 stream() << "textureSize: " << primitiveId << "_offset_itemspy.requiredTextureSize";
2128
2129
2130 m_indentLevel--;
2131 stream() << "}";
2132
2133 break;
2134 }
2135
2136 case FilterNodeInfo::Type::GaussianBlur:
2137 {
2138 // Approximate blur effect with fast blur
2139 stream() << "MultiEffect {";
2140 m_indentLevel++;
2141
2142 stream() << "id: " << primitiveId;
2143 stream() << "visible: false";
2144
2145 stream() << "source: " << inputId;
2146 stream() << "blurEnabled: true";
2147 stream() << "width: source.width";
2148 stream() << "height: source.height";
2149
2150 const qreal maxDeviation(12.0); // Decided experimentally
2151 const qreal deviation = step.filterParameter.toReal();
2152 if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Relative)
2153 stream() << "blur: Math.min(1.0, " << deviation << " * filterSourceItem.width / " << maxDeviation << ")";
2154 else
2155 stream() << "blur: " << std::min(qreal(1.0), deviation / maxDeviation);
2156 stream() << "blurMax: 64";
2157
2158 m_indentLevel--;
2159 stream() << "}";
2160
2161 break;
2162 }
2163 default:
2164 qCWarning(lcQuickVectorImage) << "Unhandled filter type: " << int(step.filterType);
2165 // Dummy item to avoid empty component
2166 stream() << "Item { id: " << primitiveId << " }";
2167 break;
2168 }
2169
2170 // Sample correct part of primitive
2171 stream() << "ShaderEffectSource {";
2172 m_indentLevel++;
2173
2174 stream() << "id: " << step.outputName;
2175 if (stepIndex < info.steps.size())
2176 stream() << "visible: false";
2177
2178 qreal x1, x2, y1, y2;
2179 step.filterPrimitiveRect.getCoords(&x1, &y1, &x2, &y2);
2180 if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Absolute) {
2181 stream() << "property real fpx1: " << x1;
2182 stream() << "property real fpy1: " << y1;
2183 stream() << "property real fpx2: " << x2;
2184 stream() << "property real fpy2: " << y2;
2185 } else if (step.csFilterParameter == FilterNodeInfo::CoordinateSystem::Relative) {
2186 // If they are relative, they are actually in the coordinate system
2187 // of the original bounds of the filtered item. This means we first have to convert
2188 // them to the filter's coordinate system first.
2189 stream() << "property real fpx1: " << x1 << " * filterSourceItem.sourceItem.originalBounds.width";
2190 stream() << "property real fpy1: " << y1 << " * filterSourceItem.sourceItem.originalBounds.height";
2191 stream() << "property real fpx2: " << x2 << " * filterSourceItem.sourceItem.originalBounds.width";
2192 stream() << "property real fpy2: " << y2 << " * filterSourceItem.sourceItem.originalBounds.height";
2193 } else { // Just match filter rect
2194 stream() << "property real fpx1: parent.filterRect.x";
2195 stream() << "property real fpy1: parent.filterRect.y";
2196 stream() << "property real fpx2: parent.filterRect.x + parent.filterRect.width";
2197 stream() << "property real fpy2: parent.filterRect.y + parent.filterRect.height";
2198 }
2199
2200 stream() << "sourceItem: " << primitiveId;
2201 stream() << "sourceRect: Qt.rect(fpx1 - parent.filterRect.x, fpy1 - parent.filterRect.y, width, height)";
2202
2203 stream() << "x: fpx1";
2204 stream() << "y: fpy1";
2205 stream() << "width: " << "fpx2 - fpx1";
2206 stream() << "height: " << "fpy2 - fpy1";
2207
2208 stream() << "ItemSpy {";
2209 m_indentLevel++;
2210 stream() << "id: " << primitiveId << "_itemspy";
2211 stream() << "anchors.fill: parent";
2212
2213 m_indentLevel--;
2214 stream() << "}";
2215 stream() << "textureSize: " << primitiveId << "_itemspy.requiredTextureSize";
2216
2217 m_indentLevel--;
2218 stream() << "}";
2219
2220 return stepIndex;
2221}
2222
2223void QQuickQmlGenerator::generateTimelineFields(const StructureNodeInfo &info)
2224{
2225 if (usingTimelineAnimation() && info.timelineInfo) {
2226 QString frameCounterRef = info.timelineInfo->frameCounterReference
2227 + QStringLiteral(".frameCounter");
2228
2229 if (info.timelineInfo->generateVisibility) {
2230 stream() << "visible: " << frameCounterRef << " >= " << info.timelineInfo->startFrame
2231 << " && " << frameCounterRef << " < " << info.timelineInfo->endFrame;
2232 }
2233
2234 if (info.timelineInfo->generateFrameCounter) {
2235 stream() << "property real frameCounter: ";
2236 if (info.timelineInfo->frameCounterMapper.isAnimated()) {
2237 // Animated frame counter remapping; overrides offset / multiplier
2238 stream(SameLine) << "0";
2239 generatePropertyTimeline(info.timelineInfo->frameCounterMapper, info.id, "frameCounter"_L1);
2240 } else {
2241 const auto offset = info.timelineInfo->frameCounterOffset;
2242 const auto multiplier = info.timelineInfo->frameCounterMultiplier;
2243 const bool needsParens = (offset && multiplier);
2244 if (needsParens)
2245 stream(SameLine) << "(";
2246 stream(SameLine) << frameCounterRef;
2247 if (offset)
2248 stream(SameLine) << (offset > 0 ? " + " : " - ") << qAbs(offset);
2249 if (needsParens)
2250 stream(SameLine) << ")";
2251 if (multiplier)
2252 stream(SameLine) << " * " << multiplier;
2253 }
2254 }
2255 }
2256}
2257
2258bool QQuickQmlGenerator::generatePatternNode(const PatternNodeInfo &info)
2259{
2260 if (info.stage == StructureNodeStage::Start) {
2261 return true;
2262 } else {
2263 startDefsSuffixBlock();
2264 stream() << "Loader {";
2265 m_indentLevel++;
2266
2267 stream() << "id: " << info.id; // This is in a different scope, so we can reuse the ID
2268 stream() << "sourceComponent: " << info.id << "_container";
2269 stream() << "width: item !== null ? item.originalBounds.width : 0";
2270 stream() << "height: item !== null ? item.originalBounds.height : 0";
2271 stream() << "visible: false";
2272 stream() << "function sourceRect(targetWidth, targetHeight) {";
2273 m_indentLevel++;
2274
2275 stream() << "return Qt.rect(0, 0, ";
2276 if (!info.isPatternRectRelativeCoordinates) {
2277 stream(SameLine) << info.patternRect.width() << ", "
2278 << info.patternRect.height();
2279 } else {
2280 stream(SameLine) << info.patternRect.width() << " * targetWidth, "
2281 << info.patternRect.height() << " * targetHeight";
2282 }
2283 stream(SameLine) << ")";
2284 m_indentLevel--;
2285 stream() << "}";
2286
2287 stream() << "function sourceOffset(targetWidth, targetHeight) {";
2288 m_indentLevel++;
2289
2290 stream() << "return Qt.vector3d(";
2291 if (!info.isPatternRectRelativeCoordinates) {
2292 stream(SameLine) << info.patternRect.x() << ", "
2293 << info.patternRect.y() << ", ";
2294 } else {
2295 stream(SameLine) << info.patternRect.x() << " * targetWidth, "
2296 << info.patternRect.y() << " * targetHeight, ";
2297 }
2298 stream(SameLine) << "0.0)";
2299 m_indentLevel--;
2300 stream() << "}";
2301
2302
2303 m_indentLevel--;
2304 stream() << "}";
2305
2306 endDefsSuffixBlock();
2307
2308 return true;
2309 }
2310}
2311
2312void QQuickQmlGenerator::generateDefsInstantiationNode(const StructureNodeInfo &info)
2313{
2314 if (Q_UNLIKELY(errorState()))
2315 return;
2316
2317 if (info.stage == StructureNodeStage::Start) {
2318 stream() << "Loader {";
2319 m_indentLevel++;
2320
2321 stream() << "sourceComponent: " << info.defsId << "_container";
2322 generateNodeBase(info);
2323 generateTimelineFields(info);
2324 } else {
2325 m_indentLevel--;
2326 stream() << "}";
2327 }
2328}
2329
2330bool QQuickQmlGenerator::generateMarkerNode(const MarkerNodeInfo &info)
2331{
2332 if (info.stage == StructureNodeStage::Start) {
2333 startDefsSuffixBlock();
2334 stream() << "QtObject {";
2335 m_indentLevel++;
2336
2337 stream() << "id: " << info.id << "_markerParameters";
2338
2339 stream() << "property bool startReversed: ";
2340 if (info.orientation == MarkerNodeInfo::Orientation::AutoStartReverse)
2341 stream(SameLine) << "true";
2342 else
2343 stream(SameLine) << "false";
2344
2345 stream() << "function autoAngle(adaptedAngle) {";
2346 m_indentLevel++;
2347 if (info.orientation == MarkerNodeInfo::Orientation::Value)
2348 stream() << "return " << info.angle;
2349 else
2350 stream() << "return adaptedAngle";
2351 m_indentLevel--;
2352 stream() << "}";
2353
2354 m_indentLevel--;
2355 stream() << "}";
2356 endDefsSuffixBlock();
2357
2358 if (!info.clipBox.isEmpty()) {
2359 stream() << "Item {";
2360 m_indentLevel++;
2361
2362 stream() << "x: " << info.clipBox.x();
2363 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2364 stream(SameLine) << " * strokeWidth";
2365 stream() << "y: " << info.clipBox.y();
2366 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2367 stream(SameLine) << " * strokeWidth";
2368 stream() << "width: " << info.clipBox.width();
2369 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2370 stream(SameLine) << " * strokeWidth";
2371 stream() << "height: " << info.clipBox.height();
2372 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2373 stream(SameLine) << " * strokeWidth";
2374 stream() << "clip: true";
2375 }
2376
2377 stream() << "Item {";
2378 m_indentLevel++;
2379
2380 if (!info.clipBox.isEmpty()) {
2381 stream() << "x: " << -info.clipBox.x();
2382 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2383 stream(SameLine) << " * strokeWidth";
2384 stream() << "y: " << -info.clipBox.y();
2385 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2386 stream(SameLine) << " * strokeWidth";
2387 }
2388
2389 stream() << "id: " << info.id;
2390
2391 stream() << "property real markerWidth: " << info.markerSize.width();
2392 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2393 stream(SameLine) << " * strokeWidth";
2394
2395 stream() << "property real markerHeight: " << info.markerSize.height();
2396 if (info.markerUnits == MarkerNodeInfo::MarkerUnits::StrokeWidth)
2397 stream(SameLine) << " * strokeWidth";
2398
2399 stream() << "function calculateMarkerScale(w, h) {";
2400 m_indentLevel++;
2401
2402 stream() << "var scaleX = 1.0";
2403 stream() << "var scaleY = 1.0";
2404 stream() << "var offsetX = 0.0";
2405 stream() << "var offsetY = 0.0";
2406 if (info.viewBox.width() > 0)
2407 stream() << "if (w > 0) scaleX = w / " << info.viewBox.width();
2408 if (info.viewBox.height() > 0)
2409 stream() << "if (h > 0) scaleY = h / " << info.viewBox.height();
2410
2411 if (info.preserveAspectRatio & MarkerNodeInfo::xyMask) {
2412 stream() << "if (scaleX != scaleY) {";
2413 m_indentLevel++;
2414
2415 if (info.preserveAspectRatio & MarkerNodeInfo::meet)
2416 stream() << "scaleX = scaleY = Math.min(scaleX, scaleY)";
2417 else
2418 stream() << "scaleX = scaleY = Math.max(scaleX, scaleY)";
2419
2420 QString overflowX = QStringLiteral("scaleX * %1 - w").arg(info.viewBox.width());
2421 QString overflowY = QStringLiteral("scaleY * %1 - h").arg(info.viewBox.height());
2422
2423 const quint8 xRatio = info.preserveAspectRatio & MarkerNodeInfo::xMask;
2424 if (xRatio == MarkerNodeInfo::xMid)
2425 stream() << "offsetX -= " << overflowX << " / 2";
2426 else if (xRatio == MarkerNodeInfo::xMax)
2427 stream() << "offsetX -= " << overflowX;
2428
2429 const quint8 yRatio = info.preserveAspectRatio & MarkerNodeInfo::yMask;
2430 if (yRatio == MarkerNodeInfo::yMid)
2431 stream() << "offsetY -= " << overflowY << " / 2";
2432 else if (yRatio == MarkerNodeInfo::yMax)
2433 stream() << "offsetY -= " << overflowY;
2434
2435 m_indentLevel--;
2436 stream() << "}";
2437 }
2438
2439 stream() << "return Qt.vector4d("
2440 << "offsetX - " << info.anchorPoint.x() << " * scaleX, "
2441 << "offsetY - " << info.anchorPoint.y() << " * scaleY, "
2442 << "scaleX, "
2443 << "scaleY)";
2444
2445 m_indentLevel--;
2446 stream() << "}";
2447
2448 stream() << "property vector4d markerScale: calculateMarkerScale(markerWidth, markerHeight)";
2449
2450 stream() << "transform: [";
2451 m_indentLevel++;
2452
2453 stream() << "Scale { xScale: " << info.id << ".markerScale.z; yScale: " << info.id << ".markerScale.w },";
2454 stream() << "Translate { x: " << info.id << ".markerScale.x; y: " << info.id << ".markerScale.y }";
2455
2456 m_indentLevel--;
2457 stream() << "]";
2458
2459 } else {
2460 generateNodeEnd(info);
2461
2462 if (!info.clipBox.isEmpty()) {
2463 m_indentLevel--;
2464 stream() << "}";
2465 }
2466 }
2467
2468 return true;
2469}
2470
2471bool QQuickQmlGenerator::generateRootNode(const StructureNodeInfo &info)
2472{
2473 if (Q_UNLIKELY(errorState()))
2474 return false;
2475
2476 const QStringList comments = m_commentString.split(u'\n');
2477
2478 if (!isNodeVisible(info)) {
2479 m_indentLevel = 0;
2480
2481 if (comments.isEmpty()) {
2482 stream() << "// Generated from SVG";
2483 } else {
2484 for (const auto &comment : comments)
2485 stream() << "// " << comment;
2486 }
2487
2488 stream() << "import QtQuick";
2489 stream() << "import QtQuick.Shapes" << Qt::endl;
2490 stream() << "Item {";
2491 m_indentLevel++;
2492
2493 double w = info.size.width();
2494 double h = info.size.height();
2495 if (w > 0)
2496 stream() << "implicitWidth: " << w;
2497 if (h > 0)
2498 stream() << "implicitHeight: " << h;
2499
2500 m_indentLevel--;
2501 stream() << "}";
2502
2503 return false;
2504 }
2505
2506 if (info.stage == StructureNodeStage::Start) {
2507 m_indentLevel = 0;
2508
2509 if (comments.isEmpty())
2510 stream() << "// Generated from SVG";
2511 else
2512 for (const auto &comment : comments)
2513 stream() << "// " << comment;
2514
2515 stream() << "import QtQuick";
2516 stream() << "import QtQuick.VectorImage";
2517 stream() << "import QtQuick.VectorImage.Helpers";
2518 stream() << "import QtQuick.Shapes";
2519 stream() << "import QtQuick.Effects";
2520 if (usingTimelineAnimation())
2521 stream() << "import QtQuick.Timeline";
2522
2523 for (const auto &import : std::as_const(m_extraImports))
2524 stream() << "import " << import;
2525
2526 stream() << Qt::endl << "Item {";
2527 m_indentLevel++;
2528
2529 double w = info.size.width();
2530 double h = info.size.height();
2531 if (w > 0)
2532 stream() << "implicitWidth: " << w;
2533 if (h > 0)
2534 stream() << "implicitHeight: " << h;
2535
2536 if (Q_UNLIKELY(!isRuntimeGenerator())) {
2537 stream() << "component AnimationsInfo : QtObject";
2538 stream() << "{";
2539 m_indentLevel++;
2540 }
2541
2542 stream() << "property bool paused: false";
2543 stream() << "property int loops: 1";
2544 stream() << "signal restart()";
2545
2546 if (Q_UNLIKELY(!isRuntimeGenerator())) {
2547 m_indentLevel--;
2548 stream() << "}";
2549 stream() << "property AnimationsInfo animations : AnimationsInfo {}";
2550 }
2551
2552 stream() << "Item {";
2553 m_indentLevel++;
2554 stream() << "width: 1";
2555 stream() << "height: 1";
2556
2557 stream() << "ItemSpy { id: __qt_toplevel_scale_itemspy; anchors.fill: parent }";
2558
2559 m_indentLevel--;
2560 stream() << "}";
2561
2562 if (!info.viewBox.isEmpty()) {
2563 stream() << "transform: [";
2564 m_indentLevel++;
2565 bool translate = !qFuzzyIsNull(info.viewBox.x()) || !qFuzzyIsNull(info.viewBox.y());
2566 if (translate)
2567 stream() << "Translate { x: " << -info.viewBox.x() << "; y: " << -info.viewBox.y() << " },";
2568 stream() << "Scale { xScale: width / " << info.viewBox.width() << "; yScale: height / " << info.viewBox.height() << " }";
2569 m_indentLevel--;
2570 stream() << "]";
2571 }
2572
2573 if (!info.forceSeparatePaths && info.isPathContainer) {
2574 m_topLevelIdString = QStringLiteral("__qt_toplevel");
2575 stream() << "id: " << m_topLevelIdString;
2576
2577 generatePathContainer(info);
2578 m_indentLevel++;
2579
2580 generateNodeBase(info);
2581 } else {
2582 m_topLevelIdString = generateNodeBase(info);
2583 if (m_topLevelIdString.isEmpty())
2584 qCWarning(lcQuickVectorImage) << "No ID specified for top level item";
2585 }
2586
2587 if (usingTimelineAnimation() && info.timelineInfo) {
2588 stream() << "property real frameCounter: " << info.timelineInfo->startFrame;
2589 stream() << "NumberAnimation on frameCounter {";
2590 m_indentLevel++;
2591 stream() << "from: " << info.timelineInfo->startFrame;
2592 stream() << "to: " << info.timelineInfo->endFrame - 0.01;
2593 stream() << "duration: " << processAnimationTime(info.timelineInfo->duration);
2594 generateAnimationBindings();
2595 m_indentLevel--;
2596 stream() << "}";
2597 stream() << "visible: frameCounter >= " << info.timelineInfo->startFrame
2598 << " && frameCounter < " << info.timelineInfo->endFrame;
2599 }
2600 } else {
2601 if (m_inShapeItemLevel > 0) {
2602 m_inShapeItemLevel--;
2603 m_indentLevel--;
2604 stream() << "}";
2605 }
2606
2607 for (const auto [coords, id] : m_easings.asKeyValueRange()) {
2608 stream() << "readonly property easingCurve " << id << ": ({ type: Easing.BezierSpline, bezierCurve: [ ";
2609 for (auto coord : coords)
2610 stream(SameLine) << coord << ", ";
2611 stream(SameLine) << "1, 1 ] })";
2612 }
2613
2614 generateNodeEnd(info);
2615 stream().flush();
2616 }
2617
2618 return true;
2619}
2620
2621void QQuickQmlGenerator::startDefsSuffixBlock()
2622{
2623 int tmp = m_oldIndentLevels.top();
2624 m_oldIndentLevels.push(m_indentLevel);
2625 m_indentLevel = tmp;
2626 m_stream.setString(&m_defsSuffix);
2627}
2628
2629void QQuickQmlGenerator::endDefsSuffixBlock()
2630{
2631 m_indentLevel = m_oldIndentLevels.pop();
2632 m_stream.setDevice(&m_result);
2633}
2634
2635QStringView QQuickQmlGenerator::indent()
2636{
2637 int indentWidth = m_indentLevel * 4;
2638 if (indentWidth > m_indentString.size())
2639 m_indentString.fill(QLatin1Char(' '), indentWidth * 2);
2640 return QStringView(m_indentString).first(indentWidth);
2641}
2642
2643QTextStream &QQuickQmlGenerator::stream(int flags)
2644{
2645 if (m_stream.device() == nullptr && m_stream.string() == nullptr)
2646 m_stream.setDevice(&m_result);
2647 else if (!(flags & StreamFlags::SameLine))
2648 m_stream << Qt::endl << indent();
2649
2650 static qint64 maxBufferSize = qEnvironmentVariableIntegerValue("QT_QUICKVECTORIMAGE_MAX_BUFFER").value_or(64 << 20); // 64MB
2651 if (m_stream.device()) {
2652 if (Q_UNLIKELY(!checkSanityLimit(m_stream.device()->size(), maxBufferSize, "buffer size"_L1)))
2653 m_stream.device()->reset();
2654 } else {
2655 if (Q_UNLIKELY(!checkSanityLimit(m_stream.string()->size(), maxBufferSize, "buffer string size"_L1)))
2656 m_stream.string()->clear();
2657 }
2658
2659 return m_stream;
2660}
2661
2662const char *QQuickQmlGenerator::shapeName() const
2663{
2664 return m_shapeTypeName.isEmpty() ? "Shape" : m_shapeTypeName.constData();
2665}
2666
2667QT_END_NAMESPACE
Combined button and popup list for selecting options.
static QString sanitizeString(const QString &input)
static int processAnimationTime(int time)