/* * Copyright 2026 Forschungszentrum J?lich * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include "LayerItem.h" #include "Layer.h" #include "TransformHandleItem.h" #include "CageControlPointItem.h" #include "CageOverlayItem.h" #include "CageMesh.h" #include "../core/IMainSystem.h" #include "../gui/MainWindow.h" #include "../gui/ImageView.h" #include "../undo/TransformLayerCommand.h" #include "../undo/MirrorLayerCommand.h" #include "../undo/MoveLayerCommand.h" #include "../undo/SetPivotCommand.h" #include "../undo/CageWarpCommand.h" #include "../undo/CageEditCommand.h" #include "../util/Interpolation.h" #include "../util/GeometryUtils.h" #include "../util/TriangleWarp.h" #include "../util/CageWarp.h" #include #include #include #include #include #include #include #include #include #include #include #include // ------------------------ LayerItem ------------------------ LayerItem::LayerItem( const QString& name, const QPixmap& pixmap, QGraphicsItem* parent ) : QGraphicsPixmapItem(pixmap,parent) { qCDebug(logEditor) << "LayerItem::LayerItem(): Pixmap processing of layer " << name << "..."; { m_name = name; m_image = pixmap.toImage(); m_originalImage = pixmap.toImage(); m_originalImageType = ImageType::Original; init(); } } LayerItem::LayerItem( const QString& name, const QImage& image, QGraphicsItem* parent ) : QGraphicsPixmapItem(parent) , m_name(name) , m_originalImage(image) , m_image(image) { qCDebug(logEditor) << "LayerItem::LayerItem(): Image (size =" << image.size() << ") processing of layer " << name << "..."; { m_originalImageType = ImageType::Original; m_nogui = (qobject_cast(qApp) == nullptr); if ( !m_nogui ) { setPixmap(QPixmap::fromImage(m_image)); init(); } } } LayerItem::~LayerItem() { delete m_cageEditStack; } // >>> QString LayerItem::operationModeName( int mode ) { switch ( mode ) { case None: return "None"; case Info: return "Info"; case CageEdit: return "CageEdit"; case Translate: return "Translate"; case Rotate: return "Rotate"; case Scale: return "Scale"; case Flip: return "Flip"; case Flop: return "Flop"; case Perspective: return "Perspective"; case CageWarp: return "CageWarp"; case Select: return "Select"; case MovePoint: return "MovePoint"; case AddPoint: return "AddPoint"; case DeletePoint: return "DeletePoint"; case TranslatePolygon: return "TranslatePolygon"; case SmoothPolygon: return "SmoothPolygon"; case ReducePolygon: return "ReducePolygon"; case DeletePolygon: return "DeletePolygon"; } return "Unknown"; } // >>> void LayerItem::applyPerspective() { QImage warped = m_perspective.apply(m_originalImage); // OLD: setPixmap(QPixmap::fromImage(warped)); m_image = warped; if ( !m_nogui && qobject_cast(qApp) ) { setPixmap(QPixmap::fromImage(warped)); } } // returns unified rect of the pixmap AND the m_cageMesh QRectF LayerItem::boundingRect() const { QSize s = pixmap().isNull() ? m_image.size() : pixmap().size(); QRectF pixmapRect(offset(),s); if ( m_cageMesh.isActive() ) { QRectF cageRect = QPolygonF(m_cageMesh.points()).boundingRect(); QRectF united = pixmapRect.united(cageRect); return united; } // Expand to include the pivot cross when it lies outside the pixmap if ( m_hasPivot && m_operationMode == OperationMode::Rotate ) { const QPointF p = mapFromScene(m_pivot); const qreal r = 16.0; pixmapRect = pixmapRect.united(QRectF(p.x() - r, p.y() - r, r * 2, r * 2)); } return pixmapRect; } QPainterPath LayerItem::shape() const { QPainterPath path = QGraphicsPixmapItem::shape(); if ( m_hasPivot && m_operationMode == OperationMode::Rotate ) { const QPointF p = mapFromScene(m_pivot); const qreal r = 16.0; path.addRect(QRectF(p.x() - r, p.y() - r, r * 2, r * 2)); } return path; } // QGraphicsPixmapItem with BoundingRectShape bypasses virtual shape() in its contains() // and tests against the raw pixmap rect directly. Override contains() so that hits in // the pivot area are recognised even when the pivot lies outside the pixmap. bool LayerItem::contains(const QPointF& point) const { const bool base = QGraphicsPixmapItem::contains(point); if ( base ) return true; if ( m_hasPivot && m_operationMode == OperationMode::Rotate ) { const QPointF p = mapFromScene(m_pivot); const qreal r = 16.0; return QRectF(p.x() - r, p.y() - r, r * 2, r * 2).contains(point); } return false; } // Called by ImageView::mousePressEvent to bypass Qt's internal itemsAtPosition // dispatch (which uses a 1?1 rect query and may miss the pivot area). Sets up // the pivot drag state and returns true if scenePos hits the pivot marker. bool LayerItem::beginPivotDragIfHit(const QPointF& scenePos) { if ( m_operationMode != OperationMode::Rotate || !isSelected() ) return false; const QPointF itemPos = mapFromScene(scenePos); if ( QLineF(itemPos, pivot()).length() > 12.0 ) return false; m_pivotAtDragStart = pivotScene(); m_pivot = m_pivotAtDragStart; m_pivotDragging = true; m_pivotSelected = true; m_pivotWasSet = m_hasPivot; m_mouseOperationActive = true; m_startPos = pos(); update(); return true; } void LayerItem::updatePivotTo(const QPointF& scenePos) { prepareGeometryChange(); m_pivot = scenePos; m_hasPivot = true; update(); } void LayerItem::finishPivotDrag() { const QPointF newPivotScene = m_pivot; m_pivotDragging = false; m_pivotSelected = false; update(); if ( m_undoStack && QLineF(newPivotScene, m_pivotAtDragStart).length() > 0.5 ) m_undoStack->push(new SetPivotCommand(this, m_pivotAtDragStart, newPivotScene, m_pivotWasSet)); setSelected(true); } // per default moveable layer item // NOTE: QGraphicsItem::ItemIsSelectable expands the size of the QGraphicsPixmapItem by +1+1 ! void LayerItem::init() { setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges | QGraphicsItem::ItemIsFocusable); setTransformationMode(EditorStyle::instance().transformationMode()); // for on screen visualization setAcceptedMouseButtons(Qt::LeftButton); setShapeMode(QGraphicsPixmapItem::BoundingRectShape); // setShapeMode(QGraphicsPixmapItem::MaskShape); // the better choice but complex setZValue(2); m_showBoundingBox = true; // Lasso pen m_lassoPen = QPen(Qt::green); m_lassoPen.setWidth(0); m_lassoPen.setStyle(Qt::SolidLine); // Selection pen m_selectedPen = QPen(Qt::red); m_selectedPen.setWidth(0); m_selectedPen.setStyle(Qt::SolidLine); } // ------------------------ Self info ------------------------ void LayerItem::printself( bool debugSave ) { qInfo() << " LayerItem::printself(): name =" << name() << ", id =" << m_index << ", visible =" << isVisible(); QRectF rect = m_nogui ? m_image.rect() : boundingRect(); QString geometry = QString("%1x%2+%3+%4").arg(rect.width()).arg(rect.height()).arg(pos().x()).arg(pos().y()); qInfo() << " + position =" << pos(); qInfo() << " + geometry =" << geometry; qInfo() << " + originalImageType =" << m_originalImageType; qInfo() << " + selected =" << isSelected(); qInfo() << " + zValue =" << zValue(); qInfo() << " + cage: enabled =" << m_cageEnabled << ", edited =" << m_cageEditing; qInfo() << " + cage overlay =" << (m_cageOverlay != nullptr ? "ok" : "null"); qInfo() << " + operation mode =" << m_operationMode; qInfo() << " + bounding box =" << m_showBoundingBox; qInfo() << " + parent =" << ( m_parent != nullptr ? "null" : "ok" ); } // ------------------------ Getter / Setter ------------------------ ImageView* LayerItem::getParentImageView() { MainWindow* parent = m_parent != nullptr ? dynamic_cast(m_parent) : nullptr; if ( parent != nullptr ) return parent->getViewer(); return nullptr; } QImage& LayerItem::image( int id ) { return id == 1 ? m_originalImage : m_image; } QString LayerItem::getAlphaMaskData( bool base64Encoding ) { QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); if ( 1 == 1 ) { QImage alphaImage = m_image.convertToFormat(QImage::Format_Alpha8); QImage indexedImage = alphaImage.convertToFormat(QImage::Format_Indexed8); QList palette; for ( int i = 0; i < 256; ++i ) { if ( i == 0 ) palette.append(qRgba(0, 0, 0, 255)); else palette.append(qRgba(255, 255, 255, 255)); } indexedImage.setColorTable(palette); for ( int y = 0; y < indexedImage.height(); ++y ) { uchar *pixels = indexedImage.scanLine(y); for ( int x = 0; x < indexedImage.width(); ++x ) { pixels[x] = (pixels[x] > 0) ? 255 : 0; } } indexedImage.save(&buffer, "PNG"); } else { m_image.save(&buffer, "PNG"); } return QString::fromUtf8(ba.toBase64()); } void LayerItem::setInActive( bool isInActive ) { if ( !m_layer ) return; m_layer->m_active = !isInActive; } void LayerItem::setImage( const QImage &image ) { m_image = image; updatePixmap(); } void LayerItem::setLayer( Layer *layer ) { m_layer = layer; } void LayerItem::setType( LayerType layerType ) { m_type = layerType; if ( m_type == LayerType::MainImage ) { setFlag(QGraphicsItem::ItemIsMovable, false); setAcceptedMouseButtons(Qt::NoButton); } else { setFlag(QGraphicsItem::ItemIsMovable, true); setAcceptedMouseButtons(Qt::LeftButton); setAcceptedMouseButtons(Qt::RightButton); } } void LayerItem::setFileInfo( const QString &filePath ) { // set filename m_filename = filePath; // compute checksum QFile file(filePath); if ( file.open(QIODevice::ReadOnly) ) { QCryptographicHash hash(QCryptographicHash::Md5); while ( !file.atEnd() ) { hash.addData(file.read(8192)); } m_checksum = hash.result().toHex(); } } void LayerItem::setUndoStack( QUndoStack* stack ) { m_undoStack = stack; } void LayerItem::setImageRect( const QRectF& rect ) { prepareGeometryChange(); } void LayerItem::setOriginalImage( const QImage& aImage, ImageType imageType) { qCDebug(logEditor) << "LayerItem::setOriginalImage(): Processing..."; { m_originalImage = aImage; m_originalImageType = imageType; m_cageApplied = imageType == ImageType::Warped ? true : false; } } const QImage& LayerItem::originalImage() { if ( m_originalImage.format() != QImage::Format_ARGB32 ) m_originalImage = m_originalImage.convertToFormat(QImage::Format_ARGB32); return m_originalImage; } QString LayerItem::name() const { if ( m_index == 0 ) return "MainImage"; if ( m_layer ) return m_layer->name(); return m_name; } // ------------------------ Update ------------------------ void LayerItem::updatePixmap() { if ( m_suppressPixmapUpdate ) return; if ( qobject_cast(qApp) ) { setPixmap(QPixmap::fromImage(m_image)); if ( m_redOverlay ) { const QPixmap& px = pixmap(); if ( !px.isNull() ) { m_redOverlayPixmap = QPixmap(px.size()); m_redOverlayPixmap.fill(QColor(255, 0, 0, 100)); QPainter op(&m_redOverlayPixmap); op.setCompositionMode(QPainter::CompositionMode_DestinationIn); op.drawPixmap(0, 0, px); } } } } void LayerItem::resetPixmap() { if ( m_suppressPixmapUpdate ) return; if ( qobject_cast(qApp) ) { setPixmap(QPixmap::fromImage(m_originalImage)); } } void LayerItem::updateImageRegion( const QRect& rect ) { if ( rect.isEmpty() ) return; QPixmap pm = pixmap(); if ( pm.isNull() ) return; QPainter painter(&pm); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.drawImage(rect.topLeft(),m_image,rect); painter.end(); setPixmap(pm); update(rect); } void LayerItem::applyActiveLutToRegion( const QRect& rect ) { if ( m_activeLut.isEmpty() || m_originalImage.isNull() ) return; if ( m_originalImage.format() != QImage::Format_ARGB32 ) return; const QRect r = rect & m_image.rect(); if ( r.isEmpty() ) return; for ( int y = r.top(); y <= r.bottom(); ++y ) { const QRgb* src = reinterpret_cast(m_originalImage.constScanLine(y)); QRgb* dst = reinterpret_cast(m_image.scanLine(y)); for ( int x = r.left(); x <= r.right(); ++x ) { int gray = qGray(src[x]); QRgb mapped = m_activeLut[gray]; dst[x] = qRgba(qRed(mapped), qGreen(mapped), qBlue(mapped), qAlpha(src[x])); } } } void LayerItem::updateOriginalImage() { m_originalImage = m_image; } void LayerItem::applyLutWithTransform( const QVector& lut ) { const QImage& original = originalImage(); if ( original.isNull() || original.format() != QImage::Format_ARGB32 ) return; // Build LUT-applied version of m_originalImage (without modifying it). QImage lutApplied = original.copy(); for ( int y = 0; y < lutApplied.height(); ++y ) { const QRgb* src = reinterpret_cast(original.constScanLine(y)); QRgb* dst = reinterpret_cast(lutApplied.scanLine(y)); for ( int x = 0; x < lutApplied.width(); ++x ) { int gray = qGray(src[x]); QRgb mapped = (gray < lut.size()) ? lut[gray] : src[x]; dst[x] = qRgba(qRed(mapped), qGreen(mapped), qBlue(mapped), qAlpha(src[x])); } } if ( m_totalTransform.isIdentity() ) { m_image = lutApplied; setActiveLut(lut); updatePixmap(); } else { // Re-apply the stored geometric transform to the LUT-applied source image. // Do NOT touch pos() here: commands like TransformLayerCommand may apply a // pivot-based position correction AFTER setImageTransform() that we must not // overwrite. The output dimensions are identical to the current m_image // (same transform, same source size), so the existing pos() remains correct. if ( !m_nogui && EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::System ) { m_image = Interpolation::transformWithHighQuality(lutApplied, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Bicubic ) { m_image = Interpolation::transformBicubic(lutApplied, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Nearest ) { m_image = lutApplied.transformed(m_totalTransform, Qt::FastTransformation); } else if ( m_nogui ) { m_image = Interpolation::transformBicubic(lutApplied, m_totalTransform); } else { m_image = lutApplied.transformed(m_totalTransform, Qt::SmoothTransformation); } setActiveLut(lut); updatePixmap(); } } // ------------------------ Selected ------------------------ void LayerItem::setIsSelected( int caller, bool isSelected ) { qCDebug(logEditor) << "LayerItem::setIsSelected(" << caller << "): name =" << name() << ", select =" << isSelected << ", operationMode =" << m_operationMode; { MainWindow* parent = m_parent != nullptr ? dynamic_cast(m_parent) : nullptr; if ( parent != nullptr ) { if ( isSelected ) { parent->getViewer()->setSelectedLayer(1,this); parent->setLayerOperationMode(m_operationMode,false); parent->updateLayerOperationParameter("LayerItem::setIsSelected",m_name,LayerItem::OperationMode::Rotate,m_currentRotation); } } if ( !isSelected ) setCageVisible(1,false); QGraphicsItem::setSelected(isSelected); } } // ------------------------ Mirror ------------------------ void LayerItem::mirror( int plane ) { qCDebug(logEditor) << "LayerItem::mirror(): name =" << name(); { if ( m_undoStack != nullptr ) { m_undoStack->push(new MirrorLayerCommand(this, m_index, plane)); } else { qWarning() << "CRITICAL - LayerItem::mirror(): m_undoStack is null"; } } } void LayerItem::setMirror( int mirrorPlane ) { qCDebug(logEditor) << "LayerItem::setMirror(): mirrorPlane =" << mirrorPlane; { if ( mirrorPlane > 0 ) { QTransform transform; transform.scale(mirrorPlane == 1 ? +1 : -1,mirrorPlane == 1 ? -1 : +1); setImageTransform(transform); } } } // ------------------------ Transform ------------------------ void LayerItem::shift( const QPointF &aShift ) { setPos(pos()+aShift); } void LayerItem::shiftTo( const QPointF &aPosition ) { setPos(aPosition); } void LayerItem::scale( double xscale, double yscale ) { qCDebug(logEditor) << "LayerItem::scale(): name =" << name() << ", scaleX =" << xscale << ", scaleY =" << yscale; { MainWindow* parent = m_parent != nullptr ? dynamic_cast(m_parent) : nullptr; if ( parent != nullptr && m_undoStack != nullptr ) { if ( m_undoStack->count() > 0 ) { QString scaleLayerName = QString("Scale Layer %1").arg(id()); const QUndoCommand* newestCmd = m_undoStack->command(m_undoStack->count() - 1); if ( newestCmd != nullptr ) { if ( scaleLayerName == newestCmd->text() ) { const TransformLayerCommand *layerCmd = static_cast(newestCmd); if ( layerCmd != nullptr ) { QTransform layerTransform = layerCmd->transform(); m_undoStack->push(new TransformLayerCommand(this, transform(), layerTransform.inverted())); parent->getViewer()->setCageVisible(this,LayerItem::OperationMode::Scale,true); } else { qWarning() << "LayerItem::scale(): static_cast failure for last undo command."; } } else { qWarning() << "LayerItem::scale(): Name mismatch: " << scaleLayerName << " not " << newestCmd->text(); } } } parent->updateLayerOperationParameter("LayerItem::scale",m_name,LayerItem::OperationMode::Scale,xscale,yscale); } } } void LayerItem::resetTotalTransform() { qCDebug(logEditor) << "LayerItem::resetTotalTransform(): Processing..."; { m_totalTransform = QTransform(); setImageTransform(m_totalTransform); } } void LayerItem::resetImageState( const QImage& image, const QPointF& position, const QTransform& transform ) { qCDebug(logEditor) << "LayerItem::resetImageState(): name =" << name() << ", position =" << position; { prepareGeometryChange(); setTransform(QTransform()); const QPointF p = EditorStyle::instance().allowIntegerMoveOnly() ? QPointF(qRound(position.x()), qRound(position.y())) : position; setPos(p); m_image = image; m_totalTransform = transform; updatePixmap(); } } void LayerItem::setImageTransform( const QTransform& transform, bool combine ) { qCDebug(logEditor) << "LayerItem::setImageTransform(): nonGUI =" << m_nogui << ", position =" << pos() << ", combine =" << combine << ", originalImageType =" << m_originalImageType; { // *** DO NOT USE INTERNAL TRANSFORMATIONS *** if ( 1 == 2 && !m_nogui && qobject_cast(qApp) ) { qDebug() << " + internal pixmap processing: transform_origin =" << QGraphicsPixmapItem::transformOriginPoint() << ", pos =" << QGraphicsPixmapItem::pos(); m_totalTransform = transform; m_image = m_originalImage.transformed(m_totalTransform, Qt::SmoothTransformation); prepareGeometryChange(); QPointF imageCenter = QRectF(m_originalImage.rect()).center(); QPointF sceneCenter = mapToScene(imageCenter); QPointF newImageCenter(m_image.width() / 2.0, m_image.height() / 2.0); setPixmap(QPixmap::fromImage(m_image)); setPos(sceneCenter - newImageCenter); // QGraphicsPixmapItem::setTransform(transform,combine); return; } // *** hard QImage transformation *** qCDebug(logEditor) << "LayerItem::setImageTransform(): ALTERNATIVE PROCESS FOR CASE WHERE NO PIXMAP IS AVAILABLE"; // check whether original image is-up-to-date if ( m_cageApplied && m_originalImageType == ImageType::Original ) { // qDebug() << "LayerItem::setImageTransform(): Need image update"; m_originalImageType = ImageType::Warped; m_originalImage = m_image.copy(); } // transform image prepareGeometryChange(); QPointF sceneCenter = qobject_cast(qApp) ? mapToScene(QRectF(pixmap().rect()).center()) : mapToScene(QRectF(m_originalImage.rect()).center()); QPointF imageCenter = QRectF(m_originalImage.rect()).center(); if ( combine ) { m_totalTransform.translate(imageCenter.x(), imageCenter.y()); m_totalTransform *= transform; m_totalTransform.translate(-imageCenter.x(), -imageCenter.y()); } else { m_totalTransform = QTransform(); m_totalTransform.translate(imageCenter.x(), imageCenter.y()); m_totalTransform *= transform; m_totalTransform.translate(-imageCenter.x(), -imageCenter.y()); } // the interpolation step (Qt only supports nearest neighbor und linear interpolation) if ( !m_nogui && EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::System ) { // !!! only in gui mode !!! m_image = Interpolation::transformWithHighQuality(m_originalImage, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Bicubic ) { // this use external bicubic interpolation m_image = Interpolation::transformBicubic(m_originalImage, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Nearest ) { m_image = m_originalImage.transformed(m_totalTransform,Qt::FastTransformation); } else if ( m_nogui ) { m_image = Interpolation::transformBicubic(m_originalImage, m_totalTransform); } else { m_image = m_originalImage.transformed(m_totalTransform,Qt::SmoothTransformation); } // end QPointF newImageCenter(m_image.width() / 2.0, m_image.height() / 2.0); setPos(sceneCenter - newImageCenter); // reset transform setTransform(QTransform()); // >>> updatePixmap(); } } // Re-applies the stored m_totalTransform to an updated m_originalImage without // modifying the transform itself. For an identity transform the image is used // as-is. In either case the item's scene position is adjusted so its visual // centre stays at the same scene point (same centre-alignment as setImageTransform). void LayerItem::reapplyImageTransform() { prepareGeometryChange(); QPointF sceneCenter = qobject_cast(qApp) ? mapToScene(QRectF(pixmap().rect()).center()) : mapToScene(QRectF(m_originalImage.rect()).center()); if ( m_totalTransform.isIdentity() ) { m_image = m_originalImage; } else if ( !m_nogui && EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::System ) { m_image = Interpolation::transformWithHighQuality(m_originalImage, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Bicubic ) { m_image = Interpolation::transformBicubic(m_originalImage, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Nearest ) { m_image = m_originalImage.transformed(m_totalTransform, Qt::FastTransformation); } else if ( m_nogui ) { m_image = Interpolation::transformBicubic(m_originalImage, m_totalTransform); } else { m_image = m_originalImage.transformed(m_totalTransform, Qt::SmoothTransformation); } QPointF newImageCenter(m_image.width() / 2.0, m_image.height() / 2.0); setPos(sceneCenter - newImageCenter); setTransform(QTransform()); updatePixmap(); } void LayerItem::restoreTransformState( const QTransform& totalTransform, const QPointF& restoredPos ) { prepareGeometryChange(); m_totalTransform = totalTransform; if ( m_totalTransform.isIdentity() ) { m_image = m_originalImage; } else if ( !m_nogui && EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::System ) { m_image = Interpolation::transformWithHighQuality(m_originalImage, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Bicubic ) { m_image = Interpolation::transformBicubic(m_originalImage, m_totalTransform); } else if ( EditorStyle::instance().interpolationMode() == EditorStyle::InterpolationMode::Nearest ) { m_image = m_originalImage.transformed(m_totalTransform, Qt::FastTransformation); } else if ( m_nogui ) { m_image = Interpolation::transformBicubic(m_originalImage, m_totalTransform); } else { m_image = m_originalImage.transformed(m_totalTransform, Qt::SmoothTransformation); } setPos(restoredPos); setTransform(QTransform()); updatePixmap(); } // ------------------------ Paint ------------------------ void LayerItem::paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget ) { { QGraphicsPixmapItem::paint(painter,option,widget); if ( m_redOverlay && !m_redOverlayPixmap.isNull() ) { painter->drawPixmap(QPointF(0, 0), m_redOverlayPixmap); } if ( m_operationMode == OperationMode::Perspective ) { return; } if ( isSelected() || m_isMultiSelected ) { painter->setPen(m_selectedPen); painter->setBrush(Qt::NoBrush); painter->drawRect(boundingRect()); } else if ( m_showBoundingBox ) { painter->setPen(m_lassoPen); painter->setBrush(Qt::NoBrush); painter->drawRect(boundingRect()); } // rotation centre marker if ( m_operationMode == OperationMode::Rotate && ( isSelected() || m_isMultiSelected ) ) { const QPointF c = pivot(); const qreal arm = 8.0; QPen crossPen(m_pivotSelected ? Qt::yellow : Qt::red, 1.5); crossPen.setCosmetic(true); painter->setPen(crossPen); painter->setBrush(Qt::NoBrush); painter->drawLine(c + QPointF(-arm, 0), c + QPointF(arm, 0)); painter->drawLine(c + QPointF(0, -arm), c + QPointF(0, arm)); painter->drawEllipse(c, 4.0, 4.0); } // render cage if ( m_cageEnabled ) { painter->setPen(QPen(Qt::red,0)); painter->setBrush(Qt::NoBrush); painter->drawPolygon(m_cage.data(), m_cage.size()); } } } void LayerItem::paintStrokeSegment( const QPoint& p0, const QPoint& p1, const QColor &color, int radius, float hardness ) { qCDebug(logEditor) << "LayerItem::paintStrokeSegment(): Processing..."; if ( radius < 0.0 ) return; { const float spacing = std::max(1.0f, radius * 0.35f); const float dx = p1.x() - p0.x(); const float dy = p1.y() - p0.y(); const float dist = std::sqrt(dx*dx + dy*dy); if ( dist <= 0.0f ) { Interpolation::dab(m_image, p0, color, radius, hardness); return; } const int steps = std::ceil(dist / spacing); for ( int i = 0; i <= steps; ++i ) { float t = float(i) / float(steps); QPoint p( int(p0.x() + t * dx), int(p0.y() + t * dy) ); Interpolation::dab(m_image, p, color, radius, hardness); } // --- QRect dirtyRect = QRect(p0, QSize(1,1)); dirtyRect |= QRect(p1, QSize(1,1)); int pad = radius + 2; dirtyRect.adjust(-pad, -pad, pad, pad); dirtyRect &= m_image.rect(); updateImageRegion(dirtyRect); // --- // setPixmap(QPixmap::fromImage(m_image)); } } // ------------------------ Cage ------------------------ QImage LayerItem::applyCageWarp( const QString &caller ) { qCDebug(logEditor) << "LayerItem::applyCageWarp(" << caller << "): name =" << name() << ", meshActive =" << m_cageMesh.isActive() << ", activeCagePointId =" << m_cageMesh.activeCagePointId() << ", cageEnabled =" << m_cageEnabled << ", cageEditing =" << m_cageEditing << ", intialized =" << m_cageMesh.isInitialized(); { // Fast path: use a pre-computed warped image cached from JSON (avoids recomputing on project load) if (!m_pendingCacheImage.isNull()) { QImage cached = m_pendingCacheImage; m_pendingCacheImage = QImage(); m_cageMesh.setActiveCagePointId(-1); m_cageMesh.setOffset(0, 0); setOffset(QPointF(0, 0)); if (!m_suppressPixmapUpdate) setPixmap(QPixmap::fromImage(cached)); m_image = cached; m_cageApplied = true; return cached.copy(); } #if 0 // removed by CLAUDE if ( !m_cageEditing ) { return m_cageMesh.image(); } #endif if ( !m_cageMesh.isInitialized(true) ) { m_cageMesh.setImage(m_image); } if ( Config::gpuCageWarpProcessing || EditorStyle::instance().useGPU() ) { qCDebug(logEditor) << "LayerItem::applyCageWarp(): GPU cage warp processing..."; if ( m_cageWarpRenderer == nullptr ) { m_cageWarpRenderer = new CageWarpRenderer(); m_cageWarpRenderer->setSourceImage(m_cageMesh.image()); } m_cageWarpRenderer->setGridSize(m_cageMesh.cols(), m_cageMesh.rows()); CageWarpRenderer::WarpOptions options; options.cageInterpolation = EditorStyle::instance().gpuCatmullRom() ? CageWarpRenderer::CageInterpolation::CatmullRom : CageWarpRenderer::CageInterpolation::Bilinear; options.inverseMapping = CageWarpRenderer::InverseMapping::Newton; options.inverseIterations = 10; QImage warped = m_cageWarpRenderer->warp(m_cageMesh.points(),nullptr,options); // The GPU inverse-field warp processes the entire output bounding box, which can // produce opaque output in areas that are transparent in the source (alpha=0). // Apply a CPU alpha mask using the cage mesh forward mapping ? same approach as // TriangleWarp ? to restore correct transparency. if (!warped.isNull()) { warped = warped.convertToFormat(QImage::Format_ARGB32); const QImage srcA = m_cageMesh.image().convertToFormat(QImage::Format_ARGB32); const QVector& pts = m_cageMesh.points(); const int gcols = m_cageMesh.cols(); const int grows = m_cageMesh.rows(); if (!srcA.isNull() && pts.size() == gcols * grows && gcols >= 2 && grows >= 2) { // Compute output origin = bounding box min of all warped cage points double minX = pts[0].x(), minY = pts[0].y(); for (const QPointF& p : pts) { minX = qMin(minX, p.x()); minY = qMin(minY, p.y()); } const QPointF origin(std::floor(minX), std::floor(minY)); const double srcW = srcA.width(), srcH = srcA.height(); // Build alpha mask by scanning each cage cell in output (target) space QVector alphaMask(warped.width() * warped.height(), false); for (int cy = 0; cy + 1 < grows; ++cy) { for (int cx = 0; cx + 1 < gcols; ++cx) { const QVector dstQuad = { pts[ cy * gcols + cx ] - origin, pts[ cy * gcols + cx + 1] - origin, pts[(cy + 1) * gcols + cx + 1] - origin, pts[(cy + 1) * gcols + cx ] - origin }; const QVector srcQuad = { QPointF( cx * srcW / (gcols - 1), cy * srcH / (grows - 1)), QPointF((cx + 1) * srcW / (gcols - 1), cy * srcH / (grows - 1)), QPointF((cx + 1) * srcW / (gcols - 1), (cy + 1) * srcH / (grows - 1)), QPointF( cx * srcW / (gcols - 1), (cy + 1) * srcH / (grows - 1)) }; const QRectF bbox = QPolygonF(dstQuad).boundingRect(); const int yMin = qMax(0, int(bbox.top())); const int yMax = qMin(warped.height()-1, int(bbox.bottom()) + 1); const int xMin = qMax(0, int(bbox.left())); const int xMax = qMin(warped.width()-1, int(bbox.right()) + 1); for (int py = yMin; py <= yMax; ++py) { QPointF start(-1, -1); for (int px = xMin; px <= xMax; ++px) { const QPointF p(px, py); if (!GeometryUtils::pointInQuad(p, dstQuad)) continue; int cnt = 0; const QPointF srcP = GeometryUtils::barycentric(p, dstQuad, srcQuad, &cnt, start); const int sx = qBound(0, int(srcP.x()), srcA.width() - 1); const int sy = qBound(0, int(srcP.y()), srcA.height() - 1); if (qAlpha(srcA.pixel(sx, sy)) > 0) alphaMask[py * warped.width() + px] = true; } } } } // Zero out pixels not covered by the alpha mask for (int py = 0; py < warped.height(); ++py) { QRgb* row = reinterpret_cast(warped.scanLine(py)); for (int px = 0; px < warped.width(); ++px) { if (!alphaMask[py * warped.width() + px]) row[px] = qRgba(0, 0, 0, 0); } } } } setOffset(QPointF(0, 0)); // reset pixmap offset accumulated during drag normalization if (!m_suppressPixmapUpdate) setPixmap(QPixmap::fromImage(warped)); m_image = warped; QGraphicsPixmapItem::setPos(QGraphicsPixmapItem::pos() + m_cageMesh.getOffset()); m_cageApplied = true; return m_image.copy(); } else { // NOT YET WORKING: QuadWarp::WarpResult warped = QuadWarp::warp(m_cageMesh.image(),m_cageMesh); if( m_cageMesh.needUpdate() ) { TriangleWarp::WarpResult warped = TriangleWarp::warp(m_image, m_cageMesh.image(),m_cageMesh); m_cageMesh.setNeedUpdate(false); m_cageMesh.setActiveCagePointId(-1); m_cageMesh.setOffset(0,0); // CLAUDE reset after each drawing if ( !warped.image.isNull() ) { setOffset(QPointF(0, 0)); // reset pixmap offset accumulated during drag normalization if (!m_suppressPixmapUpdate) setPixmap(QPixmap::fromImage(warped.image)); m_image = warped.image; QGraphicsPixmapItem::setPos(QGraphicsPixmapItem::pos()); // QGraphicsPixmapItem::setPos(QGraphicsPixmapItem::pos() + m_cageMesh.getOffset()); m_cageApplied = true; return m_image.copy(); } else { qCritical() << "CRITICAL - LayerItem::applyCageWarp(): WARNING: Image isNull."; } } } return QImage(); } } void LayerItem::enableCage( int cols, int rows ) { qCDebug(logEditor) << "LayerItem::enableCage(): cols =" << cols << ", rows =" << rows << ", initialized =" << m_cageMesh.isInitialized() << ", enabled =" << m_cageEnabled; { if ( m_cageMesh.isInitialized() ) return; int ncols = cols == -1 ? m_cageMesh.cols() : cols; int nrows = rows == -1 ? m_cageMesh.rows() : rows; m_cageMesh.create(boundingRect(), ncols, nrows); if ( !m_cageMesh.isInitialized() ) m_cageMesh.setImage(m_image); m_cageEnabled = true; if ( !scene() ) return; if ( !m_cageOverlay ) { m_cageOverlay = new CageOverlayItem(this); m_cageOverlay->setParentItem(this); } m_cageOverlay->setVisible(true); // Handles qDeleteAll(m_handles); m_handles.clear(); for ( int i = 0; i < m_cageMesh.pointCount(); ++i ) { auto* h = new CageControlPointItem(this, i); h->setParentItem(this); h->setPos(m_cageMesh.point(i)); int col = i % m_cageMesh.cols(); int row = i / m_cageMesh.cols(); int dx = ( col == 0 ) ? 0 : ( ( col == m_cageMesh.cols()-1 ) ? -2*m_cageMesh.getRadius() : -m_cageMesh.getRadius() ); int dy = ( row == 0 ) ? 0 : ( ( row == m_cageMesh.rows()-1 ) ? -2*m_cageMesh.getRadius() : -m_cageMesh.getRadius() ); h->setRect(dx, dy, 2*m_cageMesh.getRadius(), 2*m_cageMesh.getRadius() ); m_handles << h; } } } // --- what about the grid ??? --- void LayerItem::initCage( const QVector& pts, const QRectF &rect, int nrows, int ncolumns ) { qCDebug(logEditor) << "LayerItem::initCage(): cageOverlay =" << (m_cageOverlay!=nullptr?"ok":"null") << ", rect =" << rect << ", rows =" << nrows << ", ncolumns =" << ncolumns; { m_cageMesh.setIsInitialized(); m_cageMesh.create(rect,ncolumns,nrows); // m_cageMesh.setImage(m_image); // ADDED by CLAUDE // 04.01.2026: REMOVED by Hartmut: // Multiple undo/redo operations result in cumulative distortions m_cageMesh.setPoints(pts); if ( m_cageOverlay == nullptr ) { m_cageOverlay = new CageOverlayItem(this); m_cageOverlay->setParentItem(this); } } } void LayerItem::setCageVisible( int caller, bool isVisible ) { qCDebug(logEditor) << "LayerItem::setCageVisible(): caller =" << caller << ", name =" << name() << ", visible =" << isVisible << ", cageOverlay =" << (m_cageOverlay != nullptr?"ok":"none") << ", cageEditing =" << m_cageEditing << "|" << m_cageEnabled << ", nControlPoints =" << m_cageMesh.pointCount(); { ImageView *viewer = getParentImageView(); if ( m_cageOverlay != nullptr && viewer != nullptr ) { viewer->enableCageLayer(this,isVisible); if ( isVisible ) { m_cageMesh.setActive(true); m_cageEnabled = true; m_cageEditing = true; for ( int i = 0; i < m_handles.size(); ++i ) { m_handles[i]->setVisible(true); } m_cageOverlay->setVisible(true); } else { #if 0 // CLAUDE says: This is potentially wrong. This will reset // m_selectedCageLayer to null in MainWindow, which is not // necessarily what we want to do when making other inactive // cages invisible. Let the parent MainWindow decide about this. if ( m_parent != nullptr ) { MainWindow* parent = dynamic_cast(m_parent); if ( parent && parent->getViewer() != nullptr ) { parent->getViewer()->setActiveCageLayer(nullptr); } } #endif m_cageMesh.setActive(false); m_cageEnabled = false; m_cageEditing = false; for ( int i = 0; i < m_handles.size(); ++i ) { m_handles[i]->setVisible(false); } m_cageOverlay->setVisible(false); } update(); } else { // qCritical() << "CRITICAL: LayerItem::setCageVisible(): No Cage Overlay!"; } } } void LayerItem::setCageVisible( LayerItem::OperationMode mode, bool isVisible, bool pushBackImage ) { qCDebug(logEditor) << "LayerItem::setCageVisible(): name =" << name() << ", mode =" << mode << ", isVisible =" << isVisible << ", pushBackImage =" << pushBackImage << ", cageOverlay =" << (m_cageOverlay != nullptr?"ok":"none"); { switch ( mode ) { case LayerItem::OperationMode::CageWarp: if ( m_cageOverlay != nullptr ) { if( m_handles.size() == 0 ) { // create handles if they have not been created (when read from .json) // CLAUDE -- this should be moved to a more convenient place. qDeleteAll(m_handles); m_handles.clear(); for ( int i = 0; i < m_cageMesh.pointCount(); ++i ) { auto* h = new CageControlPointItem(this, i); h->setParentItem(this); h->setPos(m_cageMesh.point(i)); int col = i % m_cageMesh.cols(); int row = i / m_cageMesh.cols(); int dx = ( col == 0 ) ? 0 : ( ( col == m_cageMesh.cols()-1 ) ? -2*m_cageMesh.getRadius() : -m_cageMesh.getRadius() ); int dy = ( row == 0 ) ? 0 : ( ( row == m_cageMesh.rows()-1 ) ? -2*m_cageMesh.getRadius() : -m_cageMesh.getRadius() ); h->setRect(dx, dy, 2*m_cageMesh.getRadius(), 2*m_cageMesh.getRadius() ); m_handles << h; } } for ( int i = 0; i < m_handles.size(); ++i ) { m_handles[i]->setVisible(isVisible); } m_cageOverlay->setVisible(isVisible); } else { qCDebug(logEditor) << "WARNING: m_cageOverlay is null."; } break; case LayerItem::OperationMode::Scale: case LayerItem::OperationMode::Perspective: if ( m_parent != nullptr ) { MainWindow* parent = dynamic_cast(m_parent); if ( parent && parent->getViewer() != nullptr ) { parent->getViewer()->setCageVisible(this,mode,isVisible); } } break; default: qDebug() << " + unprocessed mode " << mode; } if ( isVisible == false && pushBackImage == true ) { setOriginalImage(m_cageMesh.image()); } } } int LayerItem::changeNumberOfActiveCagePoints( int step ) { qCDebug(logEditor) << "LayerItem::changeNumberOfActiveCagePoints(): cage =" << m_cageMesh.cols() << ", step =" << step; { int expo_c = std::log2(m_cageMesh.cols()-1); expo_c -= step > 0 ? 0 : 1; int ds_c = pow(2, expo_c); int columns = m_cageMesh.cols() + step * ds_c; int rows; if (EditorStyle::instance().squareCageQuads()) { const QRectF br = boundingRect(); rows = (br.width() > 0 && columns > 1) ? qMax(3, qRound(br.height() * (columns - 1) / br.width()) + 1) : qMax(3, m_cageMesh.rows() + step * ds_c); } else { int expo_r = std::log2(qMax(2, m_cageMesh.rows()) - 1); expo_r -= step > 0 ? 0 : 1; int ds_r = pow(2, expo_r); rows = m_cageMesh.rows() + step * ds_r; } if ( rows >= 3 && columns >= 3 ) { // Snapshot state before resize for cage edit undo const QVector ptsBefore = m_cageMesh.points(); const QVector origBefore = m_cageMesh.originalPoints(); const int colsBefore = m_cageMesh.cols(); const int rowsBefore = m_cageMesh.rows(); // resize() uses general bilinear interpolation and handles arbitrary size // changes (including non-power-of-2 steps for squareCageQuads). if( step < 0 ) m_cageMesh.setNeedUpdate(); // CLAUDE: not needed if subdivided, needed if coarsened m_cageMesh.update(boundingRect(), std::max(3,columns), std::max(3,rows)); // update handles qDeleteAll(m_handles); m_handles.clear(); for ( int i = 0; i < m_cageMesh.pointCount(); ++i ) { auto* h = new CageControlPointItem(this, i); h->setParentItem(this); h->setPos(m_cageMesh.point(i)); int col = i % m_cageMesh.cols(); int row = i / m_cageMesh.cols(); int dx = ( col == 0 ) ? 0 : ( ( col == m_cageMesh.cols()-1 ) ? -2*m_cageMesh.getRadius() : -m_cageMesh.getRadius() ); int dy = ( row == 0 ) ? 0 : ( ( row == m_cageMesh.rows()-1 ) ? -2*m_cageMesh.getRadius() : -m_cageMesh.getRadius() ); h->setRect(dx, dy, 2*m_cageMesh.getRadius(), 2*m_cageMesh.getRadius() ); m_handles << h; } // Grid resize never shifts the layer position, so posBefore == posAfter == pos(). cageEditStack()->push(new CageEditCommand(this, ptsBefore, origBefore, colsBefore, rowsBefore, pos(), m_cageMesh.points(), m_cageMesh.originalPoints(), m_cageMesh.cols(), m_cageMesh.rows(), pos(), step > 0 ? "Increase cage grid" : "Decrease cage grid")); return rows; } return 0; } } void LayerItem::setNumberOfActiveCagePoints( int nControlPoints ) { enableCage(nControlPoints,nControlPoints); } void LayerItem::setCageWarpProperty( int type, double value ) { if ( type == 1 ) { m_cageMesh.setNumberOfRelaxationsSteps(int(value)); } else if ( type == 2 ) { m_cageMesh.setStiffness(value); } else if ( type == 3 ) { m_cageMesh.setFixedBoundaries(value>0?true:false); } else { return; } m_cageMesh.setNeedUpdate(); } void LayerItem::setCagePoints( const QVector& pts ) { m_cageMesh.setPoints(pts); update(); } QVector LayerItem::cagePoints() const { return m_cageMesh.points(); } void LayerItem::updateCagePoint( TransformHandleItem* handle, const QPointF& localPos ) { qCDebug(logEditor) << "LayerItem::updateCagePoint(): Processing..."; { int idx = m_handles.indexOf(handle); if ( idx < 0 ) return; m_cage[idx] = localPos; update(); } } // -------------- cage edit undo/redo (separate per-layer stack) -------------- QUndoStack* LayerItem::cageEditStack() { if ( !m_cageEditStack ) { m_cageEditStack = new QUndoStack(); // Capture the cage state at the very beginning of this editing session. // resetCageToPixmap() uses this to restore in one step instead of // iterating through the whole undo history. m_cageInitialPts = m_cageMesh.points(); m_cageInitialOrigPts = m_cageMesh.originalPoints(); m_cageInitialCols = m_cageMesh.cols(); m_cageInitialRows = m_cageMesh.rows(); m_cageInitialPos = pos(); } return m_cageEditStack; } void LayerItem::snapshotCageState() { m_cageSnapPts = m_cageMesh.points(); m_cageSnapOrigPts = m_cageMesh.originalPoints(); m_cageSnapCols = m_cageMesh.cols(); m_cageSnapRows = m_cageMesh.rows(); m_cageSnapPos = pos(); } void LayerItem::restoreCageState( const QVector& pts, const QVector& origPts, int cols, int rows, const QPointF& scenePos ) { setPos(scenePos); m_cageMesh.restore(pts, origPts, cols, rows); // Rebuild cage control point handles (same pattern as changeNumberOfActiveCagePoints) const int radius = m_cageMesh.getRadius(); qDeleteAll(m_handles); m_handles.clear(); for ( int i = 0; i < m_cageMesh.pointCount(); ++i ) { auto* h = new CageControlPointItem(this, i); h->setParentItem(this); h->setPos(m_cageMesh.point(i)); int col = i % cols; int row = i / cols; int dx = (col == 0) ? 0 : ((col == cols - 1) ? -2 * radius : -radius); int dy = (row == 0) ? 0 : ((row == rows - 1) ? -2 * radius : -radius); h->setRect(dx, dy, 2 * radius, 2 * radius); m_handles << h; } applyCageWarp("restoreCageState"); } // ---------------------------------------------------------------------------- void LayerItem::resetCageToPixmap() { qCDebug(logEditor) << "LayerItem::resetCageToPixmap(): size =" << m_handles.size(); { if ( m_cageInitialCols > 0 ) { // Restore the cage to the state before the first edit in this session. // This reverses all point moves AND all grid resizes in one step. restoreCageState(m_cageInitialPts, m_cageInitialOrigPts, m_cageInitialCols, m_cageInitialRows, m_cageInitialPos); } else { // No edit history yet: reset all points to a uniform grid over the pixmap. prepareGeometryChange(); const qreal w = pixmap().width(); const qreal h = pixmap().height(); setOffset(0, 0); const int ncols = m_cageMesh.cols(); const int nrows = m_cageMesh.rows(); const qreal dx = w / (ncols - 1); const qreal dy = h / (nrows - 1); QList resetPoints; for ( int i = 0; i < nrows; ++i ) { for ( int j = 0; j < ncols; ++j ) resetPoints << QPointF(j * dx, i * dy); } m_cageMesh.setPoints(resetPoints); for ( int i = 0; i < m_handles.size(); ++i ) { if ( i < resetPoints.size() ) m_handles[i]->setPos(resetPoints[i]); } applyCageWarp("LayerItem"); } // Clear edit history ? the cage is back to its starting state. // Keep m_cageInitialCols so repeated Reset presses are idempotent. if ( m_cageEditStack ) m_cageEditStack->clear(); } } /** BEST */ void LayerItem::setCagePoint( int idx, const QPointF& pos ) { qCDebug(logEditor) << "LayerItem::setCagePoint(): index =" << idx << ", point =" << pos; { QPointF localPos = mapFromScene(pos); if ( EditorStyle::instance().allowIntegerMoveOnly() ) localPos = QPointF(qRound(localPos.x()), qRound(localPos.y())); if ( EditorStyle::instance().noSelfIntersection() ) { const QVector& pts = m_cageMesh.points(); const int cols = m_cageMesh.cols(); const int rows = m_cageMesh.rows(); const int row = idx / cols; const int col = idx % cols; // True if segments AB and CD properly intersect (strict, not just touching) auto segmentsIntersect = [](const QPointF& a, const QPointF& b, const QPointF& c, const QPointF& d) -> bool { auto cr = [](const QPointF& u, const QPointF& v){ return u.x()*v.y() - u.y()*v.x(); }; double d1 = cr(d-c, a-c), d2 = cr(d-c, b-c); double d3 = cr(b-a, c-a), d4 = cr(b-a, d-a); return ((d1>0&&d2<0)||(d1<0&&d2>0)) && ((d3>0&&d4<0)||(d3<0&&d4>0)); }; // Signed area ? 2 via shoelace ? positive = valid TL?TR?BR?BL winding auto signedArea2 = [](const QPointF& p0, const QPointF& p1, const QPointF& p2, const QPointF& p3) -> double { return p0.x()*(p1.y()-p3.y()) + p1.x()*(p2.y()-p0.y()) + p2.x()*(p3.y()-p1.y()) + p3.x()*(p0.y()-p2.y()); }; // Quad is invalid if opposite edges cross (horizontal/vertical escape) // OR if the overall winding inverts (diagonal escape) auto isSelfIntersecting = [&](const QPointF& p0, const QPointF& p1, const QPointF& p2, const QPointF& p3) -> bool { return segmentsIntersect(p0, p1, p2, p3) || segmentsIntersect(p1, p2, p3, p0) || signedArea2(p0, p1, p2, p3) <= 0; }; // Check all quads the moving point (at candidate pos) participates in auto quadsOk = [&](const QPointF& candidate) -> bool { // Temporarily override pt() for this candidate auto ptC = [&](int r, int c) -> QPointF { int i = r * cols + c; return (i == idx) ? candidate : pts[i]; }; if ( row < rows-1 && col < cols-1 ) if ( isSelfIntersecting(ptC(row,col), ptC(row,col+1), ptC(row+1,col+1), ptC(row+1,col)) ) return false; if ( row < rows-1 && col > 0 ) if ( isSelfIntersecting(ptC(row,col-1), ptC(row,col), ptC(row+1,col), ptC(row+1,col-1)) ) return false; if ( row > 0 && col > 0 ) if ( isSelfIntersecting(ptC(row-1,col-1), ptC(row-1,col), ptC(row,col), ptC(row,col-1)) ) return false; if ( row > 0 && col < cols-1 ) if ( isSelfIntersecting(ptC(row-1,col), ptC(row-1,col+1), ptC(row,col+1), ptC(row,col)) ) return false; return true; }; const QPointF current = pts[idx]; if ( quadsOk(localPos) ) { // full move OK ? nothing to do, localPos stays } else if ( quadsOk(QPointF(localPos.x(), current.y())) ) { localPos = QPointF(localPos.x(), current.y()); // slide along X only } else if ( quadsOk(QPointF(current.x(), localPos.y())) ) { localPos = QPointF(current.x(), localPos.y()); // slide along Y only } else { return; // no valid partial move either } } m_cageMesh.setPoint(idx,localPos); QRectF newBounds = QPolygonF(m_cageMesh.points()).boundingRect(); if ( newBounds.x() != 0 || newBounds.y() != 0 ) { qreal dx = newBounds.x(); qreal dy = newBounds.y(); prepareGeometryChange(); setPos(mapToScene(QPointF(dx, dy))); m_cageMesh.addOffset(dx,dy); setOffset(offset() - QPointF(dx, dy)); // counter-shift pixmap to keep image visually stable during drag QList pts = m_cageMesh.points(); for ( int i = 0; i < pts.size(); ++i ) { pts[i] -= QPointF(dx, dy); } m_cageMesh.setPoints(pts); } for ( int i = 0; i < m_handles.size(); ++i ) { m_handles[i]->setPos(m_cageMesh.point(i)); } m_cageMesh.setActiveCagePointId(idx); m_cageMesh.relax(); if ( EditorStyle::instance().useGPU() && EditorStyle::instance().liveWarp() ) { applyCageWarp("setCagePoint"); } update(); } } /** */ void LayerItem::commitCageTransform( const QVector &cage ) { qCDebug(logEditor) << "LayerItem::commitCageTransform(): Processing..."; { if ( m_cage.size() < 4 || m_originalCage.size() < 4 ) return; QTransform t; if ( QTransform::quadToQuad(m_originalCage.mid(0, 4),cage.mid(0, 4),t) ) { setTransform(t, false); } // Neue Basis f?r weitere Transformationen m_cage = cage; m_originalCage = m_cage; update(); } } void LayerItem::beginCageEdit() { qCDebug(logEditor) << "LayerItem::beginCageEdit(): Processing..."; { m_cageEditing = true; m_startPos = pos(); m_startTransform = transform(); } } void LayerItem::endCageEdit( int idx, const QPointF& startPos ) { qCDebug(logEditor) << "LayerItem::endCageEdit(): Processing..."; { QVector cage; for ( int i=0 ; ipush(new TransformLayerCommand(this, m_startPos, pos(), m_startTransform, transform())); } } LayerItem::OperationMode LayerItem::getPolygonOperationMode() { if ( m_parent != nullptr ) { MainWindow* parent = dynamic_cast(m_parent); if ( parent != nullptr ) { return parent->getViewer()->getPolygonOperationMode(); } } return LayerItem::OperationMode::None; } void LayerItem::setPolygonOperationMode( OperationMode mode ) { if ( m_polygonOperationMode == mode ) return; m_polygonOperationMode = mode; } void LayerItem::setOperationMode( OperationMode mode ) { qCDebug(logEditor) << "LayerItem::setOperationMode(): name =" << name() << ", index =" << m_index << ", mode =" << m_operationMode << "|" << mode; { if ( m_operationMode == mode ) { return; } if ( m_operationMode == OperationMode::CageWarp ) { setCageVisible(2,false); // setOriginalImage(m_image); } // boundingRect() changes when m_hasPivot toggles the pivot-cross expansion if ( m_hasPivot ) { prepareGeometryChange(); } m_operationMode = mode; if ( m_operationMode == OperationMode::CageWarp ) { setCageVisible(3,true); } update(); } } void LayerItem::setRotationAngle( double angleDelta ) { qCDebug(logEditor) << "LayerItem::setRotationAngle(): deltaRotationAngle =" << angleDelta << ", currentRotation =" << m_currentRotation; { QString name = "Rotate Layer"; TransformLayerCommand::LayerTransformType trafoType = TransformLayerCommand::LayerTransformType::Rotate; name += QString(" %1").arg(m_index); // setImageTransform() always rotates around imageCenter regardless of what transform // is passed (translate/rotate mix due to Qt's pre/post-multiply asymmetry cancels out). // A pure rotation R is therefore correct here. QTransform t; t.rotate(angleDelta); // When a custom pivot is set, compute a scene-space position adjustment so the // rotation visually happens around the pivot point instead of the image center. QPointF posAdj; if ( m_hasPivot ) { const QPointF pivotScene = m_pivot; // already scene coords const QPointF centerScene = mapToScene(boundingRect().center()); const double rad = qDegreesToRadians(angleDelta); const double cosA = qCos(rad), sinA = qSin(rad); const QPointF off = centerScene - pivotScene; const QPointF rotOff(cosA * off.x() - sinA * off.y(), sinA * off.x() + cosA * off.y()); posAdj = (pivotScene + rotOff) - centerScene; } m_currentRotation += angleDelta; auto* cmd = new TransformLayerCommand(this, m_startPos, pos(), m_currentRotation, m_startTransform, t, name, trafoType); if ( !posAdj.isNull() ) cmd->setPositionAdjust(posAdj); m_undoStack->push(cmd); } } // ------------------------ Mouse events ------------------------ bool LayerItem::isValidMouseEventOperation() { if ( m_parent != nullptr ) { MainWindow* parent = dynamic_cast(m_parent); MainWindow::MainOperationMode opMode = parent != nullptr ? parent->getOperationMode(): MainWindow::MainOperationMode::None; return opMode == MainWindow::MainOperationMode::ImageLayer ? true : false; } return false; } void LayerItem::mouseDoubleClickEvent( QGraphicsSceneMouseEvent* event ) { qCDebug(logEditor) << "LayerItem::mouseDoubleClickEvent(): index =" << m_index << ", mode =" << m_operationMode; { if ( isSelected() && isValidMouseEventOperation() ) { if ( m_operationMode == OperationMode::Rotate ) { // Accept without action: prevents QGraphicsScene from calling clearSelection() // when the double-click lands in the extended bounding rect (pivot area outside pixmap). event->accept(); return; } if ( m_operationMode == OperationMode::CageWarp ) { if ( m_cageEnabled == false ) { MainWindow* parent = m_parent != nullptr ? dynamic_cast(m_parent) : nullptr; if ( parent && parent->getViewer() != nullptr ) { parent->getViewer()->setActiveCageLayer(this); enableCage(m_cageMesh.cols(),m_cageMesh.rows()); return; } } else { setCageVisible(4,false); } } else if ( m_operationMode == OperationMode::Perspective ) { qCDebug(logEditor) << " + perpective call +"; } else if ( m_operationMode == OperationMode::Flip ) { mirror(IMainSystem::instance()->getLayerOperationParameter(LayerItem::OperationMode::Flip) > 0 ? 1 : 2); } } } } void LayerItem::mousePressEvent( QGraphicsSceneMouseEvent* event ) { qCDebug(logEditor) << "LayerItem::mousePressEvent(): layer =" << name() << ", selected =" << isSelected() << ", zValue =" << zValue() << ", operationMode =" << m_operationMode << ", active =" << m_mouseOperationActive << ", event_modifiers =" << event->modifiers(); { if ( event->button() != Qt::LeftButton ) { QGraphicsPixmapItem::mousePressEvent(event); return; } if ( event->modifiers() & Qt::AltModifier ) { // Multi-select (Alt/Option): toggle this layer in/out of the co-move group ImageView* view = getParentImageView(); if ( view ) { if ( view->isInLayerSelection(this) ) view->removeLayerFromSelection(this); else view->addLayerToSelection(this); for ( LayerItem* l : view->selectedLayers() ) l->resetDragStartPos(); } QGraphicsPixmapItem::mousePressEvent(event); return; } if ( isSelected() || event->modifiers() & Qt::ControlModifier ) { MainWindow* parent = m_parent != nullptr ? dynamic_cast(m_parent) : nullptr; if ( isValidMouseEventOperation() && parent != nullptr ) { parent->updateLayerOperationParameter("LayerItem::mousePressEvent",m_name,LayerItem::OperationMode::Rotate,m_currentRotation); if ( m_operationMode == OperationMode::Translate ) { m_startPos = pos(); m_mouseOperationActive = true; m_startTransform = transform(); QGraphicsPixmapItem::mousePressEvent(event); // Clear multi-selection group on normal drag start (primary layer tracked via isSelected()) ImageView* view = getParentImageView(); if ( view ) view->clearLayerSelection(); } else if ( m_operationMode == OperationMode::Rotate ) { // Check whether the click lands on the pivot marker (12 px hit tolerance in item coords) const QPointF clickItemPos = event->pos(); if ( QLineF(clickItemPos, pivot()).length() <= 12.0 ) { m_pivotAtDragStart = pivotScene(); // scene coords m_pivot = m_pivotAtDragStart; // ensure release without drag doesn't corrupt m_pivot m_pivotDragging = true; m_pivotSelected = true; m_pivotWasSet = m_hasPivot; m_mouseOperationActive = true; m_startPos = pos(); event->accept(); update(); } else { m_pivotDragging = false; m_pivotSelected = false; // If the click is in the extended bounding rect (around the pivot) // but outside the actual pixmap, consume it silently. Without this, // the base-class call below would ignore() the event (BoundingRectShape // only covers the pixmap rect), causing propagation to MainImage which // would deselect this layer. const QSizeF sz = pixmap().isNull() ? QSizeF(m_image.size()) : QSizeF(pixmap().size()); if ( !QRectF(offset(), sz).contains(event->pos()) ) { event->accept(); return; } m_startLayerRotation = m_currentRotation; m_rotationPressScreenPos = event->screenPos(); m_mouseOperationActive = true; m_startPos = pos(); QGraphicsPixmapItem::mousePressEvent(event); } } else { m_mouseOperationActive = false; } // --- updating --- parent->setSelectedLayer(7,QString("Layer %1").arg(m_index)); // to ensure that message is shown even layer is already selected parent->showMessage(QString("Select layer %1").arg(m_index)); // Ctrl: transparent red overlay (see-through mode) if ( event->modifiers() & Qt::ControlModifier ) { setOpacity(EditorStyle::instance().layerOverlayOpacity()); // Build alpha-masked red overlay once (reused every frame during drag) const QPixmap& px = pixmap(); if ( !px.isNull() ) { m_redOverlayPixmap = QPixmap(px.size()); m_redOverlayPixmap.fill(QColor(255, 0, 0, 100)); QPainter op(&m_redOverlayPixmap); op.setCompositionMode(QPainter::CompositionMode_DestinationIn); op.drawPixmap(0, 0, px); } m_redOverlay = true; update(); } } } } } void LayerItem::mouseMoveEvent( QGraphicsSceneMouseEvent* event ) { qCDebug(logEditor) << "LayerItem::mouseMoveEvent(): mode =" << m_operationMode; { if ( isSelected() ) { if ( m_operationMode == OperationMode::Translate ) { QPointF posBefore = pos(); QGraphicsPixmapItem::mouseMoveEvent(event); QPointF delta = pos() - posBefore; if ( !delta.isNull() ) { ImageView* view = getParentImageView(); if ( view ) { for ( LayerItem* l : view->selectedLayers() ) if ( l != this ) l->setPos( l->pos() + delta ); } } } else if ( m_operationMode == OperationMode::Rotate ) { if ( m_pivotDragging ) { prepareGeometryChange(); m_pivot = mapToScene(event->pos()); // store in scene coords m_hasPivot = true; update(); event->accept(); } else { MainWindow* parent = m_parent != nullptr ? dynamic_cast(m_parent) : nullptr; if ( parent != nullptr ) { QPointF delta = QPointF(event->screenPos()) - QPointF(m_rotationPressScreenPos); double angleDelta = m_startLayerRotation + delta.x() * EditorStyle::instance().rotationSingleStep() / 10.0 - m_currentRotation; setRotationAngle(angleDelta); parent->updateLayerOperationParameter("LayerItem::mouseMoveEvent",m_name,LayerItem::OperationMode::Rotate,m_currentRotation); event->accept(); } } } } } } void LayerItem::mouseReleaseEvent( QGraphicsSceneMouseEvent* event ) { qCDebug(logEditor) << "LayerItem::mouseReleaseEvent(): index =" << m_index << ", name =" << name(); { setOpacity(1.0); if ( m_redOverlay ) { m_redOverlay = false; m_redOverlayPixmap = QPixmap(); update(); } if ( !isSelected() ) return; if ( !m_undoStack ) { QGraphicsPixmapItem::mouseReleaseEvent(event); return; } if ( m_operationMode == OperationMode::Translate && !( m_layer && m_layer->creator() == "Inpainting" ) ) { // Build group: primary (this) + co-selected layers ImageView* view = getParentImageView(); QList group; group.append(this); if ( view ) { for ( LayerItem* l : view->selectedLayers() ) if ( l != this ) group.append(l); } struct Move { LayerItem* layer; QPointF from; QPointF to; }; QList moves; for ( LayerItem* l : group ) { QPointF from = l->dragStartPos(); QPointF to = EditorStyle::instance().allowIntegerMoveOnly() ? QPointF(qRound(l->pos().x()), qRound(l->pos().y())) : l->pos(); if ( to != from ) moves.append(Move{l, from, to}); } if ( !moves.isEmpty() ) { if ( moves.size() > 1 ) m_undoStack->beginMacro("Move Layers"); for ( const auto& mv : moves ) m_undoStack->push(new MoveLayerCommand(mv.layer, mv.from, mv.to, mv.layer->id())); if ( moves.size() > 1 ) m_undoStack->endMacro(); } } else if ( m_operationMode == OperationMode::Rotate ) { const bool pivotWasDragging = m_pivotDragging; if ( m_pivotDragging ) { const QPointF newPivotScene = m_pivot; // scene coords m_pivotDragging = false; m_pivotSelected = false; update(); // Only push command when the pivot actually moved if ( QLineF(newPivotScene, m_pivotAtDragStart).length() > 0.5 ) { m_undoStack->push(new SetPivotCommand(this, m_pivotAtDragStart, newPivotScene, m_pivotWasSet)); } } else if ( EditorStyle::instance().allowIntegerMoveOnly() && m_undoStack->index() > 0 ) { const QPointF snapped(qRound(pos().x()), qRound(pos().y())); const QPointF adj = snapped - pos(); if ( !adj.isNull() ) { auto* cmd = const_cast(m_undoStack->command(m_undoStack->index() - 1)); if ( auto* rotCmd = dynamic_cast(cmd) ) rotCmd->setPositionAdjust(adj); setPos(snapped); } } if ( pivotWasDragging ) { // For pivot press/drag: skip the base-class release to prevent // clearSelection() firing on click-without-drag, which would emit // selectionChanged() and trigger app-level layer deactivation before // our setSelected(true) can restore it. setSelected(true); return; } } // m_operationMode = None; QGraphicsPixmapItem::mouseReleaseEvent(event); // Qt's default handler deselects the item on click-without-drag (toggle behavior). // We always want the layer to stay selected after an active press. setSelected(true); } } // ------------------------ Handles ------------------------ void LayerItem::updateHandles() { qCDebug(logEditor) << "LayerItem::updateHandles(): Processing..."; { qDeleteAll(m_handles); m_handles.clear(); if ( !isSelected() ) return; QRectF r = boundingRect(); auto addScale = [&](QPointF p) { auto* h = new TransformHandleItem(this, TransformHandleItem::Scale); h->setParentItem(this); h->setPos(p); m_handles << h; }; addScale(r.topLeft()); addScale(r.topRight()); addScale(r.bottomLeft()); addScale(r.bottomRight()); auto* rot = new TransformHandleItem(this, TransformHandleItem::Rotate); rot->setParentItem(this); rot->setPos(QPointF(r.center().x(), r.top() - 30)); m_handles << rot; } } // ----- void LayerItem::applyPerspectiveQuad( const QVector& quad ) { qCDebug(logEditor) << "LayerItem::applyPerspectiveQuad(): Processing..."; { QRectF r = boundingRect(); QVector src = { r.topLeft(), r.topRight(), r.bottomRight(), r.bottomLeft() }; QTransform warp; if ( QTransform::quadToQuad(src, quad, warp) ) { this->setTransform(warp); } } }