X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.g2d%2Fsrc%2Forg%2Fsimantics%2Fg2d%2Fdiagram%2Fhandler%2Fimpl%2FPickContextImpl.java;h=a891d62b70a360f7e750097f57cc2e0b26368ef5;hp=1f6a76903e9247e1c7d8aa3b21da14cc3d833616;hb=48135dcd03588783f9c1b688aaa53cdaacba6ef2;hpb=b75a6bbcc34a3e88f94d04d0389ed0d2e37b6511 diff --git a/bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/handler/impl/PickContextImpl.java b/bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/handler/impl/PickContextImpl.java index 1f6a76903..a891d62b7 100644 --- a/bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/handler/impl/PickContextImpl.java +++ b/bundles/org.simantics.g2d/src/org/simantics/g2d/diagram/handler/impl/PickContextImpl.java @@ -17,7 +17,13 @@ import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.IDiagram; @@ -25,6 +31,7 @@ import org.simantics.g2d.diagram.handler.PickContext; import org.simantics.g2d.diagram.handler.PickRequest; import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy; import org.simantics.g2d.element.ElementClass; +import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.handler.ElementLayers; @@ -34,7 +41,13 @@ import org.simantics.g2d.element.handler.Pick; import org.simantics.g2d.element.handler.Pick2; import org.simantics.g2d.element.handler.Transform; import org.simantics.g2d.layers.ILayers; +import org.simantics.g2d.scenegraph.SceneGraphConstants; import org.simantics.g2d.utils.GeometryUtils; +import org.simantics.scenegraph.INode; +import org.simantics.scenegraph.g2d.IG2DNode; +import org.simantics.scenegraph.g2d.nodes.spatial.RTreeNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Toni Kalajainen @@ -43,6 +56,12 @@ public class PickContextImpl implements PickContext { public static final PickContextImpl INSTANCE = new PickContextImpl(); + private static final Logger LOGGER = LoggerFactory.getLogger(PickContextImpl.class); + + private static final boolean PERF = false; + + private static final ThreadLocal perThreadElementBounds = ThreadLocal.withInitial(() -> new Rectangle2D.Double()); + @Override public void pick( IDiagram diagram, @@ -53,163 +72,312 @@ public class PickContextImpl implements PickContext { assert(request!=null); assert(finalResult!=null); - ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS); + long startTime = PERF ? System.nanoTime() : 0L; + + List result = pickElements(diagram, request); - Collection result = finalResult; - if (request.pickSorter != null) { - // Need a temporary List for PickSorter - result = new ArrayList(); + if (PERF) { + long endTime = System.nanoTime(); + LOGGER.info("[picked " + result.size() + " elements @ " + request.pickArea + "] total pick time : " + ((endTime - startTime)*1e-6)); } - Rectangle2D elementBounds = new Rectangle2D.Double(); - nextElement: - for (IElement e : diagram.getSnapshot()) - { + + if (!result.isEmpty()) { + if (request.pickSorter != null) { + List elems = new ArrayList<>(result); + request.pickSorter.sort(elems); + finalResult.addAll(elems); + } else { + finalResult.addAll(result); + } + } + } + + private static class PickFilter implements Predicate { + + private final PickRequest request; + private final ILayers layers; + private final boolean checkLayers; + + public PickFilter(PickRequest request, ILayers layers) { + this.request = request; + this.layers = layers; + this.checkLayers = layers != null && !layers.getIgnoreFocusSettings(); + } + + @Override + public boolean test(IElement e) { // Ignore hidden elements. if (ElementUtils.isHidden(e)) - continue; + return false; ElementClass ec = e.getElementClass(); - - if(layers != null && !layers.getIgnoreFocusSettings()) { + + if (checkLayers) { ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class); - if(el != null) { - if(!el.isFocusable(e, layers)) continue; - } - } - - if (request.pickFilter!=null && !request.pickFilter.accept(e)) continue; - - Transform t = e.getElementClass().getSingleItem(Transform.class); - AffineTransform canvasToElement = t.getTransform(e); - if (canvasToElement==null) continue; - double det = canvasToElement.getDeterminant(); - if (det == 0) { - // Singular transform, just reset the rotation/scaling part to - // allow picking to proceed. - // TODO: this may modify the internal transform value of an element which is not intended - canvasToElement.setToTranslation( - canvasToElement.getTranslateX(), - canvasToElement.getTranslateY()); + if (el != null && !el.isFocusable(e, layers)) + return false; } - // Get bounds, ignore elements that have no bounds - InternalSize b = e.getElementClass().getAtMostOneItemOfClass(InternalSize.class); - if (b==null) continue; - elementBounds.setFrame(Double.NaN, Double.NaN, Double.NaN, Double.NaN); - b.getBounds(e, elementBounds); - if (Double.isNaN(elementBounds.getWidth()) || Double.isNaN(elementBounds.getHeight())) - continue; - - Shape elementBoundsOnCanvas = GeometryUtils.transformShape(elementBounds, canvasToElement); - if (elementBoundsOnCanvas instanceof Rectangle2D) - elementBoundsOnCanvas = elementBoundsOnCanvas.getBounds2D(); - elementBoundsOnCanvas = elementBoundsOnCanvas.getBounds2D(); - org.simantics.scenegraph.utils.GeometryUtils.expandRectangle((Rectangle2D)elementBoundsOnCanvas, 1e-3, 1e-3, 1e-3, 1e-3); - + if (request.pickFilter != null && !request.pickFilter.accept(e)) + return false; + + // Get InternalSize for retrieving bounds, ignore elements that have no bounds + InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class); + if (b == null) + return false; + + // Anything that is without a transformation is not pickable + AffineTransform canvasToElement = getTransform(e); + if (canvasToElement == null) + return false; + + Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement); + if (elementBoundsOnCanvas == null) + return false; + // Pick with pick handler(s) - List pickHandlers = e.getElementClass().getItemsByClass(Pick.class); - if (!pickHandlers.isEmpty()) - { - // Rough filtering with bounds + List pickHandlers = ec.getItemsByClass(Pick.class); + if (!pickHandlers.isEmpty()) { + // Rough filtering with bounds if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) { -// System.out.println("Element bounds discards " + e.getElementClass()); - continue; + // System.out.println("Element bounds discards " + e.getElementClass()); + return false; } - - // Convert pick shape to element coordinates -// AffineTransform elementToCanvas; -// try { -// elementToCanvas = canvasToElement.createInverse(); -// } catch (NoninvertibleTransformException e1) { -// throw new RuntimeException(e1); -// } -// Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas); - for (Pick p : pickHandlers) - { - if (p instanceof Pick2) { - Pick2 p2 = (Pick2) p; - //if (p2.pick(e, pickShapeInElementCoords, request.pickPolicy, result) > 0) - if (p2.pick(e, request.pickArea, request.pickPolicy, result) > 0) - continue nextElement; - } else { - //if (p.pickTest(e, pickShapeInElementCoords, request.pickPolicy)) { - if (p.pickTest(e, request.pickArea, request.pickPolicy)) { - result.add(e); - continue nextElement; - } + + // NOTE: this doesn't support Pick2 interface anymore + for (Pick p : pickHandlers) { + if (p.pickTest(e, request.pickArea, request.pickPolicy)) { + return true; } } - continue nextElement; + return false; } - - // Pick with shape handler(s) - List shapeHandlers = e.getElementClass().getItemsByClass(Outline.class); - if (!shapeHandlers.isEmpty()) - { - // Rough filtering with bounds - if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) continue; - - // Convert pick shape to element coordinates - AffineTransform elementToCanvas; - try { - elementToCanvas = canvasToElement.createInverse(); - } catch (NoninvertibleTransformException e1) { - throw new RuntimeException(e1); + + // Pick with Outline handler(s) + List outlineHandlers = ec.getItemsByClass(Outline.class); + if (!outlineHandlers.isEmpty()) + return pickByOutline(e, request, elementBoundsOnCanvas, canvasToElement, outlineHandlers); + + // Finally, pick by rectangle + return pickByBounds(e, request, elementBoundsOnCanvas); + } + + } + + private static Rectangle2D toBoundingRectangle(Shape shape) { + if (shape instanceof Rectangle2D) + return (Rectangle2D) shape; + else + return shape.getBounds2D(); + } + + private static List pickElements(IDiagram diagram, PickRequest request) { + ILayers layers = diagram.getHint(DiagramHints.KEY_LAYERS); + + // Get the scene graph nodes that intersect the pick-requests pick shape + INode spatialRoot = request.pickContext != null + ? request.pickContext.getSceneGraph().lookupNode(SceneGraphConstants.SPATIAL_ROOT_NODE_ID) + : null; + + if (spatialRoot instanceof RTreeNode) { + // Optimized picking version that no longer supports Pick2 interface + // and therefore doesn't support connections modelled as + // branchpoint/edge subelements of connection elements. + + RTreeNode rtree = (RTreeNode) spatialRoot; + Map nodeToElement = diagram.getHint(DiagramHints.NODE_TO_ELEMENT_MAP); + if (nodeToElement != null) { + // The most optimized version + return rtree.intersectingNodes(toBoundingRectangle(request.pickArea), new ArrayList<>()) + .stream() + .parallel() + .map(nodeToElement::get) + .filter(Objects::nonNull) + .filter(new PickFilter(request, layers)) + .collect(Collectors.toList()); + } else { + // Slower version for when DiagramHints.NODE_TO_ELEMENT_MAP is not used + Set nodes = + new HashSet<>( + rtree.intersectingNodes( + toBoundingRectangle(request.pickArea), + new ArrayList<>()) + ); + + return diagram.getSnapshot().stream() + .parallel() + // Choose only elements that are under the pick region bounds-wise + .filter(e -> { + INode node = e.getHint(ElementHints.KEY_SG_NODE); + return nodes.contains(node); + }) + // Perform comprehensive picking + .filter(new PickFilter(request, layers)) + .collect(Collectors.toList()); + } + + } else { + + // Fall-back logic that ends up processing everything. + // This still supports all the old logic and the Pick2 + // interface in element classes. + List result = new ArrayList<>(); + boolean checkLayers = layers != null && !layers.getIgnoreFocusSettings(); + + // Do not do this in parallel mode to keep results in proper Z order + diagram.getSnapshot().stream().forEachOrdered(e -> { + // Ignore hidden elements. + if (ElementUtils.isHidden(e)) + return; + + ElementClass ec = e.getElementClass(); + + if (checkLayers) { + ElementLayers el = ec.getAtMostOneItemOfClass(ElementLayers.class); + if (el != null && !el.isFocusable(e, layers)) + return; } - Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas); - - // Intersection with one shape is enough - if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) - { - for (Outline es : shapeHandlers) - { - Shape elementShape = es.getElementShape(e); - if (elementShape==null) continue nextElement; - if (GeometryUtils.intersects(pickShapeInElementCoords, elementShape)) - { - result.add(e); - continue nextElement; + + if (request.pickFilter != null && !request.pickFilter.accept(e)) + return; + + // Get InternalSize for retrieving bounds, ignore elements that have no bounds + InternalSize b = ec.getAtMostOneItemOfClass(InternalSize.class); + if (b == null) + return; + + // Anything that is without a transformation is not pickable + AffineTransform canvasToElement = getTransform(e); + if (canvasToElement == null) + return; + + Rectangle2D elementBoundsOnCanvas = getElementBounds(e, b, canvasToElement); + if (elementBoundsOnCanvas == null) + return; + + // Pick with pick handler(s) + List pickHandlers = ec.getItemsByClass(Pick.class); + if (!pickHandlers.isEmpty()) { + // Rough filtering with bounds + if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) { + // System.out.println("Element bounds discards " + e.getElementClass()); + return; + } + + for (Pick p : pickHandlers) { + if (p instanceof Pick2) { + Pick2 p2 = (Pick2) p; + if (p2.pick(e, request.pickArea, request.pickPolicy, result) > 0) + return; + } else { + if (p.pickTest(e, request.pickArea, request.pickPolicy)) { + result.add(e); + return; + } } } - continue nextElement; + return; } - - // Contains of all shapes is required - if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) - { - for (Outline es : shapeHandlers) - { - Shape elementShape = es.getElementShape(e); - if (!GeometryUtils.contains(pickShapeInElementCoords, elementShape)) - continue nextElement; + + // Pick with shape handler(s) + List outlineHandlers = ec.getItemsByClass(Outline.class); + if (!outlineHandlers.isEmpty()) { + if (pickByOutline(e, request, elementBoundsOnCanvas, canvasToElement, outlineHandlers)) { + result.add(e); } - result.add(e); - continue nextElement; + return; } - continue nextElement; - } - - // Pick by rectangle - if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) - { - if (GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) + + // Finally, pick by bounding rectangle + if (pickByBounds(e, request, elementBoundsOnCanvas)) result.add(e); + }); + + return result; + + } + } + + private static AffineTransform getTransform(IElement e) { + // Anything that is without a transformation is not pickable + Transform t = e.getElementClass().getSingleItem(Transform.class); + AffineTransform canvasToElement = t.getTransform(e); + if (canvasToElement == null) + return null; + + double det = canvasToElement.getDeterminant(); + if (det == 0) { + // Singular transform, only take the translation from it to move on. + canvasToElement = AffineTransform.getTranslateInstance(canvasToElement.getTranslateX(), canvasToElement.getTranslateY()); + } + + return canvasToElement; + } + + private static Rectangle2D getElementBounds(IElement e, InternalSize b, AffineTransform transform) { + Rectangle2D elementBounds = perThreadElementBounds.get(); + elementBounds.setFrame(Double.NaN, Double.NaN, Double.NaN, Double.NaN); + b.getBounds(e, elementBounds); + if (Double.isNaN(elementBounds.getWidth()) || Double.isNaN(elementBounds.getHeight())) + return null; + + // Get optionally transformed axis-aligned bounds of the element, + // expanded by 1mm in each direction. + Rectangle2D transformedBounds = transform != null + ? toBoundingRectangle(GeometryUtils.transformShape(elementBounds, transform)) + : elementBounds; + return org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(transformedBounds, 1e-3); + } + + private static boolean pickByOutline(IElement e, PickRequest request, Rectangle2D elementBoundsOnCanvas, AffineTransform canvasToElement, List outlineHandlers) { + // Rough filtering with bounds + if (!GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) + return false; + + // Convert pick shape to element coordinates + AffineTransform elementToCanvas; + try { + elementToCanvas = canvasToElement.createInverse(); + } catch (NoninvertibleTransformException ex) { + throw new RuntimeException(ex); + } + Shape pickShapeInElementCoords = GeometryUtils.transformShape(request.pickArea, elementToCanvas); + + // Intersection with one shape is enough + if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) { + for (Outline es : outlineHandlers) { + Shape elementShape = es.getElementShape(e); + if (elementShape == null) + return false; + if (GeometryUtils.intersects(pickShapeInElementCoords, elementShape)) { + return true; + } } - - else - - if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) - { - if (GeometryUtils.contains(request.pickArea, elementBoundsOnCanvas)) - result.add(e); + return false; + } + + // Contains of all shapes is required + if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) { + for (Outline es : outlineHandlers) { + Shape elementShape = es.getElementShape(e); + if (!GeometryUtils.contains(pickShapeInElementCoords, elementShape)) + return false; } - + return true; } - if (request.pickSorter != null) { - request.pickSorter.sort((List) result); - finalResult.addAll(result); + return false; + } + + private static boolean pickByBounds(IElement e, PickRequest request, Rectangle2D elementBoundsOnCanvas) { + if (request.pickPolicy == PickPolicy.PICK_INTERSECTING_OBJECTS) { + if (GeometryUtils.intersects(request.pickArea, elementBoundsOnCanvas)) + return true; + } else if (request.pickPolicy == PickPolicy.PICK_CONTAINED_OBJECTS) { + if (GeometryUtils.contains(request.pickArea, elementBoundsOnCanvas)) + return true; } + return false; } }