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