Path2D zerlegen und Objekt drauf "laufen" lassen

h4nk

Mitglied
Hallo Leute, kurze Frage:
Wir versuchen ein Auto (Shape-Objekt) auf einem Pfad entlangfahren zu lassen. Das klappt soweit auch super, indem wir ein Path2D-Objekt mit dem PathIterator zerlegen, die entstandenen Punkte in einer Arraylist speichern und die Autoposition dann auf die der Pfadpunkte setzen (mit neu berechnetem Winkel).

So sieht das in Code aus:
Java:
// Erstellt den Pfad, auf dem gefahren werden sollen
Path2D path = new Path2D.Double();

// Anfang des Pfads
path.moveTo(100, 200);

// Form des Pfades wird bestimmt, eine Gerade gefolgt von einer Kurve
path.lineTo(100, 100); // Gerade
path.curveTo(100, 100, 100, 0, 0, 0); // Kurve


// Zerlegen des Pfades in Punkte, Points2D

// Zerlegt den Pfad in Punkte, um ihm folgen zu können. Argument flatness: Abstand der Punkte.
PathIterator pi = pathShape.getPathIterator(null, 0.1);

// Speichert die Punkte ins points-Array
while (!pi.isDone()) {
    double[] koordinaten = new double[6];

    switch (pi.currentSegment(koordinaten)) {
        case PathIterator.SEG_MOVETO:
            points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
            break;
        case PathIterator.SEG_LINETO:
            points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
            break;
    }
    pi.next();
}

Problem:
Unser Problem ist jetzt, dass anscheinend nur der Pfadbereich mit der Kurve zerlegt und abgefahren wird. Bei der Anfangsgerade teleportiert sich das Auto direkt vom Start- zum Endpunkt. Woran liegt das und wie können wir das beheben?

EDIT: Das Problem dürfte sein, dass Geraden aus Anfangs und Endpunkt gezeichnet werden und die Gerade somit vom PathIterator auch nur in die beiden zerlegt wird. Wie könnte man das umgehen. Die Punkte auf der Gerade "per Hand" also per Schleife selbst der Punkte-Arraylist hinzufügen?


Falls jemand das Ganze Programm testen will (nur eine Klasse "PathFollow" mit inneren Klassen):

Java:
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class PathFollow {

    public static void main(String[] args) {
        new PathFollow();
    }

    // JFrame wird wird erstellt
    public PathFollow() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private Shape pathShape;
        private List<Point2D> points;
        private Shape car;

        private double angle;
        private Point2D pos;
        private int index;

        protected static final double LAUFZEIT = 5000; // 5 seconds...

        private Long startTime;

        public TestPane() {
            // Erstellt den Pfad, auf dem gefahren werden sollen
            Path2D path = new Path2D.Double();

            // Anfang des Pfads
            path.moveTo(100, 200);

            // Form des Pfades wird bestimmt
            path.lineTo(100, 100);
            path.curveTo(100, 100, 100, 0, 0, 0);

            pathShape = path;
            car = new Rectangle(0, 0, 10, 10);
            points = new ArrayList<>(25);

            // Zerlegt den Pfad in Punkte, um ihm folgen zu können. Argument flatness: Abstand der Punkte.
            PathIterator pi = pathShape.getPathIterator(null, 0.1);

            // Speichert die Punkte ins points-Array
            while (!pi.isDone()) {
                double[] koordinaten = new double[6];

                switch (pi.currentSegment(koordinaten)) {
                    case PathIterator.SEG_MOVETO:
                        points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
                        break;
                    case PathIterator.SEG_LINETO:
                        points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
                        break;
                }
                pi.next();
            }

            // Erstellt einen Timer, der aufhört, wenn die vorgegebene Laufzeit erreicht wurde (hier 5sek.)
            Timer timer = new Timer(40, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {

                    // Holt aktuelle Zeit in ms
                    if (startTime == null) {
                        startTime = System.currentTimeMillis();
                    }

                    // Spielzeit ist die aktuelle Zeit minus die Startzeit
                    long playTime = System.currentTimeMillis() - startTime;

                    // Fortschritt: 0.0 - 1.0, Prozent. Stoppt, wenn eingestellte Laufzeit erreicht ist.
                    double progress = playTime / LAUFZEIT;
                    if (progress >= 1.0) {
                        progress = 1d;
                        ((Timer) e.getSource()).stop();
                    }

                    // i dont even fucking know
                    int index = Math.min(   Math.max(0, (int) (points.size() * progress))   ,    points.size() - 1  );

                    // Index gibt an, wo sich das Auto gerade befindet. Außerdem wird der Winkel zwischen dem aktuellen Punkt und dem nächsten berechnet.
                    pos = points.get(index);
                    if (index < points.size() - 1) {
                        angle = angleTo(pos, points.get(index + 1));
                    }
                    System.out.println("Index: " + index + " , Winkel: " + angle);
                    repaint();
                }
            });
            timer.start();
        }

        // Größe des JPanels. 200x200 Pixel.
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            // Zeichnet Pfad, kann auch weggelassen werden
            g2d.draw(pathShape);

            // Objekt, mit dem das Auto gedreht wird
            AffineTransform affTransform = new AffineTransform();

            if (pos != null) {
                // Speichert Grenzen/Größe des Autos als Rechteck ab
                Rectangle bounds = car.getBounds();
                // AffineTransform-Objekt soll rotieren um den Ursprung, deshalb: bounds.width / 2. (Legt quasi Aufgabe fest)
                affTransform.rotate(angle, (bounds.width / 2), (bounds.width / 2));

                // Constructs a Path2D object from an Shape object, transformed by an AffineTransform object. (übergibt das Auto und die Transformation, die darauf angewendet werden soll)
                Path2D player = new Path2D.Double(car, affTransform);

                // Versetzt den Koordinatenursprung
                g2d.translate(pos.getX() - (bounds.width / 2), pos.getY() - (bounds.height / 2));
                g2d.draw(player);

            }
            g2d.dispose();
        }

        // Zu Bogenmaß
        protected double angleTo(Point2D from, Point2D to) {
            double angle = Math.atan2(to.getY() - from.getY(), to.getX() - from.getX());
            return angle;
        }
    }
}
 
Zuletzt bearbeitet:
Nur mal schnell skizziert:
Der Abstand zwei Punkte: sqrt((x-x)^2+(y-y)^2)
Das Gefährt müsste sich jetzt umgekehrt proportional schnell zum Abstand des nächsten Punkts bewegen.
 
Danke, für die Rückmeldung! Leider verstehe ich nicht ganz, wie das im Bezug auf das Problem, dass Geraden nur in zwei Punkte zerlegt werden, weiterhilft. Könntest du das etwas ausführen?
 
Du hast Geraden und Kurven als Geraden gespeichert. Du musst alle Geraden nochmal zerlegen in gleichlange Teilstücke. Darauf kann sich dann dein Objekt bewegen. Toll wärs, wenn alle Geraden gleichlang der kürzesten Gerade sind. Dann bewegt sich das Objekt gleichschnell über alle Geraden.
 
Moin, hatte etwas Zeit, mir das anzuschauen.

Also erst mal Flatness:
PathIterator pi = pathShape.getPathIterator(null, 0.1);
"flatness - the maximum distance that the line segments used to approximate the curved segments are allowed to deviate from any point on the original curve"
https://stackoverflow.com/questions...-what-flattnes-parameter-does-in-pathiterator

Ergebnis: Die Angabe 0.1 hat leider keinen Effekt. 🙁 (Sonst wärs ja alles ganz einfach).

Dann hab ich aus Line einfach mal auch Curve gemacht:
Java:
            // Form des Pfades wird bestimmt
            path.curveTo(102, 200, 101, 150, 100, 100); // hier ne Änderung
            path.curveTo(100, 100, 100, 0, 0, 0);

Das Objekt bewegt sich nicht ganz so schnell, ist aber mehr ein Hack (also ne Behelfslösung):

ani1.gif

Toll wärs:
Java:
            // Speichert die Punkte ins points-Array
            while (!pi.isDone()) {
                double[] koordinaten = new double[6];

                switch (pi.currentSegment(koordinaten)) {
                    case PathIterator.SEG_MOVETO:
                        points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
                        break;
                    case PathIterator.SEG_LINETO:
                        points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
                        break;
                }
                pi.next();
            }

Hier müsstest du ein paar Berechnungen vornehmen - und zusätzliche Punkte in points speichern. Also nochmal etwas zerstückeln.

Andere Möglichkeit seh ich gerad nicht. 🙁
 
Ich habs mal zerstückelt/zerlegt für Dich:
Java:
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class PathFollow {

    public static void main(String[] args) {
        new PathFollow();
    }

    // JFrame wird wird erstellt
    public PathFollow() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private Shape pathShape;
        private List<Point2D> points;
        private Shape car;

        private double angle;
        private Point2D pos;
        private int index;

        protected static final double LAUFZEIT = 5000; // 5 seconds...

        private Long startTime;

        public TestPane() {
            // Erstellt den Pfad, auf dem gefahren werden sollen
            Path2D path = new Path2D.Double();

            // Anfang des Pfads
            path.moveTo(100, 200);

            // Form des Pfades wird bestimmt
            path.lineTo(100, 100);
            path.curveTo(100, 100, 100, 0, 0, 0);

            pathShape = path;
            car = new Rectangle(0, 0, 10, 10);
            points = new ArrayList<>(25);

            // Zerlegt den Pfad in Punkte, um ihm folgen zu können. Argument flatness: Abstand der Punkte.
            PathIterator pi = pathShape.getPathIterator(null, 0.1);

            // Speichert die Punkte ins points-Array
            while (!pi.isDone()) {
                double[] koordinaten = new double[6];

                switch (pi.currentSegment(koordinaten)) {
                    case PathIterator.SEG_MOVETO:
                        points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
                        break;
                    case PathIterator.SEG_LINETO:
                        Point2D p1 = points.get(points.size() - 1);
                        Point2D p2 = new Point2D.Double(koordinaten[0], koordinaten[1]);
                        double d = p1.distance(p2);
                        double i = d / 0.5;
                        for (int j = 0; j < i; j++) {
                            Point2D p3 = new Point2D.Double(p1.getX() + (j / i) * (p2.getX() - p1.getX()), p1.getY() + (j / i) * (p2.getY() - p1.getY()));
                            points.add(p3);
                        }
                        break;
                }
                pi.next();
            }

            // Erstellt einen Timer, der aufhört, wenn die vorgegebene Laufzeit erreicht wurde (hier 5sek.)
            Timer timer = new Timer(40, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {

                    // Holt aktuelle Zeit in ms
                    if (startTime == null) {
                        startTime = System.currentTimeMillis();
                    }

                    // Spielzeit ist die aktuelle Zeit minus die Startzeit
                    long playTime = System.currentTimeMillis() - startTime;

                    // Fortschritt: 0.0 - 1.0, Prozent. Stoppt, wenn eingestellte Laufzeit erreicht ist.
                    double progress = playTime / LAUFZEIT;
                    if (progress >= 1.0) {
                        progress = 1d;
                        ((Timer) e.getSource()).stop();
                    }

                    // i dont even fucking know
                    int index = Math.min(Math.max(0, (int) (points.size() * progress)), points.size() - 1);

                    // Index gibt an, wo sich das Auto gerade befindet. Außerdem wird der Winkel zwischen dem aktuellen Punkt und dem nächsten berechnet.
                    pos = points.get(index);
                    if (index < points.size() - 1) {
                        angle = angleTo(pos, points.get(index + 1));
                    }
                    System.out.println("Index: " + index + " , Winkel: " + angle);
                    repaint();
                }
            });
            timer.start();
        }

        // Größe des JPanels. 200x200 Pixel.
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            // Zeichnet Pfad, kann auch weggelassen werden
            g2d.draw(pathShape);

            // Objekt, mit dem das Auto gedreht wird
            AffineTransform affTransform = new AffineTransform();

            if (pos != null) {
                // Speichert Grenzen/Größe des Autos als Rechteck ab
                Rectangle bounds = car.getBounds();
                // AffineTransform-Objekt soll rotieren um den Ursprung, deshalb: bounds.width / 2. (Legt quasi Aufgabe fest)
                affTransform.rotate(angle, (bounds.width / 2), (bounds.width / 2));

                // Constructs a Path2D object from an Shape object, transformed by an AffineTransform object. (übergibt das Auto und die Transformation, die darauf angewendet werden soll)
                Path2D player = new Path2D.Double(car, affTransform);

                // Versetzt den Koordinatenursprung
                g2d.translate(pos.getX() - (bounds.width / 2), pos.getY() - (bounds.height / 2));
                g2d.draw(player);

            }
            g2d.dispose();
        }

        // Zu Bogenmaß
        protected double angleTo(Point2D from, Point2D to) {
            double angle = Math.atan2(to.getY() - from.getY(), to.getX() - from.getX());
            return angle;
        }
    }
}

ani2.gif

Das Objekt/Gefährt bewegt sich jetzt gleichschnell über die gesamte Strecke.
 
Konnte erst heute wieder reingucken, sorry. Vielen vielen Danke für die Mühe! Ich bin gestern auch zu einer Lösung gekommen, bei der das Auto sich allerdings in der Kurve langsamer bewegt. Das ist aber durchaus ein Vorteil, da realitätsnah. Im Grunde habe ich einfach eine Methode geschrieben, die den Start und Endpunkt einer Geraden bekommt und dann die Zwischenpunkte errechnet plus abspeichert. Das funktionierte vor allem so einfach, weil die Linien immer gerade sind, sprich entweder beide x-Werte oder beide y-Werte der Punkte sind gleich.

Sah dann so aus (kann man natürlich sauberer machen und muss noch etwas angepasst werden, aber funktioniert erstmal für den Fall😀):

Java:
public void generatePointsOnLine(Point2D higher, Point2D lower) {
    int anzahl = 100;
    double difference;
    if (higher.getX() == lower.getX()) {
        difference = higher.getY() - lower.getY();
        for (int i = 0; i < anzahl; i++) {
            points.add(new Point2D.Double(higher.getX(), higher.getY() - ((difference * i / anzahl))));
        }
    } else if (higher.getY() == lower.getY()) {
        difference = higher.getX() - lower.getX();
        for (int i = 0; i < anzahl; i++) {
            points.add(new Point2D.Double(higher.getX() - (difference * i / anzahl), higher.getY()));
        }
}
 
Ok, du hast es in eine Methode gesteckt.
Du hast ja einen Standvektor und Aufvektor, dabei würde auch bleiben.
Und dann ist da ja noch ein leichtes Schlingern des Fahrzeugs. Ist das gewollt?
Ich weiß, echt-runde Bogen schwer...
Nebenbei: Im zweiten Gif hatte ich die leicht-magnetische Kraft erst von rechts (unten), dann von links noch drin, deswegen ist die Linie nicht ganz gerade. Spielt aber keine Rolle.
Das schöne ist ja, mit dem leicht verstecken Verfahren, lassen sich ALLE beliebigen herstellen. Also alle Muster Formen Figuren die man sich vorstellen kann.^^ Insofern hast du da mächtiges Werkzeug.
 
Du hast doch immer noch Standvektor und Aufvektor. Auch wenn das nicht ganz die korrekte Bezeichnung ist. Dabei solltest du es auch belassen, bei der Rechnung. So bewegen sich zB auch Flugzeuge im 3D-Raum - wenn sie entlang einer Geraden geradeaus fliegen... Dann ist Kollisionsberechnung und so ein Spaß auch kein Problem - wenn sie windschief sind.
Also durch die Berechnung mit Standvektor und Aufvektor kannst du dass modular gestalten - und deine Kurve könnte in 2D, 3D, 4D,... sein:
Java:
                        Point2D p1 = points.get(points.size() - 1);
                        Point2D p2 = new Point2D.Double(koordinaten[0], koordinaten[1]);
                        double d = p1.distance(p2);
                        double i = d / 0.5;
                        for (int j = 0; j < i; j++) {
                            Point2D p3 = new Point2D.Double(p1.getX() + (j / i) * (p2.getX() - p1.getX()), p1.getY() + (j / i) * (p2.getY() - p1.getY()));
                            points.add(p3);
                        }

0.5 ist allerdings Nonsens... Setze einen "guten" Wert dafür ein. Ein passender/geeigneter Wert könnte zB sein: 1.42. 😉

Hö?!?! Wie ist er da denn drauf gestoßen? Das ist einfach sqrt(2) aufgerundet. 🙂
Good luck, Best wishes
 
Bearbeitung: Aber iwo muss da noch etwas anders:
Code:
d = dist...
i = d / 1.42
j = d / i
for k ...
  k*i*(...)
  ...

Das war vorerst etwas nachlässig/schlampig glaub ich.
 
Sorry, immer noch nicht richtig, das ist der Uhrzeit geschuldet... So gehts:
Java:
            // Zerlegt den Pfad in Punkte, um ihm folgen zu können. Argument flatness: Abstand der Punkte.
            PathIterator pi = pathShape.getPathIterator(null, 0.1);

            // Speichert die Punkte ins points-Array
            while (!pi.isDone()) {
                double[] koordinaten = new double[6];

                switch (pi.currentSegment(koordinaten)) {
                    case PathIterator.SEG_MOVETO:
                        points.add(new Point2D.Double(koordinaten[0], koordinaten[1]));
                        break;
                    case PathIterator.SEG_LINETO:
                        Point2D p1 = points.get(points.size() - 1);
                        Point2D p2 = new Point2D.Double(koordinaten[0], koordinaten[1]);
                        double d = p1.distance(p2);
                        double i = d / 1.42;
                        // double j = d / i;
                        for (int k = 0; k < i; k++) {
                            Point2D p3 = new Point2D.Double(p1.getX() + k / i * (p2.getX() - p1.getX()), p1.getY() + k / i * (p2.getY() - p1.getY()));
                            points.add(p3);
                        }
                        break;
                }
                pi.next();
            }

            System.out.println("points.size() = " + points.size());

Dein Path wird dabei zB genau in 210 Punkte zerlegt - und mit 1.42 kann man jetzt etwas "experimentieren".

Also wenn nix weiter ist, dann ist das Thema jetzt fertig, sorry, dass ich 3mal gepostet hatte.
 

Zurück
Oben