This guide explains how to add new objects and engine extensions in GraphCompose without fighting the current architecture.
GraphCompose does not render directly from application calls.
The usual flow is:
- application code describes the document through canonical DSL/modules/nodes
- the canonical layout compiler prepares deterministic layout fragments
- internal engine components carry content, style, size, and parent/child relationships
- layout and pagination systems calculate size, placement, and page fragments
- rendering systems inspect resolved fragments/components and draw them
That means a new object usually needs the right answer in four areas:
- what canonical node or engine model it should introduce
- which components must be attached to the entity
- whether it participates in parent/child layout
- how it gets rendered
Entity is the ECS core object, not the preferred home for new layout helpers.
Use these ownership rules when adding or refactoring engine code:
- put geometry reads in
EntityBounds - put parent container size propagation and page-shift updates in
ParentContainerUpdater - keep render-order policy in rendering helpers such as
EntityRenderOrder - treat
Entity.bounding*andEntity.updateParentContainer*as deprecated compatibility wrappers
Rule of thumb:
- if the logic needs
Placement,ContentSize,Margin, or parent traversal semantics, it probably belongs in a helper or system utility - if the logic only needs identity, component access, or canonical child order, it may belong on
Entity
The old fluent entity builders are no longer production authoring API. They now
live under src/test/java/com/demcha/compose/testsupport/engine/assembly and
exist to keep low-level engine tests readable. Production features should start
from DocumentDsl, canonical nodes, node definitions, or internal engine model
types.
Use EmptyBox.java when the new object is a leaf entity or a small custom object that does not manage children itself.
Examples in the codebase:
- TextBuilder.java
- ImageBuilder.java
- CircleBuilder.java
- LineBuilder.java
- LinkBuilder.java
- ElementBuilder.java
This is the right choice for the exact case you asked about: an object that does not expand into a child-owning container and just needs base entity functionality plus layout/render participation.
What EmptyBox<T> gives you:
- entity creation
- auto-generated
EntityName - fluent
addComponent(...) - parent/child helpers
- access to
EntityManager - default
build()behavior through the builder hierarchy
This EntityManager access is for low-level engine tests and compatibility
harnesses only. Canonical application behavior should be described through
GraphCompose.document(...), DocumentSession, and DocumentDsl; session
features such as guideLines(true) are not routed through EntityManager.
Use ShapeBuilderBase.java when the object is still a leaf, but you want common shape helpers such as:
- fill color
- stroke
- corner radius
Examples:
Use ContainerBuilder.java when the new object owns child entities and participates in parent/child layout.
Examples:
Use this path when the object should call addChild(...) and arrange nested entities.
Special note for the low-level module test harness:
ModuleBuilderis a low-level test harness for full-width section behavior- it resolves to the full available width of its parent minus its own horizontal margin
- it should usually live under a normal root
vContainer(...)or a canonical semantic page flow such asDocumentSession.dsl().pageFlow() - nested horizontal/vertical composition should happen inside the module through regular containers
If the object should render something visible, the entity needs a renderable marker component.
Examples:
Those renderable components are render markers. Prefer keeping them backend-neutral and let renderer-owned handlers perform format-specific drawing.
There are three different ideas in the engine that are easy to mix up:
- render marker: tells the active renderer how to draw the entity
Expendable: tells the container expansion phase that the parent box may grow to fit childrenBreakable: tells the page breaker that the entity's own content may continue across pages
Use them for different reasons:
- add a render marker when the object is visible
- add
Expendableonly when the entity is a true parent-like box that should resize because of child content - add
Breakableonly when the entity itself can be split or continued during pagination
Examples:
Containeris bothExpendableandBreakablebecause it owns children and may span pagesBlockTextisBreakablebecause its content can flow across pagesImageComponentis neitherExpendablenorBreakable; it is a fixed leaf renderableCircleis a fixed leaf renderable likeImageComponent; it renders, but it should not be markedExpendableLineis also a fixed leaf renderable; it draws inside its resolved box and should stay non-breakable unless the engine gets a true multi-page line contract later
Important:
Expendableis not a pagination flagBreakableis not a child-sizing flag- if a long leaf object is not
Breakable, the engine treats it as a single block and moves it to the next page when needed
Leaf parity rule:
- if two objects are conceptually fixed leaf renderables, such as
ImageComponent,Circle, andLine, they should use the same layout contract - that usually means the same kind of
ContentSize, the same padding-aware inner draw area, and the same non-breakable pagination behavior - if one of them behaves differently in containers or multi-page flow, first check the render/layout contract before changing pagination rules
Attach the components that describe what the object is and how it should look.
Examples:
Text,TextStyleFillColor,Stroke,CornerRadiusImageData
The renderer reads those components later during the render pass.
If the engine needs to place the object, it usually needs a size signal.
The most common component is:
For simple fixed-size objects, set ContentSize directly in the builder.
For measured objects, compute size in build() before the entity is registered.
Example:
- TextBuilder.java calls
TextComponent.autoMeasureText(...)when auto-size is enabled.
The layout engine expects the usual layout metadata to be present when needed:
AnchorMarginPaddingAlignfor containersParentComponentfor child entities
You normally do not add Placement yourself. The layout system calculates placement later.
Use this when the object is visible, does not manage children, and should behave like text/image/rectangle.
Steps:
- create a canonical node and node definition, or an internal engine model/assembler if the feature is not public authoring
- in
initialize(), attach the render marker component - add canonical DSL methods that attach the data/style intent
- set or calculate
ContentSize - register the entity through
build() - add test-support builder helpers only when low-level engine tests need direct entity setup
Leaf rule of thumb:
- most leaf renderables should not implement
Expendable - only implement
Breakableif the leaf's content can really continue across pages
Use this when the object owns child entities and arranges them.
Steps:
- extend
ContainerBuilder<T> - add the render/container marker in
initialize() - ensure the container has the alignment / axis semantics it needs
- use
addChild(...)to wire child entities - provide
ContentSizeor logic that lets layout compute it correctly
Container rule of thumb:
- containers that must resize around children usually need
Expendable - containers whose content may continue on another page usually need
Breakable - some containers need both markers
- if the container models semantic section behavior, decide whether width should be inherited from the parent before letting child layout run
Some engine objects look like containers from the outside, but still need their own leaf rendering contract inside.
The test-support table harness is the current low-level example:
- the table root is a breakable vertical container
- each row is a non-breakable leaf entity with explicit
ContentSize - each row renders all of its cells through a dedicated row renderable instead of exposing each cell as a separate child entity
This pattern is useful when:
- the parent object should flow across pages
- one logical child block must stay atomic
- rendering needs sibling-aware behavior, such as page-break separators
Why the table uses this contract:
- rows must move to the next page as units
- column widths are negotiated once at the table level
- cell borders and page-break separators are easier to render consistently from a row-level payload
Relevant files:
Rule of thumb:
- make the root breakable only if the object as a whole can continue across pages
- keep logical row-like units as fixed leaves when the user would perceive splitting them as a bug
- if separators depend on where page fragments start or end, compute that in the render phase from resolved
Placement, not only from builder-time metadata
If the object is not directly rendered and mostly groups behavior, it may not need a new render component at all. In that case you may only need:
- a builder/helper method
- a composition pattern over existing entities
- or a template-layer helper in
com.demcha.compose.document.templates.*
Built-in templates now use a canonical compose-first contract on top of
DocumentSession.
Use this split:
- a public template interface in
com.demcha.compose.document.templates.apiexposescompose(DocumentSession, ...) - a canonical built-in class under
com.demcha.compose.document.templates.builtinsexposes stable ids, names, and theme defaults - a dedicated scene composer under a domain support package such as
com.demcha.compose.document.templates.support.cvorsupport.businessowns document composition throughTemplateComposeTarget - focused canonical tests and examples keep
compose(DocumentSession, ...)stable while the scene composer owns the reusable document structure
Practical rules:
- keep current
render(...): PDDocumentandrender(..., Path)overloads only as deprecated compatibility adapters - keep
GraphCompose.document(...), page size selection, margins,document.buildPdf(), anddocument.toPdfBytes()in canonical examples and integrations - let deprecated bridge adapters call into the canonical
DocumentSessionPDF path rather than owning production layout/render logic - put the actual document structure, sections, tables, and paragraph assembly in the scene composer
- keep scene composers backend-neutral: no
PDDocument,PDPage,PDRectangle, or low-level PDF composer imports - keep public examples and integration docs compose-first: show
compose(DocumentSession, ...)before any deprecated convenience path - pass theme or style collaborators into the scene composer constructor instead of hard-wiring backend assumptions into composition code
- when a built-in template already has a good scene split, extend that pattern instead of reintroducing backend-specific logic into the composition layer
Current guard rails:
CanonicalTemplateComposerPdfBoundaryTestkeeps scene-composition classes free ofPDDocument,PDPage,PDRectangle, and low-level PDF composer imports- canonical template API tests keep
compose(DocumentSession, ...)aligned with the built-ins
Rule of thumb:
- canonical
*TemplateV1built-ins should feel like reusable public templates *TemplateComposerplusTemplateComposeTargetshould feel like the reusable document composition core
The current PDF path works through renderable components and the PDF renderer system.
Preferred extension pattern for new backends:
- keep engine components as format-neutral render markers
- register a backend-specific
TextMeasurementSystem - register renderer-side handlers for marker types
- keep backend-only helper drawing in renderer-owned helper packages when the code is not an entity render marker
- keep renderer ordering policy in the rendering layer rather than in pagination utilities
Important files:
- Render.java
- RenderPassSession.java
- RenderStream.java
- PdfRenderingSystemECS.java
- PdfRenderSession.java
- EntityRenderOrder.java
Migration rule for new engine components:
- implement backend-neutral
Render, not backend-specific render interfaces - move PDF drawing into
...render.pdf.handlers - use
TextMeasurementSystemfor text width and line metrics instead of reaching throughLayoutSystem - place PDF-only helper objects in
...render.pdf.helpers - keep page-surface lifetime in a backend-specific
RenderPassSession, not in engine builders or render markers - keep resolved draw ordering in renderer-owned or renderer-neutral rendering helpers such as
EntityRenderOrder - register a render handler for every engine render marker because the PDF entity path no longer supports a backend-specific render fallback
The current render seam is deliberately narrower than a full backend abstraction. Use these rules when extending it:
RenderStream<T>should create one render-pass session, not one stream per entity- renderer orchestrators such as
PdfRenderingSystemECS.process(...)should open one session for the whole pass - single-page handlers should use the session-managed page surface directly
- multi-page handlers should request page surfaces explicitly per fragment or page
- page creation or annotation-only work should use the session's page-availability helper instead of opening a dummy drawing surface
- handlers must restore graphics state and text state before returning
- handlers must never close a session-owned surface
- backend-specific text lifecycle helpers belong in backend handlers or backend sessions, not in shared engine interfaces
The practical rule is:
- the builder creates the entity and attaches the right renderable component
- during rendering, GraphCompose checks
entity.hasRender() - if the render component supports the active renderer, the renderer executes its drawing logic
When a feature can affect resolved coordinates, layering, child order, or pagination, add or update a layout snapshot test in addition to any unit tests.
Recommended rule:
- unit tests prove the local math
- layout snapshot tests prove the composed document geometry
- PDF render tests prove the final backend output still looks sane
Prefer pairing a snapshot assertion with an existing render test when the document is complex or business-critical.
See layout-snapshot-testing.md for the baseline locations, update flow, and concrete examples.
So if your new object needs custom drawing, it is not enough to add a builder. You also need a renderable component with the correct renderer implementation.
The layout side uses entity components, not builder classes directly.
Important files:
- LayoutTraversalContext.java
- LayoutSystem.java
- ComputedPosition.java
- PageBreaker.java
- EntityBounds.java
- ParentContainerUpdater.java
In practice:
Anchor,Margin,Padding,ContentSize, and parent/child links are what matter to layout- the builder is just the place where you attach those components
LayoutTraversalContextshould build one deterministic hierarchy snapshot per pass instead of letting each subsystem rediscover roots and children independentlyParentComponentis the authoritative parent relation, whileEntity.childrenis the canonical sibling order- if those two sources disagree, traversal code should warn loudly and use a deterministic fallback rather than silently hiding the inconsistency
- during pagination, descendants should be resolved before parent containers so parent size updates caused by child page shifts are reflected before parent placement is finalized
Use the helpers directly when that intent is what you need:
- read bounds and edges through
EntityBoundsinstead of adding more bound helpers toEntity - update parent container size or shifted positions through
ParentContainerUpdaterinstead of growing theEntityAPI further
See pagination-ordering.md for a focused explanation of this rule, including why a Circle case can fail while an Image case appears to work.
If those components are missing or inconsistent, the renderer cannot save you later.
Add a method to ComponentBuilder.java when:
- a low-level engine test needs direct entity assembly
- the object is not ready or not appropriate for public canonical authoring
- you want it tracked alongside other pending test harness builders before render/layout execution
Do not add a method there if the new object is only an internal helper for templates.
- choose the correct builder base class
- add the render marker in
initialize()if the object is drawable - add
Expendableonly for parent-like boxes that should grow because of children - add
Breakableonly for entities whose own content can span pages - attach content/style components through fluent methods
- provide
ContentSizedirectly or calculate it inbuild() - attach
Anchor/Margin/Paddingas needed - use
addChild(...)only for true container objects - add a
DocumentDslmethod if this should be public API - add tests for layout and rendering behavior
- leaf text with measured size: TextBuilder.java
- shape-like object: RectangleBuilder.java
- fixed leaf line object: LineBuilder.java
- container: ModuleBuilder.java
- template-level composition helper: CvTemplateComposer.java
LayerStackNode is the canonical atomic overlay composite. Its layers
share the same bounding box and are painted in source order — first
layer behind, last layer in front. Use it whenever two or more nodes
need to sit at the same coordinates with explicit alignment instead of
stacking vertically.
- inside
pageFlowdirectly (root flow); - inside any
SectionNodebody; - inside a row column slot — the layout compiler treats stacks as atomic overlays, distinct from the still-forbidden nested horizontal rows or splittable tables.
- measurement: stack outer size =
max(child outer size)plus stack padding and margin. Smaller layers are aligned inside the resolved box; - pagination: stacks are atomic — they always move whole to the next page when they do not fit on the current page;
- alignment: each layer carries its own
LayerAlign(TOP_LEFT,CENTER,BOTTOM_RIGHT, etc.) that resolves inside the inner box obtained by subtracting stack padding from the outer box.
Use addLayerStack(Consumer<LayerStackBuilder>) on any flow builder
(page flow, section, container) to drop a stack inline. Place the
background as the first layer, then add foreground layers with the
desired alignment:
section.addLayerStack(stack -> stack
.name("MonogramBadge")
.back(new EllipseBuilder()
.name("MonogramRing")
.size(78, 78)
.stroke(DocumentStroke.of(MONOGRAM_RING, 1.25))
.build())
.layer(new ParagraphBuilder()
.name("MonogramInitials")
.text("M | H")
.textStyle(monogramStyle())
.align(TextAlign.CENTER)
.build(),
LayerAlign.CENTER));- monogram and initial badges,
- watermark stamps on certificates and invoices,
- image-with-caption hero blocks,
- status labels overlaid on cards,
- decorative seals, signatures, and embossed marks.
RowBuilder.add(...) accepts LayerStackNode directly when the stack
is constructed manually. Nested rows and tables remain forbidden inside
row slots — only stacks pass the atomic-overlay capability check.
- the PDF path is the supported renderer today
- Word-related classes exist in source, but they should be treated as experimental