From 5b573b69963f6598b7bb490184ce977ed0f60e83 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 28 Feb 2025 02:19:00 +1000 Subject: [PATCH] Canvas draw commands, broken script serialisation --- .../entities/src/CanvasEntityItem.cpp.in | 177 +++++++- libraries/entities/src/CanvasEntityItem.h.in | 13 +- .../entities/src/EntityScriptingInterface.cpp | 14 +- .../entities/src/EntityScriptingInterface.h | 38 +- libraries/script-engine/src/CanvasCommand.cpp | 429 ++++++++++++++++++ libraries/script-engine/src/CanvasCommand.h | 240 ++++++++++ 6 files changed, 881 insertions(+), 30 deletions(-) create mode 100644 libraries/script-engine/src/CanvasCommand.cpp create mode 100644 libraries/script-engine/src/CanvasCommand.h diff --git a/libraries/entities/src/CanvasEntityItem.cpp.in b/libraries/entities/src/CanvasEntityItem.cpp.in index dfcfb62724..8f6d50e9da 100644 --- a/libraries/entities/src/CanvasEntityItem.cpp.in +++ b/libraries/entities/src/CanvasEntityItem.cpp.in @@ -8,6 +8,8 @@ #include "CanvasEntityItem.h" #include +#include +#include #include "EntitiesLogging.h" #include "EntityItemProperties.h" @@ -16,8 +18,8 @@ EntityItemPointer CanvasEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { std::shared_ptr entity(new CanvasEntityItem(entityID), [](CanvasEntityItem* ptr) { ptr->deleteLater(); }); entity->setProperties(properties); - size_t bufferSize = 4 * static_cast(properties._width) * static_cast(properties._height); - entity->_imageData = QByteArray(bufferSize, (unsigned char)255); + size_t size = 4 * static_cast(properties._width) * static_cast(properties._height); + entity->_imageData = QByteArray(size, 0); return entity; } @@ -82,8 +84,8 @@ bool CanvasEntityItem::setSubClassProperties(const EntityItemProperties& propert // reallocate and resize if the size properties are changed if (oldWidth != getWidth() || oldHeight != getHeight()) { - size_t bufferSize = 4 * static_cast(getWidth()) * static_cast(getHeight()); - _imageData = QByteArray(bufferSize, (unsigned char)255); + size_t size = 4 * static_cast(_width) * static_cast(_height); + _imageData = QByteArray(size, 0); } return somethingChanged; @@ -97,28 +99,171 @@ EntityItemProperties CanvasEntityItem::getProperties(const EntityPropertyFlags& return properties; } -QByteArray& CanvasEntityItem::getImageData() { - return _imageData; -} - -void CanvasEntityItem::setImageData(const QByteArray& data) { - if (data.length() != _width * _height * 4) { +void CanvasEntityItem::setImageData(const CanvasImage& image) { + if (image.buffer.length() != _imageData.length()) { return; } - _imageData = data; + std::memcpy(_imageData.data(), image.buffer.constData(), _imageData.length()); +} + +void CanvasEntityItem::paintCommands(const QVector& queue) { + using V = CanvasCommand::Variant; + using Hint = CanvasCommand::RenderHint; + + auto destImage = QImage(reinterpret_cast(_imageData.data()), _width, _height, QImage::Format_RGBA8888); + QPen pen; + QBrush brush; + QPainter p; + p.begin(&destImage); + + for (int i = 0; i < queue.length(); i++) { + auto cmd = queue[i]; + + switch (cmd.kind()) { + case V::Invalid: + qCCritical(entities) << "CanvasEntityItem::paintCommands: Invalid command at index " << i; + break; + + case V::SetStrokeWidth: { + auto props = cmd._setStrokeWidth; + pen.setWidthF(props.width); + p.setPen(pen); + break; + } + + case V::SetColor: { + auto props = cmd._setColor; + pen.setColor(props.color); + brush.setColor(props.color); + p.setPen(pen); + p.setBrush(brush); + break; + } + + case V::SetHints: { + auto props = cmd._setHints; + p.setRenderHint(QPainter::Antialiasing, (props.hints & Hint::PrimitiveAntialiasing) != 0); + p.setRenderHint(QPainter::TextAntialiasing, (props.hints & Hint::TextAntialiasing) != 0); + p.setRenderHint(QPainter::SmoothPixmapTransform, (props.hints & Hint::BilinearImageScaling) != 0); + break; + } + + case V::SetBlendMode: { + auto props = cmd._setBlendMode; + p.setCompositionMode(props.mode); + break; + } + + case V::SetFont: { + auto props = cmd._setFont; + p.setFont(QFont(props.family, props.size, props.weight, props.italic)); + break; + } + + case V::ClearRect: { + auto props = cmd._clearRect; + p.eraseRect(props.rect); + break; + } + + case V::FillPath: { + auto props = cmd._fillPath; + p.fillPath(props.path, brush); + break; + } + + case V::FillRect: { + auto props = cmd._fillRect; + p.fillRect(props.rect, brush); + break; + } + + case V::FillEllipse: { + auto props = cmd._fillEllipse; + auto pathHack = QPainterPath(); + pathHack.addEllipse(props.rect); + p.fillPath(pathHack, brush); + break; + } + + case V::FillText: { + auto props = cmd._fillText; + + // unbounded text + if (props.rect.width() == 0.0 && props.rect.height() == 0.0) { + p.drawText(QPointF(props.rect.x(), props.rect.y()), props.text); + } else { + p.drawText(props.rect, props.flag, props.text); + } + break; + } + + case V::StrokePath: { + auto props = cmd._strokePath; + p.strokePath(props.path, pen); + break; + } + + case V::StrokeRect: { + auto props = cmd._strokeRect; + p.drawRect(props.rect); + break; + } + + case V::StrokeArc: { + auto props = cmd._strokeArc; + p.drawArc(props.rect, props.startAngle * 16, props.spanAngle * 16); + break; + } + + case V::StrokeEllipse: { + auto props = cmd._strokeEllipse; + p.drawEllipse(props.rect); + break; + } + + case V::Point: { + auto props = cmd._point; + p.drawPoint(props.point); + break; + } + + case V::Line: { + auto props = cmd._line; + p.drawLine(props.line); + break; + } + + case V::ImageCopy: { + auto props = cmd._imageCopy; + if (props.image.buffer.length() != static_cast(4 * props.image.width * props.image.height)) { + qCCritical(entities) << "CanvasEntityItem::paintCommands: Source CanvasImage buffer has incorrect size, expected " << (4 * props.image.width * props.image.height) << ", got " << props.image.buffer.length(); + break; + } + auto img = QImage(reinterpret_cast(props.image.buffer.constData()), props.image.width, props.image.height, QImage::Format_RGBA8888); + p.drawImage(props.dst, img, props.src); + break; + } + } + } + + p.end(); +} + +void CanvasEntityItem::commit() { + if (_commandQueue.length() > 0) { + paintCommands(_commandQueue); + _commandQueue.clear(); + } auto texture = gpu::Texture::createStrict(gpu::Element::COLOR_SRGBA_32, _width, _height); texture->setStoredMipFormat(gpu::Element::COLOR_SRGBA_32); texture->setAutoGenerateMips(false); - texture->assignStoredMip(0, _width * _height * 4, reinterpret_cast(_imageData.data())); + texture->assignStoredMip(0, _imageData.length(), reinterpret_cast(_imageData.constData())); texture->setSource("CanvasEntityRenderer"); _texture = texture; setNeedsRenderUpdate(true); somethingChangedNotification(); } - -void CanvasEntityItem::setImageSubData(const QByteArray& data, uint32_t dx, uint32_t dy, uint32_t dw, uint32_t dh, uint32_t sx, uint32_t sy, uint32_t sw, uint32_t sh) { - qCWarning(entities) << "CanvasEntityItem::setImageSubData unimplemented!"; -} diff --git a/libraries/entities/src/CanvasEntityItem.h.in b/libraries/entities/src/CanvasEntityItem.h.in index 5d2581c3d3..dcf3471e74 100644 --- a/libraries/entities/src/CanvasEntityItem.h.in +++ b/libraries/entities/src/CanvasEntityItem.h.in @@ -10,6 +10,7 @@ #include "EntityItem.h" #include +#include class CanvasEntityItem : public EntityItem { public: @@ -25,16 +26,22 @@ public: ALLOW_INSTANTIATION // This class can be instantiated ENTITY_PROPERTY_SUBCLASS_METHODS - QByteArray& getImageData(); - void setImageData(const QByteArray& data); - void setImageSubData(const QByteArray& data, uint32_t dx, uint32_t dy, uint32_t dw, uint32_t dh, uint32_t sx, uint32_t sy, uint32_t sw, uint32_t sh); + QByteArray& getImageData() { return _imageData; } + void setImageData(const CanvasImage& image); + + void pushCommands(const QVector& queue) { _commandQueue.append(queue); } + + void commit(); gpu::TexturePointer getTexture() const { return _texture; } protected: @Canvas_ENTITY_PROPS@ + void paintCommands(const QVector& queue); + QByteArray _imageData; + QVector _commandQueue; gpu::TexturePointer _texture; }; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index c751f0680a..e5b2d32df1 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -2691,7 +2691,7 @@ glm::vec3 EntityScriptingInterface::localToWorldDimensions(glm::vec3 localDimens } } -void EntityScriptingInterface::canvasSubmitImage(const QUuid& entityID, const QByteArray& imageData) { +void EntityScriptingInterface::canvasPushImage(const QUuid& entityID, const CanvasImage& image) { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (!entity) { return; @@ -2700,11 +2700,17 @@ void EntityScriptingInterface::canvasSubmitImage(const QUuid& entityID, const QB if (entity->getType() == EntityTypes::Canvas) { auto canvas = std::dynamic_pointer_cast(entity); - if (imageData.length() != canvas->getImageData().length()) { - qCDebug(entities) << "canvasSubmitImage with different sized buffers on " << entityID << ": input size: " << imageData.length() << ", canvas size: " << canvas->getImageData().length(); + if (image.buffer.length() != (int)(4 * image.width * image.height)) { + qCWarning(entities) << "canvasPushImage: \"image\" has invalid buffer size, expected " << (4 * image.width * image.height) << ", got " << image.buffer.length(); + return; } - canvas->setImageData(imageData); + if (image.width != canvas->getWidth() || image.height != canvas->getHeight()) { + qCWarning(entities) << "canvasPushImage: \"image\" dimensions don't match canvas, expected " << canvas->getWidth() << "x" << canvas->getHeight() << ", got " << image.width << "x" << image.height; + return; + } + + canvas->setImageData(image); } else { qCWarning(entities) << "canvasSubmitImage called on a non-canvas entity " << entityID; } diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index cdc427da1f..530e40cbed 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,6 +34,7 @@ #include #include #include +#include #include "PolyVoxEntityItem.h" #include "LineEntityItem.h" @@ -2196,15 +2197,38 @@ public slots: Q_INVOKABLE const EntityPropertyInfo getPropertyInfo(const QString& propertyName) const; /*@jsdoc - * Submits an sRGBA8 buffer to a Canvas entity. The buffer must be the correct - * size for the canvas entity (4 * width * height) or nothing will happen. - * This function only has client-side effects, the image data will not be - * sent across the network. - * @function Entities.canvasSubmitImage + * Replaces the contents of a canvas entity's image buffer. + * @function Entities.canvasPushImage * @param {Uuid} entityID - The Canvas entity that this image will be submitted to. - * @param {ArrayBuffer} imageData - The sRGBA8 image data to submit. + * @param {CanvasImage} image - The image to submit. */ - Q_INVOKABLE void canvasSubmitImage(const QUuid& entityID, const QByteArray& imageData); + Q_INVOKABLE void canvasPushImage(const QUuid& entityID, const CanvasImage& image); + + /*@jsdoc + * Retrieves a copy of the current sRGBA8 pixel buffer of a canvas as an ArrayBuffer. + * The contents are determined by the last call to Entities.canvasCommit. + * @function Entities.canvasGetImage + * @param {Uuid} entityID - The canvas entity to retrieve the buffer from. + * @returns {CanvasImage} - The image data. + */ + Q_INVOKABLE CanvasImage canvasGetImage(const QUuid& entityID); + + /*@jsdoc + * Pushes a list of high-level drawing commands into a Canvas entity's internal queue. + * Pushed commands will not execute until Entities.canvasCommit is called. + * @function Entities.canvasPushCommands + * @param {Uuid} entityID - The canvas entity to push commands to. + * @param {Object[]} commands - The drawing commands to push. See CanvasCommand for more info. + */ + Q_INVOKABLE void canvasPushCommands(const QUuid& entityID, const QVector& commands); + + /*@jsdoc + * Completes any pending drawing commands and updates the texture of a Canvas entity. + * The Canvas entity's internal command queue is cleared after this function is called. + * @function Entities.canvasCommit + * @param {Uuid} entityID - The canvas entity to update the texture of. + */ + Q_INVOKABLE void canvasCommit(const QUuid* entityID); signals: /*@jsdoc diff --git a/libraries/script-engine/src/CanvasCommand.cpp b/libraries/script-engine/src/CanvasCommand.cpp new file mode 100644 index 0000000000..d8a8d33319 --- /dev/null +++ b/libraries/script-engine/src/CanvasCommand.cpp @@ -0,0 +1,429 @@ +// +// CanvasCommand.cpp +// libraries/script-engine/src +// +// Created by Ada on 2025-02-27 +// Copyright 2025 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// SPDX-License-Identifier: Apache-2.0 +// + +#include "CanvasCommand.h" + +#include + +#include "ScriptEngine.h" +#include "ScriptEngineCast.h" +#include "ScriptValue.h" +#include "ScriptManager.h" +#include "ScriptValueUtils.h" + +STATIC_SCRIPT_TYPES_INITIALIZER((+[](ScriptManager* manager){ + auto scriptEngine = manager->engine().get(); + + scriptRegisterMetaType(scriptEngine); + scriptRegisterMetaType(scriptEngine, "CanvasImage"); + scriptRegisterMetaType(scriptEngine, "CanvasPath"); + scriptRegisterMetaType, qVectorCanvasCommandToScriptValue, qVectorCanvasCommandFromScriptValue>(scriptEngine); +})); + +const QString CMD_TYPE_PROP_NAME = "type"; + +const QString IMG_WIDTH_PROP_NAME = "width"; +const QString IMG_HEIGHT_PROP_NAME = "height"; +const QString IMG_BUFFER_PROP_NAME = "buffer"; + +ScriptValue canvasCommandToScriptValue(ScriptEngine* engine, const CanvasCommand& cmd) { + using Variant = CanvasCommand::Variant; + + ScriptValue obj = engine->newObject(); + + obj.setProperty(CMD_TYPE_PROP_NAME, (uint)cmd.kind()); + + switch (cmd.kind()) { + case Variant::SetStrokeWidth: { + auto props = cmd._setStrokeWidth; + obj.setProperty("width", props.width); + return obj; + } + + case Variant::SetColor: { + auto props = cmd._setColor; + obj.setProperty("color", props.color); + return obj; + } + + case Variant::SetHints: { + auto props = cmd._setHints; + obj.setProperty("hints", props.hints); + return obj; + } + + case Variant::SetBlendMode: { + auto props = cmd._setBlendMode; + obj.setProperty("mode", props.mode); + return obj; + } + + case Variant::SetFont: { + auto props = cmd._setFont; + obj.setProperty("family", props.family); + obj.setProperty("size", props.size); + obj.setProperty("weight", props.weight); + obj.setProperty("italic", props.italic); + return obj; + } + + case Variant::FillPath: { + auto props = cmd._fillPath; + obj.setProperty("path", props.path); + return obj; + } + + case Variant::FillRect: { + auto props = cmd._fillRect; + obj.setProperty("x", props.rect.x()); + obj.setProperty("y", props.rect.y()); + obj.setProperty("w", props.rect.width()); + obj.setProperty("h", props.rect.height()); + return obj; + } + + case Variant::FillEllipse: { + auto props = cmd._fillEllipse; + obj.setProperty("x", props.rect.x()); + obj.setProperty("y", props.rect.y()); + obj.setProperty("w", props.rect.width()); + obj.setProperty("h", props.rect.height()); + return obj; + } + + case Variant::FillText: { + auto props = cmd._fillText; + obj.setProperty("x", props.rect.x()); + obj.setProperty("y", props.rect.y()); + obj.setProperty("w", props.rect.width()); + obj.setProperty("h", props.rect.height()); + obj.setProperty("text", props.text); + obj.setProperty("flag", static_cast(props.flag)); + return obj; + } + + case Variant::StrokePath: { + auto props = cmd._strokePath; + obj.setProperty("path", props.path); + return obj; + } + + case Variant::StrokeRect: { + auto props = cmd._strokeRect; + obj.setProperty("x", props.rect.x()); + obj.setProperty("y", props.rect.y()); + obj.setProperty("w", props.rect.width()); + obj.setProperty("h", props.rect.height()); + return obj; + } + + case Variant::StrokeArc: { + auto props = cmd._strokeArc; + obj.setProperty("x", props.rect.x()); + obj.setProperty("y", props.rect.y()); + obj.setProperty("w", props.rect.width()); + obj.setProperty("h", props.rect.height()); + obj.setProperty("startAngle", props.startAngle); + obj.setProperty("spanAngle", props.spanAngle); + return obj; + } + + case Variant::StrokeEllipse: { + auto props = cmd._strokeEllipse; + obj.setProperty("x", props.rect.x()); + obj.setProperty("y", props.rect.y()); + obj.setProperty("w", props.rect.width()); + obj.setProperty("h", props.rect.height()); + return obj; + } + + case Variant::Point: { + auto props = cmd._point; + obj.setProperty("x", props.point.x()); + obj.setProperty("y", props.point.y()); + return obj; + } + + case Variant::Line: { + auto props = cmd._line; + obj.setProperty("x1", props.line.x1()); + obj.setProperty("y1", props.line.y1()); + obj.setProperty("x2", props.line.x2()); + obj.setProperty("y2", props.line.y2()); + return obj; + } + + case Variant::ImageCopy: { + auto props = cmd._imageCopy; + obj.setProperty("srcX", props.src.x()); + obj.setProperty("srcY", props.src.y()); + obj.setProperty("srcW", props.src.width()); + obj.setProperty("srcH", props.src.height()); + obj.setProperty("destX", props.dst.x()); + obj.setProperty("destY", props.dst.y()); + obj.setProperty("destW", props.dst.width()); + obj.setProperty("destH", props.dst.height()); + obj.setProperty("image", props.image); + return obj; + } + + default: { + return obj; + } + } +} + +bool canvasCommandFromScriptValue(const ScriptValue& object, CanvasCommand& cmd) { + using Variant = CanvasCommand::Variant; + + uint type = object.property(CMD_TYPE_PROP_NAME).toVariant().toUInt(); + + if (type == static_cast(Variant::SetStrokeWidth)) { + cmd.set(CanvasCommand::SetStrokeWidth { object.property("width").toNumber() }); + } else if (type == static_cast(Variant::SetColor)) { + glm::u8vec3 c; + if (!u8vec3FromScriptValue(object.property("color"), c)) { return false; } + + // FIXME: we have a script RGB color type but not an RGBA one + cmd.set(CanvasCommand::SetColor { QColor(c[0], c[1], c[2], 255) }); + } else if (type == static_cast(Variant::SetHints)) { + cmd.set(CanvasCommand::SetHints { + static_cast(object.property("hints").toVariant().toUInt()) + }); + } else if (type == static_cast(Variant::SetBlendMode)) { + cmd.set(CanvasCommand::SetBlendMode { + static_cast(object.property("mode").toVariant().toUInt()) + }); + } else if (type == static_cast(Variant::SetFont)) { + cmd.set(CanvasCommand::SetFont { + object.property("family").toString(), + object.property("size").toVariant().toInt(), + object.property("weight").toVariant().toInt(), + object.property("italic").toBool(), + }); + } else if (type == static_cast(Variant::ClearRect)) { + cmd.set(CanvasCommand::ClearRect { + QRect( + object.property("x").toVariant().toInt(), + object.property("y").toVariant().toInt(), + object.property("w").toVariant().toInt(), + object.property("h").toVariant().toInt() + ) + }); + } else if (type == static_cast(Variant::FillPath)) { + cmd.set(CanvasCommand::FillPath { + qPainterPathFromScriptValue(object.property("path")) + }); + } else if (type == static_cast(Variant::FillRect)) { + cmd.set(CanvasCommand::FillRect { + QRectF( + object.property("x").toNumber(), + object.property("y").toNumber(), + object.property("w").toNumber(), + object.property("h").toNumber() + ) + }); + } else if (type == static_cast(Variant::FillEllipse)) { + cmd.set(CanvasCommand::FillEllipse { + QRectF( + object.property("x").toNumber(), + object.property("y").toNumber(), + object.property("w").toNumber(), + object.property("h").toNumber() + ) + }); + } else if (type == static_cast(Variant::FillText)) { + cmd.set(CanvasCommand::FillText { + QRectF( + object.property("x").toNumber(), + object.property("y").toNumber(), + object.property("w").toNumber(), + object.property("h").toNumber() + ), + object.property("text").toString(), + static_cast(object.property("flag").toVariant().toUInt()), + }); + } else if (type == static_cast(Variant::StrokePath)) { + cmd.set(CanvasCommand::StrokePath { + qPainterPathFromScriptValue(object.property("path")) + }); + } else if (type == static_cast(Variant::StrokeRect)) { + cmd.set(CanvasCommand::StrokeRect { + QRectF( + object.property("x").toNumber(), + object.property("y").toNumber(), + object.property("w").toNumber(), + object.property("h").toNumber() + ) + }); + } else if (type == static_cast(Variant::StrokeArc)) { + cmd.set(CanvasCommand::StrokeArc { + QRectF( + object.property("x").toNumber(), + object.property("y").toNumber(), + object.property("w").toNumber(), + object.property("h").toNumber() + ), + object.property("startAngle").toNumber(), + object.property("spanAngle").toNumber(), + }); + } else if (type == static_cast(Variant::StrokeEllipse)) { + cmd.set(CanvasCommand::StrokeEllipse { + QRectF( + object.property("x").toNumber(), + object.property("y").toNumber(), + object.property("w").toNumber(), + object.property("h").toNumber() + ) + }); + } else if (type == static_cast(Variant::Point)) { + cmd.set(CanvasCommand::Point { + QPointF( + object.property("x").toNumber(), + object.property("y").toNumber() + ) + }); + } else if (type == static_cast(Variant::Line)) { + cmd.set(CanvasCommand::Line { + QLineF( + object.property("x1").toNumber(), + object.property("y1").toNumber(), + object.property("x2").toNumber(), + object.property("y2").toNumber() + ) + }); + } else if (type == static_cast(Variant::ImageCopy)) { + cmd.set(CanvasCommand::ImageCopy { + QRectF( + object.property("srcX").toNumber(), + object.property("srcY").toNumber(), + object.property("srcW").toNumber(), + object.property("srcH").toNumber() + ), + QRectF( + object.property("destX").toNumber(), + object.property("destY").toNumber(), + object.property("destW").toNumber(), + object.property("destH").toNumber() + ), + canvasImageFromScriptValue(object.property("image")), + }); + } else { + cmd.set(CanvasCommand::Invalid()); + } + + return true; +} + +CanvasCommand canvasCommandFromScriptValue(const ScriptValue& object) { + CanvasCommand cmd; + canvasCommandFromScriptValue(object, cmd); + return cmd; +} + +QVector qVectorCanvasCommandFromScriptValue(const ScriptValue& object) { + QVector list; + qVectorCanvasCommandFromScriptValue(object, list); + return list; +} + +bool qVectorCanvasCommandFromScriptValue(const ScriptValue& array, QVector& list) { + int length = array.property("length").toInteger(); + + for (int i = 0; i < length; i++) { + list << canvasCommandFromScriptValue(array.property(i)); + } + + return true; +} + +ScriptValue canvasImageToScriptValue(ScriptEngine* engine, const CanvasImage& img) { + ScriptValue obj = engine->newObject(); + obj.setProperty(IMG_WIDTH_PROP_NAME, img.width); + obj.setProperty(IMG_HEIGHT_PROP_NAME, img.height); + obj.setProperty(IMG_BUFFER_PROP_NAME, img.buffer); + return obj; +} + +bool canvasImageFromScriptValue(const ScriptValue& object, CanvasImage& img) { + img.width = object.property(IMG_WIDTH_PROP_NAME).toVariant().toUInt(); + img.height = object.property(IMG_HEIGHT_PROP_NAME).toVariant().toUInt(); + img.buffer = object.property(IMG_BUFFER_PROP_NAME).toVariant().toByteArray(); + return true; +} + +CanvasImage canvasImageFromScriptValue(const ScriptValue& object) { + CanvasImage img = {}; + canvasImageFromScriptValue(object, img); + return img; +} + +ScriptValue qPainterPathToScriptValue(ScriptEngine* engine, const QPainterPath& path) { + ScriptValue array = engine->newArray(path.elementCount()); + + for (int i = 0; i < path.elementCount(); i++) { + ScriptValue obj = engine->newObject(); + auto elem = path.elementAt(i); + + // curves have another two points + if (elem.type == QPainterPath::CurveToElement) { + obj.setProperty("type", static_cast(elem.type)); + obj.setProperty("c1x", static_cast(elem.x)); + obj.setProperty("c1y", static_cast(elem.y)); + obj.setProperty("c2x", static_cast(path.elementAt(i + 1).x)); + obj.setProperty("c2y", static_cast(path.elementAt(i + 1).y)); + obj.setProperty("x", static_cast(path.elementAt(i + 2).x)); + obj.setProperty("y", static_cast(path.elementAt(i + 2).y)); + + i += 2; + } else { + obj.setProperty("type", static_cast(elem.type)); + obj.setProperty("x", static_cast(elem.x)); + obj.setProperty("y", static_cast(elem.y)); + } + } + + return array; +} + +bool qPainterPathFromScriptValue(const ScriptValue& array, QPainterPath& path) { + int length = array.property("length").toInteger(); + path.reserve(length); + + for (int i = 0; i < length; i++) { + ScriptValue obj = array.property(i); + uint type = obj.property("type").toVariant().toUInt(); + auto x = obj.property("x").toNumber(); + auto y = obj.property("y").toNumber(); + + if (type == QPainterPath::CurveToElement) { + auto c1x = obj.property("c1x").toNumber(); + auto c1y = obj.property("c1y").toNumber(); + auto c2x = obj.property("c2x").toNumber(); + auto c2y = obj.property("c2y").toNumber(); + + path.cubicTo(c1x, c1y, c2x, c2y, x, y); + } else if (type == QPainterPath::LineToElement) { + path.lineTo(x, y); + } else { + path.moveTo(x, y); + } + } + + return true; +} + +QPainterPath qPainterPathFromScriptValue(const ScriptValue& object) { + QPainterPath p; + qPainterPathFromScriptValue(object, p); + return p; +} diff --git a/libraries/script-engine/src/CanvasCommand.h b/libraries/script-engine/src/CanvasCommand.h new file mode 100644 index 0000000000..08c0c2f313 --- /dev/null +++ b/libraries/script-engine/src/CanvasCommand.h @@ -0,0 +1,240 @@ +// +// CanvasCommand.h +// libraries/script-engine/src +// +// Created by Ada on 2025-02-27 +// Copyright 2025 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// SPDX-License-Identifier: Apache-2.0 +// + +/// @addtogroup ScriptEngine +/// @{ + +#ifndef hifi_CanvasCommand_h +#define hifi_CanvasCommand_h + +#include "ScriptValue.h" + +#include +#include +#include + +class ScriptEngine; + +struct CanvasImage { + QByteArray buffer; + int width, height; +}; + +class CanvasCommand { +public: + enum class Variant: uint8_t { + Invalid, + SetStrokeWidth, + SetColor, + SetHints, + SetBlendMode, + SetFont, + ClearRect, + FillPath, + FillRect, + FillEllipse, + FillText, + StrokePath, + StrokeRect, + StrokeArc, + StrokeEllipse, + Point, + Line, + ImageCopy, + }; + + enum RenderHint: uint8_t { + PrimitiveAntialiasing = (1 << 0), + TextAntialiasing = (1 << 1), + BilinearImageScaling = (1 << 2), + }; + + struct Invalid {}; + struct SetStrokeWidth { qreal width; }; + struct SetColor { QColor color; }; + struct SetHints { RenderHint hints; }; + struct SetBlendMode { QPainter::CompositionMode mode; }; + struct SetFont { QString family; int size; int weight; bool italic; }; + struct ClearRect { QRect rect; }; + struct FillPath { QPainterPath path; }; + struct FillRect { QRectF rect; }; + struct FillEllipse { QRectF rect; }; + struct FillText { QRectF rect; QString text; Qt::AlignmentFlag flag; }; + struct StrokePath { QPainterPath path; }; + struct StrokeRect { QRectF rect; }; + struct StrokeArc { QRectF rect; qreal startAngle, spanAngle; }; + struct StrokeEllipse { QRectF rect; }; + struct Point { QPointF point; }; + struct Line { QLineF line; }; + struct ImageCopy { QRectF src; QRectF dst; CanvasImage image; }; + + CanvasCommand() noexcept: _invalid(), _tag(Variant::Invalid) {} + CanvasCommand(Invalid cmd) noexcept: _invalid(), _tag(Variant::Invalid) {} + CanvasCommand(SetStrokeWidth cmd) noexcept: _setStrokeWidth(cmd), _tag(Variant::SetStrokeWidth) {} + CanvasCommand(SetColor cmd) noexcept: _setColor(cmd), _tag(Variant::SetColor) {} + CanvasCommand(SetHints cmd) noexcept: _setHints(cmd), _tag(Variant::SetHints) {} + CanvasCommand(SetBlendMode cmd) noexcept: _setBlendMode(cmd), _tag(Variant::SetBlendMode) {} + CanvasCommand(SetFont cmd) noexcept: _setFont(cmd), _tag(Variant::SetFont) {} + CanvasCommand(ClearRect cmd) noexcept: _clearRect(cmd), _tag(Variant::ClearRect) {} + CanvasCommand(FillPath cmd) noexcept: _fillPath(cmd), _tag(Variant::FillPath) {} + CanvasCommand(FillRect cmd) noexcept: _fillRect(cmd), _tag(Variant::FillRect) {} + CanvasCommand(FillEllipse cmd) noexcept: _fillEllipse(cmd), _tag(Variant::FillEllipse) {} + CanvasCommand(FillText cmd) noexcept: _fillText(cmd), _tag(Variant::FillText) {} + CanvasCommand(StrokePath cmd) noexcept: _strokePath(cmd), _tag(Variant::StrokePath) {} + CanvasCommand(StrokeRect cmd) noexcept: _strokeRect(cmd), _tag(Variant::StrokeRect) {} + CanvasCommand(StrokeArc cmd) noexcept: _strokeArc(cmd), _tag(Variant::StrokeArc) {} + CanvasCommand(StrokeEllipse cmd) noexcept: _strokeEllipse(cmd), _tag(Variant::StrokeArc) {} + CanvasCommand(Point cmd) noexcept: _point(cmd), _tag(Variant::Point) {} + CanvasCommand(Line cmd) noexcept: _line(cmd), _tag(Variant::Line) {} + CanvasCommand(ImageCopy cmd) noexcept: _imageCopy(cmd), _tag(Variant::ImageCopy) {} + + ~CanvasCommand() noexcept { + switch (_tag) { + case Variant::Invalid: _invalid.~Invalid(); break; + case Variant::SetStrokeWidth: _setStrokeWidth.~SetStrokeWidth(); break; + case Variant::SetColor: _setColor.~SetColor(); break; + case Variant::SetHints: _setHints.~SetHints(); break; + case Variant::SetBlendMode: _setBlendMode.~SetBlendMode(); break; + case Variant::SetFont: _setFont.~SetFont(); break; + case Variant::ClearRect: _clearRect.~ClearRect(); break; + case Variant::FillPath: _fillPath.~FillPath(); break; + case Variant::FillRect: _fillRect.~FillRect(); break; + case Variant::FillEllipse: _fillEllipse.~FillEllipse(); break; + case Variant::FillText: _fillText.~FillText(); break; + case Variant::StrokePath: _strokePath.~StrokePath(); break; + case Variant::StrokeRect: _strokeRect.~StrokeRect(); break; + case Variant::StrokeArc: _strokeArc.~StrokeArc(); break; + case Variant::StrokeEllipse: _strokeEllipse.~StrokeEllipse(); break; + case Variant::Point: _point.~Point(); break; + case Variant::Line: _line.~Line(); break; + case Variant::ImageCopy: _imageCopy.~ImageCopy(); break; + } + } + + CanvasCommand(const CanvasCommand& other) noexcept { + _tag = other._tag; + switch (other._tag) { + case Variant::Invalid: _invalid = other._invalid; break; + case Variant::SetStrokeWidth: _setStrokeWidth = _setStrokeWidth; break; + case Variant::SetColor: _setColor = other._setColor; break; + case Variant::SetHints: _setHints = other._setHints; break; + case Variant::SetBlendMode: _setBlendMode = other._setBlendMode; break; + case Variant::SetFont: _setFont = other._setFont; break; + case Variant::ClearRect: _clearRect = other._clearRect; break; + case Variant::FillPath: _fillPath = other._fillPath; break; + case Variant::FillRect: _fillRect = other._fillRect; break; + case Variant::FillEllipse: _fillEllipse = other._fillEllipse; break; + case Variant::FillText: _fillText = other._fillText; break; + case Variant::StrokePath: _strokePath = other._strokePath; break; + case Variant::StrokeRect: _strokeRect = other._strokeRect; break; + case Variant::StrokeArc: _strokeArc = other._strokeArc; break; + case Variant::StrokeEllipse: _strokeEllipse = other._strokeEllipse; break; + case Variant::Point: _point = other._point; break; + case Variant::Line: _line = other._line; break; + case Variant::ImageCopy: _imageCopy = other._imageCopy; break; + } + } + + CanvasCommand(CanvasCommand&& other) noexcept { + _tag = other._tag; + switch (other._tag) { + case Variant::Invalid: _invalid = other._invalid; break; + case Variant::SetStrokeWidth: _setStrokeWidth = _setStrokeWidth; break; + case Variant::SetColor: _setColor = other._setColor; break; + case Variant::SetHints: _setHints = other._setHints; break; + case Variant::SetBlendMode: _setBlendMode = other._setBlendMode; break; + case Variant::SetFont: _setFont = other._setFont; break; + case Variant::ClearRect: _clearRect = other._clearRect; break; + case Variant::FillPath: _fillPath = other._fillPath; break; + case Variant::FillRect: _fillRect = other._fillRect; break; + case Variant::FillEllipse: _fillEllipse = other._fillEllipse; break; + case Variant::FillText: _fillText = other._fillText; break; + case Variant::StrokePath: _strokePath = other._strokePath; break; + case Variant::StrokeRect: _strokeRect = other._strokeRect; break; + case Variant::StrokeArc: _strokeArc = other._strokeArc; break; + case Variant::StrokeEllipse: _strokeEllipse = other._strokeEllipse; break; + case Variant::Point: _point = other._point; break; + case Variant::Line: _line = other._line; break; + case Variant::ImageCopy: _imageCopy = other._imageCopy; break; + } + } + + void set(Invalid&& cmd) { _tag = Variant::Invalid; _invalid = cmd; } + void set(SetStrokeWidth&& cmd) { _tag = Variant::SetStrokeWidth; _setStrokeWidth = cmd; } + void set(SetColor&& cmd) { _tag = Variant::SetColor; _setColor = cmd; } + void set(SetHints&& cmd) { _tag = Variant::SetHints; _setHints = cmd; } + void set(SetBlendMode&& cmd) { _tag = Variant::SetBlendMode; _setBlendMode = cmd; } + void set(SetFont&& cmd) { _tag = Variant::SetFont; _setFont = cmd; } + void set(ClearRect&& cmd) { _tag = Variant::ClearRect; _clearRect = cmd; } + void set(FillPath&& cmd) { _tag = Variant::FillPath; _fillPath = cmd; } + void set(FillRect&& cmd) { _tag = Variant::FillRect; _fillRect = cmd; } + void set(FillEllipse&& cmd) { _tag = Variant::FillEllipse; _fillEllipse = cmd; } + void set(FillText&& cmd) { _tag = Variant::FillText; _fillText = cmd; } + void set(StrokePath&& cmd) { _tag = Variant::StrokePath; _strokePath = cmd; } + void set(StrokeRect&& cmd) { _tag = Variant::StrokeRect; _strokeRect = cmd; } + void set(StrokeArc&& cmd) { _tag = Variant::StrokeArc; _strokeArc = cmd; } + void set(StrokeEllipse&& cmd) { _tag = Variant::StrokeEllipse; _strokeEllipse = cmd; } + void set(Point&& cmd) { _tag = Variant::Point; _point = cmd; } + void set(Line&& cmd) { _tag = Variant::Line; _line = cmd; } + void set(ImageCopy&& cmd) { _tag = Variant::ImageCopy; _imageCopy = cmd; } + + Variant kind() const { return _tag; } + + union { + Invalid _invalid; + SetStrokeWidth _setStrokeWidth; + SetColor _setColor; + SetHints _setHints; + SetBlendMode _setBlendMode; + SetFont _setFont; + ClearRect _clearRect; + FillPath _fillPath; + FillRect _fillRect; + FillText _fillText; + FillEllipse _fillEllipse; + StrokePath _strokePath; + StrokeRect _strokeRect; + StrokeArc _strokeArc; + StrokeEllipse _strokeEllipse; + Point _point; + Line _line; + ImageCopy _imageCopy; + }; + +private: + Variant _tag; +}; + +void registerCanvasMetaTypes(ScriptEngine *engine); + +ScriptValue canvasCommandToScriptValue(ScriptEngine* engine, const CanvasCommand& cmd); +bool canvasCommandFromScriptValue(const ScriptValue& object, CanvasCommand& cmd); +CanvasCommand canvasCommandFromScriptValue(const ScriptValue& object); + +Q_DECLARE_METATYPE(QPainterPath) +ScriptValue qPainterPathToScriptValue(ScriptEngine* engine, const QPainterPath& path); +bool qPainterPathFromScriptValue(const ScriptValue& object, QPainterPath& path); +QPainterPath qPainterPathFromScriptValue(const ScriptValue& object); + +Q_DECLARE_METATYPE(QVector) +ScriptValue qVectorCanvasCommandToScriptValue(ScriptEngine* engine, const QVector& list); +bool qVectorCanvasCommandFromScriptValue(const ScriptValue& object, QVector& list); +QVector qVectorCanvasCommandFromScriptValue(const ScriptValue& object); + +Q_DECLARE_METATYPE(CanvasImage) +ScriptValue canvasImageToScriptValue(ScriptEngine* engine, const CanvasImage& img); +bool canvasImageFromScriptValue(const ScriptValue& object, CanvasImage& img); +CanvasImage canvasImageFromScriptValue(const ScriptValue& object); + +#endif // hifi_CanvasCommand_h + +/// @}