kopia lustrzana https://github.com/jameshball/osci-render
commit
84d66a9e50
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="
|
||||
M16.5,21
|
||||
C13.5,21 12.31,16.76 11.05,12.28
|
||||
C10.14,9.04 9,5 7.5,5
|
||||
C4.11,5 4,11.93 4,12
|
||||
h-2
|
||||
C2,11.63 2.06,3 7.5,3
|
||||
C10.5,3 11.71,7.25 12.97,11.74
|
||||
C13.83,14.8 15,19 16.5,19
|
||||
C19.94,19 20.03,12.07 20.03,12
|
||||
h2
|
||||
C22.03,12.37 21.97,21 16.5,21
|
||||
Z" /></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 541 B |
|
@ -1,11 +1,20 @@
|
|||
package audio;
|
||||
|
||||
import engine.Camera;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.xml.sax.SAXException;
|
||||
import parser.FileParser;
|
||||
import parser.ObjParser;
|
||||
import parser.SvgParser;
|
||||
import shapes.Shape;
|
||||
import shapes.Shapes;
|
||||
|
||||
// Helper class for AudioClient that deals with optional program arguments.
|
||||
final class AudioArgs {
|
||||
|
||||
final String objFilePath;
|
||||
final String filePath;
|
||||
final float[] optionalArgs;
|
||||
|
||||
AudioArgs(String[] args) throws IllegalAudioArgumentException {
|
||||
|
@ -13,7 +22,7 @@ final class AudioArgs {
|
|||
throw new IllegalAudioArgumentException();
|
||||
}
|
||||
|
||||
objFilePath = args[0];
|
||||
filePath = args[0];
|
||||
optionalArgs = new float[args.length - 1];
|
||||
|
||||
for (int i = 0; i < optionalArgs.length; i++) {
|
||||
|
@ -21,8 +30,16 @@ final class AudioArgs {
|
|||
}
|
||||
}
|
||||
|
||||
String objFilePath() {
|
||||
return objFilePath;
|
||||
List<List<Shape>> getFramesFromFile() throws IOException, ParserConfigurationException, SAXException {
|
||||
if (filePath.matches(".*\\.obj")) {
|
||||
return new ObjParser(filePath, rotateSpeed(), cameraX(), cameraY(), cameraZ(), focalLength(),
|
||||
isDefaultPosition()).getShapes();
|
||||
} else if (filePath.matches(".*\\.svg")) {
|
||||
return Shapes.normalize(new SvgParser(filePath).getShapes());
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Provided file extension in file " + filePath + " not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
float rotateSpeed() {
|
||||
|
@ -53,7 +70,7 @@ final class AudioArgs {
|
|||
return optionalArgs.length > n ? optionalArgs[n] : defaultVal;
|
||||
}
|
||||
|
||||
private class IllegalAudioArgumentException extends IllegalArgumentException {
|
||||
private static class IllegalAudioArgumentException extends IllegalArgumentException {
|
||||
|
||||
private static final String USAGE = "Incorrect usage.\nUsage: osci-render objFilePath "
|
||||
+ "[rotateSpeed] [focalLength] [cameraX] [cameraY] [cameraZ]";
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
package audio;
|
||||
|
||||
import engine.Camera;
|
||||
import engine.Vector3;
|
||||
import engine.WorldObject;
|
||||
import java.util.ArrayList;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.IntStream;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.xml.sax.SAXException;
|
||||
import shapes.Shape;
|
||||
import shapes.Shapes;
|
||||
import shapes.Vector2;
|
||||
|
||||
public class AudioClient {
|
||||
|
||||
private static final int SAMPLE_RATE = 192000;
|
||||
private static double OBJ_ROTATE_SPEED = Math.PI / 1000;
|
||||
private static final float ROTATE_SPEED = 0;
|
||||
private static final float TRANSLATION_SPEED = 0;
|
||||
private static final Vector2 TRANSLATION = new Vector2(0.3, 0.3);
|
||||
|
@ -22,7 +17,7 @@ public class AudioClient {
|
|||
private static final float WEIGHT = Shape.DEFAULT_WEIGHT;
|
||||
|
||||
// args:
|
||||
// args[0] - path of .obj file
|
||||
// args[0] - path of .obj or .svg file
|
||||
// args[1] - rotation speed of object
|
||||
// args[2] - focal length of camera
|
||||
// args[3] - x position of camera
|
||||
|
@ -31,7 +26,8 @@ public class AudioClient {
|
|||
//
|
||||
// example:
|
||||
// osci-render models/cube.obj 3
|
||||
public static void main(String[] programArgs) {
|
||||
public static void main(String[] programArgs)
|
||||
throws IOException, ParserConfigurationException, SAXException {
|
||||
// TODO: Calculate weight of lines using depth.
|
||||
// Reduce weight of lines drawn multiple times.
|
||||
// Find intersections of lines to (possibly) improve line cleanup.
|
||||
|
@ -39,20 +35,8 @@ public class AudioClient {
|
|||
|
||||
AudioArgs args = new AudioArgs(programArgs);
|
||||
|
||||
OBJ_ROTATE_SPEED *= args.rotateSpeed();
|
||||
|
||||
Vector3 cameraPos = new Vector3(args.cameraX(), args.cameraY(), args.cameraZ());
|
||||
WorldObject object = new WorldObject(args.objFilePath());
|
||||
|
||||
// If camera position arguments haven't been specified, automatically work out the position of
|
||||
// the camera based on the size of the object in the camera's view.
|
||||
Camera camera = args.isDefaultPosition() ? new Camera(args.focalLength(), object)
|
||||
: new Camera(args.focalLength(), cameraPos);
|
||||
|
||||
Vector3 rotation = new Vector3(0, OBJ_ROTATE_SPEED, OBJ_ROTATE_SPEED);
|
||||
|
||||
System.out.println("Begin pre-render...");
|
||||
List<List<? extends Shape>> frames = preRender(object, rotation, camera);
|
||||
List<List<Shape>> frames = args.getFramesFromFile();
|
||||
System.out.println("Finish pre-render");
|
||||
System.out.println("Connecting to audio player");
|
||||
AudioPlayer player = new AudioPlayer(SAMPLE_RATE, frames, ROTATE_SPEED, TRANSLATION_SPEED,
|
||||
|
@ -60,33 +44,4 @@ public class AudioClient {
|
|||
System.out.println("Starting audio stream");
|
||||
player.play();
|
||||
}
|
||||
|
||||
private static List<List<? extends Shape>> preRender(WorldObject object, Vector3 rotation,
|
||||
Camera camera) {
|
||||
List<List<? extends Shape>> preRenderedFrames = new ArrayList<>();
|
||||
// Number of frames it will take to render a full rotation of the object.
|
||||
int numFrames = (int) (2 * Math.PI / OBJ_ROTATE_SPEED);
|
||||
|
||||
for (int i = 0; i < numFrames; i++) {
|
||||
preRenderedFrames.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
AtomicInteger renderedFrames = new AtomicInteger();
|
||||
|
||||
// pre-renders the WorldObject in parallel
|
||||
IntStream.range(0, numFrames).parallel().forEach((frameNum) -> {
|
||||
WorldObject clone = object.clone();
|
||||
clone.rotate(rotation.scale(frameNum));
|
||||
// Finds all lines to draw the object from the camera's view and then 'sorts' them by finding
|
||||
// a hamiltonian path, which dramatically helps with rendering a clean image.
|
||||
preRenderedFrames.set(frameNum, Shapes.sortLines(camera.draw(clone)));
|
||||
int numRendered = renderedFrames.getAndIncrement();
|
||||
|
||||
if (numRendered % 50 == 0) {
|
||||
System.out.println("Rendered " + numRendered + " frames of " + (numFrames + 1) + " total");
|
||||
}
|
||||
});
|
||||
|
||||
return preRenderedFrames;
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ public class AudioPlayer {
|
|||
|
||||
private final XtFormat FORMAT;
|
||||
|
||||
private final List<List<? extends Shape>> frames;
|
||||
private final List<List<Shape>> frames;
|
||||
private int currentFrame = 0;
|
||||
private int currentShape = 0;
|
||||
private int audioFramesDrawn = 0;
|
||||
|
@ -33,12 +33,12 @@ public class AudioPlayer {
|
|||
|
||||
private volatile boolean stopped;
|
||||
|
||||
public AudioPlayer(int sampleRate, List<List<? extends Shape>> frames) {
|
||||
public AudioPlayer(int sampleRate, List<List<Shape>> frames) {
|
||||
this.FORMAT = new XtFormat(new XtMix(sampleRate, XtSample.FLOAT32), 0, 0, 2, 0);
|
||||
this.frames = frames;
|
||||
}
|
||||
|
||||
public AudioPlayer(int sampleRate, List<List<? extends Shape>> frames, float rotateSpeed,
|
||||
public AudioPlayer(int sampleRate, List<List<Shape>> frames, float rotateSpeed,
|
||||
float translateSpeed, Vector2 translateVector, float scale, float weight) {
|
||||
this(sampleRate, frames);
|
||||
setRotateSpeed(rotateSpeed);
|
||||
|
@ -59,11 +59,10 @@ public class AudioPlayer {
|
|||
|
||||
double totalAudioFrames = shape.getWeight() * shape.getLength();
|
||||
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
|
||||
Vector2 nextVector = shape.nextVector(drawingProgress);
|
||||
|
||||
for (int c = 0; c < FORMAT.outputs; c++) {
|
||||
((float[]) output)[f * FORMAT.outputs] = shape.nextX(drawingProgress);
|
||||
((float[]) output)[f * FORMAT.outputs + 1] = shape.nextY(drawingProgress);
|
||||
}
|
||||
((float[]) output)[f * FORMAT.outputs] = (float) nextVector.getX();
|
||||
((float[]) output)[f * FORMAT.outputs + 1] = (float) nextVector.getY();
|
||||
|
||||
audioFramesDrawn++;
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@ public class Camera {
|
|||
private static final double VERTEX_VALUE_THRESHOLD = 1;
|
||||
private static final double CAMERA_MOVE_INCREMENT = -0.1;
|
||||
private static final int SAMPLE_RENDER_SAMPLES = 50;
|
||||
private static final double EPSILON = 0.001;
|
||||
|
||||
private final double focalLength;
|
||||
|
||||
private double focalLength;
|
||||
private double clipping = 0.001;
|
||||
private Vector3 pos;
|
||||
|
||||
public Camera(double focalLength, Vector3 pos) {
|
||||
|
@ -100,7 +101,7 @@ public class Camera {
|
|||
}
|
||||
|
||||
private Vector2 project(Vector3 vertex) {
|
||||
if (vertex.getZ() - pos.getZ() < clipping) {
|
||||
if (vertex.getZ() - pos.getZ() < EPSILON) {
|
||||
return new Vector2();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,18 +6,20 @@ public final class Vector3 {
|
|||
|
||||
private final double x, y, z;
|
||||
|
||||
public Vector3() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.z = 0;
|
||||
}
|
||||
|
||||
public Vector3(double x, double y, double z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
public Vector3(double xyz) {
|
||||
this(xyz, xyz, xyz);
|
||||
}
|
||||
|
||||
public Vector3() {
|
||||
this(0, 0, 0);
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package engine;
|
|||
import com.mokiat.data.front.parser.*;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
|
@ -15,24 +16,6 @@ public class WorldObject {
|
|||
private Vector3 position;
|
||||
private Vector3 rotation;
|
||||
|
||||
public WorldObject(String filename) {
|
||||
this.vertices = new ArrayList<>();
|
||||
this.edgeData = new ArrayList<>();
|
||||
this.position = new Vector3();
|
||||
this.rotation = new Vector3();
|
||||
|
||||
loadFromFile(filename);
|
||||
}
|
||||
|
||||
public WorldObject(String filename, Vector3 position, Vector3 rotation) {
|
||||
this.vertices = new ArrayList<>();
|
||||
this.edgeData = new ArrayList<>();
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
|
||||
loadFromFile(filename);
|
||||
}
|
||||
|
||||
public WorldObject(List<Vector3> vertices, List<Integer> edgeData, Vector3 position,
|
||||
Vector3 rotation) {
|
||||
this.vertices = vertices;
|
||||
|
@ -41,6 +24,16 @@ public class WorldObject {
|
|||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
public WorldObject(String filename, Vector3 position, Vector3 rotation) throws IOException {
|
||||
this(new ArrayList<>(), new ArrayList<>(), position, rotation);
|
||||
loadFromFile(filename);
|
||||
}
|
||||
|
||||
public WorldObject(String filename) throws IOException {
|
||||
this(filename, new Vector3(), new Vector3());
|
||||
}
|
||||
|
||||
|
||||
public void rotate(Vector3 theta) {
|
||||
rotation = rotation.add(theta);
|
||||
}
|
||||
|
@ -49,6 +42,14 @@ public class WorldObject {
|
|||
rotation = new Vector3();
|
||||
}
|
||||
|
||||
public void move(Vector3 translation) {
|
||||
position = position.add(translation);
|
||||
}
|
||||
|
||||
public void resetPosition() {
|
||||
position = new Vector3();
|
||||
}
|
||||
|
||||
public List<Vector3> getVertices() {
|
||||
List<Vector3> newVertices = new ArrayList<>();
|
||||
|
||||
|
@ -63,30 +64,26 @@ public class WorldObject {
|
|||
return edgeData;
|
||||
}
|
||||
|
||||
private void loadFromFile(String filename) {
|
||||
try (InputStream in = new FileInputStream(filename)) {
|
||||
final IOBJParser parser = new OBJParser();
|
||||
final OBJModel model = parser.parse(in);
|
||||
private void loadFromFile(String filename) throws IOException {
|
||||
InputStream in = new FileInputStream(filename);
|
||||
final IOBJParser parser = new OBJParser();
|
||||
final OBJModel model = parser.parse(in);
|
||||
|
||||
for (OBJVertex vertex : model.getVertices()) {
|
||||
vertices.add(new Vector3(vertex.x, vertex.y, vertex.z));
|
||||
}
|
||||
for (OBJVertex vertex : model.getVertices()) {
|
||||
vertices.add(new Vector3(vertex.x, vertex.y, vertex.z));
|
||||
}
|
||||
|
||||
for (OBJObject object : model.getObjects()) {
|
||||
for (OBJMesh mesh : object.getMeshes()) {
|
||||
for (OBJFace face : mesh.getFaces()) {
|
||||
List<OBJDataReference> references = face.getReferences();
|
||||
for (OBJObject object : model.getObjects()) {
|
||||
for (OBJMesh mesh : object.getMeshes()) {
|
||||
for (OBJFace face : mesh.getFaces()) {
|
||||
List<OBJDataReference> references = face.getReferences();
|
||||
|
||||
for (int i = 0; i < references.size(); i++) {
|
||||
edgeData.add(references.get(i).vertexIndex);
|
||||
edgeData.add(references.get((i + 1) % references.size()).vertexIndex);
|
||||
}
|
||||
for (int i = 0; i < references.size(); i++) {
|
||||
edgeData.add(references.get(i).vertexIndex);
|
||||
edgeData.add(references.get((i + 1) % references.size()).vertexIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new IllegalArgumentException("Cannot load mesh data from: " + filename);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,34 +2,27 @@ package parser;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.xml.sax.SAXException;
|
||||
import shapes.Shape;
|
||||
|
||||
public abstract class FileParser {
|
||||
|
||||
public static String fileExtension;
|
||||
protected abstract String getFileExtension();
|
||||
|
||||
public static String getFileExtension() {
|
||||
return fileExtension;
|
||||
}
|
||||
|
||||
public FileParser(String path) throws IOException, SAXException, ParserConfigurationException {
|
||||
checkFileExtension(path);
|
||||
parseFile(path);
|
||||
}
|
||||
|
||||
private static void checkFileExtension(String path) throws IllegalArgumentException {
|
||||
Pattern pattern = Pattern.compile("\\." + getFileExtension() + "$");
|
||||
if (!pattern.matcher(path).find()) {
|
||||
protected void checkFileExtension(String path) throws IllegalArgumentException {
|
||||
if (!hasCorrectFileExtension(path)) {
|
||||
throw new IllegalArgumentException(
|
||||
"File to parse is not a ." + getFileExtension() + " file.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasCorrectFileExtension(String path) {
|
||||
return path.matches(".*\\." + getFileExtension());
|
||||
}
|
||||
|
||||
protected abstract void parseFile(String path)
|
||||
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException;
|
||||
|
||||
public abstract List<? extends Shape> getShapes();
|
||||
public abstract List<List<Shape>> getShapes();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package parser;
|
||||
|
||||
import engine.Camera;
|
||||
import engine.Vector3;
|
||||
import engine.WorldObject;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.IntStream;
|
||||
import shapes.Shape;
|
||||
import shapes.Shapes;
|
||||
|
||||
public class ObjParser extends FileParser {
|
||||
|
||||
private static double OBJ_ROTATE_SPEED = Math.PI / 1000;
|
||||
|
||||
private List<List<Shape>> shapes;
|
||||
|
||||
private final float rotateSpeed;
|
||||
private final float cameraX;
|
||||
private final float cameraY;
|
||||
private final float cameraZ;
|
||||
private final float focalLength;
|
||||
private final boolean isDefaultPosition;
|
||||
|
||||
public ObjParser(String path, float rotateSpeed, float cameraX, float cameraY, float cameraZ,
|
||||
float focalLength, boolean isDefaultPosition) throws IOException {
|
||||
checkFileExtension(path);
|
||||
shapes = new ArrayList<>();
|
||||
this.rotateSpeed = rotateSpeed;
|
||||
this.cameraX = cameraX;
|
||||
this.cameraY = cameraY;
|
||||
this.cameraZ = cameraZ;
|
||||
this.focalLength = focalLength;
|
||||
this.isDefaultPosition = isDefaultPosition;
|
||||
parseFile(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFileExtension() {
|
||||
return "obj";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseFile(String path) throws IllegalArgumentException, IOException {
|
||||
OBJ_ROTATE_SPEED *= rotateSpeed;
|
||||
|
||||
Vector3 cameraPos = new Vector3(cameraX, cameraY, cameraZ);
|
||||
WorldObject object = new WorldObject(path);
|
||||
|
||||
// If camera position arguments haven't been specified, automatically work out the position of
|
||||
// the camera based on the size of the object in the camera's view.
|
||||
Camera camera = isDefaultPosition ? new Camera(focalLength, object)
|
||||
: new Camera(focalLength, cameraPos);
|
||||
|
||||
Vector3 rotation = new Vector3(0, OBJ_ROTATE_SPEED, OBJ_ROTATE_SPEED);
|
||||
|
||||
shapes = preRender(object, rotation, camera);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<List<Shape>> getShapes() {
|
||||
return shapes;
|
||||
}
|
||||
|
||||
private static List<List<Shape>> preRender(WorldObject object, Vector3 rotation,
|
||||
Camera camera) {
|
||||
List<List<Shape>> preRenderedFrames = new ArrayList<>();
|
||||
// Number of frames it will take to render a full rotation of the object.
|
||||
int numFrames = (int) (2 * Math.PI / OBJ_ROTATE_SPEED);
|
||||
|
||||
for (int i = 0; i < numFrames; i++) {
|
||||
preRenderedFrames.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
AtomicInteger renderedFrames = new AtomicInteger();
|
||||
|
||||
// pre-renders the WorldObject in parallel
|
||||
IntStream.range(0, numFrames).parallel().forEach((frameNum) -> {
|
||||
WorldObject clone = object.clone();
|
||||
clone.rotate(rotation.scale(frameNum));
|
||||
// Finds all lines to draw the object from the camera's view and then 'sorts' them by finding
|
||||
// a hamiltonian path, which dramatically helps with rendering a clean image.
|
||||
preRenderedFrames.set(frameNum, Shapes.sortLines(camera.draw(clone)));
|
||||
int numRendered = renderedFrames.getAndIncrement();
|
||||
|
||||
if (numRendered % 50 == 0) {
|
||||
System.out.println("Rendered " + numRendered + " frames of " + (numFrames + 1) + " total");
|
||||
}
|
||||
});
|
||||
|
||||
return preRenderedFrames;
|
||||
}
|
||||
}
|
|
@ -1,42 +1,347 @@
|
|||
package parser;
|
||||
|
||||
import static parser.XmlUtil.asList;
|
||||
import static parser.XmlUtil.getNodeValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.xml.sax.SAXException;
|
||||
import shapes.CubicBezierCurve;
|
||||
import shapes.Line;
|
||||
import shapes.QuadraticBezierCurve;
|
||||
import shapes.Shape;
|
||||
import shapes.Vector2;
|
||||
|
||||
public class SvgParser extends FileParser {
|
||||
|
||||
private final List<? extends Shape> shapes;
|
||||
private final List<Shape> shapes;
|
||||
private final Map<Character, Function<List<Float>, List<? extends Shape>>> commandMap;
|
||||
|
||||
static {
|
||||
fileExtension = "svg";
|
||||
}
|
||||
private Vector2 currPoint;
|
||||
private Vector2 initialPoint;
|
||||
private Vector2 prevCubicControlPoint;
|
||||
private Vector2 prevQuadraticControlPoint;
|
||||
|
||||
public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException {
|
||||
super(path);
|
||||
checkFileExtension(path);
|
||||
shapes = new ArrayList<>();
|
||||
commandMap = new HashMap<>();
|
||||
initialiseCommandMap();
|
||||
parseFile(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseFile(String path)
|
||||
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
|
||||
// Map command chars to function calls.
|
||||
private void initialiseCommandMap() {
|
||||
commandMap.put('M', (args) -> parseMoveTo(args, true));
|
||||
commandMap.put('m', (args) -> parseMoveTo(args, false));
|
||||
commandMap.put('L', (args) -> parseLineTo(args, true, true, true));
|
||||
commandMap.put('l', (args) -> parseLineTo(args, false, true, true));
|
||||
commandMap.put('H', (args) -> parseLineTo(args, true, true, false));
|
||||
commandMap.put('h', (args) -> parseLineTo(args, false, true, false));
|
||||
commandMap.put('V', (args) -> parseLineTo(args, true, false, true));
|
||||
commandMap.put('v', (args) -> parseLineTo(args, false, false, true));
|
||||
commandMap.put('C', (args) -> parseCurveTo(args, true, true, false));
|
||||
commandMap.put('c', (args) -> parseCurveTo(args, false, true, false));
|
||||
commandMap.put('S', (args) -> parseCurveTo(args, true, true, true));
|
||||
commandMap.put('s', (args) -> parseCurveTo(args, false, true, true));
|
||||
commandMap.put('Q', (args) -> parseCurveTo(args, true, false, false));
|
||||
commandMap.put('q', (args) -> parseCurveTo(args, false, false, false));
|
||||
commandMap.put('T', (args) -> parseCurveTo(args, true, false, true));
|
||||
commandMap.put('t', (args) -> parseCurveTo(args, false, false, true));
|
||||
commandMap.put('A', (args) -> parseEllipticalArc(args, true));
|
||||
commandMap.put('a', (args) -> parseEllipticalArc(args, false));
|
||||
commandMap.put('Z', this::parseClosePath);
|
||||
commandMap.put('z', this::parseClosePath);
|
||||
}
|
||||
|
||||
private Document getSvgDocument(String path)
|
||||
throws IOException, SAXException, ParserConfigurationException {
|
||||
// opens XML reader for svg file.
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setValidating(true);
|
||||
factory.setIgnoringElementContentWhitespace(true);
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
File file = new File(path);
|
||||
Document doc = builder.parse(file);
|
||||
|
||||
return builder.parse(file);
|
||||
}
|
||||
|
||||
// Does error checking against SVG path and returns array of SVG commands and arguments
|
||||
private String[] preProcessPath(String path) throws IllegalArgumentException {
|
||||
// Replace all commas with spaces and then remove unnecessary whitespace
|
||||
path = path.replace(',', ' ');
|
||||
path = path.replace("-", " -");
|
||||
path = path.replaceAll("\\s+", " ");
|
||||
path = path.replaceAll("(^\\s|\\s$)", "");
|
||||
|
||||
// If there are any characters in the path that are illegal
|
||||
if (path.matches("[^mlhvcsqtazMLHVCSQTAZ\\-.\\d\\s]")) {
|
||||
throw new IllegalArgumentException("Illegal characters in SVG path.");
|
||||
// If there are more than 1 letters or delimiters next to one another
|
||||
} else if (path.matches("[a-zA-Z.\\-]{2,}")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Multiple letters or delimiters found next to one another in SVG path.");
|
||||
// First character in path must be a command
|
||||
} else if (path.matches("^[a-zA-Z]")) {
|
||||
throw new IllegalArgumentException("Start of SVG path is not a letter.");
|
||||
}
|
||||
|
||||
// Split on SVG path characters to get a list of instructions, keeping the SVG commands
|
||||
return path.split("(?=[mlhvcsqtazMLHVCSQTAZ])");
|
||||
}
|
||||
|
||||
// Returns list of SVG path data attributes
|
||||
private static List<String> getSvgPathAttributes(Document svg) {
|
||||
List<String> paths = new ArrayList<>();
|
||||
|
||||
for (Node elem : asList(svg.getElementsByTagName("path"))) {
|
||||
paths.add(getNodeValue(elem, "d"));
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static List<Float> splitCommand(String command) {
|
||||
List<Float> nums = new ArrayList<>();
|
||||
String[] decimalSplit = command.split("\\.");
|
||||
|
||||
try {
|
||||
if (decimalSplit.length == 1) {
|
||||
nums.add(Float.parseFloat(decimalSplit[0]));
|
||||
} else {
|
||||
nums.add(Float.parseFloat(decimalSplit[0] + "." + decimalSplit[1]));
|
||||
|
||||
for (int i = 2; i < decimalSplit.length; i++) {
|
||||
nums.add(Float.parseFloat("." + decimalSplit[i]));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println(Arrays.toString(decimalSplit));
|
||||
System.out.println(command);
|
||||
}
|
||||
|
||||
return nums;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends Shape> getShapes() {
|
||||
return shapes;
|
||||
protected String getFileExtension() {
|
||||
return "svg";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseFile(String filePath)
|
||||
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
|
||||
Document svg = getSvgDocument(filePath);
|
||||
List<Node> svgElem = asList(svg.getElementsByTagName("svg"));
|
||||
|
||||
if (svgElem.size() != 1) {
|
||||
throw new IllegalArgumentException("SVG has either zero or more than one svg element.");
|
||||
}
|
||||
|
||||
// Get all d attributes within path elements in the SVG file.
|
||||
for (String path : getSvgPathAttributes(svg)) {
|
||||
currPoint = new Vector2();
|
||||
prevCubicControlPoint = null;
|
||||
prevQuadraticControlPoint = null;
|
||||
String[] commands = preProcessPath(path);
|
||||
|
||||
for (String command : commands) {
|
||||
char commandChar = command.charAt(0);
|
||||
List<Float> nums = null;
|
||||
|
||||
if (commandChar != 'z' && commandChar != 'Z') {
|
||||
// Split the command into number strings and convert them into floats.
|
||||
nums = Arrays.stream(command.substring(1).split(" "))
|
||||
.filter(Predicate.not(String::isBlank))
|
||||
.flatMap((numString) -> splitCommand(numString).stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Use the nums to get a list of shapes, using the first character in the command to specify
|
||||
// the function to use.
|
||||
shapes.addAll(commandMap.get(commandChar).apply(nums));
|
||||
|
||||
if (!String.valueOf(commandChar).matches("[csCS]")) {
|
||||
prevCubicControlPoint = null;
|
||||
}
|
||||
if (!String.valueOf(commandChar).matches("[qtQT]")) {
|
||||
prevQuadraticControlPoint = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<List<Shape>> getShapes() {
|
||||
List<List<Shape>> frames = new ArrayList<>();
|
||||
frames.add(shapes);
|
||||
return frames;
|
||||
}
|
||||
|
||||
// Parses moveto commands (M and m commands)
|
||||
private List<? extends Shape> parseMoveTo(List<Float> args, boolean isAbsolute) {
|
||||
if (args.size() % 2 != 0 || args.size() < 2) {
|
||||
throw new IllegalArgumentException("SVG moveto command has incorrect number of arguments.");
|
||||
}
|
||||
|
||||
Vector2 vec = new Vector2(args.get(0), args.get(1));
|
||||
|
||||
if (isAbsolute) {
|
||||
currPoint = vec;
|
||||
initialPoint = currPoint;
|
||||
if (args.size() > 2) {
|
||||
return parseLineTo(args.subList(2, args.size() - 1), true, true, true);
|
||||
}
|
||||
} else {
|
||||
currPoint = currPoint.translate(vec);
|
||||
initialPoint = currPoint;
|
||||
if (args.size() > 2) {
|
||||
return parseLineTo(args.subList(2, args.size() - 1), false, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// Parses close path commands (Z and z commands)
|
||||
private List<? extends Shape> parseClosePath(List<Float> args) {
|
||||
if (!currPoint.equals(initialPoint)) {
|
||||
Line line = new Line(currPoint, initialPoint);
|
||||
currPoint = initialPoint;
|
||||
return List.of(line);
|
||||
} else {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
// Parses lineto commands (L, l, H, h, V, and v commands)
|
||||
// isHorizontal and isVertical should be true for parsing L and l commands
|
||||
// Only isHorizontal should be true for parsing H and h commands
|
||||
// Only isVertical should be true for parsing V and v commands
|
||||
private List<? extends Shape> parseLineTo(List<Float> args, boolean isAbsolute,
|
||||
boolean isHorizontal, boolean isVertical) {
|
||||
int expectedArgs = isHorizontal && isVertical ? 2 : 1;
|
||||
|
||||
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
|
||||
throw new IllegalArgumentException("SVG lineto command has incorrect number of arguments.");
|
||||
}
|
||||
|
||||
List<Line> lines = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < args.size(); i += expectedArgs) {
|
||||
Vector2 newPoint;
|
||||
|
||||
if (expectedArgs == 1) {
|
||||
newPoint = new Vector2(args.get(i), args.get(i));
|
||||
} else {
|
||||
newPoint = new Vector2(args.get(i), args.get(i + 1));
|
||||
}
|
||||
|
||||
if (isHorizontal && !isVertical) {
|
||||
newPoint = isAbsolute ? newPoint.setY(currPoint.getY()) : newPoint.setY(0);
|
||||
} else if (isVertical && !isHorizontal) {
|
||||
newPoint = isAbsolute ? newPoint.setX(currPoint.getX()) : newPoint.setX(0);
|
||||
}
|
||||
|
||||
if (!isAbsolute) {
|
||||
newPoint = currPoint.translate(newPoint);
|
||||
}
|
||||
|
||||
lines.add(new Line(currPoint, newPoint));
|
||||
currPoint = newPoint;
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Parses curveto commands (C, c, S, s, Q, q, T, and t commands)
|
||||
// isCubic should be true for parsing C, c, S, and s commands
|
||||
// isCubic should be false for parsing Q, q, T, and t commands
|
||||
// isSmooth should be true for parsing S, s, T, and t commands
|
||||
// isSmooth should be false for parsing C, c, Q, and q commands
|
||||
private List<? extends Shape> parseCurveTo(List<Float> args, boolean isAbsolute, boolean isCubic,
|
||||
boolean isSmooth) {
|
||||
int expectedArgs = isCubic ? 4 : 2;
|
||||
if (!isSmooth) {
|
||||
expectedArgs += 2;
|
||||
}
|
||||
|
||||
if (args.size() % expectedArgs != 0 || args.size() < expectedArgs) {
|
||||
throw new IllegalArgumentException("SVG curveto command has incorrect number of arguments.");
|
||||
}
|
||||
|
||||
List<Shape> curves = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < args.size(); i += expectedArgs) {
|
||||
Vector2 controlPoint1;
|
||||
Vector2 controlPoint2 = new Vector2();
|
||||
|
||||
if (isSmooth) {
|
||||
if (isCubic) {
|
||||
controlPoint1 = prevCubicControlPoint == null ? currPoint
|
||||
: prevCubicControlPoint.reflectRelativeToVector(currPoint);
|
||||
} else {
|
||||
controlPoint1 = prevQuadraticControlPoint == null ? currPoint
|
||||
: prevQuadraticControlPoint.reflectRelativeToVector(currPoint);
|
||||
}
|
||||
} else {
|
||||
controlPoint1 = new Vector2(args.get(i), args.get(i + 1));
|
||||
}
|
||||
|
||||
if (isCubic) {
|
||||
controlPoint2 = new Vector2(args.get(i + 2), args.get(i + 3));
|
||||
}
|
||||
|
||||
Vector2 newPoint = new Vector2(args.get(i + expectedArgs - 2),
|
||||
args.get(i + expectedArgs - 1));
|
||||
|
||||
if (!isAbsolute) {
|
||||
controlPoint1 = currPoint.translate(controlPoint1);
|
||||
controlPoint2 = currPoint.translate(controlPoint2);
|
||||
newPoint = currPoint.translate(newPoint);
|
||||
}
|
||||
|
||||
if (isCubic) {
|
||||
curves.add(new CubicBezierCurve(currPoint, controlPoint1, controlPoint2, newPoint));
|
||||
currPoint = newPoint;
|
||||
prevCubicControlPoint = controlPoint2;
|
||||
} else {
|
||||
curves.add(new QuadraticBezierCurve(currPoint, controlPoint1, newPoint));
|
||||
currPoint = newPoint;
|
||||
prevQuadraticControlPoint = controlPoint1;
|
||||
}
|
||||
}
|
||||
|
||||
return curves;
|
||||
}
|
||||
|
||||
private List<? extends Shape> parseEllipticalArc(List<Float> args, boolean isAbsolute) {
|
||||
// TODO: Properly implement
|
||||
|
||||
if (args.size() % 7 != 0 || args.size() < 7) {
|
||||
throw new IllegalArgumentException(
|
||||
"SVG elliptical arc command has incorrect number of arguments.");
|
||||
}
|
||||
|
||||
List<Float> lineToArgs = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < args.size(); i += 7) {
|
||||
lineToArgs.add(args.get(i + 5));
|
||||
lineToArgs.add(args.get(i + 6));
|
||||
}
|
||||
|
||||
return parseLineTo(lineToArgs, isAbsolute, true, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package parser;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.RandomAccess;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
public final class XmlUtil {
|
||||
|
||||
private XmlUtil() {
|
||||
}
|
||||
|
||||
public static List<Node> asList(NodeList n) {
|
||||
return n.getLength() == 0 ?
|
||||
Collections.emptyList() : new NodeListWrapper(n);
|
||||
}
|
||||
|
||||
static final class NodeListWrapper extends AbstractList<Node>
|
||||
implements RandomAccess {
|
||||
|
||||
private final NodeList list;
|
||||
|
||||
NodeListWrapper(NodeList l) {
|
||||
list = l;
|
||||
}
|
||||
|
||||
public Node get(int index) {
|
||||
return list.item(index);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return list.getLength();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getNodeValue(Node node, String namedItem) {
|
||||
return node.getAttributes().getNamedItem(namedItem).getNodeValue();
|
||||
}
|
||||
}
|
|
@ -6,68 +6,53 @@ public class CubicBezierCurve extends Shape {
|
|||
private final Vector2 p1;
|
||||
private final Vector2 p2;
|
||||
private final Vector2 p3;
|
||||
private final double factor;
|
||||
private final Vector2 translation;
|
||||
|
||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight,
|
||||
double factor, Vector2 translation) {
|
||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) {
|
||||
this.p0 = p0;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.p3 = p3;
|
||||
this.weight = weight;
|
||||
this.factor = factor;
|
||||
this.translation = translation;
|
||||
Line temp = new Line(p0, p3);
|
||||
this.length = temp.length;
|
||||
}
|
||||
|
||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double factor,
|
||||
Vector2 translation) {
|
||||
this(p0, p1, p2, p3, DEFAULT_WEIGHT, factor, translation);
|
||||
}
|
||||
|
||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double factor) {
|
||||
this(p0, p1, p2, p3, DEFAULT_WEIGHT, factor, new Vector2());
|
||||
this.length = new Line(p0, p3).length;
|
||||
}
|
||||
|
||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) {
|
||||
this(p0, p1, p2, p3, DEFAULT_WEIGHT, 1, new Vector2());
|
||||
this(p0, p1, p2, p3, DEFAULT_WEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextX(double t) {
|
||||
return (float) (Math.pow(1 - t, 3) * factor * p0.getX()
|
||||
+ 3 * Math.pow(1 - t, 2) * t * factor * p1.getX()
|
||||
+ 3 * (1 - t) * Math.pow(t, 2) * factor * p2.getX()
|
||||
+ Math.pow(t, 3) * factor * p3.getX());
|
||||
public Vector2 nextVector(double t) {
|
||||
return p0.scale(Math.pow(1 - t, 3))
|
||||
.add(p1.scale(3 * Math.pow(1 - t, 2) * t))
|
||||
.add(p2.scale(3 * (1 - t) * Math.pow(t, 2)))
|
||||
.add(p3.scale(Math.pow(t, 3)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextY(double t) {
|
||||
return (float) (Math.pow(1 - t, 3) * factor * p0.getY()
|
||||
+ 3 * Math.pow(1 - t, 2) * t * factor * p1.getY()
|
||||
+ 3 * (1 - t) * Math.pow(t, 2) * factor * p2.getY()
|
||||
+ Math.pow(t, 3) * factor * p3.getY());
|
||||
public CubicBezierCurve rotate(double theta) {
|
||||
return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta),
|
||||
p3.rotate(theta), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape rotate(double theta) {
|
||||
return this;
|
||||
public CubicBezierCurve scale(double factor) {
|
||||
return scale(new Vector2(factor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape scale(double factor) {
|
||||
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation);
|
||||
public CubicBezierCurve scale(Vector2 vector) {
|
||||
return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector),
|
||||
p3.scale(vector), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape translate(Vector2 vector) {
|
||||
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation.translate(vector));
|
||||
public CubicBezierCurve translate(Vector2 vector) {
|
||||
return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector),
|
||||
p3.translate(vector), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape setWeight(double weight) {
|
||||
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation);
|
||||
public CubicBezierCurve setWeight(double weight) {
|
||||
return new CubicBezierCurve(p0, p1, p2, p3, weight);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,17 +26,12 @@ public final class Ellipse extends Shape {
|
|||
}
|
||||
|
||||
@Override
|
||||
public float nextX(double drawingProgress) {
|
||||
return (float) (position.getX()
|
||||
+ a * Math.cos(2 * Math.PI * drawingProgress) * Math.cos(rotation)
|
||||
- b * Math.sin(2 * Math.PI * drawingProgress) * Math.sin(rotation));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextY(double drawingProgress) {
|
||||
return (float) (position.getY()
|
||||
+ a * Math.cos(2 * Math.PI * drawingProgress) * Math.sin(rotation)
|
||||
+ b * Math.sin(2 * Math.PI * drawingProgress) * Math.cos(rotation));
|
||||
public Vector2 nextVector(double drawingProgress) {
|
||||
double theta = 2 * Math.PI * drawingProgress;
|
||||
return position.add(new Vector2(
|
||||
a * Math.cos(theta) * Math.cos(rotation) - b * Math.sin(theta) * Math.sin(rotation),
|
||||
a * Math.cos(theta) * Math.sin(rotation) + b * Math.sin(theta) * Math.cos(rotation)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -50,7 +45,13 @@ public final class Ellipse extends Shape {
|
|||
|
||||
@Override
|
||||
public Ellipse scale(double factor) {
|
||||
return new Ellipse(a * factor, b * factor, weight, rotation, position.scale(factor));
|
||||
return scale(new Vector2(factor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ellipse scale(Vector2 vector) {
|
||||
return new Ellipse(a * vector.getX(), b * vector.getY(), weight, rotation,
|
||||
position.scale(vector));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package shapes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class Line extends Shape {
|
||||
|
||||
private final Vector2 a;
|
||||
|
@ -43,18 +46,18 @@ public final class Line extends Shape {
|
|||
return new Line(a.scale(factor), b.scale(factor), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Line scale(Vector2 vector) {
|
||||
return new Line(a.scale(vector), b.scale(vector), weight);
|
||||
}
|
||||
|
||||
public Line copy() {
|
||||
return new Line(a.copy(), b.copy(), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextX(double drawingProgress) {
|
||||
return (float) (getX1() + (getX2() - getX1()) * drawingProgress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextY(double drawingProgress) {
|
||||
return (float) (getY1() + (getY2() - getY1()) * drawingProgress);
|
||||
public Vector2 nextVector(double drawingProgress) {
|
||||
return a.add(b.sub(a).scale(drawingProgress));
|
||||
}
|
||||
|
||||
public Vector2 getA() {
|
||||
|
@ -97,6 +100,20 @@ public final class Line extends Shape {
|
|||
return new Line(getX1(), getY1(), getX2(), y2);
|
||||
}
|
||||
|
||||
public static List<Line> pathToLines(double... path) {
|
||||
List<Line> lines = new ArrayList<>();
|
||||
|
||||
Vector2 prev = new Vector2(path[0], path[1]);
|
||||
|
||||
for (int i = 2; i < path.length; i += 2) {
|
||||
Vector2 dest = new Vector2(path[i], path[i + 1]);
|
||||
lines.add(new Line(prev, dest));
|
||||
prev = dest;
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Line setWeight(double weight) {
|
||||
return new Line(getX1(), getY1(), getX2(), getY2(), weight);
|
||||
|
@ -114,4 +131,12 @@ public final class Line extends Shape {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Line{" +
|
||||
"a=" + a +
|
||||
", b=" + b +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,52 @@
|
|||
package shapes;
|
||||
|
||||
public class QuadraticBezierCurve extends CubicBezierCurve {
|
||||
public class QuadraticBezierCurve extends Shape {
|
||||
|
||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight,
|
||||
double factor, Vector2 translation) {
|
||||
super(p0, p1, p1, p2, weight, factor, translation);
|
||||
}
|
||||
private final Vector2 p0;
|
||||
private final Vector2 p1;
|
||||
private final Vector2 p2;
|
||||
|
||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double factor,
|
||||
Vector2 translation) {
|
||||
super(p0, p1, p1, p2, factor, translation);
|
||||
}
|
||||
|
||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double factor) {
|
||||
super(p0, p1, p1, p2, factor);
|
||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) {
|
||||
this.p0 = p0;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.weight = weight;
|
||||
this.length = new Line(p0, p2).length;
|
||||
}
|
||||
|
||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
|
||||
super(p0, p1, p1, p2);
|
||||
this(p0, p1, p2, DEFAULT_WEIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector2 nextVector(double t) {
|
||||
return p1.add(p0.sub(p1).scale(Math.pow(1 - t, 2)))
|
||||
.add(p2.sub(p1).scale(Math.pow(t, 2)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuadraticBezierCurve rotate(double theta) {
|
||||
return new QuadraticBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuadraticBezierCurve scale(double factor) {
|
||||
return new QuadraticBezierCurve(p0.scale(factor), p1.scale(factor), p2.scale(factor), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuadraticBezierCurve scale(Vector2 vector) {
|
||||
return new QuadraticBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuadraticBezierCurve translate(Vector2 vector) {
|
||||
return new QuadraticBezierCurve(p0.translate(vector), p1.translate(vector),
|
||||
p2.translate(vector), weight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuadraticBezierCurve setWeight(double weight) {
|
||||
return new QuadraticBezierCurve(p0, p1, p2, weight);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@ public abstract class Shape {
|
|||
protected double weight = DEFAULT_WEIGHT;
|
||||
protected double length;
|
||||
|
||||
public abstract float nextX(double drawingProgress);
|
||||
|
||||
public abstract float nextY(double drawingProgress);
|
||||
public abstract Vector2 nextVector(double drawingProgress);
|
||||
|
||||
public abstract Shape rotate(double theta);
|
||||
|
||||
public abstract Shape scale(double factor);
|
||||
|
||||
public abstract Shape scale(Vector2 vector);
|
||||
|
||||
public abstract Shape translate(Vector2 vector);
|
||||
|
||||
public abstract Shape setWeight(double weight);
|
||||
|
|
|
@ -12,8 +12,39 @@ import org.jgrapht.graph.AsSubgraph;
|
|||
import org.jgrapht.graph.DefaultUndirectedWeightedGraph;
|
||||
import org.jgrapht.graph.DefaultWeightedEdge;
|
||||
|
||||
// Helper functions for the Shape interface.
|
||||
public class Shapes {
|
||||
|
||||
// Normalises shapes between the coords -1 and 1 for proper scaling on an oscilloscope. May not
|
||||
// work perfectly with curves that heavily deviate from their start and end points.
|
||||
public static List<List<Shape>> normalize(List<List<Shape>> shapeLists) {
|
||||
double maxVertex = 0;
|
||||
|
||||
for (List<Shape> shapes : shapeLists) {
|
||||
for (Shape shape : shapes) {
|
||||
Vector2 startVector = shape.nextVector(0);
|
||||
Vector2 endVector = shape.nextVector(1);
|
||||
|
||||
double maxX = Math.max(Math.abs(startVector.getX()), Math.abs(endVector.getX()));
|
||||
double maxY = Math.max(Math.abs(startVector.getY()), Math.abs(endVector.getY()));
|
||||
|
||||
maxVertex = Math.max(Math.max(maxX, maxY), maxVertex);
|
||||
}
|
||||
}
|
||||
|
||||
double factor = 2 / maxVertex;
|
||||
|
||||
for (List<Shape> shapes : shapeLists) {
|
||||
for (int i = 0; i < shapes.size(); i++) {
|
||||
shapes.set(i, shapes.get(i)
|
||||
.scale(new Vector2(factor, -factor))
|
||||
.translate(new Vector2(-1, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
return shapeLists;
|
||||
}
|
||||
|
||||
public static List<Shape> generatePolygram(int sides, int angleJump, Vector2 start,
|
||||
double weight) {
|
||||
List<Shape> polygon = new ArrayList<>();
|
||||
|
|
|
@ -17,6 +17,10 @@ public final class Vector2 extends Shape {
|
|||
this(x, y, Shape.DEFAULT_WEIGHT);
|
||||
}
|
||||
|
||||
public Vector2(double xy) {
|
||||
this(xy, xy, Shape.DEFAULT_WEIGHT);
|
||||
}
|
||||
|
||||
public Vector2() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
@ -41,14 +45,21 @@ public final class Vector2 extends Shape {
|
|||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextX(double drawingProgress) {
|
||||
return (float) getX();
|
||||
public Vector2 add(Vector2 vector) {
|
||||
return translate(vector);
|
||||
}
|
||||
|
||||
public Vector2 sub(Vector2 vector) {
|
||||
return new Vector2(getX() - vector.getX(), getY() - vector.getY());
|
||||
}
|
||||
|
||||
public Vector2 reflectRelativeToVector(Vector2 vector) {
|
||||
return translate(vector.sub(this).scale(2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float nextY(double drawingProgress) {
|
||||
return (float) getY();
|
||||
public Vector2 nextVector(double drawingProgress) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -61,7 +72,12 @@ public final class Vector2 extends Shape {
|
|||
|
||||
@Override
|
||||
public Vector2 scale(double factor) {
|
||||
return new Vector2(getX() * factor, getY() * factor);
|
||||
return scale(new Vector2(factor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector2 scale(Vector2 vector) {
|
||||
return new Vector2(getX() * vector.getX(), getY() * vector.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -102,4 +118,12 @@ public final class Vector2 extends Shape {
|
|||
|
||||
return (double) Math.round(value) / factor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Vector2{" +
|
||||
"x=" + x +
|
||||
", y=" + y +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import shapes.Line;
|
|||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class TestSuite {
|
||||
public class LineTest {
|
||||
// TODO: Create tests for shapes.Shapes class.
|
||||
|
||||
@Test
|
|
@ -0,0 +1,45 @@
|
|||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.junit.Test;
|
||||
import org.xml.sax.SAXException;
|
||||
import parser.SvgParser;
|
||||
import shapes.Line;
|
||||
import shapes.Shape;
|
||||
|
||||
public class SvgParserTest {
|
||||
|
||||
private List<? extends Shape> getShapes(SvgParser parser) {
|
||||
return parser.getShapes().get(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lineToGeneratesALineShape()
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
SvgParser svgParser = new SvgParser("test/images/line-to.svg");
|
||||
assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.75, 1, 0, 0, 0.5, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void horizontalLineToGeneratesAHorizontalLineShape()
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
SvgParser svgParser = new SvgParser("test/images/horizontal-line-to.svg");
|
||||
assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verticalLineToGeneratesAVerticalLineShape()
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
SvgParser svgParser = new SvgParser("test/images/vertical-line-to.svg");
|
||||
assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.5, 0.75, 0.5, 0, 0.5, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closingASubPathDrawsLineToInitialPoint()
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
SvgParser svgParser = new SvgParser("test/images/closing-subpath.svg");
|
||||
assertEquals(getShapes(svgParser), Line.pathToLines(0.5, 0.5, 0.75, 0.5, 0.75, 0.75, 0.5, 0.75, 0.5, 0.5));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
|
||||
M0.5,0.5
|
||||
h0.25
|
||||
v0.25
|
||||
h-0.25
|
||||
Z" /></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 326 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
|
||||
M0.5,0.5
|
||||
h0.25
|
||||
H0
|
||||
H0.5" /></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 318 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
|
||||
M0.5,0.5
|
||||
l0.25,0.5
|
||||
L0,0
|
||||
L0.5,0.5" /></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 328 B |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1" height="1" viewBox="0 0 1 1"><path d="
|
||||
M0.5,0.5
|
||||
v0.25
|
||||
V0
|
||||
V0.5" /></svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 318 B |
Ładowanie…
Reference in New Issue