From 35571e1c709d2ebccde89ca0e78cddc3ac16c3c8 Mon Sep 17 00:00:00 2001 From: Christian Schneider Date: Wed, 15 Dec 2021 15:36:57 +0100 Subject: [PATCH] klighd.piccolo: important change in the drawing coordinate system application, solves #56 * now the local transform of the (main) diagram's clip node is *not* applied anymore if the diagram is clipped, while (for non-clipped diagrams) the transform of the diagram's root KNodeTopNode is supposed to be the identity under all circumstances; * updated related code portions concerning the zooming, the clipping functionality, the outline, and the magnifier lens --- .../controller/DiagramZoomController.java | 120 ++++++++++++++---- .../KlighdMagnificationLensEventHandler.java | 15 ++- .../piccolo/internal/nodes/KNodeNode.java | 67 ++++++++-- .../internal/nodes/KlighdDisposingLayer.java | 2 +- .../internal/nodes/KlighdMainCamera.java | 8 +- .../piccolo/viewer/PiccoloOutlinePage.java | 51 ++++---- 6 files changed, 190 insertions(+), 73 deletions(-) diff --git a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/controller/DiagramZoomController.java b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/controller/DiagramZoomController.java index d9914b9eb..43e04e2aa 100644 --- a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/controller/DiagramZoomController.java +++ b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/controller/DiagramZoomController.java @@ -161,10 +161,8 @@ public void zoom(final ZoomStyle zoomStyle, final KGraphElement desiredFocusElem * time to animate in ms */ private void zoomToActualSize(final int duration) { - final KNode displayedKNode = this.canvasCamera.getDisplayedKNodeNode().getViewModelElement(); - - final PBounds newBounds = toPBoundsIncludingPortsAndLabels(displayedKNode); - + final PBounds newBounds = toPBoundsIncludingPortsAndLabelsOfDisplayedKNode(); + this.canvasCamera.animateViewToTransform( PAffineTransform.getTranslateInstance(-newBounds.x, -newBounds.y), duration); } @@ -185,10 +183,8 @@ private void zoomToActualSize(final int duration) { */ private void zoomToFit(final int duration, boolean narrowDownToContents, final Spacing defaultZoomToFitContentSpacing) { - final KNode displayedKNode = this.canvasCamera.getDisplayedKNodeNode().getViewModelElement(); - - final PBounds newBounds = toPBoundsIncludingPortsAndLabels( - displayedKNode, narrowDownToContents, defaultZoomToFitContentSpacing); + final PBounds newBounds = toPBoundsIncludingPortsAndLabelsOfDisplayedKNode( + narrowDownToContents, defaultZoomToFitContentSpacing); if (this.canvasCamera.getBoundsReference().isEmpty()) { // this case occurs while initializing the DiagramEditorPart @@ -225,18 +221,23 @@ public void zoomToFocus(final KNode focus, final int duration) { * diagram canvas area, see also {@link ZoomStyle#ZOOM_TO_FOCUS_OR_INCREASE_TO_FIT} */ private void zoomToFocus(final KNode focus, final int duration, final boolean increaseToFit) { + final PBounds diagramBounds = toPBoundsIncludingPortsAndLabelsOfDisplayedKNode(); final KNode displayedKNode = this.canvasCamera.getDisplayedKNodeNode().getViewModelElement(); // fetch bounds of the whole visible diagram - final PBounds focusBounds = toPBoundsIncludingPortsAndLabels(focus); + final PBounds focusBounds; - // we need the bounds in view coordinates (absolute), hence for - // a KNode add the translations of all parent nodes + if (focus == displayedKNode) { + focusBounds = diagramBounds; - if (focus != displayedKNode) { + } else { + focusBounds = toPBoundsIncludingPortsAndLabels(focus); KNode parent = focus.getParent(); - while (parent != null && parent != displayedKNode.getParent()) { + // we need the bounds in view coordinates (absolute), hence for + // a KNode add the translations of all parent nodes + + while (parent != null && parent != displayedKNode) { final double scale = parent.getProperty(CoreOptions.SCALE_FACTOR).doubleValue(); focusBounds.setSize(scale * focusBounds.width, scale * focusBounds.height); @@ -253,7 +254,6 @@ private void zoomToFocus(final KNode focus, final int duration, final boolean in final PBounds viewBounds = canvasCamera.getViewBounds(); if (increaseToFit) { - final PBounds diagramBounds = toPBoundsIncludingPortsAndLabels(displayedKNode); final boolean fullyContains = viewBounds.getWidth() > diagramBounds.getWidth() && viewBounds.getHeight() > diagramBounds.getHeight(); @@ -285,9 +285,7 @@ private void zoomToFocus(final KNode focus, final int duration, final boolean in * time to animate */ public void zoomToLevel(final float newZoomLevel, final int duration) { - final KNode displayedKNode = this.canvasCamera.getDisplayedKNodeNode().getViewModelElement(); - - final PBounds nodeBounds = toPBoundsIncludingPortsAndLabels(displayedKNode); + final PBounds nodeBounds = toPBoundsIncludingPortsAndLabelsOfDisplayedKNode(); // it would be possible to use PCamera#scaleViewAboutPoint(scale, x, y), // however this method does not allow for animation @@ -301,12 +299,9 @@ public void zoomToLevel(final float newZoomLevel, final int duration) { origBounds.height * oldZoomLevel / newZoomLevel); // add the necessary translation - final double normalizedWidth = origBounds.width * oldZoomLevel; - final double normalizedHeight = origBounds.height * oldZoomLevel; - final double transX = (origBounds.width - normalizedWidth / newZoomLevel) / 2f; - final double transY = (origBounds.height - normalizedHeight / newZoomLevel) / 2f; - - newBounds.moveBy(transX, transY); + newBounds.moveBy( + (origBounds.width - newBounds.width) / 2f, + (origBounds.height - newBounds.height) / 2f); // make sure at least some of the diagram is visible after zooming to scale 1 final PDimension dim = newBounds.deltaRequiredToContain(nodeBounds); @@ -336,13 +331,18 @@ public void zoomToStay(final EObject focusElement, final KVector previousPositio final KNode displayedKNode = this.canvasCamera.getDisplayedKNodeNode().getViewModelElement(); // Fetch bounds of the focused element. - final PBounds focusBounds = boundsComputer.toPBounds(focus); + final PBounds focusBounds; - // We need the bounds in view coordinates (absolute), hence for - // an element add the translations of all parent elements. + if (focus == displayedKNode) { + focusBounds = toPBoundsIncludingPortsAndLabelsOfDisplayedKNode(); - if (focus != displayedKNode) { + } else { + focusBounds = boundsComputer.toPBounds(focus); EObject parent = focus.eContainer(); + + // We need the bounds in view coordinates (absolute), hence for + // an element add the translations of all parent elements. + while (parent != null && parent != displayedKNode.getParent()) { while (!(parent instanceof KShapeLayout) && !(parent instanceof EMapPropertyHolder) && parent != null) { parent = parent.eContainer(); @@ -371,6 +371,41 @@ public void zoomToStay(final EObject focusElement, final KVector previousPositio canvasCamera.animateViewToTransform(transform, duration); } + /** + * Converts the current diagram clip KNode's layout data into {@link PBounds} s.t. its ports + * and labels are included, while its scale and offset are excluded. + * + * @return the requested bounding box in form of a {@link PBounds} + */ + private PBounds toPBoundsIncludingPortsAndLabelsOfDisplayedKNode() { + return toPBoundsIncludingPortsAndLabelsOfDisplayedKNode(false, null); + } + + /** + * Converts the current diagram clip KNode's layout data into {@link PBounds} s.t. its ports + * and labels are included, while its scale and offset are excluded. + * + * @param doComputeSubDiagramSize + * set to true yields the bounding box of the nested diagram's content + * including node's ports and labels if visible, with false + * the bounds of the given node including its port and labels if visible + * are returned + * @param defaultZoomToFitContentSpacing + * default spacing to be applied if narrowDownToContents is + * true, see also + * {@link de.cau.cs.kieler.klighd.util.KlighdProperties#ZOOM_TO_FIT_CONTENT_SPACING}, + * may be null. + * @return the requested bounding box in form of a {@link PBounds} + */ + private PBounds toPBoundsIncludingPortsAndLabelsOfDisplayedKNode( + final boolean doComputeSubDiagramSize, final Spacing defaultZoomToFitContentSpacing) { + final KNode displayedKNode = this.canvasCamera.getDisplayedKNodeNode().getViewModelElement(); + + final PBounds bounds = toPBoundsIncludingPortsAndLabels(displayedKNode, + doComputeSubDiagramSize, defaultZoomToFitContentSpacing); + return eliminateOffsetAndScale(bounds, displayedKNode); + } + /** * Converts node's layout data into {@link PBounds} s.t. node's ports * and labels are included, respects an attached {@link LayoutOptions#SCALE_FACTOR}. @@ -406,4 +441,35 @@ protected PBounds toPBoundsIncludingPortsAndLabels(final KNode node, return boundsComputer.toPBoundsIncludingPortsAndLabels( node, doComputeSubDiagramSize, defaultZoomToFitContentSpacing, true); } + + /** + * Reduces the given bounds by node's translation and scale. + * It's required for computing the visible diagram's bounding box as clipped diagrams are drawn + * without applying the clip node's transform (containing the scale and offset), see + * KNodeNode#fullPaint(PPaintContext), and scaling and translating the KNodeTopNode (root node) + * is invalid. + * + * Note that bounds' (x,y) translation may differ from node's (x,y) + * translation because of labels, ports, and port labels having negative x and y coordinates, + * i.e., are placed in left/above node. In that case bounds will than + * have negative x and/or y coordinates, which is on purpose. + * + * @param bounds + * full bounds of some {@link KNode} including its ports and labels, scaled by + * node's scale and translated by node's (x,y) + * @param node + * the {@link KNode} containing the reference scale and offset + * @return bounds being adjusted as described above (within the argument object) + */ + protected PBounds eliminateOffsetAndScale(final PBounds bounds, final KNode node) { + + bounds.moveBy(-node.getXpos(), -node.getYpos()); + + final float scaleInverse = 1 / node.getProperty(CoreOptions.SCALE_FACTOR).floatValue(); + bounds.setRect( + bounds.getX() * scaleInverse, bounds.getY() * scaleInverse, + bounds.getWidth() * scaleInverse, bounds.getHeight() * scaleInverse); + + return bounds; + } } diff --git a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/events/KlighdMagnificationLensEventHandler.java b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/events/KlighdMagnificationLensEventHandler.java index eb9557bbb..d5c0219b0 100644 --- a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/events/KlighdMagnificationLensEventHandler.java +++ b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/events/KlighdMagnificationLensEventHandler.java @@ -153,8 +153,16 @@ private Point2D determineLensOffset(final PInputEvent event) { /** * Composes a {@link PAffineTransform} incorporating the lens magnification according to the - * corresponding preference setting, the inverse of the clip node's scaling, and event's mouse - * pointer position in terms the diagram coordinates (rather than canvas coordinates). + * corresponding preference setting and event's mouse pointer position in terms the + * diagram coordinates (rather than canvas coordinates). + * + * Note that no attention need to be paid to the current diagram clip node's scale and offset. + * They're not applied while drawing them via the main camera, and since 'mainCamera' is + * the top element in the paintContext's cameraStack while drawing via 'lensCamera' + * (as its a transitive parent of 'lensCamera') KNodeNode#fullPaint(PPaintContext) + * will behave the same way as when called via 'mainCamera'. + * + * See also {@link PCamera#fullPaint(edu.umd.cs.piccolo.util.PPaintContext)}. * * @param event * the {@link PInputEvent} to get the diagram position from @@ -167,9 +175,6 @@ private PAffineTransform createViewTransform(final PInputEvent event) { final float scale = STORE.getFloat(KlighdPreferences.MAGNIFICATION_LENS_SCALE) / 100f; viewTransform.scale(scale, scale); - final double clipScale = mainCamera.getDisplayedKNodeNode().getScale(); - viewTransform.scale(1 / clipScale, 1 / clipScale); - final Point2D pos = event.getPosition(); viewTransform.translate(-pos.getX(), -pos.getY()); return viewTransform; diff --git a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KNodeNode.java b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KNodeNode.java index 5c6999abd..9763385e3 100644 --- a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KNodeNode.java +++ b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KNodeNode.java @@ -516,7 +516,7 @@ public boolean fullPick(final PPickPath pickPath) { // instead we want this kNodeNode be the picked node anyway so, ... if (!fullPick && isRootLayer) { pickPath.pushNode(this); - pickPath.pushTransform(getTransform()); + pickPath.pushTransform(new PAffineTransform()); return true; } @@ -538,10 +538,14 @@ public boolean fullPickOri(final PPickPath pickPath) { // The filter is in charge of masking out the rendering while the diagram is // clipped to this node and it's being drawn via the diagram's main camera! + final boolean isRootLayer = this.isRootLayer; if (getVisible() && (getPickable() || getChildrenPickable()) - && fullIntersectsOri(pickPath.getPickBounds())) { + && (isRootLayer || fullIntersectsOri(pickPath.getPickBounds()))) { + final PAffineTransform transform = isRootLayer ? new PAffineTransform() : getTransformReference(true); + pickPath.pushNode(this); - pickPath.pushTransform(getTransformReference(true)); + pickPath.pushTransform(transform); + final boolean applyScale = nodeScale != null; @@ -563,7 +567,7 @@ && fullIntersectsOri(pickPath.getPickBounds())) { final int count = getChildrenCount(); for (int i = count - 1; i >= 0; i--) { final PNode each = (PNode) getChildrenReference().get(i); - if (this.isRootLayer) { + if (isRootLayer) { if (i == 0 && each != this.childArea) { // do not try to pick the node's figure if the main diagram is clipped to // this node @@ -592,21 +596,58 @@ && fullIntersectsOri(pickPath.getPickBounds())) { kpp.popNodeScale(); } - pickPath.popTransform(getTransformReference(false)); + pickPath.popTransform(transform); pickPath.popNode(this); } return false; } + /** + * {@inheritDoc} + */ + @Override + public void repaintFrom(PBounds localBounds, PNode childOrThis) { + if (this.isRootLayer) { + // This customization reports a dirty area, e.g. reported by some child, to the attached + // cameras without adjusting its bounds by 'this.transform'. Attached cameras are expected + // only in case the (main) diagram is clipped to this node. In that case the main camera + // and optionally the magnifier lens camera. + // The customization is necessary as we now ignore this node's 'transform' once the (main)) + // diagram is clipped to this node, because of https://github.com/kieler/KLighD/issues/56, + // see also the corresponding changes in #fullPickOri(...) and #fullPaint(...) + // + // The above check of 'this.isRootLayer' is actually redundant, as cameras should only be + // registered in case of 'this.isRootLayer' is equal to 'true', but is kept for documentation + // and easier understanding. + super.notifyCameras(localBounds); + } + + // Apart from the above immediate camera notification, the common propagation is kept in order to + // keep the outline always up to date, which happens via propagating up to the root KNodeTopNode + // and along that way to the outline camera + super.repaintFrom(localBounds, childOrThis); + } + + /** + * {@inheritDoc} + */ + @Override + protected void notifyCameras(PBounds parentBounds) { + // this customization is symmetric to the above one and just suppresses superfluous repaint + // requests of potentially incorrect bounds as we ignore 'this.transform' if the (main) + // diagram is clipped to this node, see above. + // Otherwise no attached cameras are expected at the time writing. + } /** * {@inheritDoc} */ @Override public void fullPaint(final PPaintContext paintContext) { + final boolean isRootLayer = this.isRootLayer; final KlighdPaintContext kpc = (KlighdPaintContext) paintContext; - if (!this.isRootLayer && this.visibilityHelper != null + if (!isRootLayer && this.visibilityHelper != null && this.visibilityHelper.isNotVisibleOn(kpc)) { return; } @@ -621,8 +662,12 @@ public void fullPaint(final PPaintContext paintContext) { // In contrast, the rendering figure is supposed to be drawn at all times // while the diagram is drawn via the outline view's camera! - if (getVisible() && (kpc.isOutline() || fullIntersectsOri(paintContext.getLocalClip()))) { - final PAffineTransform transform = getTransformReference(false); + // the following flag is false if drawn on the outline view, for example + final boolean isRootAndDrawnViaMainCamera = isRootLayer && this.getCamerasReference().contains(paintContext.getCamera()); + + if (getVisible() && (kpc.isOutline() || (isRootAndDrawnViaMainCamera) || fullIntersectsOri(paintContext.getLocalClip()))) { + final PAffineTransform transform = isRootAndDrawnViaMainCamera ? new PAffineTransform() : getTransformReference(false); + paintContext.pushTransform(transform); paintContext.pushTransparency(getTransparency()); @@ -641,10 +686,8 @@ public void fullPaint(final PPaintContext paintContext) { for (int i = 0; i < count; i++) { final PNode each = (PNode) getChildrenReference().get(i); - if (this.isRootLayer) { - // the following flag is false if drawn on the outline view, for example - final boolean drawnViaMainCamera = this.getCamerasReference().contains(paintContext.getCamera()); - if (drawnViaMainCamera) { + if (isRootLayer) { + if (isRootAndDrawnViaMainCamera) { if (i == 0 && each != this.childArea) { // do not draw the node's figure on the main diagram if it is clipped to this node continue; diff --git a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdDisposingLayer.java b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdDisposingLayer.java index 10ae118f4..210f8a36e 100644 --- a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdDisposingLayer.java +++ b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdDisposingLayer.java @@ -139,7 +139,7 @@ public void validateFullPaint() { isValidatingPaint = false; if (!tempRect.isEmpty()) { - repaintFrom(tempRect, this); + super.repaintFrom(tempRect, this); } } diff --git a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdMainCamera.java b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdMainCamera.java index 6fccf82d6..3cbac784b 100644 --- a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdMainCamera.java +++ b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/internal/nodes/KlighdMainCamera.java @@ -202,13 +202,13 @@ public void exchangeDisplayedKNodeNode(final KNodeAbstractNode node, // resizing, translation, and integration into the parent inode's figure of that // child area node is automatically respected. - t = NodeUtil.localToParent(node.getParent(), prevNode.getParent()); + t = NodeUtil.localToParent(node, prevNode); } else if (node.isAncestorOf(prevNode)) { // Case b) is symmetric to a): child & ancestor are exchanged and the resulting // transform must be inverted since it is to be applied "outward". - t = NodeUtil.invert(NodeUtil.localToParent(prevNode.getParent(), node.getParent())); + t = NodeUtil.invert(NodeUtil.localToParent(prevNode, node)); } else { // In case c) first the closest common ancestor (iKNodeNode) is determined @@ -229,11 +229,11 @@ public void exchangeDisplayedKNodeNode(final KNodeAbstractNode node, // ... apply case b) between 'prevNode's parent (child area node) and // 'commonAncestor's child area node, ... - t = NodeUtil.invert(NodeUtil.localToParent(prevNode.getParent(), caChildArea)); + t = NodeUtil.invert(NodeUtil.localToParent(prevNode, caChildArea)); // ... and apply case a) between 'node's parent (child area node) and // 'commonAncestor's child area node. - t.concatenate(NodeUtil.localToParent(node.getParent(), caChildArea)); + t.concatenate(NodeUtil.localToParent(node, caChildArea)); } } diff --git a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/viewer/PiccoloOutlinePage.java b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/viewer/PiccoloOutlinePage.java index 4984c8c8e..9a6aeefe5 100644 --- a/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/viewer/PiccoloOutlinePage.java +++ b/plugins/de.cau.cs.kieler.klighd.piccolo/src/de/cau/cs/kieler/klighd/piccolo/viewer/PiccoloOutlinePage.java @@ -504,10 +504,9 @@ public void controlResized(final ControlEvent e) { * graph layout. Note, that the bounds of the canvas are automatically updated of the size of * the outline page change, since the outlineCanvas is provided by this class via * {@link #getControl()}. The canvas in turn immediately updates the bounds of its camera, see - * {@link edu.umd.cs.piccolox.swt.PSWTCanvas#setBounds(int, int, int, int) - * PSWTCanvas#.setBounds(int, int, int, int)}. Thus we don't need to care about the canvas' and - * camera's (full) bounds and can limit the modifications to the camera's view bounds (/view - * transform). + * {@link KlighdCanvas#setBounds(int, int, int, int) KlighdCanvas.setBounds(int, int, int, + * int)}. Thus we don't need to care about the canvas' and camera's (full) bounds and can limit + * the modifications to the camera's view bounds (/view transform). */ private void adjustCamera() { if (rootNode == null) { @@ -547,7 +546,7 @@ private void adjustOutlineRect() { // get the new bounds final PBounds bounds = originalCamera.getViewBounds(); final PAffineTransform localToParent = - NodeUtil.localToParent(displayedNode.getParent(), topNode); + NodeUtil.localToParent(displayedNode, topNode); localToParent.preConcatenate(outlineCanvas.getCamera().getViewTransformReference()); localToParent.transform(bounds, bounds); @@ -556,9 +555,6 @@ private void adjustOutlineRect() { viewportOutlineRect.setPathToRoundRectangle( (float) bounds.x, (float) bounds.y, (float) bounds.width, (float) bounds.height, viewportOutlineRectCornerWidth, viewportOutlineRectCornerWidth); - - // schedule a repaint - outlineCanvas.getCamera().invalidatePaint(); } @@ -636,34 +632,41 @@ private class OutlineDragHandler extends PDragSequenceEventHandler { protected void startDrag(final PInputEvent event) { isDragging = true; super.startDrag(event); - final Point2D pos = event.getPosition(); + final Point2D clickPosInDiagramCoords = event.getPosition(); final KlighdMainCamera originalCamera = topNode.getDiagramMainCamera(); // In clipped diagrams the accumulated 'translate' offset ((x,y) positions) - // of the displayed inode's parent pnodes (!) must be applied the determined - // view bounds in order to get the "actual" click position. - // Note however that the displayed inode's translate (x,y) must not be taken - // into account because it is not contained in 'originalCamera.getViewBounds()'! + // of the displayed inode and all of its parent pnodes (!) must be applied to the determined + // view/diagram bounds in order to obtain the "actual" (true) click position. // Since PCamera.paintCameraView() calls 'fullPaint(...)' on each displayed PLayer // the transforms of those layers are applied on top of the camera's view transform! - // For that reason the global translation of - // 'originalCamera.getDisplayedLayer().getParent()' is calculated. This way an - // optional translation of the parent inode's child area is also respected! - final Point2D clipOffset = - originalCamera.getDisplayedKNodeNode().getParent().getGlobalTranslation(); - - final PBounds outlineRectBounds = + // Hence, for the outline view the transforms for each and every KNodeNode are always applied, + // while for the main diagram the transforms of the clip node's parent pnodes are ignored + // (of course because those nodes are simply invisible for the main diagram camera) + // as well is the clip node's transform, see KNodeNode#fullPaint(PPaintContext) + // For that reason the global translation of 'originalCamera.getDisplayedKNodeNode()' is calculated. + // Optional translations of some parent inodes' child areas are also respected this way. + final Point2D clipOffset = originalCamera.getDisplayedKNodeNode().getGlobalTranslation(); + + // determine the bounds of the currently visible main diagram area (excerpt) in diagram coordinates + // and normalize those to the diagram's root KNodeTopNode (assuming the diagram is clipped, + // 'clipOffset' is supposed to be (0,0) otherwise) + final PBounds trueBoundsOfVisibleMainDiagramArea = originalCamera.getViewBounds().moveBy(clipOffset.getX(), clipOffset.getY()); // if the user clicks outside the outline rect, // center it on this point before dragging starts - final boolean withinRect = outlineRectBounds.contains(pos); - if (!withinRect) { + final boolean clickedWithinVisibleMainDaigramExcerpt = + trueBoundsOfVisibleMainDiagramArea.contains(clickPosInDiagramCoords); + if (!clickedWithinVisibleMainDaigramExcerpt) { // translate the camera by the delta between click // and current center point of the bounds - final Point2D center = outlineRectBounds.getCenter2D(); - originalCamera.translateView(center.getX() - pos.getX(), center.getY() - pos.getY()); + final Point2D center = trueBoundsOfVisibleMainDiagramArea.getCenter2D(); + originalCamera.translateView( + center.getX() - clickPosInDiagramCoords.getX(), + center.getY() - clickPosInDiagramCoords.getY() + ); } }