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;
|
package audio;
|
||||||
|
|
||||||
import engine.Camera;
|
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.
|
// Helper class for AudioClient that deals with optional program arguments.
|
||||||
final class AudioArgs {
|
final class AudioArgs {
|
||||||
|
|
||||||
final String objFilePath;
|
final String filePath;
|
||||||
final float[] optionalArgs;
|
final float[] optionalArgs;
|
||||||
|
|
||||||
AudioArgs(String[] args) throws IllegalAudioArgumentException {
|
AudioArgs(String[] args) throws IllegalAudioArgumentException {
|
||||||
|
@ -13,7 +22,7 @@ final class AudioArgs {
|
||||||
throw new IllegalAudioArgumentException();
|
throw new IllegalAudioArgumentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
objFilePath = args[0];
|
filePath = args[0];
|
||||||
optionalArgs = new float[args.length - 1];
|
optionalArgs = new float[args.length - 1];
|
||||||
|
|
||||||
for (int i = 0; i < optionalArgs.length; i++) {
|
for (int i = 0; i < optionalArgs.length; i++) {
|
||||||
|
@ -21,8 +30,16 @@ final class AudioArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String objFilePath() {
|
List<List<Shape>> getFramesFromFile() throws IOException, ParserConfigurationException, SAXException {
|
||||||
return objFilePath;
|
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() {
|
float rotateSpeed() {
|
||||||
|
@ -53,7 +70,7 @@ final class AudioArgs {
|
||||||
return optionalArgs.length > n ? optionalArgs[n] : defaultVal;
|
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 "
|
private static final String USAGE = "Incorrect usage.\nUsage: osci-render objFilePath "
|
||||||
+ "[rotateSpeed] [focalLength] [cameraX] [cameraY] [cameraZ]";
|
+ "[rotateSpeed] [focalLength] [cameraX] [cameraY] [cameraZ]";
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
package audio;
|
package audio;
|
||||||
|
|
||||||
import engine.Camera;
|
import java.io.IOException;
|
||||||
import engine.Vector3;
|
|
||||||
import engine.WorldObject;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import java.util.stream.IntStream;
|
import org.xml.sax.SAXException;
|
||||||
import shapes.Shape;
|
import shapes.Shape;
|
||||||
import shapes.Shapes;
|
|
||||||
import shapes.Vector2;
|
import shapes.Vector2;
|
||||||
|
|
||||||
public class AudioClient {
|
public class AudioClient {
|
||||||
|
|
||||||
private static final int SAMPLE_RATE = 192000;
|
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 ROTATE_SPEED = 0;
|
||||||
private static final float TRANSLATION_SPEED = 0;
|
private static final float TRANSLATION_SPEED = 0;
|
||||||
private static final Vector2 TRANSLATION = new Vector2(0.3, 0.3);
|
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;
|
private static final float WEIGHT = Shape.DEFAULT_WEIGHT;
|
||||||
|
|
||||||
// args:
|
// args:
|
||||||
// args[0] - path of .obj file
|
// args[0] - path of .obj or .svg file
|
||||||
// args[1] - rotation speed of object
|
// args[1] - rotation speed of object
|
||||||
// args[2] - focal length of camera
|
// args[2] - focal length of camera
|
||||||
// args[3] - x position of camera
|
// args[3] - x position of camera
|
||||||
|
@ -31,7 +26,8 @@ public class AudioClient {
|
||||||
//
|
//
|
||||||
// example:
|
// example:
|
||||||
// osci-render models/cube.obj 3
|
// 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.
|
// TODO: Calculate weight of lines using depth.
|
||||||
// Reduce weight of lines drawn multiple times.
|
// Reduce weight of lines drawn multiple times.
|
||||||
// Find intersections of lines to (possibly) improve line cleanup.
|
// Find intersections of lines to (possibly) improve line cleanup.
|
||||||
|
@ -39,20 +35,8 @@ public class AudioClient {
|
||||||
|
|
||||||
AudioArgs args = new AudioArgs(programArgs);
|
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...");
|
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("Finish pre-render");
|
||||||
System.out.println("Connecting to audio player");
|
System.out.println("Connecting to audio player");
|
||||||
AudioPlayer player = new AudioPlayer(SAMPLE_RATE, frames, ROTATE_SPEED, TRANSLATION_SPEED,
|
AudioPlayer player = new AudioPlayer(SAMPLE_RATE, frames, ROTATE_SPEED, TRANSLATION_SPEED,
|
||||||
|
@ -60,33 +44,4 @@ public class AudioClient {
|
||||||
System.out.println("Starting audio stream");
|
System.out.println("Starting audio stream");
|
||||||
player.play();
|
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 XtFormat FORMAT;
|
||||||
|
|
||||||
private final List<List<? extends Shape>> frames;
|
private final List<List<Shape>> frames;
|
||||||
private int currentFrame = 0;
|
private int currentFrame = 0;
|
||||||
private int currentShape = 0;
|
private int currentShape = 0;
|
||||||
private int audioFramesDrawn = 0;
|
private int audioFramesDrawn = 0;
|
||||||
|
@ -33,12 +33,12 @@ public class AudioPlayer {
|
||||||
|
|
||||||
private volatile boolean stopped;
|
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.FORMAT = new XtFormat(new XtMix(sampleRate, XtSample.FLOAT32), 0, 0, 2, 0);
|
||||||
this.frames = frames;
|
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) {
|
float translateSpeed, Vector2 translateVector, float scale, float weight) {
|
||||||
this(sampleRate, frames);
|
this(sampleRate, frames);
|
||||||
setRotateSpeed(rotateSpeed);
|
setRotateSpeed(rotateSpeed);
|
||||||
|
@ -59,11 +59,10 @@ public class AudioPlayer {
|
||||||
|
|
||||||
double totalAudioFrames = shape.getWeight() * shape.getLength();
|
double totalAudioFrames = shape.getWeight() * shape.getLength();
|
||||||
double drawingProgress = totalAudioFrames == 0 ? 1 : audioFramesDrawn / totalAudioFrames;
|
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] = (float) nextVector.getX();
|
||||||
((float[]) output)[f * FORMAT.outputs] = shape.nextX(drawingProgress);
|
((float[]) output)[f * FORMAT.outputs + 1] = (float) nextVector.getY();
|
||||||
((float[]) output)[f * FORMAT.outputs + 1] = shape.nextY(drawingProgress);
|
|
||||||
}
|
|
||||||
|
|
||||||
audioFramesDrawn++;
|
audioFramesDrawn++;
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,10 @@ public class Camera {
|
||||||
private static final double VERTEX_VALUE_THRESHOLD = 1;
|
private static final double VERTEX_VALUE_THRESHOLD = 1;
|
||||||
private static final double CAMERA_MOVE_INCREMENT = -0.1;
|
private static final double CAMERA_MOVE_INCREMENT = -0.1;
|
||||||
private static final int SAMPLE_RENDER_SAMPLES = 50;
|
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;
|
private Vector3 pos;
|
||||||
|
|
||||||
public Camera(double focalLength, Vector3 pos) {
|
public Camera(double focalLength, Vector3 pos) {
|
||||||
|
@ -100,7 +101,7 @@ public class Camera {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 project(Vector3 vertex) {
|
private Vector2 project(Vector3 vertex) {
|
||||||
if (vertex.getZ() - pos.getZ() < clipping) {
|
if (vertex.getZ() - pos.getZ() < EPSILON) {
|
||||||
return new Vector2();
|
return new Vector2();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,20 @@ public final class Vector3 {
|
||||||
|
|
||||||
private final double x, y, z;
|
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) {
|
public Vector3(double x, double y, double z) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.z = z;
|
this.z = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Vector3(double xyz) {
|
||||||
|
this(xyz, xyz, xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3() {
|
||||||
|
this(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public double getX() {
|
public double getX() {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package engine;
|
||||||
import com.mokiat.data.front.parser.*;
|
import com.mokiat.data.front.parser.*;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -15,24 +16,6 @@ public class WorldObject {
|
||||||
private Vector3 position;
|
private Vector3 position;
|
||||||
private Vector3 rotation;
|
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,
|
public WorldObject(List<Vector3> vertices, List<Integer> edgeData, Vector3 position,
|
||||||
Vector3 rotation) {
|
Vector3 rotation) {
|
||||||
this.vertices = vertices;
|
this.vertices = vertices;
|
||||||
|
@ -41,6 +24,16 @@ public class WorldObject {
|
||||||
this.rotation = rotation;
|
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) {
|
public void rotate(Vector3 theta) {
|
||||||
rotation = rotation.add(theta);
|
rotation = rotation.add(theta);
|
||||||
}
|
}
|
||||||
|
@ -49,6 +42,14 @@ public class WorldObject {
|
||||||
rotation = new Vector3();
|
rotation = new Vector3();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void move(Vector3 translation) {
|
||||||
|
position = position.add(translation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPosition() {
|
||||||
|
position = new Vector3();
|
||||||
|
}
|
||||||
|
|
||||||
public List<Vector3> getVertices() {
|
public List<Vector3> getVertices() {
|
||||||
List<Vector3> newVertices = new ArrayList<>();
|
List<Vector3> newVertices = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -63,8 +64,8 @@ public class WorldObject {
|
||||||
return edgeData;
|
return edgeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFromFile(String filename) {
|
private void loadFromFile(String filename) throws IOException {
|
||||||
try (InputStream in = new FileInputStream(filename)) {
|
InputStream in = new FileInputStream(filename);
|
||||||
final IOBJParser parser = new OBJParser();
|
final IOBJParser parser = new OBJParser();
|
||||||
final OBJModel model = parser.parse(in);
|
final OBJModel model = parser.parse(in);
|
||||||
|
|
||||||
|
@ -84,10 +85,6 @@ public class WorldObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new IllegalArgumentException("Cannot load mesh data from: " + filename);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldObject clone() {
|
public WorldObject clone() {
|
||||||
|
|
|
@ -2,34 +2,27 @@ package parser;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
import shapes.Shape;
|
import shapes.Shape;
|
||||||
|
|
||||||
public abstract class FileParser {
|
public abstract class FileParser {
|
||||||
|
|
||||||
public static String fileExtension;
|
protected abstract String getFileExtension();
|
||||||
|
|
||||||
public static String getFileExtension() {
|
protected void checkFileExtension(String path) throws IllegalArgumentException {
|
||||||
return fileExtension;
|
if (!hasCorrectFileExtension(path)) {
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"File to parse is not a ." + getFileExtension() + " file.");
|
"File to parse is not a ." + getFileExtension() + " file.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasCorrectFileExtension(String path) {
|
||||||
|
return path.matches(".*\\." + getFileExtension());
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract void parseFile(String path)
|
protected abstract void parseFile(String path)
|
||||||
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException;
|
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;
|
package parser;
|
||||||
|
|
||||||
|
import static parser.XmlUtil.asList;
|
||||||
|
import static parser.XmlUtil.getNodeValue;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
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.DocumentBuilder;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
import org.xml.sax.SAXException;
|
import org.xml.sax.SAXException;
|
||||||
|
import shapes.CubicBezierCurve;
|
||||||
|
import shapes.Line;
|
||||||
|
import shapes.QuadraticBezierCurve;
|
||||||
import shapes.Shape;
|
import shapes.Shape;
|
||||||
|
import shapes.Vector2;
|
||||||
|
|
||||||
public class SvgParser extends FileParser {
|
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 {
|
private Vector2 currPoint;
|
||||||
fileExtension = "svg";
|
private Vector2 initialPoint;
|
||||||
}
|
private Vector2 prevCubicControlPoint;
|
||||||
|
private Vector2 prevQuadraticControlPoint;
|
||||||
|
|
||||||
public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException {
|
public SvgParser(String path) throws IOException, SAXException, ParserConfigurationException {
|
||||||
super(path);
|
checkFileExtension(path);
|
||||||
shapes = new ArrayList<>();
|
shapes = new ArrayList<>();
|
||||||
|
commandMap = new HashMap<>();
|
||||||
|
initialiseCommandMap();
|
||||||
|
parseFile(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// Map command chars to function calls.
|
||||||
protected void parseFile(String path)
|
private void initialiseCommandMap() {
|
||||||
throws ParserConfigurationException, IOException, SAXException, IllegalArgumentException {
|
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();
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
factory.setValidating(true);
|
|
||||||
factory.setIgnoringElementContentWhitespace(true);
|
factory.setIgnoringElementContentWhitespace(true);
|
||||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
File file = new File(path);
|
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
|
@Override
|
||||||
public List<? extends Shape> getShapes() {
|
protected String getFileExtension() {
|
||||||
return shapes;
|
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 p1;
|
||||||
private final Vector2 p2;
|
private final Vector2 p2;
|
||||||
private final Vector2 p3;
|
private final Vector2 p3;
|
||||||
private final double factor;
|
|
||||||
private final Vector2 translation;
|
|
||||||
|
|
||||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight,
|
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, double weight) {
|
||||||
double factor, Vector2 translation) {
|
|
||||||
this.p0 = p0;
|
this.p0 = p0;
|
||||||
this.p1 = p1;
|
this.p1 = p1;
|
||||||
this.p2 = p2;
|
this.p2 = p2;
|
||||||
this.p3 = p3;
|
this.p3 = p3;
|
||||||
this.weight = weight;
|
this.weight = weight;
|
||||||
this.factor = factor;
|
this.length = new Line(p0, p3).length;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CubicBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) {
|
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
|
@Override
|
||||||
public float nextX(double t) {
|
public Vector2 nextVector(double t) {
|
||||||
return (float) (Math.pow(1 - t, 3) * factor * p0.getX()
|
return p0.scale(Math.pow(1 - t, 3))
|
||||||
+ 3 * Math.pow(1 - t, 2) * t * factor * p1.getX()
|
.add(p1.scale(3 * Math.pow(1 - t, 2) * t))
|
||||||
+ 3 * (1 - t) * Math.pow(t, 2) * factor * p2.getX()
|
.add(p2.scale(3 * (1 - t) * Math.pow(t, 2)))
|
||||||
+ Math.pow(t, 3) * factor * p3.getX());
|
.add(p3.scale(Math.pow(t, 3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float nextY(double t) {
|
public CubicBezierCurve rotate(double theta) {
|
||||||
return (float) (Math.pow(1 - t, 3) * factor * p0.getY()
|
return new CubicBezierCurve(p0.rotate(theta), p1.rotate(theta), p2.rotate(theta),
|
||||||
+ 3 * Math.pow(1 - t, 2) * t * factor * p1.getY()
|
p3.rotate(theta), weight);
|
||||||
+ 3 * (1 - t) * Math.pow(t, 2) * factor * p2.getY()
|
|
||||||
+ Math.pow(t, 3) * factor * p3.getY());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Shape rotate(double theta) {
|
public CubicBezierCurve scale(double factor) {
|
||||||
return this;
|
return scale(new Vector2(factor));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Shape scale(double factor) {
|
public CubicBezierCurve scale(Vector2 vector) {
|
||||||
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation);
|
return new CubicBezierCurve(p0.scale(vector), p1.scale(vector), p2.scale(vector),
|
||||||
|
p3.scale(vector), weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Shape translate(Vector2 vector) {
|
public CubicBezierCurve translate(Vector2 vector) {
|
||||||
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation.translate(vector));
|
return new CubicBezierCurve(p0.translate(vector), p1.translate(vector), p2.translate(vector),
|
||||||
|
p3.translate(vector), weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Shape setWeight(double weight) {
|
public CubicBezierCurve setWeight(double weight) {
|
||||||
return new CubicBezierCurve(p0, p1, p2, p3, weight, factor, translation);
|
return new CubicBezierCurve(p0, p1, p2, p3, weight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,17 +26,12 @@ public final class Ellipse extends Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float nextX(double drawingProgress) {
|
public Vector2 nextVector(double drawingProgress) {
|
||||||
return (float) (position.getX()
|
double theta = 2 * Math.PI * drawingProgress;
|
||||||
+ a * Math.cos(2 * Math.PI * drawingProgress) * Math.cos(rotation)
|
return position.add(new Vector2(
|
||||||
- b * Math.sin(2 * Math.PI * drawingProgress) * Math.sin(rotation));
|
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
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -50,7 +45,13 @@ public final class Ellipse extends Shape {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Ellipse scale(double factor) {
|
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
|
@Override
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package shapes;
|
package shapes;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public final class Line extends Shape {
|
public final class Line extends Shape {
|
||||||
|
|
||||||
private final Vector2 a;
|
private final Vector2 a;
|
||||||
|
@ -43,18 +46,18 @@ public final class Line extends Shape {
|
||||||
return new Line(a.scale(factor), b.scale(factor), weight);
|
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() {
|
public Line copy() {
|
||||||
return new Line(a.copy(), b.copy(), weight);
|
return new Line(a.copy(), b.copy(), weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float nextX(double drawingProgress) {
|
public Vector2 nextVector(double drawingProgress) {
|
||||||
return (float) (getX1() + (getX2() - getX1()) * drawingProgress);
|
return a.add(b.sub(a).scale(drawingProgress));
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float nextY(double drawingProgress) {
|
|
||||||
return (float) (getY1() + (getY2() - getY1()) * drawingProgress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 getA() {
|
public Vector2 getA() {
|
||||||
|
@ -97,6 +100,20 @@ public final class Line extends Shape {
|
||||||
return new Line(getX1(), getY1(), getX2(), y2);
|
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
|
@Override
|
||||||
public Line setWeight(double weight) {
|
public Line setWeight(double weight) {
|
||||||
return new Line(getX1(), getY1(), getX2(), getY2(), weight);
|
return new Line(getX1(), getY1(), getX2(), getY2(), weight);
|
||||||
|
@ -114,4 +131,12 @@ public final class Line extends Shape {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Line{" +
|
||||||
|
"a=" + a +
|
||||||
|
", b=" + b +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,52 @@
|
||||||
package shapes;
|
package shapes;
|
||||||
|
|
||||||
public class QuadraticBezierCurve extends CubicBezierCurve {
|
public class QuadraticBezierCurve extends Shape {
|
||||||
|
|
||||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight,
|
private final Vector2 p0;
|
||||||
double factor, Vector2 translation) {
|
private final Vector2 p1;
|
||||||
super(p0, p1, p1, p2, weight, factor, translation);
|
private final Vector2 p2;
|
||||||
}
|
|
||||||
|
|
||||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double factor,
|
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double weight) {
|
||||||
Vector2 translation) {
|
this.p0 = p0;
|
||||||
super(p0, p1, p1, p2, factor, translation);
|
this.p1 = p1;
|
||||||
}
|
this.p2 = p2;
|
||||||
|
this.weight = weight;
|
||||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2, double factor) {
|
this.length = new Line(p0, p2).length;
|
||||||
super(p0, p1, p1, p2, factor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public QuadraticBezierCurve(Vector2 p0, Vector2 p1, Vector2 p2) {
|
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 weight = DEFAULT_WEIGHT;
|
||||||
protected double length;
|
protected double length;
|
||||||
|
|
||||||
public abstract float nextX(double drawingProgress);
|
public abstract Vector2 nextVector(double drawingProgress);
|
||||||
|
|
||||||
public abstract float nextY(double drawingProgress);
|
|
||||||
|
|
||||||
public abstract Shape rotate(double theta);
|
public abstract Shape rotate(double theta);
|
||||||
|
|
||||||
public abstract Shape scale(double factor);
|
public abstract Shape scale(double factor);
|
||||||
|
|
||||||
|
public abstract Shape scale(Vector2 vector);
|
||||||
|
|
||||||
public abstract Shape translate(Vector2 vector);
|
public abstract Shape translate(Vector2 vector);
|
||||||
|
|
||||||
public abstract Shape setWeight(double weight);
|
public abstract Shape setWeight(double weight);
|
||||||
|
|
|
@ -12,8 +12,39 @@ import org.jgrapht.graph.AsSubgraph;
|
||||||
import org.jgrapht.graph.DefaultUndirectedWeightedGraph;
|
import org.jgrapht.graph.DefaultUndirectedWeightedGraph;
|
||||||
import org.jgrapht.graph.DefaultWeightedEdge;
|
import org.jgrapht.graph.DefaultWeightedEdge;
|
||||||
|
|
||||||
|
// Helper functions for the Shape interface.
|
||||||
public class Shapes {
|
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,
|
public static List<Shape> generatePolygram(int sides, int angleJump, Vector2 start,
|
||||||
double weight) {
|
double weight) {
|
||||||
List<Shape> polygon = new ArrayList<>();
|
List<Shape> polygon = new ArrayList<>();
|
||||||
|
|
|
@ -17,6 +17,10 @@ public final class Vector2 extends Shape {
|
||||||
this(x, y, Shape.DEFAULT_WEIGHT);
|
this(x, y, Shape.DEFAULT_WEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Vector2(double xy) {
|
||||||
|
this(xy, xy, Shape.DEFAULT_WEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
public Vector2() {
|
public Vector2() {
|
||||||
this(0, 0);
|
this(0, 0);
|
||||||
}
|
}
|
||||||
|
@ -41,14 +45,21 @@ public final class Vector2 extends Shape {
|
||||||
return new Vector2(x, y);
|
return new Vector2(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public Vector2 add(Vector2 vector) {
|
||||||
public float nextX(double drawingProgress) {
|
return translate(vector);
|
||||||
return (float) getX();
|
}
|
||||||
|
|
||||||
|
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
|
@Override
|
||||||
public float nextY(double drawingProgress) {
|
public Vector2 nextVector(double drawingProgress) {
|
||||||
return (float) getY();
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -61,7 +72,12 @@ public final class Vector2 extends Shape {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Vector2 scale(double factor) {
|
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
|
@Override
|
||||||
|
@ -102,4 +118,12 @@ public final class Vector2 extends Shape {
|
||||||
|
|
||||||
return (double) Math.round(value) / factor;
|
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.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
public class TestSuite {
|
public class LineTest {
|
||||||
// TODO: Create tests for shapes.Shapes class.
|
// TODO: Create tests for shapes.Shapes class.
|
||||||
|
|
||||||
@Test
|
@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