Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing three point circle brush #346

Open
wants to merge 4 commits into
base: sponge-1.12
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@
*/
package com.thevoxelbox.voxelsniper.brush.shape;

import com.flowpowered.math.GenericMath;
import com.flowpowered.math.matrix.Matrix4d;
import com.flowpowered.math.vector.Vector3d;
import com.flowpowered.math.vector.Vector4d;
import com.thevoxelbox.voxelsniper.Message;
import com.thevoxelbox.voxelsniper.SnipeData;
import com.thevoxelbox.voxelsniper.Undo;
import com.thevoxelbox.voxelsniper.brush.Brush;
import com.thevoxelbox.voxelsniper.brush.PerformBrush;
import org.spongepowered.api.text.format.TextColors;

import java.util.ArrayList;
import java.util.List;

@Brush.BrushInfo(
name = "3-point Circle",
aliases = {"tpc", "threepointcircle"},
Expand All @@ -39,186 +46,147 @@
)
public class ThreePointCircleBrush extends PerformBrush {

private Vector3d coordsOne;
private Vector3d coordsTwo;
private Vector3d coordsThree;
private Tolerance tolerance = Tolerance.DEFAULT;
private List<Vector3d> coords;
private static final String[] NUMBER_DESC = {"First", "Second", "Third"};

/**
* Default Constructor.
*/
public ThreePointCircleBrush() {
coords = new ArrayList<>();
}

@Override
protected final void arrow(final SnipeData v) {
if (this.coordsOne == null) {
this.coordsOne = this.targetBlock.getPosition().add(0.5, 0.5, 0.5);
v.sendMessage(TextColors.GRAY, "First Corner set.");
} else if (this.coordsTwo == null) {
this.coordsTwo = this.targetBlock.getPosition().add(0.5, 0.5, 0.5);
v.sendMessage(TextColors.GRAY, "Second Corner set.");
} else if (this.coordsThree == null) {
this.coordsThree = this.targetBlock.getPosition().add(0.5, 0.5, 0.5);
v.sendMessage(TextColors.GRAY, "Third Corner set.");
} else {
this.coordsOne = this.targetBlock.getPosition().add(0.5, 0.5, 0.5);
this.coordsTwo = null;
this.coordsThree = null;
v.sendMessage(TextColors.GRAY, "First Corner set.");
if (this.coords.size() == 3) {
this.coords.clear();
}

this.coords.add(this.targetBlock.getPosition());
String message = NUMBER_DESC[coords.size() - 1] + " corner set.";
v.sendMessage(TextColors.GRAY, message);
}

@Override
protected final void powder(final SnipeData v) {
if (this.coordsOne == null || this.coordsTwo == null || this.coordsThree == null) {
if (this.coords.size() != 3) {
v.sendMessage(TextColors.RED, "ERROR: Set all points before creating the circle");
return;
}
v.sendMessage(TextColors.RED, "Unfortunately the three-point circle is currently disabled as it does not work :(");

Vector3d coordOne = this.coords.get(0);
Vector3d coordTwo = this.coords.get(1);
Vector3d coordThree = this.coords.get(2);

// Calculate triangle defining vectors
// final Vector3d vectorOne = this.coordsTwo.sub(this.coordsOne);
// final Vector3d vectorTwo = this.coordsThree.sub(this.coordsOne);
// final Vector3d vectorThree = this.coordsThree.sub(vectorTwo);
//
// // Redundant data check
// if (vectorOne.length() == 0 || vectorTwo.length() == 0 || vectorThree.length() == 0) {
// v.sendMessage(TextColors.RED, "ERROR: Invalid points, try again.");
// this.coordsOne = null;
// this.coordsTwo = null;
// this.coordsThree = null;
// return;
// }
//
// // Calculate normal vector of the plane.
// final Vector3d normalVector = vectorOne.cross(vectorTwo);
//
// // Calculate constant term of the plane.
// final double planeConstant = normalVector.getX() * this.coordsOne.getX() + normalVector.getY() * this.coordsOne.getY()
// + normalVector.getZ() * this.coordsOne.getZ();
//
// final Vector3d midpointOne = this.coordsOne.add(this.coordsTwo.sub(this.coordsOne).mul(0.5));
// final Vector3d midpointTwo = this.coordsOne.add(this.coordsThree.sub(this.coordsOne).mul(0.5));
//
// // Find perpendicular vectors to two sides in the plane
// final Vector3d perpendicularOne = normalVector.cross(vectorOne);
// final Vector3d perpendicularTwo = normalVector.cross(vectorTwo);
//
// // determine value of parametric variable at intersection of two
// // perpendicular bisectors
// final Vector3d tNumerator = midpointTwo.sub(midpointOne).cross(perpendicularTwo);
// final Vector3d tDenominator = perpendicularOne.cross(perpendicularTwo);
// final double t = tNumerator.length() / tDenominator.length();
//
// // Calculate Circumcenter and Brushcenter.
// final Vector3d circumcenter = perpendicularOne.mul(t);
// circumcenter.add(midpointOne);
//
// final Vector3d brushCenter = new Vector3d(Math.round(circumcenter.getX()), Math.round(circumcenter.getY()), Math.round(circumcenter.getZ()));
//
// // Calculate radius of circumcircle and determine brushsize
// final double radius = circumcenter.distance(this.coordsOne);
// final int brushSize = (int) (Math.ceil(radius) + 1);
// this.undo = new Undo((int) (4 * v.getBrushSize() * v.getBrushSize()));
// for (int x = -brushSize; x <= brushSize; x++) {
// for (int y = -brushSize; y <= brushSize; y++) {
// for (int z = -brushSize; z <= brushSize; z++) {
// // Calculate distance from center
// final double tempDistance = Math.pow(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2), .5);
//
// // gets corner-on blocks
// final double cornerConstant = normalVector.getX() * (circumcenter.getX() + x) + normalVector.getY() * (circumcenter.getY() + y)
// + normalVector.getZ() * (circumcenter.getZ() + z);
//
// // gets center-on blocks
// final double centerConstant = normalVector.getX() * (circumcenter.getX() + x + .5)
// + normalVector.getY() * (circumcenter.getY() + y + .5) + normalVector.getZ() * (circumcenter.getZ() + z + .5);
//
// // Check if point is within sphere and on plane (some
// // tolerance given)
// if (tempDistance <= radius && (Math.abs(cornerConstant - planeConstant) < this.tolerance.getValue()
// || Math.abs(centerConstant - planeConstant) < this.tolerance.getValue())) {
// perform(v, brushCenter.getFloorX() + x, brushCenter.getFloorY() + y, brushCenter.getFloorZ() + z);
// }
//
// }
// }
// }
//
// v.sendMessage(TextColors.GREEN, "Done.");
// v.owner().storeUndo(this.undo);
// this.undo = null;
//
// // Reset Brush
// this.coordsOne = null;
// this.coordsTwo = null;
// this.coordsThree = null;
Vector3d delta1 = coordTwo.sub(coordOne);
Vector3d delta2 = coordThree.sub(coordOne);
Vector3d normalVector = delta1.cross(delta2);

if (normalVector.lengthSquared() < GenericMath.DBL_EPSILON) {
v.sendMessage(TextColors.RED, "ERROR: Invalid points, try again.");
return;
}

// Column vectors for the A matrix since we are trying to solve the equation
// Ax = b where A is:
// | x1, y1, z1, 1 |
// | x2, y2, z2, 1 |
// | x3, y3, z3, 1 |
// | Nx, Ny, Nz, 0 |
// Where xi, yi, zi represents the x, y, or z component of the ith coord and
// Nx, Ny, and Nz are the components of the normal vector
// and b is:
// | -x1^2 - y1^2 - z1^2 |
// | -x2^2 - y2^2 - z2^2 |
// | -x3^2 - y3^2 - z3^2 |
// | -2 * N * coordsOne |
//
// The first three equations come from the equation of a sphere centered on the three
// points we get while the fourth ensures the point is on the same plane as the other
// three points in the circle. This method is loosely based on method 1 from
// https://www.qc.edu.hk/math/Advanced%20Level/circle%20given%203%20points.htm but has
// been adapted for 3D.
Vector4d A1 = new Vector4d(coordOne.getX(), coordTwo.getX(), coordThree.getX(), normalVector.getX());
Vector4d A2 = new Vector4d(coordOne.getY(), coordTwo.getY(), coordThree.getY(), normalVector.getY());
Vector4d A3 = new Vector4d(coordOne.getZ(), coordTwo.getZ(), coordThree.getZ(), normalVector.getZ());
Vector4d A4 = new Vector4d(1, 1, 1, 0);

double u1 = - coordOne.lengthSquared();
double u2 = - coordTwo.lengthSquared();
double u3 = - coordThree.lengthSquared();

Vector4d b = new Vector4d(u1, u2, u3, -2 * normalVector.dot(coordOne));

// Use Cramer's rule to calculate the center point
double detA = columnsToMatrix(A1, A2, A3, A4).determinant();
double detA1 = columnsToMatrix(b, A2, A3, A4).determinant();
double detA2 = columnsToMatrix(A1, b, A3, A4).determinant();
double detA3 = columnsToMatrix(A1, A2, b, A4).determinant();

Vector3d center = new Vector3d(
(detA1 / detA) / -2,
(detA2 / detA) / -2,
(detA3 / detA) / -2
);

double radius = center.distance(coordOne);

// Create two normal vectors on the plane that can act as the x and y unit vectors for drawing the circle
Vector3d xPrime = coordOne.sub(center).normalize();
Vector3d yPrime = normalVector.cross(xPrime).normalize();


this.undo = new Undo((int) (Math.PI * radius * radius));

// Use the parametric description of a circle with respect to theta and r to determine where to place the
// next block
for (double r = 0; r <= radius; r += .9) {
double deltaTheta = .8 / r;

for (double currentAngle = 0; currentAngle <= 2 * Math.PI * r; currentAngle += deltaTheta) {
Vector3d blockPos = xPrime.mul(Math.cos(currentAngle))
.add(yPrime.mul(Math.sin(currentAngle)))
.mul(r).add(center);

int x = (int) Math.round(blockPos.getX());
int y = (int) Math.round(blockPos.getY());
int z = (int) Math.round(blockPos.getZ());

perform(v, x, y, z);
}
}

v.sendMessage(TextColors.GREEN, "Done.");
v.owner().storeUndo(this.undo);
this.undo = null;

// Reset Brush
this.coords.clear();

}

private Matrix4d columnsToMatrix(Vector4d a, Vector4d b, Vector4d c, Vector4d d) {
return new Matrix4d(
a.getX(), b.getX(), c.getX(), d.getX(),
a.getY(), b.getY(), c.getY(), d.getY(),
a.getZ(), b.getZ(), c.getZ(), d.getZ(),
a.getW(), b.getW(), c.getW(), d.getW()
);
}

@Override
public final void info(final Message vm) {
vm.brushName(this.info.name());
switch (this.tolerance) {
case ACCURATE:
vm.custom(TextColors.GOLD, "Mode: Accurate");
break;
case DEFAULT:
vm.custom(TextColors.GOLD, "Mode: Default");
break;
case SMOOTH:
vm.custom(TextColors.GOLD, "Mode: Smooth");
break;
default:
vm.custom(TextColors.GOLD, "Mode: Unknown");
break;
}

}

@Override
public final void parameters(final String[] par, final SnipeData v) {
if (par.length > 0 && par[0].equalsIgnoreCase("info")) {
if (par.length == 1 && par[0].equalsIgnoreCase("info")) {
v.sendMessage(TextColors.YELLOW,
"3-Point Circle Brush instructions: Select three corners with the arrow brush, then generate the Circle with the powder brush.");
String toleranceOptions = "";
for (final Tolerance tolerance : Tolerance.values()) {
if (!toleranceOptions.isEmpty()) {
toleranceOptions += "|";
}
toleranceOptions += tolerance.name().toLowerCase();
}
v.sendMessage(TextColors.GOLD, "/b tpc " + toleranceOptions + " -- Toggle the calculations to emphasize accuracy or smoothness");
return;
}

for (int i = 0; i < par.length; i++) {
final String parameter = par[i].toUpperCase();
try {
this.tolerance = Tolerance.valueOf(parameter);
v.sendMessage(TextColors.AQUA, "Brush set to " + this.tolerance.name().toLowerCase() + " tolerance.");
return;
} catch (final IllegalArgumentException exception) {
v.getVoxelMessage().brushMessage("No such tolerance.");
}
}
}

/**
* Enumeration on Tolerance values.
*
* @author MikeMatrix
*/
private enum Tolerance {
DEFAULT(1000), ACCURATE(10), SMOOTH(2000);

private int value;

Tolerance(final int value) {
this.value = value;
}

public int getValue() {
return this.value;
} else {
v.sendMessage(TextColors.RED, "Do /b tpc info for information on this brush");
}
}
}