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