Skip to content
Merged
Show file tree
Hide file tree
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
@@ -1,6 +1,5 @@
package com.demcha.compose.document.style;

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

Expand Down Expand Up @@ -338,25 +337,7 @@ static Polygon star(double width, double height, int points) {
if (points < 3) {
throw new IllegalArgumentException("star needs at least 3 points: " + points);
}
double outerRadius = 0.5;
// Inner/outer ratio of a true star polygon (the inner ring sits on the
// chords between outer points); it tends to 1 as the point count grows
// and equals the classic 0.382 at five points. Below five points the
// formula degenerates, so fall back to a fixed spiky ratio.
double innerRatio = points >= 5
? Math.cos(2 * Math.PI / points) / Math.cos(Math.PI / points)
: 0.38;
double innerRadius = 0.5 * innerRatio;
double start = Math.PI / 2.0; // first outer vertex faces up
List<ShapePoint> vertices = new ArrayList<>(points * 2);
for (int i = 0; i < points * 2; i++) {
double radius = (i % 2 == 0) ? outerRadius : innerRadius;
double angle = start + i * Math.PI / points;
double x = clampUnit(0.5 + radius * Math.cos(angle));
double y = clampUnit(0.5 + radius * Math.sin(angle));
vertices.add(new ShapePoint(x, y));
}
return new Polygon(width, height, vertices);
return new Polygon(width, height, ShapeRings.toPoints(ShapeRings.star(points)));
}

/**
Expand Down Expand Up @@ -396,7 +377,7 @@ static Polygon arrow(double width, double height, Direction direction, ArrowStyl
{0.00, 0.00}, {1.00, 0.50}, {0.00, 1.00}
};
};
return new Polygon(width, height, directional(base, direction));
return new Polygon(width, height, ShapeRings.toPoints(ShapeRings.directional(base, direction)));
}

/**
Expand Down Expand Up @@ -437,7 +418,7 @@ static Polygon chevron(double width, double height, Direction direction) {
{0.00, 1.00}, {1.00, 0.50}, {0.00, 0.00},
{thickness, 0.00}, {1.00 - thickness, 0.50}, {thickness, 1.00}
};
return new Polygon(width, height, directional(base, direction));
return new Polygon(width, height, ShapeRings.toPoints(ShapeRings.directional(base, direction)));
}

/**
Expand Down Expand Up @@ -465,11 +446,11 @@ static Polygon checkmark(double width, double height) {
static Polygon checkmark(double width, double height, CheckmarkStyle style) {
Objects.requireNonNull(style, "style");
List<ShapePoint> ring = switch (style) {
case CLASSIC -> toPoints(new double[][]{
case CLASSIC -> ShapeRings.toPoints(new double[][]{
{0.45, 0.00}, {1.00, 0.72}, {0.86, 0.92},
{0.42, 0.34}, {0.16, 0.58}, {0.04, 0.44}
});
case HEAVY -> toPoints(ShapeRings.checkmarkBand(0.13));
case HEAVY -> ShapeRings.toPoints(ShapeRings.checkmarkBand(0.13));
};
return new Polygon(width, height, ring);
}
Expand All @@ -489,7 +470,7 @@ static Polygon plus(double width, double height) {
{1.00, high}, {high, high}, {high, 1.00}, {low, 1.00},
{low, high}, {0.00, high}, {0.00, low}, {low, low}
};
return new Polygon(width, height, toPoints(points));
return new Polygon(width, height, ShapeRings.toPoints(points));
}

/**
Expand All @@ -505,58 +486,7 @@ static Polygon regularPolygon(double width, double height, int sides) {
if (sides < 3) {
throw new IllegalArgumentException("regular polygon needs at least 3 sides: " + sides);
}
double start = Math.PI / 2.0;
List<ShapePoint> vertices = new ArrayList<>(sides);
for (int i = 0; i < sides; i++) {
double angle = start + i * 2.0 * Math.PI / sides;
vertices.add(new ShapePoint(
clampUnit(0.5 + 0.5 * Math.cos(angle)),
clampUnit(0.5 + 0.5 * Math.sin(angle))));
}
return new Polygon(width, height, vertices);
}

private static List<ShapePoint> directional(double[][] base, Direction direction) {
Direction resolved = direction == null ? Direction.RIGHT : direction;
List<ShapePoint> points = new ArrayList<>(base.length);
for (double[] vertex : base) {
double x = vertex[0];
double y = vertex[1];
double tx;
double ty;
switch (resolved) {
case LEFT -> {
tx = 1.0 - x;
ty = y;
}
case UP -> {
tx = y;
ty = x;
}
case DOWN -> {
tx = y;
ty = 1.0 - x;
}
default -> {
tx = x;
ty = y;
}
}
points.add(new ShapePoint(clampUnit(tx), clampUnit(ty)));
}
return points;
}

private static List<ShapePoint> toPoints(double[][] raw) {
List<ShapePoint> points = new ArrayList<>(raw.length);
for (double[] vertex : raw) {
points.add(new ShapePoint(clampUnit(vertex[0]), clampUnit(vertex[1])));
}
return points;
}

private static double clampUnit(double value) {
return value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value);
return new Polygon(width, height, ShapeRings.toPoints(ShapeRings.regularPolygon(sides)));
}

private static void requirePositive(String label, double value) {
Expand Down
103 changes: 103 additions & 0 deletions src/main/java/com/demcha/compose/document/style/ShapeRings.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.demcha.compose.document.style;

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

/**
* Package-private geometry helpers that compute raw vertex rings for the more
* involved {@link ShapeOutline} figures, keeping the vector math out of the
Expand All @@ -15,6 +18,106 @@ final class ShapeRings {
private ShapeRings() {
}

/**
* Wraps a raw vertex ring into {@link ShapePoint}s, clamping each
* coordinate into the unit box.
*
* @param raw vertex ring in unit-box coordinates (may stray slightly out)
* @return the clamped points in the same order
*/
static List<ShapePoint> toPoints(double[][] raw) {
List<ShapePoint> points = new ArrayList<>(raw.length);
for (double[] vertex : raw) {
points.add(new ShapePoint(clampUnit(vertex[0]), clampUnit(vertex[1])));
}
return points;
}

private static double clampUnit(double value) {
return value < 0.0 ? 0.0 : (value > 1.0 ? 1.0 : value);
}

/**
* Builds the vertex ring of an {@code n}-pointed star inscribed in the unit
* box, first outer point facing up. Outer points sit on radius 0.5; the
* inner ring uses the true-star ratio (the inner vertices land on the
* chords between outer points) for five-plus points, falling back to a
* fixed spiky ratio below five where that formula degenerates.
*
* @param points number of outer points (caller validates {@code >= 3})
* @return {@code 2 * points} ring vertices in draw order
*/
static double[][] star(int points) {
double innerRatio = points >= 5
? Math.cos(2 * Math.PI / points) / Math.cos(Math.PI / points)
: 0.38;
double innerRadius = 0.5 * innerRatio;
double start = Math.PI / 2.0; // first outer vertex faces up
double[][] ring = new double[points * 2][2];
for (int i = 0; i < points * 2; i++) {
double radius = (i % 2 == 0) ? 0.5 : innerRadius;
double angle = start + i * Math.PI / points;
ring[i][0] = 0.5 + radius * Math.cos(angle);
ring[i][1] = 0.5 + radius * Math.sin(angle);
}
return ring;
}

/**
* Builds the vertex ring of a regular {@code sides}-gon inscribed in the
* unit box, first vertex facing up.
*
* @param sides number of sides (caller validates {@code >= 3})
* @return {@code sides} ring vertices in draw order
*/
static double[][] regularPolygon(int sides) {
double start = Math.PI / 2.0;
double[][] ring = new double[sides][2];
for (int i = 0; i < sides; i++) {
double angle = start + i * 2.0 * Math.PI / sides;
ring[i][0] = 0.5 + 0.5 * Math.cos(angle);
ring[i][1] = 0.5 + 0.5 * Math.sin(angle);
}
return ring;
}

/**
* Reorients a right-pointing base ring toward {@code direction} by the
* axis-aligned remap SVG figures use (mirror for LEFT, transpose for
* UP/DOWN). Coordinates stay in the unit box.
*
* @param base right-pointing vertex ring
* @param direction target direction; {@code null} keeps RIGHT
* @return the reoriented ring
*/
static double[][] directional(double[][] base, ShapeOutline.Direction direction) {
ShapeOutline.Direction resolved = direction == null ? ShapeOutline.Direction.RIGHT : direction;
double[][] ring = new double[base.length][2];
for (int i = 0; i < base.length; i++) {
double x = base[i][0];
double y = base[i][1];
switch (resolved) {
case LEFT -> {
ring[i][0] = 1.0 - x;
ring[i][1] = y;
}
case UP -> {
ring[i][0] = y;
ring[i][1] = x;
}
case DOWN -> {
ring[i][0] = y;
ring[i][1] = 1.0 - x;
}
default -> {
ring[i][0] = x;
ring[i][1] = y;
}
}
}
return ring;
}

/**
* Builds a constant-width checkmark band of perpendicular half-thickness
* {@code half} around a fixed left-tip → elbow → right-tip centreline, with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;

class ShapeRingsTest {

Expand All @@ -25,4 +26,53 @@ void thickerBandPushesTheOuterElbowLower() {

assertThat(thick[0][1]).isLessThan(thin[0][1]);
}

@Test
void starHasTwoVerticesPerPointWithFirstFacingUp() {
double[][] ring = ShapeRings.star(5);

assertThat(ring.length).isEqualTo(10);
// First outer vertex faces up: centred x, top y.
assertThat(ring[0][0]).isCloseTo(0.5, within(1e-9));
assertThat(ring[0][1]).isCloseTo(1.0, within(1e-9));
// Outer vertices (even indices) sit farther from centre than inner ones.
double outer = Math.hypot(ring[0][0] - 0.5, ring[0][1] - 0.5);
double inner = Math.hypot(ring[1][0] - 0.5, ring[1][1] - 0.5);
assertThat(outer).isGreaterThan(inner);
}

@Test
void regularPolygonInscribesNVerticesFirstFacingUp() {
double[][] hex = ShapeRings.regularPolygon(6);

assertThat(hex.length).isEqualTo(6);
assertThat(hex[0][0]).isCloseTo(0.5, within(1e-9));
assertThat(hex[0][1]).isCloseTo(1.0, within(1e-9));
for (double[] v : hex) {
assertThat(Math.hypot(v[0] - 0.5, v[1] - 0.5)).isCloseTo(0.5, within(1e-9));
}
}

@Test
void directionalMirrorsForLeftAndTransposesForUp() {
double[][] base = {{0.0, 0.35}, {1.0, 0.5}, {0.0, 0.65}};

double[][] left = ShapeRings.directional(base, ShapeOutline.Direction.LEFT);
assertThat(left[1][0]).isCloseTo(0.0, within(1e-9)); // tip mirrored to the left edge

double[][] up = ShapeRings.directional(base, ShapeOutline.Direction.UP);
assertThat(up[1][1]).isCloseTo(1.0, within(1e-9)); // tip transposed to the top edge

// null defaults to RIGHT (identity).
assertThat(ShapeRings.directional(base, null)[1][0]).isCloseTo(1.0, within(1e-9));
}

@Test
void toPointsClampsOutOfBoxCoordinates() {
var points = ShapeRings.toPoints(new double[][]{{-0.2, 0.5}, {1.3, 0.5}, {0.5, 0.5}});

assertThat(points.get(0).x()).isCloseTo(0.0, within(1e-9));
assertThat(points.get(1).x()).isCloseTo(1.0, within(1e-9));
assertThat(points.get(2).x()).isCloseTo(0.5, within(1e-9));
}
}