diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e6191891..19c44ff5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -228,6 +228,14 @@ Entries land here as they merge.
### Bug fixes
+- **A stray non-drawing element no longer breaks a whole SVG icon.** A
+ visible-painted SVG element that lowers to a moveto-only or moveto+close
+ path — `d="M12 12"`, a zero-length arc, the stray subpaths real exporters
+ emit — drew no ink, yet a lone moveto threw at `SvgIcon#node(...)` (an empty
+ `PathNode`) and a moveto+close rendered blank. `SvgIconReader` now drops a
+ layer with no drawing segment, so one degenerate element no longer fails the
+ icon; an icon of only such elements still fails loudly with "no drawable
+ geometry".
- **Stacked bars anchor at zero even with an explicit positive axis minimum.**
A stacked bar chart with `valueAxis().min(positive)` lifted the baseline
while segment heights stayed measured from zero, so the stack overshot its
diff --git a/src/main/java/com/demcha/compose/document/svg/SvgIconReader.java b/src/main/java/com/demcha/compose/document/svg/SvgIconReader.java
index c1745111d..557424232 100644
--- a/src/main/java/com/demcha/compose/document/svg/SvgIconReader.java
+++ b/src/main/java/com/demcha/compose/document/svg/SvgIconReader.java
@@ -192,6 +192,13 @@ private static void emitLayer(Element element, String d, Paint paint,
return;
}
SvgPath geometry = SvgPath.parseTransformed(d, matrix, box[0], box[1], box[2], box[3]);
+ if (!geometry.hasDrawingSegment()) {
+ // A moveto-only or moveto+close element (e.g. d="M12 12", or a
+ // zero-length arc that lowers to a lone moveto) draws no ink. Drop
+ // it so one degenerate element a real-world exporter emitted does
+ // not fail the whole icon at SvgIcon#node(...).
+ return;
+ }
// Gradients resolve here, where the shape's geometry (the
// objectBoundingBox reference) and accumulated affine exist. The flat
diff --git a/src/main/java/com/demcha/compose/document/svg/SvgPath.java b/src/main/java/com/demcha/compose/document/svg/SvgPath.java
index a48a5fcb0..813362df2 100644
--- a/src/main/java/com/demcha/compose/document/svg/SvgPath.java
+++ b/src/main/java/com/demcha/compose/document/svg/SvgPath.java
@@ -154,6 +154,25 @@ public double aspectRatio() {
return sourceWidth / sourceHeight;
}
+ /**
+ * Whether this path actually draws ink: it carries at least one
+ * {@code lineTo} / {@code cubicTo} after the leading {@code moveTo}. A
+ * moveto-only path, or a moveto followed only by {@code close} (which a
+ * degenerate SVG element such as {@code d="M12 12"} or a zero-length arc
+ * lowers to), draws nothing.
+ *
+ * @return {@code true} if a drawing segment is present
+ */
+ boolean hasDrawingSegment() {
+ for (DocumentPathSegment segment : segments) {
+ if (segment instanceof DocumentPathSegment.LineTo
+ || segment instanceof DocumentPathSegment.CubicTo) {
+ return true;
+ }
+ }
+ return false;
+ }
+
// ------------------------------------------------------------------
// Normalization (y-flip into the unit box)
// ------------------------------------------------------------------
diff --git a/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java b/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java
index 32c0691ea..ce12c8ca4 100644
--- a/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java
+++ b/src/test/java/com/demcha/compose/document/svg/SvgIconTest.java
@@ -67,6 +67,35 @@ void invisibleElementsProduceNoLayers() {
assertThat(icon.layers()).hasSize(1);
}
+ @Test
+ void degenerateGeometryIsDroppedSoOneStrayElementDoesNotFailTheIcon() {
+ // Real exporters emit stray moveto-only / moveto+close subpaths that
+ // draw nothing. They must not break the whole icon: a lone moveto used
+ // to throw at node() (an empty PathNode), a moveto+close rendered blank.
+ SvgIcon icon = SvgIcon.parse("""
+
+ """);
+
+ // Only the drawable box survives; both degenerate paths are dropped.
+ assertThat(icon.layers()).hasSize(1);
+ assertThat(icon.node(24)).isNotNull(); // threw before the drop guard
+ }
+
+ @Test
+ void anIconOfOnlyDegenerateGeometryFailsLoudly() {
+ assertThatThrownBy(() -> SvgIcon.parse("""
+
+ """))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("no drawable geometry");
+ }
+
@Test
void basicShapesLowerToPathGeometry() {
SvgIcon icon = SvgIcon.parse("""