ShapeContainerNode is a composite whose bounding box is dictated by a
geometric outline (rectangle, rounded rectangle, ellipse, or circle), and
which hosts one or more child layers anchored inside that outline. It
differs from LayerStackNode in two ways:
LayerStackNode |
ShapeContainerNode |
|
|---|---|---|
| Bbox | max(child outer size) |
The outline's intrinsic size |
| Clipping | None — children may escape the bbox | ClipPolicy.CLIP_PATH (default), CLIP_BOUNDS, or OVERFLOW_VISIBLE |
| Frame / fill | Needs a separate back layer | First-class part of the node (fill, stroke, corner radius) |
| Pagination | ATOMIC |
SHAPE_ATOMIC — outline plus every layer stays on one page |
See ADR 0001 for the rationale.
The shortest path: drop a circle on the page and put a label inside it.
The default ClipPolicy.CLIP_PATH ensures the label is clipped to the
circle's outline if it ever overflows.
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentTextStyle;
DocumentColor brand = DocumentColor.rgb(180, 40, 40);
DocumentTextStyle headline = DocumentTextStyle.builder()
.size(18)
.color(DocumentColor.WHITE)
.build();
document.pageFlow(page -> page
.addCircle(80, brand, c -> c
.name("BrandCircle")
.center(new ParagraphBuilder()
.text("M&A")
.textStyle(headline)
.build())));ShapeContainerBuilder exposes the same nine-point alignment vocabulary
as LayerStackBuilder, so a badge in topRight plus a label in center
reads naturally:
document.pageFlow(page -> page
.addEllipse(160, 100, brand, e -> e
.name("EllipseBadge")
.topRight(new ParagraphBuilder()
.text("NEW")
.textStyle(DocumentTextStyle.builder().size(10).color(DocumentColor.WHITE).build())
.build())
.center(new ParagraphBuilder()
.text("Featured project")
.textStyle(DocumentTextStyle.builder().size(14).color(DocumentColor.WHITE).build())
.build())));The badge sits at the ellipse's top-right corner and travels with it —
under SHAPE_ATOMIC the whole composite moves as one unit if it doesn't
fit on the current page.
A rounded rectangle outline reads as a card. Combine it with a fluent
RichText body for a lightweight callout:
import com.demcha.compose.document.dsl.RichText;
document.pageFlow(page -> page
.addContainer(card -> card
.name("Highlight")
.roundedRect(220, 90, 12)
.fillColor(DocumentColor.rgb(20, 30, 60))
.padding(12)
.center(new ParagraphBuilder()
.rich(RichText.of()
.text("Status: ")
.bold("Pending"))
.textStyle(DocumentTextStyle.builder()
.size(13)
.color(DocumentColor.WHITE)
.build())
.build())));import com.demcha.compose.document.style.ClipPolicy;| Policy | When to use |
|---|---|
CLIP_PATH (default) |
The container is a shape and children should respect its outline. Choose this for anything that reads as "circle/ellipse/card with content inside". |
CLIP_BOUNDS |
Children should stay inside the axis-aligned bounding box but path clipping is unnecessary or undesirable (rectangle/rounded-rectangle outlines, no decorative overhangs). |
OVERFLOW_VISIBLE |
The outline is decorative and floating overlays should be allowed to stick out (e.g. a badge that overlaps the outline edge by design). The layout layer skips emitting clip markers entirely. |
.addContainer(c -> c.circle(80).clipPolicy(ClipPolicy.OVERFLOW_VISIBLE) ... )A ShapeContainerNode is SHAPE_ATOMIC: the outline plus every layer
stays on one page. A container too tall to fit on a single page raises
AtomicNodeTooLargeException with the offending semantic name so the
caller can either shrink the outline or pick OVERFLOW_VISIBLE.
session.add(new ShapeContainerBuilder()
.name("OversizedCard")
.roundedRect(100, 600, 6) // taller than the inner page
.center(label)
.build());
session.layoutGraph(); // → AtomicNodeTooLargeException
// message contains "OversizedCard"Apache POI cannot express a graphics-state path clip, so the
DocxSemanticBackend renders the container's layers inline without the
outline frame and without clipping, and logs a one-time
docx.export.shape-container-fallback capability warning per export
pass. Authors who need the outline must export to PDF.
This is documented in canonical-legacy-parity.md under the "Surfaces and structure" section.
For CLIP_PATH / CLIP_BOUNDS, the canonical layout compiler emits this
fragment sequence on the shape's page:
- Outline fragment —
EllipseFragmentPayload(circle / ellipse) orShapeFragmentPayload(rectangle / rounded-rectangle, with corner radius). Rendered first so it sits behind the layers. - Clip-begin marker —
ShapeClipBeginPayloadcarrying the outline geometry and chosen policy. The PDF backend turns this intosaveGraphicsState() + add path + clip(). - Layer fragments — each child layer is compiled normally; its fragments land on a page surface that is already restricted to the outline.
- Clip-end marker —
ShapeClipEndPayloadwith the same owner path. The PDF backend issuesrestoreGraphicsState()so subsequent fragments draw without the clip.
ShapeContainerInvariantsTest pins the begin/end pair invariant; you
can rely on it across releases.
- ADR 0001 — Shape-as-container — design rationale.
ShapeContainerBuilder— full builder surface.ShapeOutline— sealed value type for the supported outline kinds.