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