From 2589a1ca450630cd6044e7a9cde8039391f31aa6 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 6 Jul 2026 17:49:44 +0100 Subject: [PATCH] refactor(engine): remove the dead legacy-ECS execution classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete the legacy Entity-Component-System execution layer the canonical pipeline no longer uses: the ECS layout/pagination systems (LayoutSystem, PaginationLayoutSystem, PageBreaker, PageLayoutCalculator, TextBlockProcessor, the container aligner/expander/layout-manager/module-width-resolver), the renderable render-marker components (Circle, Rectangle, Container, Row, Line, Link, Module, Element, HContainer, VContainer, TableRow, BarcodeComponent, ChunkedBlockText, ImageComponent, PageBreakComponent), EntityRenderOrder, RenderingSystemBase, the guide-render impls, and the value objects only they used — 53 classes plus 3 orphaned unit tests and 4 emptied packages. No public API, ServiceLoader, or reflection reaches them; the canonical pipeline (document.layout + document.backend.fixed.pdf) does its own layout, pagination, and rendering. The Entity/EntityManager model and the engine.debug snapshot types stay — they still back the canonical layout coordinate/geometry and the graph-compose-testing snapshot assertions. Repoint the contributor docs from the deleted classes to the surviving TextComponent/BlockText markers and the PdfFragmentRenderHandler surface, remove a dead logback appender, and trim the pagination package doc. --- CONTRIBUTING.md | 14 +- docs/architecture/overview.md | 10 +- docs/architecture/package-map.md | 6 +- docs/architecture/pagination-ordering.md | 16 +- docs/contributing/implementation-guide.md | 40 +- docs/operations/layout-snapshot-testing.md | 2 +- qa/src/test/resources/logback.xml | 7 - .../content/bookmark/BookmarkEntry.java | 36 -- .../components/content/link/DisplayText.java | 8 - .../engine/components/content/link/Email.java | 58 --- .../components/content/link/LinkUrl.java | 36 -- .../components/content/link/package-info.java | 7 - .../components/content/shape/BorderSides.java | 30 -- .../content/shape/CornerRadius.java | 6 - .../components/content/shape/FillColor.java | 25 - .../components/content/shape/LinePath.java | 35 -- .../content/table/TableLayoutData.java | 26 - .../content/table/TableResolvedCell.java | 2 +- .../content/table/TableRowData.java | 16 - .../content/text/BlockTextLineMetrics.java | 149 ------ .../compose/engine/components/core/Units.java | 36 -- .../components/geometry/Expendable.java | 18 - .../components/geometry/ModuleWidthSeed.java | 16 - .../engine/components/layout/Align.java | 75 --- .../engine/components/layout/StackAxis.java | 20 - .../engine/components/layout/Transform.java | 24 - .../renderable/BarcodeComponent.java | 21 - .../renderable/ChunkedBlockText.java | 17 - .../engine/components/renderable/Circle.java | 10 - .../components/renderable/Container.java | 20 - .../engine/components/renderable/Element.java | 10 - .../components/renderable/HContainer.java | 7 - .../components/renderable/ImageComponent.java | 12 - .../engine/components/renderable/Line.java | 10 - .../engine/components/renderable/Link.java | 6 - .../engine/components/renderable/Module.java | 4 - .../renderable/PageBreakComponent.java | 21 - .../components/renderable/Rectangle.java | 11 - .../engine/components/renderable/Row.java | 4 - .../components/renderable/TableRow.java | 13 - .../components/renderable/VContainer.java | 5 - .../demcha/compose/engine/core/CanvasBox.java | 41 -- .../exceptions/BigSizeElementException.java | 36 -- .../exceptions/IllegalAlignException.java | 7 - .../exceptions/TextComponentException.java | 7 - .../compose/engine/io/FileManagerSystem.java | 13 - .../compose/engine/layout/LayoutSystem.java | 474 ------------------ .../layout/container/ContainerAligner.java | 290 ----------- .../layout/container/ContainerExpander.java | 203 -------- .../container/ContainerLayoutManager.java | 136 ----- .../layout/container/ModuleWidthResolver.java | 81 --- .../engine/layout/container/package-info.java | 14 - .../compose/engine/layout/package-info.java | 17 - .../engine/pagination/PageBreaker.java | 383 -------------- .../pagination/PageLayoutCalculator.java | 270 ---------- .../pagination/PaginationLayoutSystem.java | 54 -- .../engine/pagination/TextBlockProcessor.java | 252 ---------- .../engine/pagination/YPositionOnPage.java | 8 - .../engine/pagination/package-info.java | 21 +- .../engine/render/EntityRenderOrder.java | 135 ----- .../engine/render/RenderingSystemBase.java | 113 ----- .../render/guides/impl/BoxRenderImpl.java | 71 --- .../render/guides/impl/MarginRenderImpl.java | 80 --- .../render/guides/impl/PaddingRenderImpl.java | 78 --- .../render/guides/impl/package-info.java | 7 - .../compose/engine/utils/TextSanitizer.java | 46 -- .../pagination/ContainerExpanderTest.java | 388 -------------- .../engine/pagination/PageBreakerTest.java | 179 ------- .../pagination/PageLayoutCalculatorTest.java | 288 ----------- src/test/resources/logback.xml | 7 - 70 files changed, 47 insertions(+), 4541 deletions(-) delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/bookmark/BookmarkEntry.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/link/DisplayText.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/link/Email.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/link/LinkUrl.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/link/package-info.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/shape/BorderSides.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/shape/CornerRadius.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/shape/FillColor.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/shape/LinePath.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/table/TableLayoutData.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/table/TableRowData.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/content/text/BlockTextLineMetrics.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/core/Units.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/geometry/Expendable.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/geometry/ModuleWidthSeed.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/layout/Align.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/layout/StackAxis.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/layout/Transform.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/BarcodeComponent.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/ChunkedBlockText.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Circle.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Container.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Element.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/HContainer.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/ImageComponent.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Line.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Link.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Module.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/PageBreakComponent.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Rectangle.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/Row.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/TableRow.java delete mode 100644 src/main/java/com/demcha/compose/engine/components/renderable/VContainer.java delete mode 100644 src/main/java/com/demcha/compose/engine/core/CanvasBox.java delete mode 100644 src/main/java/com/demcha/compose/engine/exceptions/BigSizeElementException.java delete mode 100644 src/main/java/com/demcha/compose/engine/exceptions/IllegalAlignException.java delete mode 100644 src/main/java/com/demcha/compose/engine/exceptions/TextComponentException.java delete mode 100644 src/main/java/com/demcha/compose/engine/io/FileManagerSystem.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/LayoutSystem.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/container/ContainerExpander.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/container/ContainerLayoutManager.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/container/ModuleWidthResolver.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/container/package-info.java delete mode 100644 src/main/java/com/demcha/compose/engine/layout/package-info.java delete mode 100644 src/main/java/com/demcha/compose/engine/pagination/PageBreaker.java delete mode 100644 src/main/java/com/demcha/compose/engine/pagination/PageLayoutCalculator.java delete mode 100644 src/main/java/com/demcha/compose/engine/pagination/PaginationLayoutSystem.java delete mode 100644 src/main/java/com/demcha/compose/engine/pagination/TextBlockProcessor.java delete mode 100644 src/main/java/com/demcha/compose/engine/pagination/YPositionOnPage.java delete mode 100644 src/main/java/com/demcha/compose/engine/render/EntityRenderOrder.java delete mode 100644 src/main/java/com/demcha/compose/engine/render/RenderingSystemBase.java delete mode 100644 src/main/java/com/demcha/compose/engine/render/guides/impl/BoxRenderImpl.java delete mode 100644 src/main/java/com/demcha/compose/engine/render/guides/impl/MarginRenderImpl.java delete mode 100644 src/main/java/com/demcha/compose/engine/render/guides/impl/PaddingRenderImpl.java delete mode 100644 src/main/java/com/demcha/compose/engine/render/guides/impl/package-info.java delete mode 100644 src/main/java/com/demcha/compose/engine/utils/TextSanitizer.java delete mode 100644 src/test/java/com/demcha/compose/engine/pagination/ContainerExpanderTest.java delete mode 100644 src/test/java/com/demcha/compose/engine/pagination/PageBreakerTest.java delete mode 100644 src/test/java/com/demcha/compose/engine/pagination/PageLayoutCalculatorTest.java diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c79afd8e7..fa8a550ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,8 +190,8 @@ not need any of them. `com.demcha.compose.document.backend.fixed.pdf`, not in `components/renderable`. - Builders and layout code get text width and line metrics from - `TextMeasurementSystem`, not from - `LayoutSystem -> RenderingSystem`, `PdfFont`, or PDFBox objects. + `TextMeasurementSystem`, not from the active renderer, `PdfFont`, + or PDFBox objects. - Keep `src/main/java/com/demcha/compose/engine/components/*` free of `org.apache.pdfbox` and `com.demcha.compose.engine.render.pdf` imports. @@ -207,8 +207,8 @@ Keep the entity core thin: page-shift propagation live in `ParentContainerUpdater`. - `Entity.bounding*` and `Entity.updateParentContainer*` are deprecated compatibility wrappers; do not copy them into new code. -- Render-order optimizations live in rendering helpers such as - `EntityRenderOrder`, not in `Entity`. +- Render-order optimizations live in the render layer (the PDF + fragment handlers), not in `Entity`. ### Guard rails @@ -311,11 +311,11 @@ marker, a new layout system, a new render-pass session): layout component plus a backend-neutral renderable marker plus a backend-owned render handler. - Marker rule of thumb: - - add `Expendable` only to parent-like boxes that should grow - because of child content + - add the container-growth marker only to parent-like boxes that + should grow because of child content - add `Breakable` only to entities whose own content may continue across pages - - do not treat `Expendable` as a pagination flag + - do not treat the container-growth marker as a pagination flag For text-heavy primitives, also read: diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 9cf9183b7..24813baf7 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -220,8 +220,8 @@ These rules apply to engine and backend contributors. Application code should not need any of them. - engine builders and layout helpers consume an engine-level - `TextMeasurementSystem` instead of reaching through `LayoutSystem` - into the active renderer + `TextMeasurementSystem` instead of reaching through the active + renderer - render marker components identify *what* needs to be rendered; *how* it is drawn lives in renderer-owned handler packages such as the `PdfFragmentRenderHandler` implementations under @@ -233,12 +233,8 @@ code should not need any of them. package imports - the PDF entity path dispatches through registered render handlers; there is no backend-specific render fallback path -- `EntityRenderOrder` is the shared render-order helper for resolved - entities. It precomputes lightweight sort entries per layer before - sorting so render ordering stays deterministic without repeated - component lookups inside the comparator hot path -Fixed leaf primitives (`Rectangle`, `Circle`, `Image`, `Line`) +Fixed leaf primitives (such as `TextComponent` and `BlockText`) follow the same engine contract: they materialize as regular entities with render/content/layout components, rely on normal `ContentSize` / `Padding` / `Margin` / `Placement`, and do not diff --git a/docs/architecture/package-map.md b/docs/architecture/package-map.md index 0b0cb0b43..24c651f15 100644 --- a/docs/architecture/package-map.md +++ b/docs/architecture/package-map.md @@ -64,11 +64,9 @@ per-package artifact. | --- | --- | --- | | `com.demcha.compose.engine.components.*` | Low-level ECS components, content payloads, style values, geometry, layout, and render markers. | Use only for engine primitives; public document authoring should go through `DocumentDsl` and semantic nodes. | | `com.demcha.compose.engine.core` | Entity manager, canvas, traversal context, and base ECS system contracts. | Keep core thin; put stage-specific logic in layout, pagination, measurement, or render packages. | -| `com.demcha.compose.engine.layout` | Low-level entity layout systems. | Preserve deterministic traversal and container sizing semantics. | -| `com.demcha.compose.engine.layout.container` | Container alignment, expansion, and module width helpers. | Helpers should operate on existing ECS state and avoid renderer dependencies. | -| `com.demcha.compose.engine.pagination` | Page-breaking helpers and pagination fallback systems. | Maintain child-first ordering and page-shift propagation rules. | +| `com.demcha.compose.engine.pagination` | Pagination markers and helpers (`Breakable`, `ParentContainerUpdater`, `Offset`). | Maintain child-first ordering and page-shift propagation rules. | | `com.demcha.compose.engine.measurement` | Text measurement contracts and font-backed implementations. | Builders/layout helpers depend on this seam instead of reaching into renderers. | -| `com.demcha.compose.engine.render` | Backend-neutral render contracts, handler registry, render ordering, and render-pass session lifetime. | Add backend-neutral contracts here, backend-specific drawing elsewhere. | +| `com.demcha.compose.engine.render` | Backend-neutral render contracts, handler registry, and render-pass session lifetime. | Add backend-neutral contracts here, backend-specific drawing elsewhere. | | `com.demcha.compose.engine.render.pdf` | Shared PDFBox primitives for the canonical fixed-layout backend: `PdfFont`, `GlyphFallbackLogger`, and the header/footer + watermark renderers under `helpers`. | Add canonical-shared PDF support here; per-fragment PDF drawing lives in the `PdfFragmentRenderHandler` implementations under `document.backend.fixed.pdf.handlers`. | | `com.demcha.compose.engine.render.word` | Experimental Word backend internals. | Treat as research until a supported public surface is designed. | | `com.demcha.compose.engine.text` | Internal text hot-path utilities shared by layout and render stages. | Keep helpers backend-neutral and avoid public authoring concepts here. | diff --git a/docs/architecture/pagination-ordering.md b/docs/architecture/pagination-ordering.md index 0afb393cf..ad794dfb5 100644 --- a/docs/architecture/pagination-ordering.md +++ b/docs/architecture/pagination-ordering.md @@ -20,26 +20,26 @@ If the parent container is processed before the child shift happens, the parent will still be based on the old value. In practice, this looks like a container guide or span box that does not extend to cover the final child position. -## The Circle vs Image confusion +## The leaf-type ordering confusion This issue can be easy to misdiagnose because it does not belong to the leaf renderable itself. -`Circle` and `ImageComponent` are both fixed leaf renderables: +Fixed leaf renderables such as `TextComponent` are all the same shape here: - they render a single box - they are not `Breakable` - when they do not fit, they are moved as a whole -So if a circle container fails to expand while an image container appears correct, the first thing +So if one leaf container fails to expand while another appears correct, the first thing to check is not the render implementation. The first thing to check is pagination order. -In one scenario, the image child may happen to be processed before its parent container, so the +In one scenario, one child may happen to be processed before its parent container, so the container placement is computed with the final child-driven size and the result looks correct. -In another scenario, a circle child may be processed after its parent container, which exposes the +In another scenario, a child may be processed after its parent container, which exposes the bug immediately. -The difference is therefore often accidental ordering, not a semantic difference between image and -circle rendering. +The difference is therefore often accidental ordering, not a semantic difference between the +leaf types. ## Correct rule @@ -77,7 +77,7 @@ If you see one of these behaviors, check pagination ordering first: When debugging a pagination tree: -1. log the `PageBreaker` processing order +1. log the page-breaker processing order 2. compare parent and child `ComputedPosition.y` 3. check whether a child shift updates parent `ContentSize` 4. verify that the parent `Placement` is created after that update, not before it diff --git a/docs/contributing/implementation-guide.md b/docs/contributing/implementation-guide.md index e54d741ce..797f50466 100644 --- a/docs/contributing/implementation-guide.md +++ b/docs/contributing/implementation-guide.md @@ -29,7 +29,7 @@ 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` +- keep render-order policy in the render layer (the `engine.render` contracts and the PDF fragment handlers) - treat `Entity.bounding*` and `Entity.updateParentContainer*` as deprecated compatibility wrappers Rule of thumb: @@ -46,8 +46,7 @@ If the object should render something visible, the entity needs a renderable mar Examples: - [TextComponent.java](../../src/main/java/com/demcha/compose/engine/components/renderable/TextComponent.java) -- [Rectangle.java](../../src/main/java/com/demcha/compose/engine/components/renderable/Rectangle.java) -- [ImageComponent.java](../../src/main/java/com/demcha/compose/engine/components/renderable/ImageComponent.java) +- [BlockText.java](../../src/main/java/com/demcha/compose/engine/components/renderable/BlockText.java) Those renderable components are render markers. Prefer keeping them backend-neutral and let renderer-owned handlers perform format-specific drawing. @@ -56,32 +55,30 @@ Those renderable components are render markers. Prefer keeping them backend-neut 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 children +- container-growth marker: tells the container expansion phase that the parent box may grow to fit children - `Breakable`: 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 `Expendable` only when the entity is a true parent-like box that should resize because of child content +- add the container-growth marker only when the entity is a true parent-like box that should resize because of child content - add `Breakable` only when the entity itself can be split or continued during pagination Examples: -- `Container` is both `Expendable` and `Breakable` because it owns children and may span pages +- a container marker is both parent-growable and `Breakable` because it owns children and may span pages - `BlockText` is `Breakable` because its content can flow across pages -- `ImageComponent` is neither `Expendable` nor `Breakable`; it is a fixed leaf renderable -- `Circle` is a fixed leaf renderable like `ImageComponent`; it renders, but it should not be marked `Expendable` -- `Line` is 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 +- `TextComponent` is a fixed leaf renderable; it renders a single resolved box and stays non-breakable Important: -- `Expendable` is not a pagination flag +- the container-growth marker is not a pagination flag - `Breakable` is 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`, and `Line`, they should use the same layout contract +- if two objects are conceptually fixed leaf renderables, 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 @@ -92,7 +89,7 @@ Attach the components that describe what the object is and how it should look. Examples: - `Text`, `TextStyle` -- `FillColor`, `Stroke`, `CornerRadius` +- `Stroke` - `ImageData` The renderer reads those components later during the render pass. @@ -116,7 +113,6 @@ The layout engine expects the usual layout metadata to be present when needed: - `Anchor` - `Margin` - `Padding` -- `Align` for containers - `ParentComponent` for child entities You normally do not add `Placement` yourself. The layout system calculates placement later. @@ -138,7 +134,7 @@ Steps: Leaf rule of thumb: -- most leaf renderables should not implement `Expendable` +- most leaf renderables should not grow to fit children - only implement `Breakable` if the leaf's content can really continue across pages ### Case 2: add a new container-like object @@ -155,7 +151,7 @@ Steps: Container rule of thumb: -- containers that must resize around children usually need `Expendable` +- containers that must resize around children usually need the container-growth marker - 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 @@ -184,7 +180,6 @@ Why the table uses this contract: Relevant files: -- [TableRow.java](../../src/main/java/com/demcha/compose/engine/components/renderable/TableRow.java) - [TableResolvedCell.java](../../src/main/java/com/demcha/compose/engine/components/content/table/TableResolvedCell.java) Rule of thumb: @@ -250,7 +245,7 @@ Preferred extension pattern for new backends: > `com.demcha.compose.document.backend.fixed.pdf`: `PdfFixedLayoutBackend` > dispatches each layout fragment to a `PdfFragmentRenderHandler` implementation > under `document.backend.fixed.pdf.handlers`. The backend-neutral `engine.render` -> *contracts* (`Render`, `RenderPassSession`, `RenderStream`, `EntityRenderOrder`) +> *contracts* (`Render`, `RenderPassSession`, `RenderStream`) > remain the shared render seam — extend PDF drawing by adding or updating a > fragment handler, not by touching those contracts. @@ -261,16 +256,15 @@ Important files: - [RenderStream.java](../../src/main/java/com/demcha/compose/engine/render/RenderStream.java) - [PdfFixedLayoutBackend.java](../../render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java) - [PdfFragmentRenderHandler.java](../../render-pdf/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFragmentRenderHandler.java) -- [EntityRenderOrder.java](../../src/main/java/com/demcha/compose/engine/render/EntityRenderOrder.java) Migration rule for new engine components: - implement backend-neutral `Render`, not backend-specific render interfaces - move PDF drawing into a `PdfFragmentRenderHandler` under `...document.backend.fixed.pdf.handlers` -- use `TextMeasurementSystem` for text width and line metrics instead of reaching through `LayoutSystem` +- use `TextMeasurementSystem` for text width and line metrics instead of reaching through the active renderer - place PDF-only helper objects alongside the backend in `...document.backend.fixed.pdf` - 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` +- keep resolved draw ordering in the render layer (the PDF fragment handlers), not in pagination utilities - register a render handler for every engine render marker because the PDF entity path no longer supports a backend-specific render fallback ### Render-pass session rules @@ -315,9 +309,7 @@ The layout side uses entity components, not builder classes directly. Important files: - [LayoutTraversalContext.java](../../src/main/java/com/demcha/compose/engine/core/LayoutTraversalContext.java) -- [LayoutSystem.java](../../src/main/java/com/demcha/compose/engine/layout/LayoutSystem.java) - [ComputedPosition.java](../../src/main/java/com/demcha/compose/engine/components/layout/coordinator/ComputedPosition.java) -- [PageBreaker.java](../../src/main/java/com/demcha/compose/engine/pagination/PageBreaker.java) - [EntityBounds.java](../../src/main/java/com/demcha/compose/engine/components/geometry/EntityBounds.java) - [ParentContainerUpdater.java](../../src/main/java/com/demcha/compose/engine/pagination/ParentContainerUpdater.java) @@ -335,7 +327,7 @@ Use the helpers directly when that intent is what you need: - read bounds and edges through `EntityBounds` instead of adding more bound helpers to `Entity` - update parent container size or shifted positions through `ParentContainerUpdater` instead of growing the `Entity` API further -See [pagination-ordering.md](../architecture/pagination-ordering.md) for a focused explanation of this rule, including why a `Circle` case can fail while an `Image` case appears to work. +See [pagination-ordering.md](../architecture/pagination-ordering.md) for a focused explanation of this rule, including why one leaf type can fail while another appears to work. If those components are missing or inconsistent, the renderer cannot save you later. @@ -343,7 +335,7 @@ If those components are missing or inconsistent, the renderer cannot save you la - choose the correct builder base class - add the render marker in `initialize()` if the object is drawable -- add `Expendable` only for parent-like boxes that should grow because of children +- add the container-growth marker only for parent-like boxes that should grow because of children - add `Breakable` only for entities whose own content can span pages - attach content/style components through fluent methods - provide `ContentSize` directly or calculate it in `build()` diff --git a/docs/operations/layout-snapshot-testing.md b/docs/operations/layout-snapshot-testing.md index 849253271..b69d2255d 100644 --- a/docs/operations/layout-snapshot-testing.md +++ b/docs/operations/layout-snapshot-testing.md @@ -34,7 +34,7 @@ Use them when you want to know that: That means the coordinates in the snapshot are: -- after `LayoutSystem` +- after layout resolution - after page-breaking decisions have been applied - before any PDFBox drawing happens diff --git a/qa/src/test/resources/logback.xml b/qa/src/test/resources/logback.xml index ea732eb0f..2adb12613 100644 --- a/qa/src/test/resources/logback.xml +++ b/qa/src/test/resources/logback.xml @@ -61,13 +61,6 @@ - - - - %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{36}) - %msg%n - - - diff --git a/src/main/java/com/demcha/compose/engine/components/content/bookmark/BookmarkEntry.java b/src/main/java/com/demcha/compose/engine/components/content/bookmark/BookmarkEntry.java deleted file mode 100644 index 4edc78056..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/bookmark/BookmarkEntry.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.demcha.compose.engine.components.content.bookmark; - -import com.demcha.compose.engine.components.core.Component; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; - -/** - * Content component that marks an entity as a PDF bookmark (outline) entry. - * - *

Attach this to a {@code Module} or any entity that has a resolved - * {@code Placement}. The renderer will create a {@code PDOutlineItem} pointing - * to the page and Y-coordinate of this entity.

- * - * @author Artem Demchyshyn - */ -@Getter -@ToString -@EqualsAndHashCode -public final class BookmarkEntry implements Component { - - /** Display title in the PDF bookmark panel. */ - private final String title; - - /** Nesting level (0 = root, 1 = child, 2 = grandchild, etc.). */ - private final int level; - - public BookmarkEntry(String title) { - this(title, 0); - } - - public BookmarkEntry(String title, int level) { - this.title = title; - this.level = Math.max(0, level); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/link/DisplayText.java b/src/main/java/com/demcha/compose/engine/components/content/link/DisplayText.java deleted file mode 100644 index 8c32a8fc6..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/link/DisplayText.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.demcha.compose.engine.components.content.link; - -import com.demcha.compose.engine.components.renderable.TextComponent; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -public class DisplayText extends TextComponent { -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/link/Email.java b/src/main/java/com/demcha/compose/engine/components/content/link/Email.java deleted file mode 100644 index c9bf99b8e..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/link/Email.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.demcha.compose.engine.components.content.link; - -import lombok.extern.slf4j.Slf4j; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - - -/** - * Represents an email link, extending the {@link LinkUrl} class. - * This class provides functionality to construct "mailto:" URLs with optional - * subject and body parameters, allowing for easy creation of clickable email links - * within entityManagers or applications. - */ -@Slf4j -public class Email extends LinkUrl { - - // Конструктор с полным набором параметров - public Email(String to, String subject, String body) { - super(buildMailto(to, subject, body)); - } - - // Конструктор только с "to" - public Email(String to) { - super(buildMailto(to, null, null)); - } - - private static String buildMailto(String to, String subject, String body) { - if (to == null || to.isBlank()) { - log.error("to is null or blank"); - to =""; - } - - StringBuilder sb = new StringBuilder("mailto:").append(to); - - // Добавляем query параметры только если они не null/blank - List params = new ArrayList<>(); - if (subject != null && !subject.isBlank()) { - params.add("subject=" + encode(subject));; - } - if (body != null && !body.isBlank()) { - params.add("body=" + encode(body));; - } - - if (!params.isEmpty()) { - sb.append("?").append(String.join("&", params)); - } - - return sb.toString(); - } - - private static String encode(String value) { - return URLEncoder.encode(value, StandardCharsets.UTF_8) - .replace("+", "%20"); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/link/LinkUrl.java b/src/main/java/com/demcha/compose/engine/components/content/link/LinkUrl.java deleted file mode 100644 index 6caf2a5a8..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/link/LinkUrl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.demcha.compose.engine.components.content.link; - -import com.demcha.compose.engine.components.core.Component; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.net.URI; -import java.net.URISyntaxException; - -@Slf4j -@Getter -@EqualsAndHashCode -public class LinkUrl implements Component { - private final String url; - private final boolean valid; - - public LinkUrl(String url) { - this.url = url; - this.valid = validateUrl(url); - } - - private boolean validateUrl(String url) { - if (url == null || url.isBlank()) { - log.warn("URL is null or blank"); - return false; - } - try { - URI uri = new URI(url); - return uri.getScheme() != null && uri.getHost() != null; - } catch (URISyntaxException e) { - log.warn("Invalid link url: '{}'", url); - return false; - } - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/link/package-info.java b/src/main/java/com/demcha/compose/engine/components/content/link/package-info.java deleted file mode 100644 index 7ec91e971..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/link/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Internal content components that carry backend-neutral payload data for renderable entities. - * - *

Ownership: Owned by the shared engine data model and consumed by measurement, layout, and render handlers.

- *

Extension rules: Extend by adding immutable payload components and pairing them with explicit render/measurement behavior.

- */ -package com.demcha.compose.engine.components.content.link; \ No newline at end of file diff --git a/src/main/java/com/demcha/compose/engine/components/content/shape/BorderSides.java b/src/main/java/com/demcha/compose/engine/components/content/shape/BorderSides.java deleted file mode 100644 index ffebf952f..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/shape/BorderSides.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.demcha.compose.engine.components.content.shape; - -import com.demcha.compose.engine.components.core.Component; - -import java.util.Arrays; -import java.util.EnumSet; -import java.util.Objects; -import java.util.Set; - -/** - * Selective border ownership for renderables that should only draw specific sides. - */ -public record BorderSides(Set sides) implements Component { - - public BorderSides { - Objects.requireNonNull(sides, "sides"); - sides = sides.isEmpty() ? Set.of() : Set.copyOf(sides); - } - - public static BorderSides of(Side... sides) { - if (sides == null || sides.length == 0) { - return new BorderSides(Set.of()); - } - return new BorderSides(EnumSet.copyOf(Arrays.asList(sides))); - } - - public static BorderSides all() { - return of(Side.ALL); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/shape/CornerRadius.java b/src/main/java/com/demcha/compose/engine/components/content/shape/CornerRadius.java deleted file mode 100644 index ff77d9b53..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/shape/CornerRadius.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.demcha.compose.engine.components.content.shape; - -import com.demcha.compose.engine.components.core.Component; - -public record CornerRadius(double radius) implements Component { -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/shape/FillColor.java b/src/main/java/com/demcha/compose/engine/components/content/shape/FillColor.java deleted file mode 100644 index 7fb0812c1..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/shape/FillColor.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.demcha.compose.engine.components.content.shape; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.style.ComponentColor; -import lombok.NonNull; - -import java.awt.*; - -public record FillColor(Color color) implements Component { - public static Color DEFAULT_FILL_COLOR = ComponentColor.MODULE_TITLE; - - public FillColor(@NonNull ComponentColor color) { - this(color.color()); - } - public static FillColor nonColor(){ - return new FillColor(Color.WHITE); - } - public static FillColor defaultColor(){ - return new FillColor(DEFAULT_FILL_COLOR); - } - - public FillColor() { - this(DEFAULT_FILL_COLOR); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/shape/LinePath.java b/src/main/java/com/demcha/compose/engine/components/content/shape/LinePath.java deleted file mode 100644 index 1db688d89..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/shape/LinePath.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.demcha.compose.engine.components.content.shape; - -import com.demcha.compose.engine.components.core.Component; - -public record LinePath(double startX, double startY, double endX, double endY) implements Component { - - public LinePath { - validateFinite("startX", startX); - validateFinite("startY", startY); - validateFinite("endX", endX); - validateFinite("endY", endY); - } - - public static LinePath horizontal() { - return new LinePath(0.0, 0.5, 1.0, 0.5); - } - - public static LinePath vertical() { - return new LinePath(0.5, 0.0, 0.5, 1.0); - } - - public static LinePath diagonalAscending() { - return new LinePath(0.0, 0.0, 1.0, 1.0); - } - - public static LinePath diagonalDescending() { - return new LinePath(0.0, 1.0, 1.0, 0.0); - } - - private static void validateFinite(String name, double value) { - if (Double.isNaN(value) || Double.isInfinite(value)) { - throw new IllegalArgumentException(name + " must be finite: " + value); - } - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/table/TableLayoutData.java b/src/main/java/com/demcha/compose/engine/components/content/table/TableLayoutData.java deleted file mode 100644 index 53133833a..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/table/TableLayoutData.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.demcha.compose.engine.components.content.table; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; - -import java.util.List; -import java.util.Objects; - -/** - * Resolved table layout metadata stored on the table root entity for tests and debugging. - */ -public record TableLayoutData( - List columnWidths, - double naturalWidth, - double finalWidth, - int rowCount, - int columnCount, - List rowEntities -) implements Component { - public TableLayoutData { - Objects.requireNonNull(columnWidths, "columnWidths"); - Objects.requireNonNull(rowEntities, "rowEntities"); - columnWidths = List.copyOf(columnWidths); - rowEntities = List.copyOf(rowEntities); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/table/TableResolvedCell.java b/src/main/java/com/demcha/compose/engine/components/content/table/TableResolvedCell.java index 1de16a0a5..2694b46a5 100644 --- a/src/main/java/com/demcha/compose/engine/components/content/table/TableResolvedCell.java +++ b/src/main/java/com/demcha/compose/engine/components/content/table/TableResolvedCell.java @@ -9,7 +9,7 @@ import java.util.Set; /** - * Fully materialized cell description rendered by {@link com.demcha.compose.engine.components.renderable.TableRow}. + * Fully materialized cell description consumed by the table row renderer. * *

{@code yOffset} shifts the cell's bottom edge relative to the row * fragment's bottom (in PDF coordinates where y grows up). Single-row diff --git a/src/main/java/com/demcha/compose/engine/components/content/table/TableRowData.java b/src/main/java/com/demcha/compose/engine/components/content/table/TableRowData.java deleted file mode 100644 index cf90d0e09..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/table/TableRowData.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.demcha.compose.engine.components.content.table; - -import com.demcha.compose.engine.components.core.Component; - -import java.util.List; -import java.util.Objects; - -/** - * Resolved row payload rendered atomically as a single leaf entity. - */ -public record TableRowData(List cells) implements Component { - public TableRowData { - Objects.requireNonNull(cells, "cells"); - cells = List.copyOf(cells); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/content/text/BlockTextLineMetrics.java b/src/main/java/com/demcha/compose/engine/components/content/text/BlockTextLineMetrics.java deleted file mode 100644 index 3db130ed9..000000000 --- a/src/main/java/com/demcha/compose/engine/components/content/text/BlockTextLineMetrics.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.demcha.compose.engine.components.content.text; - -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; - -import java.util.ArrayList; -import java.util.List; - -/** - * Resolves per-line vertical metrics for block text. - * - *

The helper prefers line-local cached metrics when they are already present - * and only falls back to the active {@link TextMeasurementSystem} for - * uncached/manual lines. That keeps layout and page-breaking logic backend- - * neutral while avoiding repeated measurement work during ordinary block-text - * processing.

- */ -public final class BlockTextLineMetrics { - - private BlockTextLineMetrics() { - } - - /** - * Resolves line metrics from a single {@link TextStyle} using the active - * measurement system. - * - * @param entityManager entity manager providing the measurement system - * @param style text style to measure, defaults to {@link TextStyle#DEFAULT_STYLE} - * @return resolved line metrics (ascent, descent, leading) - */ - public static TextMeasurementSystem.LineMetrics resolveStyleMetrics(EntityManager entityManager, TextStyle style) { - TextStyle safeStyle = style == null ? TextStyle.DEFAULT_STYLE : style; - return measurementSystem(entityManager).lineMetrics(safeStyle); - } - - /** - * Resolves line metrics for a single line, preferring cached values when present. - * - * @param entityManager entity manager providing the measurement system - * @param line line to resolve metrics for, may be {@code null} - * @param fallbackStyle style to fall back to when the line or its bodies lack style info - * @return resolved line metrics - */ - public static TextMeasurementSystem.LineMetrics resolveLineMetrics(EntityManager entityManager, - LineTextData line, - TextStyle fallbackStyle) { - if (line != null && line.hasCachedLineMetrics()) { - return line.lineMetrics(); - } - - if (line == null) { - return resolveStyleMetrics(entityManager, fallbackStyle); - } - - return resolveBodiesMetrics(entityManager, line.bodies(), fallbackStyle); - } - - /** - * Resolves mixed-style line metrics from a list of text bodies by taking the - * maximum ascent, descent, and leading across all non-null body styles. - * - *

This method always measures from scratch and does not use any cache. - * It is called by paragraph preparation and other text materialization - * paths before pagination/rendering consume the resolved line payload. - * at line-creation time to populate the per-line cache.

- * - * @param entityManager entity manager providing the measurement system - * @param bodies text bodies to measure - * @param fallbackStyle style to use when a body has no style - * @return resolved line metrics - */ - public static TextMeasurementSystem.LineMetrics resolveBodiesMetrics(EntityManager entityManager, - List bodies, - TextStyle fallbackStyle) { - if (bodies == null || bodies.isEmpty()) { - return resolveStyleMetrics(entityManager, fallbackStyle); - } - - TextMeasurementSystem measurementSystem = measurementSystem(entityManager); - double ascent = 0.0; - double descent = 0.0; - double leading = 0.0; - boolean hasMetrics = false; - - for (TextDataBody body : bodies) { - if (body == null) { - continue; - } - - TextStyle style = body.textStyle() == null ? fallbackStyle : body.textStyle(); - if (style == null) { - continue; - } - - TextMeasurementSystem.LineMetrics metrics = measurementSystem.lineMetrics(style); - ascent = Math.max(ascent, metrics.ascent()); - descent = Math.max(descent, metrics.descent()); - leading = Math.max(leading, metrics.leading()); - hasMetrics = true; - } - - if (!hasMetrics) { - return resolveStyleMetrics(entityManager, fallbackStyle); - } - - return new TextMeasurementSystem.LineMetrics(ascent, descent, leading); - } - - /** - * Batch-resolves line metrics for a list of lines. - * - * @param entityManager entity manager providing the measurement system - * @param lines lines to measure - * @param fallbackStyle style to fall back to for lines without cached metrics - * @return list of metrics in the same order as the input lines - */ - public static List resolveLineMetrics(EntityManager entityManager, - List lines, - TextStyle fallbackStyle) { - List result = new ArrayList<>(lines.size()); - for (LineTextData line : lines) { - result.add(resolveLineMetrics(entityManager, line, fallbackStyle)); - } - return result; - } - - /** - * Computes the vertical gap between two adjacent lines, taking into account - * outer gap contributions from both lines relative to the base metrics. - * - * @param previous metrics of the line above - * @param next metrics of the line below - * @param base baseline metrics (usually from the block's default style) - * @param spacing additional line spacing configured on the block - * @return total inter-line gap in layout units - */ - public static double interLineGap(TextMeasurementSystem.LineMetrics previous, - TextMeasurementSystem.LineMetrics next, - TextMeasurementSystem.LineMetrics base, - double spacing) { - return spacing + previous.outerGap(base) / 2.0 + next.outerGap(base) / 2.0; - } - - private static TextMeasurementSystem measurementSystem(EntityManager entityManager) { - return entityManager.getSystems() - .textMeasurement() - .orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to resolve block text metrics.")); - } -} diff --git a/src/main/java/com/demcha/compose/engine/components/core/Units.java b/src/main/java/com/demcha/compose/engine/components/core/Units.java deleted file mode 100644 index cd6e16ec3..000000000 --- a/src/main/java/com/demcha/compose/engine/components/core/Units.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.demcha.compose.engine.components.core; - -/** - * Represents supported measurement units for layout and rendering. - */ -public enum Units { - - /** - * Pixels — device-dependent units. - * OuterBoxSize changes with screen DPI. - */ - PX, - - /** - * Points — printing unit, 1 point = 1/72 inch. - * Common in PDF and typography. - */ - PT, - - /** - * Millimeters — metric unit. - * Often used in print layouts. - */ - MM, - - /** - * Centimeters — metric unit. - */ - CM, - - /** - * Inches — imperial unit. - */ - IN -} - diff --git a/src/main/java/com/demcha/compose/engine/components/geometry/Expendable.java b/src/main/java/com/demcha/compose/engine/components/geometry/Expendable.java deleted file mode 100644 index 7a1d8a517..000000000 --- a/src/main/java/com/demcha/compose/engine/components/geometry/Expendable.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - - -/** - * Marker for parent-like entities whose own {@code ContentSize} may grow to fit children. - * - *

This marker is consumed by the container expansion phase, not by the page breaker. - * It should be used for entities that own child entities and are expected to expand their - * box when children require more space.

- * - *

It does not mean that the entity itself can flow across pages. - * Pagination behavior is controlled separately by {@code Breakable}.

- * - *

The name is kept as-is for compatibility with the current codebase, but conceptually - * it means {@code expandable-by-children}.

- */ -public interface Expendable { -} diff --git a/src/main/java/com/demcha/compose/engine/components/geometry/ModuleWidthSeed.java b/src/main/java/com/demcha/compose/engine/components/geometry/ModuleWidthSeed.java deleted file mode 100644 index 798c4c3e4..000000000 --- a/src/main/java/com/demcha/compose/engine/components/geometry/ModuleWidthSeed.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.demcha.compose.engine.components.geometry; - -import com.demcha.compose.engine.components.core.Component; - -/** - * Root-level width hint for semantic modules. - *

- * Module layout resolves its final content width from the available width of the - * parent container whenever a parent exists. For root modules there is no parent - * inner box to inherit from, so builders may attach this seed to describe the - * available width that the module should occupy before its own horizontal margin - * is subtracted. - *

- */ -public record ModuleWidthSeed(double width) implements Component { -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/Align.java b/src/main/java/com/demcha/compose/engine/components/layout/Align.java deleted file mode 100644 index 4bb5fab37..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/Align.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import lombok.extern.slf4j.Slf4j; - -/** - * Container-oriented alignment metadata. - *

- * {@code Align} describes how children should be arranged inside a parent - * container and what spacing should separate siblings. It plays a role similar - * to {@link Anchor}, but at the container layout level rather than the per-entity - * absolute placement level. - *

- * - *

Builders usually attach this component to containers, and layout utilities - * later consume it when distributing children inside the parent's inner box.

- */ -@Slf4j -public record Align(HAnchor h, VAnchor v, double spacing) implements Component { - public static Align middle(double spacing){ - var v = VAnchor.MIDDLE; - var h = HAnchor.CENTER; - return new Align(h,v,spacing); - } - public static Align top(double spacing){ - var v = VAnchor.TOP; - var h = HAnchor.CENTER; - return new Align(h,v,spacing); - } - public static Align bottom(double spacing){ - var v = VAnchor.BOTTOM; - var h = HAnchor.CENTER; - return new Align(h,v,spacing); - } - public static Align defaultAlign(double spacing){ - log.info("Align defaultAlign"); - var v = VAnchor.DEFAULT; - var h = HAnchor.DEFAULT; - return new Align(h,v,spacing); - } - public static Align left(double spacing){ - var v = VAnchor.DEFAULT; - var h = HAnchor.LEFT; - return new Align(h,v,spacing); - } - public static Align right(double spacing){ - var v = VAnchor.DEFAULT; - var h = HAnchor.RIGHT; - return new Align(h,v,spacing); - } - - public static void alignHorizontally(Entity entity, double boundingWidth, Align align){ - var outer = OuterBoxSize.from(entity).orElseThrow(); - var position = entity.getComponent(Position.class).orElseThrow(); - - switch (align.h()) { - case LEFT -> { - entity.addComponent(new Position(0.0, position.y())); - } - case RIGHT -> { - double x = boundingWidth - outer.width(); - entity.addComponent(new Position(x, position.y())); - } - case CENTER -> { - double x = (boundingWidth - outer.width())/2; - entity.addComponent(new Position(x, position.y())); - } - - } - } -} - diff --git a/src/main/java/com/demcha/compose/engine/components/layout/StackAxis.java b/src/main/java/com/demcha/compose/engine/components/layout/StackAxis.java deleted file mode 100644 index 87169a028..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/StackAxis.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Component; - -/** - * Internal stack direction component used by container layout. - * - *

This belongs to the V2 engine layout model. Public authoring code should - * describe rows/modules through canonical document APIs instead of attaching - * this component directly.

- * - * @author Artem Demchyshyn - */ -public enum StackAxis implements Component { - VERTICAL, - HORIZONTAL, - REVERSE_VERTICAL, - REVERSE_HORIZONTAL, - DEFAULT -} diff --git a/src/main/java/com/demcha/compose/engine/components/layout/Transform.java b/src/main/java/com/demcha/compose/engine/components/layout/Transform.java deleted file mode 100644 index 60186450d..000000000 --- a/src/main/java/com/demcha/compose/engine/components/layout/Transform.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.demcha.compose.engine.components.layout; - -import com.demcha.compose.engine.components.core.Component; - -/** - * Represents an object that supports geometric transformations such as - * scaling and rotation. - * - *

Transformations affect the rendering or logical representation of - * an object, without necessarily changing its underlying data.

- * - *
    - *
  • Scale X / Scale Y – Horizontal and vertical scaling factors - * (1.0 means no scaling, values > 1 enlarge, values between 0 and 1 shrink).
  • - *
  • Rotation – Rotation of the object in degrees, measured - * clockwise, where 0° represents the original orientation.
  • - *
- */ -public record Transform(double scaleX, double scaleY, double rotationDeg) implements Component { - public Transform noScale() { - return new Transform(1.0, 1.0, 0); - } -} - diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/BarcodeComponent.java b/src/main/java/com/demcha/compose/engine/components/renderable/BarcodeComponent.java deleted file mode 100644 index 0a9eea70f..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/BarcodeComponent.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * Render marker for a barcode/QR-code leaf entity. - * - *

Like all render markers in the engine, this class describes intent - * without performing any drawing. The actual bitmap generation and PDF - * rendering is handled by {@code PdfBarcodeRenderHandler}.

- * - * @author Artem Demchyshyn - */ -@Slf4j -@EqualsAndHashCode -@NoArgsConstructor -public class BarcodeComponent implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/ChunkedBlockText.java b/src/main/java/com/demcha/compose/engine/components/renderable/ChunkedBlockText.java deleted file mode 100644 index 86156e4f2..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/ChunkedBlockText.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * Chunked block-text marker kept until the engine fully converges on the - * main block-text path. - */ -@Slf4j -@Builder -@EqualsAndHashCode(callSuper = true) -@NoArgsConstructor -public class ChunkedBlockText extends Container { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Circle.java b/src/main/java/com/demcha/compose/engine/components/renderable/Circle.java deleted file mode 100644 index 9538b7c97..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Circle.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@EqualsAndHashCode -public class Circle implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Container.java b/src/main/java/com/demcha/compose/engine/components/renderable/Container.java deleted file mode 100644 index 2bbf6a286..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Container.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.components.geometry.Expendable; -import com.demcha.compose.engine.render.Render; -import com.demcha.compose.engine.render.guides.GuidesRenderer; -import com.demcha.compose.engine.pagination.Breakable; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.EnumSet; - -/** - * Represents a basic container render marker. - */ -@Slf4j -@Getter -public class Container implements Render, Expendable, Breakable { - public static final EnumSet DEFAULT_GUIDES = - EnumSet.of(GuidesRenderer.Guide.MARGIN, GuidesRenderer.Guide.PADDING, GuidesRenderer.Guide.BOX); -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Element.java b/src/main/java/com/demcha/compose/engine/components/renderable/Element.java deleted file mode 100644 index 6a3311dbd..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Element.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.components.geometry.Expendable; -import com.demcha.compose.engine.render.Render; - -/** - * This is simply an empty element that can hold anything (image, text, etc.) as a single instance. - */ -public class Element implements Render, Expendable { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/HContainer.java b/src/main/java/com/demcha/compose/engine/components/renderable/HContainer.java deleted file mode 100644 index 6234cda1e..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/HContainer.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import lombok.ToString; - -@ToString -public class HContainer extends Container { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/ImageComponent.java b/src/main/java/com/demcha/compose/engine/components/renderable/ImageComponent.java deleted file mode 100644 index 7a5ae9996..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/ImageComponent.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@EqualsAndHashCode -@NoArgsConstructor -public class ImageComponent implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Line.java b/src/main/java/com/demcha/compose/engine/components/renderable/Line.java deleted file mode 100644 index b9b915e78..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Line.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@EqualsAndHashCode -public class Line implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Link.java b/src/main/java/com/demcha/compose/engine/components/renderable/Link.java deleted file mode 100644 index 2ec4a6be1..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Link.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; - -public class Link implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Module.java b/src/main/java/com/demcha/compose/engine/components/renderable/Module.java deleted file mode 100644 index d1c2abd64..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Module.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -public class Module extends Container{ -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/PageBreakComponent.java b/src/main/java/com/demcha/compose/engine/components/renderable/PageBreakComponent.java deleted file mode 100644 index 33f581fac..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/PageBreakComponent.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * Render marker for a forced page break. - * - *

A page break entity forces the pagination system to start a new page - * at the point where this entity appears in the layout flow. It has zero - * visual content but signals the page breaker to advance.

- * - * @author Artem Demchyshyn - */ -@Slf4j -@EqualsAndHashCode -@NoArgsConstructor -public class PageBreakComponent implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Rectangle.java b/src/main/java/com/demcha/compose/engine/components/renderable/Rectangle.java deleted file mode 100644 index f50e8cc97..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Rectangle.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.components.geometry.Expendable; -import com.demcha.compose.engine.render.Render; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@EqualsAndHashCode -public class Rectangle implements Render, Expendable { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/Row.java b/src/main/java/com/demcha/compose/engine/components/renderable/Row.java deleted file mode 100644 index 64c53ad9f..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/Row.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -public class Row extends HContainer{ -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/TableRow.java b/src/main/java/com/demcha/compose/engine/components/renderable/TableRow.java deleted file mode 100644 index cf7dc71dd..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/TableRow.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -import com.demcha.compose.engine.render.Render; - -/** - * Render marker for atomic table rows. - *

- * Backend-specific row drawing lives in renderer-side handlers so table layout - * stays reusable across output formats. - *

- */ -public class TableRow implements Render { -} diff --git a/src/main/java/com/demcha/compose/engine/components/renderable/VContainer.java b/src/main/java/com/demcha/compose/engine/components/renderable/VContainer.java deleted file mode 100644 index 65573d246..000000000 --- a/src/main/java/com/demcha/compose/engine/components/renderable/VContainer.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.demcha.compose.engine.components.renderable; - -public class VContainer extends Container { - -} diff --git a/src/main/java/com/demcha/compose/engine/core/CanvasBox.java b/src/main/java/com/demcha/compose/engine/core/CanvasBox.java deleted file mode 100644 index 5e107f9cd..000000000 --- a/src/main/java/com/demcha/compose/engine/core/CanvasBox.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.demcha.compose.engine.core; - -import com.demcha.compose.engine.components.style.Margin; -import lombok.Getter; -import lombok.experimental.Accessors; - -@Getter -@Accessors(fluent = true) -public final class CanvasBox implements Canvas { - private final float width; - private final float height; - private final float x; - private final float y; - private Margin margin; - - public CanvasBox(float width, float height, float x, float y, Margin margin) { - this.width = width; - this.height = height; - this.x = x; - this.y = y; - this.margin = margin; - } - - public CanvasBox(float width, float height, float x, float y) { - this(width, height, x, y, null); - } - - @Override - public Margin margin() { - if (this.margin == null) { - return Margin.zero(); - } - return margin; - } - - @Override - public void addMargin(Margin margin) { - this.margin = margin; - } -} - diff --git a/src/main/java/com/demcha/compose/engine/exceptions/BigSizeElementException.java b/src/main/java/com/demcha/compose/engine/exceptions/BigSizeElementException.java deleted file mode 100644 index 8507ba7cc..000000000 --- a/src/main/java/com/demcha/compose/engine/exceptions/BigSizeElementException.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.demcha.compose.engine.exceptions; - - -public class BigSizeElementException extends Throwable { - - // 💡 Best Practice: Use a constant for the template to avoid recreation - private static final String ERROR_TEMPLATE = "Element is too large. Current position on the page: %s, Element height: %s, Available space: %s"; - - // Standard constructor - public BigSizeElementException(String message) { - super(message); - } - public BigSizeElementException(Throwable e) { - super(e); - } - - // Constructor for chaining exceptions - public BigSizeElementException(String message, Throwable cause) { - super(message, cause); - } - - /** - * 🚀 Specialized Constructor - * Automatically formats the message using the context values. - * - * @param currentY The current Y position on the page - * @param elementHeight The height of the element trying to fit - * @param availableSpace The remaining space on the page - */ - public BigSizeElementException(double currentY, double elementHeight, double availableSpace) { - super(String.format(ERROR_TEMPLATE, currentY, elementHeight, availableSpace)); - } - public BigSizeElementException(double currentY, double elementHeight, double availableSpace,Object args) { - super(String.format(ERROR_TEMPLATE, currentY, elementHeight, availableSpace) + args.toString()); - } -} diff --git a/src/main/java/com/demcha/compose/engine/exceptions/IllegalAlignException.java b/src/main/java/com/demcha/compose/engine/exceptions/IllegalAlignException.java deleted file mode 100644 index 7ae121d71..000000000 --- a/src/main/java/com/demcha/compose/engine/exceptions/IllegalAlignException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.demcha.compose.engine.exceptions; - -public class IllegalAlignException extends RuntimeException { - public IllegalAlignException(String entityShouldHaveAlignComponent) { - super(entityShouldHaveAlignComponent); - } -} diff --git a/src/main/java/com/demcha/compose/engine/exceptions/TextComponentException.java b/src/main/java/com/demcha/compose/engine/exceptions/TextComponentException.java deleted file mode 100644 index c3f0ccf84..000000000 --- a/src/main/java/com/demcha/compose/engine/exceptions/TextComponentException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.demcha.compose.engine.exceptions; - -public class TextComponentException extends RuntimeException { - public TextComponentException(String message) { - super(message); - } -} diff --git a/src/main/java/com/demcha/compose/engine/io/FileManagerSystem.java b/src/main/java/com/demcha/compose/engine/io/FileManagerSystem.java deleted file mode 100644 index c118a4421..000000000 --- a/src/main/java/com/demcha/compose/engine/io/FileManagerSystem.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.demcha.compose.engine.io; - -import com.demcha.compose.engine.core.SystemECS; - -import java.io.IOException; -import java.nio.file.Path; - -public interface FileManagerSystem extends SystemECS { - - Path outPath(); - - boolean saveAtomic() throws IOException; -} diff --git a/src/main/java/com/demcha/compose/engine/layout/LayoutSystem.java b/src/main/java/com/demcha/compose/engine/layout/LayoutSystem.java deleted file mode 100644 index e1fd80599..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/LayoutSystem.java +++ /dev/null @@ -1,474 +0,0 @@ -package com.demcha.compose.engine.layout; - -import com.demcha.compose.engine.components.content.text.LineTextData; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.core.EntityName; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.Layer; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.PaddingCoordinate; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.components.renderable.BlockText; -import com.demcha.compose.engine.components.renderable.TextComponent; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.core.LayoutTraversalContext; -import com.demcha.compose.engine.exceptions.BigSizeElementException; -import com.demcha.compose.engine.render.RenderingSystemECS; -import com.demcha.compose.engine.core.SystemECS; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; -import com.demcha.compose.engine.layout.container.ContainerExpander; -import com.demcha.compose.engine.layout.container.ContainerLayoutManager; -import com.demcha.compose.engine.layout.container.ModuleWidthResolver; -import com.demcha.compose.engine.pagination.PageBreaker; -import com.demcha.compose.engine.pagination.PaginationLayoutSystem; -import com.demcha.compose.engine.pagination.TextBlockProcessor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.*; -import java.util.concurrent.TimeUnit; - -/** - * Main layout pass for GraphCompose entities. - *

- * This system turns builder-produced entities into resolved geometry. It walks - * the entity hierarchy, computes container relationships, expands parent boxes - * when needed, resolves per-entity positions, and finally delegates to the - * pagination stage so entities receive final {@code Placement} metadata. - *

- * - *

Conceptually this is the "builder intent to geometry" phase of the engine.

- * - *

Box model summary:

- *
    - *
  • {@code ContentSize}: declared content dimensions
  • - *
  • {@code InnerBoxSize}: available child area after padding
  • - *
  • {@code OuterBoxSize}: content plus padding and margin
  • - *
  • {@code Placement}: final bounding box and page span
  • - *
- */ -@Slf4j -@RequiredArgsConstructor -public class LayoutSystem> implements SystemECS { - private static final Logger LIFECYCLE_LOG = LoggerFactory.getLogger("com.demcha.compose.engine.layout"); - - private final Canvas canvas; - @Getter - private final T renderingSystem; - private TextBlockProcessor textBlockProcessor; - - /** - * Resolves an entity position from its parent context, walking the ancestor - * chain when intermediate computed positions are still missing. - * - * @param childEntity the entity to position - * @param parentEntity the parent entity, or {@code null} for a root entity - * @param entityManager entity registry providing parent lookups - * @return the resolved position when computation succeeds - */ - - private Optional calculatePositionFromParent(Entity childEntity, - Entity parentEntity, - EntityManager entityManager) { - log.debug("Starting calculation of computed position for {} from parentEntity {}", childEntity, parentEntity); - - // 0) Handle page-level (no parent) - if (parentEntity == null) { - InnerBoxSize pageArea = new InnerBoxSize(this.canvas.width(), this.canvas.height()); - - PaddingCoordinate paddingPercentCoordinate = new PaddingCoordinate(this.canvas.x(), this.canvas.y()); - ComputedPosition local = positionWithAnchor(childEntity, pageArea, paddingPercentCoordinate); - log.debug("Final computed absolute position (page-level): {}", local); - return Optional.of(local); - } - - // 1) Build ancestor chain: [root ... parent] - List chain = new ArrayList<>(); - Set seen = new HashSet<>(); - Entity cur = parentEntity; - - while (cur != null) { - if (!seen.add(cur.getUuid())) { - log.error("Cycle detected in ParentComponent chain at entity {}", cur); - throw new IllegalStateException("Cycle detected in parent chain"); - } - chain.add(cur); - - var parentCompOpt = cur.getComponent(ParentComponent.class); - if (parentCompOpt.isEmpty()) { - break; // cur is root - } - UUID gpId = parentCompOpt.get().uuid(); - cur = entityManager.getEntity(gpId).orElse(null); - } - - int depth = chain.size(); // глубина - - // 2) Ensure each ancestor has InnerBoxSize and ComputedPosition - for (int i = 0; i < depth; i++) { - Entity e = chain.get(i); // root -> ... -> parent - - var inner = InnerBoxSize.from(e).orElseThrow(); - - if (e.getComponent(ComputedPosition.class).isEmpty()) { - var parentComp = e.getComponent(ParentComponent.class); - if (parentComp.isPresent()) { - Entity p = entityManager.getEntity(parentComp.get().uuid()) - .orElseThrow(() -> new IllegalStateException("Parent not found for " + e)); - - var parentInner = InnerBoxSize.from(p).orElseThrow(); - var pagingCoordinate = PaddingCoordinate.from(parentEntity); - ComputedPosition local = positionWithAnchor(e, parentInner, pagingCoordinate); - - ComputedPosition parentAbs = p.getComponent(ComputedPosition.class) - .orElse(new ComputedPosition(0, 0)); - - e.addComponent(new ComputedPosition(local.x() + parentAbs.x(), - local.y() + parentAbs.y())); - } else { - InnerBoxSize refArea = new InnerBoxSize(this.canvas.width(), this.canvas.height()); - var pagingCoordinate = PaddingCoordinate.from(parentEntity); - ComputedPosition local = positionWithAnchor(e, refArea, pagingCoordinate); - e.addComponent(local); - } - } - } - - // 3) Compute child's final position - InnerBoxSize parentInner = InnerBoxSize.from(parentEntity).orElseThrow(); - var pagingCoordinate = PaddingCoordinate.from(parentEntity); - - ComputedPosition childLocal = positionWithAnchor(childEntity, parentInner, pagingCoordinate); - ComputedPosition parentAbs = parentEntity.getComponent(ComputedPosition.class) - .orElse(new ComputedPosition(0, 0)); - - ComputedPosition childAbs = new ComputedPosition( - childLocal.x() + parentAbs.x(), - childLocal.y() + parentAbs.y() - ); - - // Final summary log - log.debug("Position calculation summary: nodesTraversed={}, depth={}, finalPosition={}", - chain.size(), depth, childAbs); - - return Optional.of(childAbs); - } - - /** - * Computes a child's anchored position inside the supplied parent area and - * stores the result back on the entity. - * - * @param child entity being positioned - * @param parentInnerBoxSize available parent area - * @param paddingCoordinate parent padding origin - * @return the computed position, also attached to the entity - */ - - private ComputedPosition positionWithAnchor(Entity child, InnerBoxSize parentInnerBoxSize, PaddingCoordinate paddingCoordinate) { - - var computed = ComputedPosition.from(child, parentInnerBoxSize, paddingCoordinate); - child.addComponent(computed); - log.debug("Computed position with Anchor has been created: {}", computed); - log.debug("{} has been created in: {}", computed, child); - return computed; - } - - - @Override - /** - * Runs the full layout pass for the current entity graph. - * - *

- * The pass resets traversal metadata, materializes one canonical hierarchy - * snapshot, resolves module/container sizing helpers, computes per-entity - * positions and depth/layer metadata, and finally delegates to pagination so - * entities receive final {@code Placement} components. - *

- * - * @param entityManager entity registry for the current document - */ - public void process(EntityManager entityManager) { - long startNanos = System.nanoTime(); - LIFECYCLE_LOG.debug("layout.system.start entities={}", entityManager.getEntities().size()); - log.info("LayoutSystem: processing..."); - textBlockProcessor = new TextBlockProcessor(entityManager); - LayoutTraversalContext.resetTraversalState(entityManager); - - final var entities = entityManager.getEntities(); - if (entities == null || entities.isEmpty()) { - log.info("LayoutSystem: no entities to lay out"); - LIFECYCLE_LOG.debug("layout.system.end entities=0 durationMs={}", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - return; - } - - // Build one canonical hierarchy snapshot for the whole pass so layout, - // pagination, and future backends all reason over the same sibling order. - final LayoutTraversalContext traversalContext = LayoutTraversalContext.from(entityManager); - final Map parentById = traversalContext.parentById(); - final Map> childrenByParent = traversalContext.childrenByParent(); -//TODO currently en testing stage - ModuleWidthResolver.process(entityManager, canvas); - ContainerLayoutManager.process(childrenByParent, entityManager); - - // 3) Expand parent boxes if needed (any child larger than parent) - ContainerExpander.process(childrenByParent, entityManager); - - // 4) Compute roots - final Set roots = traversalContext.roots(); - - // 5) DFS with cycle detection - final Map visit = new HashMap<>(entities.size()); - entities.keySet().forEach(id -> visit.put(id, Visit.UNSEEN)); - - final var layers = entityManager.getLayers(); // layer → ordered ids (assumed provided) - final var depthById = entityManager.getDepthById(); - - for (UUID root : roots) { - dfsLayout( - root, - null, - entities, - childrenByParent, - visit, - layers, - depthById, - 1 - , entityManager - ); - } - - log.info("LayoutSystem: layout complete (nodes: {})", entities.size()); - -// Pagination - - boolean withPagination = true; - - if (withPagination) { - var pageBreaker = new PageBreaker(entityManager); - pageBreaker.process(entities, renderingSystem.canvas(), traversalContext, depthById); - } else { - PaginationLayoutSystem paginationLayoutSystem = new PaginationLayoutSystem(); - paginationLayoutSystem.process(entityManager); - } - - LIFECYCLE_LOG.debug( - "layout.system.end entities={} roots={} layers={} durationMs={}", - entities.size(), - roots.size(), - layers.size(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - /** - * Performs the recursive layout traversal for one hierarchy branch. - * - *

Parents are processed before children so child layout can depend on - * parent inner size, padding coordinates, and container ordering metadata.

- */ - - private void dfsLayout( - UUID id, - UUID parentId, - Map entities, - Map> childrenByParent, - Map visit, - Map> layers, // NEW: layer → ordered ids - Map depthById, // NEW: id → depth - int depth, // NEW: current depth - EntityManager entityManager - - ) { - Visit st = visit.getOrDefault(id, Visit.UNSEEN); - if (st == Visit.DONE) return; - if (st == Visit.ACTIVE) throw new IllegalStateException("LayoutSystem: cycle detected at entity " + id); - visit.put(id, Visit.ACTIVE); - - Entity childEntity = entities.get(id); - if (childEntity == null) { - log.warn("LayoutSystem: entity {} not found — skipping", id); - visit.put(id, Visit.DONE); - return; - } - - // --- LAYERS COLLECTION --- - layers.computeIfAbsent(depth, k -> new ArrayList<>()).add(id); - depthById.put(id, depth); - - // --- POSITION CALC --- - ComputedPosition computedPosition; - if (parentId != null) { - Entity parent = entities.get(parentId); - if (parent != null) { - computedPosition = ComputedPosition.from(childEntity, parent); - } else { - log.warn("LayoutSystem: parent {} of {} not found — using root positioning (Position + Margin)", parentId, id); - computedPosition = ComputedPosition.from(childEntity, this.canvas); - } - } else { - computedPosition = ComputedPosition.from(childEntity, canvas); - } - - // IMPORTANT: store the computed position, not (0,0) - childEntity.addComponent(computedPosition); - childEntity.addComponent(new Layer(depth)); - - if (childEntity.has(Align.class)) { - alignRearrangeBlockText(childEntity, entityManager); - } - - if (log.isDebugEnabled()) { - String name = childEntity.getComponent(EntityName.class) - .map(EntityName::value) - .orElse(id.toString()); - log.debug("LayoutSystem: {} [depth={}] positioned at ({}, {})", - name, depth, computedPosition.x(), computedPosition.y()); - } - if (BlockText.class.isAssignableFrom(childEntity.getRender().getClass())) { - try { - textBlockProcessor.processLayoutSystemTextLines(childEntity); - } catch (IOException e) { - log.error("Error during processing block text entity {}", childEntity); - throw new RuntimeException(String.format("Error during processing block text entity %s", childEntity), e); - } catch (BigSizeElementException e) { - log.error(String.format("To big size line in block text, Error during processing block text entity %s", childEntity), e); - - throw new RuntimeException(e); - } - } - - // DFS to children with depth+1 - for (UUID childId : childrenByParent.getOrDefault(id, Collections.emptyList())) { - dfsLayout(childId, id, entities, childrenByParent, visit, layers, depthById, depth + 1, entityManager); - } - - visit.put(id, Visit.DONE); - } - - /** - * Applies horizontal alignment offsets to block-text line fragments inside the - * entity's own content box. - * - *

- * This is a post-measurement adjustment step. It does not remeasure the full - * block unless a line payload has no cached width information. - *

- * - * @param blockTextBox block-text entity to realign - * @param entityManager entity registry used to resolve the text measurement - * system on demand - * @return the same entity instance after line coordinates are updated - */ - private Entity alignBlockText(Entity blockTextBox, EntityManager entityManager) { - Align align = blockTextBox - .getComponent(Align.class).orElse(Align.defaultAlign(2)); - var component = blockTextBox.getComponent(BlockTextData.class).orElseThrow(); - var size = blockTextBox.getComponent(ContentSize.class).orElseThrow(); - var padding = blockTextBox.getComponent(Padding.class).orElse(Padding.zero()); - TextStyle style = blockTextBox.getComponent(TextStyle.class).orElseThrow(); - TextMeasurementSystem measurementSystem = null; - - var lines = component.lines(); - for (LineTextData line : lines) { - // Ordinary block-text lines already carry builder-time width caches. - // The fallback is only for manually assembled/un-cached payloads. - double lineWidth = line.lineWidth(); - if (Double.isNaN(lineWidth)) { - if (measurementSystem == null) { - measurementSystem = entityManager.getSystems() - .textMeasurement() - .orElseThrow(() -> new IllegalStateException("TextMeasurementSystem is required to align block text.")); - } - lineWidth = line.width(measurementSystem, style); - } - switch (align.h()) { - case LEFT -> { - double x = line.x() + padding.left(); - line.x(x); - } - case RIGHT -> { - double x = size.width() - lineWidth - padding.right(); - line.x(x); - } - case CENTER -> { - double x = (size.width() - lineWidth + padding.left()) / 2; - line.x(x); - } - - } - - } - - - return blockTextBox; - } - - /** - * Aligns block-text payloads when the entity actually carries block-text line - * data. - * - * @param entity entity to inspect - * @param entityManager entity registry used to resolve layout helpers - * @return the aligned entity wrapped in {@link Optional}, or empty when the - * entity is not a block-text payload holder - */ - public Optional alignRearrangeBlockText(Entity entity, EntityManager entityManager) { - - if (entity.has(BlockTextData.class)) { - return Optional.of(alignBlockText(entity, entityManager)); - } else { - log.debug("Entity is not a BlockTextData"); - return Optional.empty(); - } - } - - - /** - * Returns whether the given entity must NOT be expanded. - * - *

Rules: - *

    - *
  • Has {@code TextComponent} → return {@code true} (do not expand)
  • - *
  • Has {@code Box} → return {@code false} (allow expansion)
  • - *
  • Otherwise → return {@code true} (do not expand)
  • - *
- * - * @param entity the entity to check - * @return {@code true} to skip expansion; {@code false} to allow it - */ - - @Deprecated - private boolean isExpandable(Entity entity) { - if (entity.has(TextComponent.class)) { - // Forbidden expend text Entitie - return false; - } - - return true; - } - - - @Override - public String toString() { - return "LayoutSystem"; - } - - /** - * DFS visit states used during one layout traversal. - */ - public enum Visit {UNSEEN, ACTIVE, DONE;} -} - - diff --git a/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java b/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java deleted file mode 100644 index cbd77e558..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java +++ /dev/null @@ -1,290 +0,0 @@ -package com.demcha.compose.engine.layout.container; - -import com.demcha.compose.engine.components.layout.StackAxis; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.Anchor; -import com.demcha.compose.engine.components.layout.HAnchor; -import com.demcha.compose.engine.components.layout.VAnchor; -import com.demcha.compose.engine.components.layout.coordinator.Position; -import com.demcha.compose.engine.components.renderable.Container; -import com.demcha.compose.engine.components.renderable.Module; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - *

ContainerAligner using the Strategy Pattern

- * - *

This class is responsible for aligning child entities within a parent container. - * It has been refactored from a procedural switch-based approach to use the - * Strategy design pattern. This decouples the main alignment logic from the - * specific algorithms for different layout directions (e.g., horizontal, vertical).

- * - *

Design Benefits:

- *
    - *
  • Open/Closed Principle: New layout types can be added by creating a new - * strategy class without modifying this core aligner class.
  • - *
  • Single Responsibility Principle: Each strategy class has one job: to - * implement a specific layout algorithm. The {@code ContainerAligner} class's only - * job is to select the correct strategy.
  • - *
  • Reduced Complexity: Eliminates large switch statements, making the code - * cleaner and easier to read.
  • - *
- * - * @see LayoutStrategy - */ -@Slf4j -public class ContainerAligner { - private static final double EPS = 1e-6; - - private static final Map STRATEGIES = Map.of( - StackAxis.HORIZONTAL, new HorizontalStrategy(), - StackAxis.VERTICAL, new VerticalStrategy(), - StackAxis.REVERSE_HORIZONTAL, new ReverseHorizontalStrategy(), - StackAxis.REVERSE_VERTICAL, new ReverseVerticalStrategy() - ); - - public static void align(Entity parent, EntityManager entityManager) { - if (!parent.hasAssignable(Container.class)) { - log.debug("{} is not a Container and will be skipped.", parent); - return; - } - - StackAxis stackAxis = parent.getComponent(StackAxis.class) - .orElseThrow(() -> new IllegalStateException("Container Entity must have a StackAxis component.")); - - LayoutStrategy strategy = parent.has(Module.class) - ? new ModuleVerticalStrategy() - : STRATEGIES.get(stackAxis); - - if (strategy != null) { - log.debug("Aligning children of {} using {} strategy.", parent, stackAxis); - strategy.alignChildren(parent, entityManager); - } else { - log.info("No layout strategy found for StackAxis: {}. No alignment will be performed.", stackAxis); - } - } - - private interface LayoutStrategy { - void alignChildren(Entity parent, EntityManager entityManager); - } - - private abstract static class BaseLayoutStrategy implements LayoutStrategy { - - @Override - public void alignChildren(Entity parent, EntityManager entityManager) { - Align align = parent.getComponent(Align.class).orElseThrow(); - List children = getOrderedChildren(parent, entityManager); - Axes axes = new Axes(); - - for (int i = 0; i < children.size(); i++) { - Entity child = children.get(i); - boolean isLastChild = (i == children.size() - 1); - - // IMPORTANT: Recursively align children of any nested containers first. - ContainerAligner.align(child, entityManager); - - updateChildPosition(child, axes); - updateContainerDimensions(child, axes); - - // Apply spacing between elements, but not after the last one. - if (!isLastChild) { - axes.main += align.spacing(); - } - } - - - // After processing all children, update the parent's final size. - Padding parentPadding = parent.getComponent(Padding.class).orElse(Padding.zero()); - parent.addComponent(getFinalContentSize(axes, parentPadding)); - } - - /** - * CORRECTED: This now returns children in the standard, forward order. - * The reverse logic is handled by the ReverseLayoutStrategy. - */ - protected List getOrderedChildren(Entity parent, EntityManager entityManager) { - List children = parent.getChildren().stream() - .map(id -> entityManager.getEntity(id).orElseThrow()) - .collect(Collectors.toCollection(ArrayList::new)); - java.util.Collections.reverse(children); - return children; - } - - protected abstract void updateChildPosition(Entity child, Axes axes); - - protected abstract void updateContainerDimensions(Entity child, Axes axes); - - /** - * CORRECTED: The `lastChildMargin` parameter was removed to prevent double-counting. - * The parent's size is determined by the accumulated children's outer sizes and the parent's own padding. - */ - protected abstract ContentSize getFinalContentSize(Axes axes, Padding padding); - - protected static final class Axes { - double main = 0; - double cross = 0; - } - } - - // --- Concrete Strategy Implementations --- - - private static class HorizontalStrategy extends BaseLayoutStrategy { - @Override - protected void updateChildPosition(Entity child, Axes axes) { - Position currentPos = child.getComponent(Position.class).orElse(Position.zero()); - Margin margin = child.getComponent(Margin.class).orElse(Margin.zero()); - Anchor anchor = child.getComponent(Anchor.class).orElse(Anchor.defaultAnchor()); - child.addComponent(new Position(axes.main + margin.left(), currentPos.y())); - child.addComponent(new Anchor(HAnchor.DEFAULT, anchor.v())); - } - - @Override - protected void updateContainerDimensions(Entity child, Axes axes) { - var outbox = OuterBoxSize.from(child).orElseThrow(); - axes.main += outbox.width(); - axes.cross = Math.max(axes.cross, outbox.height()); - } - - @Override - protected ContentSize getFinalContentSize(Axes axes, Padding padding) { - // CORRECTED: Do not add last child's margin. Parent padding affects both axes. - return new ContentSize(axes.main + padding.horizontal(), axes.cross + padding.vertical()); - } - } - - private static class VerticalStrategy extends BaseLayoutStrategy { - @Override - protected void updateChildPosition(Entity child, Axes axes) { - Position currentPos = child.getComponent(Position.class).orElse(Position.zero()); - Anchor anchor = child.getComponent(Anchor.class).orElse(Anchor.defaultAnchor()); - Margin margin = child.getComponent(Margin.class).orElse(Margin.zero()); - // CORRECTED: Preserves original x-position for a true vertical stack. - // The previous logic (currentPos.x() + margin.bottom()) created an unintentional diagonal layout. - child.addComponent(new Position(currentPos.x(), axes.main + margin.bottom())); - child.addComponent(new Anchor(anchor.h(), VAnchor.DEFAULT)); - } - - @Override - protected void updateContainerDimensions(Entity child, Axes axes) { - var outbox = OuterBoxSize.from(child).orElseThrow(); - axes.main += outbox.height(); - axes.cross = Math.max(axes.cross, outbox.width()); - } - - @Override - protected ContentSize getFinalContentSize(Axes axes, Padding padding) { - // CORRECTED: Do not add last child's margin. Parent padding affects both axes. - return new ContentSize(axes.cross + padding.horizontal(), axes.main + padding.vertical()); - } - } - - private abstract static class ReverseLayoutStrategy extends BaseLayoutStrategy { - /** - * CORRECTED: This class now correctly reverses the order of children. - */ - @Override - protected List getOrderedChildren(Entity parent, EntityManager entityManager) { - return parent.getChildren().stream() - .map(id -> entityManager.getEntity(id).orElseThrow()) - .collect(Collectors.toList()); // .reversed() is now correctly placed here - } - } - - private static class ReverseHorizontalStrategy extends ReverseLayoutStrategy { - private final HorizontalStrategy delegate = new HorizontalStrategy(); - - @Override - protected void updateChildPosition(Entity child, Axes axes) { - delegate.updateChildPosition(child, axes); - } - - @Override - protected void updateContainerDimensions(Entity child, Axes axes) { - delegate.updateContainerDimensions(child, axes); - } - - @Override - protected ContentSize getFinalContentSize(Axes axes, Padding padding) { - return delegate.getFinalContentSize(axes, padding); - } - } - - private static class ReverseVerticalStrategy extends ReverseLayoutStrategy { - private final VerticalStrategy delegate = new VerticalStrategy(); - - @Override - protected void updateChildPosition(Entity child, Axes axes) { - delegate.updateChildPosition(child, axes); - } - - @Override - protected void updateContainerDimensions(Entity child, Axes axes) { - delegate.updateContainerDimensions(child, axes); - } - - @Override - protected ContentSize getFinalContentSize(Axes axes, Padding padding) { - return delegate.getFinalContentSize(axes, padding); - } - } - - private static class ModuleVerticalStrategy implements LayoutStrategy { - @Override - public void alignChildren(Entity parent, EntityManager entityManager) { - Align align = parent.getComponent(Align.class).orElseThrow(); - List children = parent.getChildren().stream() - .map(id -> entityManager.getEntity(id).orElseThrow()) - .collect(Collectors.toCollection(ArrayList::new)); - java.util.Collections.reverse(children); - - double main = 0; - double resolvedWidth = parent.getComponent(ContentSize.class) - .orElseThrow(() -> new IllegalStateException("Module missing ContentSize: " + parent)) - .width(); - double innerWidth = InnerBoxSize.from(parent) - .orElseThrow(() -> new IllegalStateException("Module missing InnerBoxSize: " + parent)) - .width(); - - for (int i = 0; i < children.size(); i++) { - Entity child = children.get(i); - boolean isLastChild = (i == children.size() - 1); - - ContainerAligner.align(child, entityManager); - - Position currentPos = child.getComponent(Position.class).orElse(Position.zero()); - Anchor anchor = child.getComponent(Anchor.class).orElse(Anchor.defaultAnchor()); - Margin margin = child.getComponent(Margin.class).orElse(Margin.zero()); - - child.addComponent(new Position(currentPos.x(), main + margin.bottom())); - child.addComponent(new Anchor(anchor.h(), VAnchor.DEFAULT)); - - OuterBoxSize childOuter = OuterBoxSize.from(child).orElseThrow(); - if (childOuter.width() > innerWidth + EPS) { - log.warn("Child {} overflows module {} width contract: childOuter={} innerWidth={}", - child, parent, childOuter.width(), innerWidth); - } - - main += childOuter.height(); - if (!isLastChild) { - main += align.spacing(); - } - } - - Padding parentPadding = parent.getComponent(Padding.class).orElse(Padding.zero()); - ContentSize current = parent.getComponent(ContentSize.class).orElseThrow(); - double finalHeight = Math.max(current.height(), main + parentPadding.vertical()); - parent.addComponent(new ContentSize(resolvedWidth, finalHeight)); - } - } -} diff --git a/src/main/java/com/demcha/compose/engine/layout/container/ContainerExpander.java b/src/main/java/com/demcha/compose/engine/layout/container/ContainerExpander.java deleted file mode 100644 index 914b6e54d..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/container/ContainerExpander.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.demcha.compose.engine.layout.container; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.renderable.Module; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.exceptions.ContentSizeNotFoundException; -import com.demcha.compose.engine.components.geometry.Expendable; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; - -@Slf4j -public final class ContainerExpander { - - private static final double EPS = 1e-6; - - private ContainerExpander() { - } - - /** - * Ensures the parent's ContentSize is at least large enough to contain all children, - * based on the parent's InnerBoxSize constraints and children OuterBoxSize. - * - * @param parent container entity (must have InnerBoxSize and ContentSize) - * @param children direct children of the container - * @return true if parent's ContentSize was increased, false otherwise - */ - public static boolean expandContentSizeByChildren(Entity parent, Set children) { - Objects.requireNonNull(parent, "parent cannot be null"); - Objects.requireNonNull(children, "children cannot be null"); - - if (children.isEmpty()) { - log.debug("No children; nothing to expand for {}", parent); - return false; - } - - if (parent.has(Module.class)) { - return expandModuleContentHeightByChildren(parent, children); - } - - final InnerBoxSize inner = InnerBoxSize.from(parent).orElseThrow(() -> - new IllegalStateException("Parent is missing InnerBoxSize: " + parent)); - - // Start from current inner box constraints - double requiredContentWidth = inner.width(); - double requiredContentHeight = inner.height(); - - if (log.isTraceEnabled()) { - log.trace("Initial required size from inner box for {}: {}x{}", - parent, requiredContentWidth, requiredContentHeight); - } - - for (Entity child : children) { - Optional childOuterOpt = OuterBoxSize.from(child); - if (childOuterOpt.isEmpty()) continue; - - OuterBoxSize childOuter = childOuterOpt.get(); - if (log.isTraceEnabled()) - log.trace("Child {} outer size: {}x{}", child, childOuter.width(), childOuter.height()); - - // NOTE: if children can be offset, replace with (childRight, childBottom) calculation - requiredContentWidth = Math.max(requiredContentWidth, childOuter.width()); - requiredContentHeight = Math.max(requiredContentHeight, childOuter.height()); - } - - return applyContentSizeIfLarger(parent, requiredContentWidth, requiredContentHeight); - } - - private static boolean expandModuleContentHeightByChildren(Entity parent, Set children) { - final InnerBoxSize inner = InnerBoxSize.from(parent).orElseThrow(() -> - new IllegalStateException("Parent is missing InnerBoxSize: " + parent)); - - double requiredContentHeight = inner.height(); - - for (Entity child : children) { - Optional childOuterOpt = OuterBoxSize.from(child); - if (childOuterOpt.isEmpty()) continue; - - OuterBoxSize childOuter = childOuterOpt.get(); - if (childOuter.width() > inner.width() + EPS) { - log.warn("Child {} exceeds module {} inner width: childOuter={} innerWidth={}", - child, parent, childOuter.width(), inner.width()); - } - requiredContentHeight = Math.max(requiredContentHeight, childOuter.height()); - } - - return applyHeightIfLarger(parent, requiredContentHeight); - } - - /** - * Increases ContentSize if (required > current inner) considering EPS. - */ - private static boolean applyContentSizeIfLarger(Entity parent, double requiredInnerWidth, double requiredInnerHeight) { - final InnerBoxSize inner = InnerBoxSize.from(parent).orElseThrow(() -> - new IllegalStateException("Parent is missing InnerBoxSize: " + parent)); - - final boolean wider = requiredInnerWidth > inner.width() + EPS; - final boolean taller = requiredInnerHeight > inner.height() + EPS; - - if (!wider && !taller) { - log.debug("Parent {} content not expanded (required <= inner).", parent); - return false; - } - - final ContentSize current = parent.getComponent(ContentSize.class).orElseThrow(() -> { - log.error("All objects must have a ContentSize. Object {} doesn't have one.", parent); - return new ContentSizeNotFoundException(parent); - }); - - final double newWidth = wider ? current.width() + (requiredInnerWidth - inner.width()) : current.width(); - final double newHeight = taller ? current.height() + (requiredInnerHeight - inner.height()) : current.height(); - - // Avoid churn - if (Math.abs(newWidth - current.width()) < EPS && Math.abs(newHeight - current.height()) < EPS) { - log.debug("Computed size equals current ContentSize (within EPS); skipping update for {}", parent); - return false; - } - - ContentSize updated = new ContentSize(newWidth, newHeight); - parent.addComponent(updated); // ensure addComponent replaces old instance - - log.debug("Expanded ContentSize for {}: {}x{} -> {}x{}", - parent, current.width(), current.height(), newWidth, newHeight); - return true; - } - - private static boolean applyHeightIfLarger(Entity parent, double requiredInnerHeight) { - final InnerBoxSize inner = InnerBoxSize.from(parent).orElseThrow(() -> - new IllegalStateException("Parent is missing InnerBoxSize: " + parent)); - - final boolean taller = requiredInnerHeight > inner.height() + EPS; - if (!taller) { - log.debug("Parent {} height not expanded (required <= inner).", parent); - return false; - } - - final ContentSize current = parent.getComponent(ContentSize.class).orElseThrow(() -> { - log.error("All objects must have a ContentSize. Object {} doesn't have one.", parent); - return new ContentSizeNotFoundException(parent); - }); - - final double newHeight = current.height() + (requiredInnerHeight - inner.height()); - if (Math.abs(newHeight - current.height()) < EPS) { - log.debug("Computed height equals current ContentSize (within EPS); skipping update for {}", parent); - return false; - } - - parent.addComponent(new ContentSize(current.width(), newHeight)); - log.debug("Expanded module height for {}: {} -> {}", parent, current.height(), newHeight); - return true; - } - - /** - * The method normalizeBoxSize adjusts the size of parent entities. For each parent, it inspects the dimensions (OuterBoxSize) of all its direct children. - * It then ensures the parent's OuterBoxSize is large enough to encompass its own original size and the size of its largest child by updating it to the maximum width and maximum height found. - * In simple terms: It makes a parent container at least as big as its biggest child. - * - * @param childrenByParents map already sorted by parent - */ - public static void process(Map> childrenByParents, EntityManager entityManager) { - log.debug("Box size normalizer"); - - - for (Map.Entry> parentUuid : childrenByParents.entrySet()) { - - var entityParentOpt = entityManager.getEntity(parentUuid.getKey()); - if (entityParentOpt.isEmpty()) { - log.warn("Hasn't find a Parent Entity by id {}", parentUuid.getKey()); - continue; - } - var parentEntity = entityParentOpt.get(); - if (!isExpandable(parentEntity)) { - continue; - } - // Retriven Entiti - var childrenEntities = entityManager.getSetEntitiesFromUuids(new LinkedHashSet<>(parentUuid.getValue())); - - - if (parentEntity.has(Align.class)) { - - log.debug("It is a container with component Align"); - - - } - ContainerExpander.expandContentSizeByChildren(parentEntity, childrenEntities); - - } - } - - - - private static boolean isExpandable(Entity entity) { - if (entity.hasAssignable(Expendable.class)) { - return true; - } - log.debug("Entity {} has no Expendable", entity); - return false; - } -} diff --git a/src/main/java/com/demcha/compose/engine/layout/container/ContainerLayoutManager.java b/src/main/java/com/demcha/compose/engine/layout/container/ContainerLayoutManager.java deleted file mode 100644 index d30556a79..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/container/ContainerLayoutManager.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.demcha.compose.engine.layout.container; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.renderable.ChunkedBlockText; -import com.demcha.compose.engine.components.renderable.Container; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; - -@Slf4j -public class ContainerLayoutManager { - - /** - * Recursively aligns child entities within their parent containers (post-order). - * - * A parent container is aligned only after all of its container-children are aligned. - * Non-container parents are just marked as processed (nothing to align). - */ - public static void process(Map> childrenByParent, EntityManager entityManager) { - if (childrenByParent == null || childrenByParent.isEmpty()) { - log.debug("process(): nothing to do, childrenByParent is null or empty"); - return; - } - Objects.requireNonNull(entityManager, "entityManager"); - - - // 'visited' is a clearer name than doneList for graph-style traversals. - Set visited = new HashSet<>(); - - for (UUID parentId : childrenByParent.keySet()) { - if (!visited.contains(parentId)) { - if (isContainer(parentId, entityManager)) { - alignContainer(parentId, childrenByParent, visited, entityManager, new ArrayDeque<>()); - } else { - log.debug("Element Not container {}", entityManager.getEntity(parentId).orElseThrow(() -> new NoSuchElementException("Entity not found: " + parentId))); - visited.add(parentId); - } - } - } - } - - /** - * Depth-first post-order alignment with cycle detection. - */ - private static void alignContainer( - UUID parentId, - Map> childrenByParent, - Set visited, - EntityManager entityManager, - Deque stackForCycleCheck - ) { - log.debug("Align container start: {}", parentId); - - if (!stackForCycleCheck.add(parentId)) { - // parentId is already in the current DFS path → cycle - throw new IllegalStateException("Cycle detected in entity hierarchy: " + stackForCycleCheck); - } - - // Iterate children safely (use empty set when absent) - Collection children = childrenByParent.get(parentId); - if (children == null) { - children = Collections.emptyList(); - } - for (UUID childId : children) { - if (visited.contains(childId)) continue; - - if (isContainerSafe(childId, entityManager)) { - alignContainer(childId, childrenByParent, visited, entityManager, stackForCycleCheck); - } else { - // Non-container child: nothing to align here, but still mark as visited. - visited.add(childId); - } - } - - // Post-order: align AFTER children - alignElement(parentId, entityManager); - visited.add(parentId); - - // Pop for cycle check - UUID popped = stackForCycleCheck.removeLast(); - assert popped.equals(parentId) : "Cycle stack corrupted"; - - log.debug("Align container end: {}", parentId); - } - - private static boolean isContainer(UUID id, EntityManager em) { - Entity e = em.getEntity(id).orElseThrow(() -> new NoSuchElementException("Entity not found: " + id)); - return e.hasAssignable(Container.class); - } - - private static boolean isContainerSafe(UUID id, EntityManager em) { - return em.getEntity(id) - .map(e -> e.hasAssignable(Container.class)) - .orElse(false); - } - - private static void alignElement(UUID id, EntityManager em) { - Entity parent = em.getEntity(id).orElseThrow(() -> new NoSuchElementException("Entity not found: " + id)); - - if (!parent.hasAssignable(Container.class)) { - log.trace("alignElement(): {} is not a Container — skipping.", parent); - return; - } - - log.info("Aligning container: {}", parent); - ContainerAligner.align(parent, em); - - // Optional horizontal text alignment when container also holds text - if (parent.hasAssignable(ChunkedBlockText.class)) { - horizontallyAlign(parent, em); - } - } - - /** - * Align children horizontally inside parent's inner width. - * Uses the parent's Align component as the policy. - */ - private static void horizontallyAlign(Entity parent, EntityManager em) { - Align parentAlign = parent.getComponent(Align.class) - .orElseThrow(() -> new NoSuchElementException("Align component missing on parent: " + parent)); - - double innerW = InnerBoxSize.from(parent) - .orElseThrow(() -> new NoSuchElementException("InnerBoxSize missing on parent: " + parent)) - .width(); - - for (UUID childId : parent.getChildren()) { - Entity child = em.getEntity(childId) - .orElseThrow(() -> new NoSuchElementException("Child entity not found: " + childId)); - - Align.alignHorizontally(child, innerW, parentAlign); - } - } -} diff --git a/src/main/java/com/demcha/compose/engine/layout/container/ModuleWidthResolver.java b/src/main/java/com/demcha/compose/engine/layout/container/ModuleWidthResolver.java deleted file mode 100644 index 92f7cb884..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/container/ModuleWidthResolver.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.demcha.compose.engine.layout.container; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.ModuleWidthSeed; -import com.demcha.compose.engine.components.layout.ParentComponent; -import com.demcha.compose.engine.components.renderable.Module; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import lombok.extern.slf4j.Slf4j; - -import java.util.Comparator; -import java.util.List; - -/** - * Resolves the stable content width contract for semantic modules before - * container child alignment begins. - */ -@Slf4j -public final class ModuleWidthResolver { - private ModuleWidthResolver() { - } - - public static void process(EntityManager entityManager, Canvas canvas) { - List modules = entityManager.getEntities().values().stream() - .filter(entity -> entity.has(Module.class)) - .sorted(Comparator.comparingInt(module -> depth(module, entityManager))) - .toList(); - if (modules.isEmpty()) { - return; - } - - for (Entity module : modules) { - resolveModuleWidth(module, entityManager, canvas); - } - } - - private static void resolveModuleWidth(Entity module, EntityManager entityManager, Canvas canvas) { - Margin margin = module.getComponent(Margin.class).orElse(Margin.zero()); - ContentSize current = module.getComponent(ContentSize.class).orElse(new ContentSize(0, 0)); - double availableWidth = resolveAvailableWidth(module, entityManager, canvas); - double resolvedWidth = Math.max(0, availableWidth - margin.horizontal()); - - module.addComponent(new ContentSize(resolvedWidth, current.height())); - log.debug("Resolved module width for {}: available={} margin={} resolved={}", - module, availableWidth, margin.horizontal(), resolvedWidth); - } - - private static double resolveAvailableWidth(Entity module, EntityManager entityManager, Canvas canvas) { - ParentComponent parentComponent = module.getComponent(ParentComponent.class).orElse(null); - if (parentComponent == null) { - return module.getComponent(ModuleWidthSeed.class) - .map(ModuleWidthSeed::width) - .orElse(canvas.innerWidth()); - } - - return entityManager.getEntity(parentComponent.uuid()) - .flatMap(InnerBoxSize::from) - .map(InnerBoxSize::width) - .orElseGet(() -> { - log.warn("Module {} parent width could not be resolved; falling back to canvas inner width.", module); - return canvas.innerWidth(); - }); - } - - private static int depth(Entity entity, EntityManager entityManager) { - int depth = 0; - ParentComponent parent = entity.getComponent(ParentComponent.class).orElse(null); - - while (parent != null) { - depth++; - parent = entityManager.getEntity(parent.uuid()) - .flatMap(parentEntity -> parentEntity.getComponent(ParentComponent.class)) - .orElse(null); - } - - return depth; - } -} diff --git a/src/main/java/com/demcha/compose/engine/layout/container/package-info.java b/src/main/java/com/demcha/compose/engine/layout/container/package-info.java deleted file mode 100644 index a5f8b923f..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/container/package-info.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Container layout helpers (alignment, expansion, module-width resolution) of - * the legacy ECS engine. - * - *

Part of the parallel legacy {@code Entity}-based engine; the canonical - * {@code com.demcha.compose.document.layout} pipeline does not use it.

- * - * @deprecated Legacy ECS engine, superseded by the canonical - * {@code com.demcha.compose.document.layout} pipeline. Not reachable from - * any public API; retained only for legacy regression tests and a candidate - * for removal. Do not extend it or spend optimization effort here. - */ -@Deprecated -package com.demcha.compose.engine.layout.container; diff --git a/src/main/java/com/demcha/compose/engine/layout/package-info.java b/src/main/java/com/demcha/compose/engine/layout/package-info.java deleted file mode 100644 index 7554ebfa6..000000000 --- a/src/main/java/com/demcha/compose/engine/layout/package-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Layout systems of the legacy ECS engine — they turn - * low-level {@code Entity} graphs into resolved geometry for the original - * {@code Entity}-based pipeline. - * - *

Despite the former "shared" framing, the canonical pipeline - * ({@code com.demcha.compose.document.layout.LayoutCompiler}) imports nothing - * from this package. It is part of the parallel legacy ECS engine reachable - * only from the legacy engine regression tests.

- * - * @deprecated Legacy ECS engine, superseded by the canonical - * {@code com.demcha.compose.document.layout} pipeline. Not reachable from - * any public API; retained only for legacy regression tests and a candidate - * for removal. Do not extend it or spend optimization effort here. - */ -@Deprecated -package com.demcha.compose.engine.layout; diff --git a/src/main/java/com/demcha/compose/engine/pagination/PageBreaker.java b/src/main/java/com/demcha/compose/engine/pagination/PageBreaker.java deleted file mode 100644 index 6165a2db7..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/PageBreaker.java +++ /dev/null @@ -1,383 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.Layer; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.renderable.BlockText; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.core.LayoutTraversalContext; -import com.demcha.compose.engine.exceptions.BigSizeElementException; -import com.demcha.compose.engine.render.RenderingSystemECS; -import com.demcha.compose.engine.core.SystemECS; -import lombok.Data; -import lombok.NonNull; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.PriorityQueue; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - - -/** - * A utility class responsible for the logic of breaking content across pages. - *

- * The main responsibilities of this class are: - *

    - *
  1. building a child-first pagination order from the resolved hierarchy
  2. - *
  3. Calculating and assigning a page number and in-page coordinates for each entity.
  4. - *
  5. Handling "breakable" entities, such as {@link BlockText}, whose content can - * flow onto the next page.
  6. - *
- * The class operates in a coordinate system where the Y-axis points downwards. It does not create new pages - * but rather calculates and assigns {@link Placement} components to entities. - *

- * A critical invariant of this class is that descendants must be processed before their ancestor containers. - * The current implementation enforces that rule with a priority-based topological walk over the resolved - * hierarchy: only nodes whose children are already processed can enter the ready queue, and unrelated nodes - * are then ordered by layout position and depth. This keeps the algorithm renderer-agnostic while avoiding - * repeated ancestor-chain walks inside a comparator. - */ -@Slf4j -@Data -@Accessors(chain = true) -public class PageBreaker { - private static final Logger LIFECYCLE_LOG = LoggerFactory.getLogger("com.demcha.compose.engine.pagination"); - - private final EntityManager entityManager; - private PageLayoutCalculator pageLayoutCalculator; - private TextBlockProcessor textBlockProcessor; - - /** - * Creates a page breaker bound to one entity manager and its pagination - * helpers. - * - * @param entityManager entity registry for the current document - */ - public PageBreaker(EntityManager entityManager) { - this.entityManager = entityManager; - this.pageLayoutCalculator = new PageLayoutCalculator(entityManager); - this.textBlockProcessor = new TextBlockProcessor(entityManager); - } - - - // === Paging math ========================================================= - - /** - * Builds a {@link Placement} for the entity using a replacement Y coordinate - * and explicit page span. - * - * @param entity entity being placed - * @param yPosition resolved Y coordinate inside the target page flow - * @param startPage first page touched by the entity - * @param endPage last page touched by the entity - * @return placement preserving the entity's resolved X position and size - */ - private static Placement setYInPlacement(Entity entity, double yPosition, int startPage, int endPage) { - ComputedPosition computedPosition = entity.require(ComputedPosition.class); - var size = entity.require(ContentSize.class); - return new Placement(computedPosition.x(), yPosition, size.width(), size.height(), startPage, endPage); - } - - /** - * Convenience overload that converts a precomputed page-position result into a - * {@link Placement}. - * - * @param entity entity being placed - * @param position resolved position/page-span tuple - * @return placement preserving the entity's resolved X position and size - */ - private static Placement setYInPlacement(Entity entity, YPositionOnPage position) { - return setYInPlacement(entity, position.yPosition(), position.startPage(), position.endPage()); - } - - /** - * Runs page-breaking for the current entity set. - *

- * The ordering step guarantees that descendants are processed before ancestors while still preferring - * top-to-bottom layout order between unrelated subtrees. - */ - public void process(@NonNull Map entities, Canvas canvas) { - LayoutTraversalContext traversalContext = LayoutTraversalContext.from(entityManager); - process(entities, canvas, traversalContext, entityManager.getDepthById()); - } - - /** - * Runs page-breaking with a caller-supplied traversal snapshot and depth map. - * - *

- * This overload is used when layout has already materialized the canonical tree - * snapshot for the pass and pagination should reuse that exact hierarchy view. - *

- * - * @param entities entity map for the current document - * @param canvas page canvas configuration - * @param traversalContext canonical parent/child snapshot for this pass - * @param depthById depth map from layout traversal - */ - public void process(@NonNull Map entities, - Canvas canvas, - @NonNull LayoutTraversalContext traversalContext, - @NonNull Map depthById) { - long startNanos = System.nanoTime(); - LIFECYCLE_LOG.debug("pagination.pageBreaker.start entities={} roots={}", entities.size(), traversalContext.roots().size()); - Offset yOffset = new Offset(); - List> orderedEntities = orderedForPagination( - entities, - traversalContext.parentById(), - traversalContext.childrenByParent(), - depthById); - - orderedEntities.forEach(e -> { - Entity entity = e.getValue(); - - if (Breakable.class.isAssignableFrom(entity.getRender().getClass())) { - if (entity.hasAssignable(BlockText.class)) { - definePlacementForBlockText(canvas, entity, yOffset); - } else { - definePlacement(canvas, entity, yOffset, true); - } - - } else { - definePlacement(canvas, entity, yOffset, false); - } - }); - LIFECYCLE_LOG.debug( - "pagination.pageBreaker.end entities={} ordered={} durationMs={}", - entities.size(), - orderedEntities.size(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - } - - /** - * Builds one pagination order for the whole pass without repeated pairwise ancestry checks. - * - *

The queue only admits nodes whose descendants are already processed, which preserves - * the child-before-parent contract. Between unrelated ready nodes we still use layout - * geometry so the order remains visually intuitive and deterministic.

- */ - private List> orderedForPagination(Map entities, - Map parentById, - Map> childrenByParent, - Map depthById) { - Map remainingChildren = new LinkedHashMap<>(); - PriorityQueue ready = new PriorityQueue<>(paginationPriority(entities, depthById)); - List> ordered = new ArrayList<>(entities.size()); - - for (UUID entityId : entities.keySet()) { - int childCount = 0; - for (UUID childId : childrenByParent.getOrDefault(entityId, List.of())) { - if (entities.containsKey(childId)) { - childCount++; - } - } - remainingChildren.put(entityId, childCount); - if (childCount == 0) { - ready.add(entityId); - } - } - - while (!ready.isEmpty()) { - UUID entityId = ready.poll(); - Entity entity = entities.get(entityId); - if (entity == null) { - continue; - } - - ordered.add(Map.entry(entityId, entity)); - - UUID parentId = parentById.get(entityId); - if (parentId == null || !remainingChildren.containsKey(parentId)) { - continue; - } - - int unresolvedChildren = remainingChildren.merge(parentId, -1, Integer::sum); - if (unresolvedChildren == 0) { - ready.add(parentId); - } - } - - if (ordered.size() != entities.size()) { - throw new IllegalStateException("Cycle detected while computing pagination order."); - } - - return ordered; - } - - /** - * Returns the resolved layout Y-coordinate used for pagination ordering. - *

- * We use {@link ComputedPosition} rather than renderer-specific coordinates because pagination operates on - * the layout result, before the final rendering pass writes output streams. - */ - private double positionY(Entity entity) { - return entity.require(ComputedPosition.class).y(); - } - - /** - * Ready-queue ordering for unrelated nodes in the pagination walk. - * - *

The priority keys are chosen so that visually higher entities and deeper - * subtree leaves are processed first:

- *
    - *
  1. Y-position descending — top-of-page entities come first in engine coordinates
  2. - *
  3. Depth descending — deeper children break ties in favour of leaves
  4. - *
  5. UUID string — deterministic fallback for bit-identical layouts
  6. - *
- * - * @param entities entity map used during this pagination pass - * @param depthById precomputed depth map from the layout traversal - * @return comparator for the {@link java.util.PriorityQueue} used in - * {@link #orderedForPagination} - */ - private Comparator paginationPriority(Map entities, Map depthById) { - // Pre-compute Y and depth so PriorityQueue compares stay alloc-free. - // The previous implementation called requireEntity(entities, id) twice - // per compare (once for Y, once for depth) and finally fell back to - // UUID::toString which allocates a 36-char string for every tie-break. - Map keys = new java.util.HashMap<>(entities.size() * 2); - for (UUID id : entities.keySet()) { - Entity entity = entities.get(id); - double y = entity.require(ComputedPosition.class).y(); - int depth = depthOf(id, entity, depthById); - keys.put(id, new PaginationKey(y, depth)); - } - return (left, right) -> { - PaginationKey leftKey = keys.get(left); - PaginationKey rightKey = keys.get(right); - int yCompare = Double.compare(rightKey.y(), leftKey.y()); // Y descending - if (yCompare != 0) { - return yCompare; - } - int depthCompare = Integer.compare(rightKey.depth(), leftKey.depth()); // depth descending - if (depthCompare != 0) { - return depthCompare; - } - return left.compareTo(right); // UUID.compareTo() is alloc-free (MSB then LSB) - }; - } - - /** - * Lightweight precomputed sort key for the pagination priority queue. - */ - private record PaginationKey(double y, int depth) { - } - - /** - * Retrieves an entity from the pagination map, throwing if absent. - * - * @param entities entity map active during the current pagination pass - * @param entityId id to look up - * @return the entity, never {@code null} - * @throws IllegalStateException if the entity is missing - */ - private Entity requireEntity(Map entities, UUID entityId) { - Entity entity = entities.get(entityId); - if (entity == null) { - throw new IllegalStateException("Entity not found for pagination: " + entityId); - } - return entity; - } - - /** - * Returns the layout depth of an entity, falling back to its {@link Layer} - * value when the depth map has no entry (e.g. for dynamically added entities). - * - * @param entityId entity id to look up - * @param entity the resolved entity instance - * @param depthById precomputed depth map from the layout traversal - * @return depth value, zero if neither source has a value - */ - private int depthOf(UUID entityId, Entity entity, Map depthById) { - return depthById.getOrDefault(entityId, entity.getComponent(Layer.class) - .map(Layer::value) - .orElse(0)); - } - - /** - * Determines and adds a {@link Placement} component to an entity. - * This method updates the entity's vertical position based on the total {@code yOffset}, - * calculates its position on the page, and stores the result in a new {@code Placement} component. - *

- * For non-breakable entities, the calculator may shift the whole object to the next page. That shift can - * propagate upward into parent container sizing before this method writes the final placement. - * - * @param canvas The canvas configuration. - * @param entity The entity to process. - * @param yOffset The total Y-axis offset accumulated from previous elements. - * @param isBreakable A flag indicating whether the element can be broken across pages. - */ - private void definePlacement(Canvas canvas, Entity entity, Offset yOffset, boolean isBreakable) { - var computedPosition = entity.require(ComputedPosition.class); - entity.require(ContentSize.class); - log.debug("Defining position for {}", entity); - YPositionOnPage position; - try { - position = pageLayoutCalculator.definePositionOnPage(computedPosition.y() + yOffset.y(), entity, 0, canvas, yOffset, isBreakable); - } catch (Exception e) { - log.error("Error while defining position for {}", entity.printInfo(), e); - throw new RuntimeException(entity.printInfo(), e); - } - - Placement placement = setYInPlacement(entity, position); - entity.addComponent(placement); - } - - /** - * Handles pagination for block-text entities, including line-level splitting - * before the container-level {@link Placement} is written. - * - * @param canvas page canvas configuration - * @param entity block-text entity - * @param yOffset running pagination offset - */ - private void definePlacementForBlockText(Canvas canvas, Entity entity, Offset yOffset) { - try { - //definition a blockText - textBlockProcessor.processPageBreakerBlockText(entity, entityManager, canvas, yOffset); - //definition a placementForBlockTextContainer - definePlacement(canvas, entity, yOffset, true); - } catch (BigSizeElementException | IOException ex) { - log.error("Error while defining position for block text {}", entity.printInfo(), ex); - throw new RuntimeException(String.format("Error while defining position for block text %s, %s", entity.printInfo(), ex)); - } - - } - - - /** - * Initiates the page-breaking process for all entities managed by the {@link EntityManager}. - * This method automatically finds the active rendering system ({@link RenderingSystemECS}) to retrieve - * canvas information ({@link Canvas}). - * - */ - public void process() { - log.debug("Breaking pages"); - RenderingSystemECS renderingSystemECS = null; - log.debug("Definition a RenderingSystemECS"); - for (SystemECS system : entityManager.getSystems().getStream().toList()) { - if (RenderingSystemECS.class.isAssignableFrom(system.getClass())) { - renderingSystemECS = (RenderingSystemECS) system; - break; - } - - } - if (renderingSystemECS == null) { - log.error("No RenderingSystemECS found"); - throw new IllegalStateException("No RenderingSystemECS found"); - } - process(entityManager.getEntities(), renderingSystemECS.canvas()); - } -} - diff --git a/src/main/java/com/demcha/compose/engine/pagination/PageLayoutCalculator.java b/src/main/java/com/demcha/compose/engine/pagination/PageLayoutCalculator.java deleted file mode 100644 index c790ec55d..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/PageLayoutCalculator.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.exceptions.BigSizeElementException; -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@RequiredArgsConstructor -public class PageLayoutCalculator { - @Getter - private final EntityManager entityManager; - - - - public static double downShift(double yPosition, - double elementHeight, - double elementMarginBottom, - double elementMarginTop, - double canvasHeight, - double canvasMarginBottom, - double canvasMarginTop - ) throws BigSizeElementException { - double elementOuterHeight = elementHeight + elementMarginBottom + elementMarginTop; - double canvasInnerHeight = canvasHeight - canvasMarginBottom - canvasMarginTop; - if (elementOuterHeight > canvasInnerHeight) { - log.error("Element is too large and non-breakable — it cannot fit between the bounding lines with margins.\n" + - "requiredHeight: {} availableHeight: {}", elementOuterHeight, canvasInnerHeight); - - throw new BigSizeElementException(yPosition, elementHeight, canvasInnerHeight); - } - if (yPosition - elementMarginBottom > canvasMarginBottom) { - if (yPosition + elementHeight + elementMarginTop > canvasHeight - canvasMarginTop) { - return ((yPosition + elementHeight + elementMarginTop) - (canvasHeight - canvasMarginTop)) * -1; - } else { - return 0.0; - } - } - - - if (yPosition - elementMarginBottom < canvasMarginBottom) { - return (yPosition + elementHeight + elementMarginTop + canvasMarginTop ) * -1; - - } else { - return 0.0; - } - - } - - - /** - * Calculates the page offset relative to a starting page. - *

- * Given an absolute Y-coordinate and a fixed {@code pageHeight} ($H$), this - * determines how many pages away the coordinate is from the page that starts at Y=0. - * The offset is defined as follows (with the Y-axis pointing down): - *

    - *
  • $Y \in [0, H)$ $\rightarrow$ 0 (same page)
  • - *
  • $Y \in [H, 2H)$ $\rightarrow$ -1 (one page down)
  • - *
  • $Y \in [-H, 0)$ $\rightarrow$ 1 (one page up)
  • - *
- * The calculation formula is: $- \lfloor \frac{Y}{H} \rfloor$. - * - * @param currentPositionY The entity's absolute Y-coordinate. - * @param pageHeight The height of a single page (must be a finite positive number). - * @return The integer page offset. - * @throws IllegalArgumentException if {@code pageHeight} is not a finite positive number. - */ - public static int definePage(double currentPositionY, double pageHeight) { - if (!(pageHeight > 0.0) || Double.isNaN(pageHeight) || Double.isInfinite(pageHeight)) { - throw new IllegalArgumentException("pageHeight must be a finite positive number; was " + pageHeight); - } - - double pageInnerHeight; - double pageBottomMargin; - double pageTopMargin; - - double objectPosition; - double objectHeight; - double objectMarginTop; - double objectMarginBottom; - - log.debug("definePage: pageHeight={}, y={}", pageHeight, currentPositionY); - - // floor(y / H) is ... -2, -1, 0, 1, 2 ... - // We want offset = -floor(y/H) - int definedPage = (int) Math.floor(currentPositionY / pageHeight) * -1; - - log.debug("definePage -> offset={}", definedPage); - return definedPage; - } - - /** - * True mathematical modulo (always positive). - * a - element height - * m - canvas height - */ - private static double positiveModulo(double a, double m) { - double r = a % m; - return (r < 0) ? r + m : r; - } - - private static void debugCalculation(double currentPositionY, double objectHeight, double objectMarginTop, double objectMarginBottom, int currentPageNumber, double canvasHeight, double canvasMarginTop, double canvasMarginBottom, Offset yOffset, boolean isBreakable) { - log.debug(""" - --- Defining Position On Page --- - Object Settings: - - currentPositionY: {} - - objectHeight: {} - - objectMargin (T/B): {}/{} - Canvas Settings: - - canvasHeight: {} - - canvasMargin (T/B): {}/{} - State: - - currentPageNumber: {} - - isBreakable: {} - - yOffset (before): {} - """, - currentPositionY, objectHeight, objectMarginTop, objectMarginBottom, - canvasHeight, canvasMarginTop, canvasMarginBottom, - currentPageNumber, isBreakable, yOffset); - } - - public static double downShift(Entity e, float startY, Canvas canvas) throws BigSizeElementException { - var size = e.getComponent(ContentSize.class).orElseThrow(); - var margin = e.getComponent(Margin.class).orElse(Margin.zero()); - - - return downShift(startY, size.height(), margin.bottom(), margin.top(), canvas.height(), canvas.margin().bottom(), canvas.margin().top()); - } - - - /** - * Internal delegate method for {@link #definePositionOnPage}. - */ - private YPositionOnPage definePositionOnPage(double currentPositionY, - @NonNull Entity entity, - int currentPageNumber, double canvasHigh, double canvasMarginTop, double canvasMarginBottom, //Canvas Settings - @NonNull Offset yOffset, - boolean isBreakable) { - var size = entity.getComponent(ContentSize.class).orElseThrow(); - var margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - - double objectHeight = size.height(); - double objectMarginTop = margin.top(); - double objectMarginBottom = margin.bottom(); - try { - return calculatePageCoordinates(currentPositionY, objectHeight, objectMarginTop, objectMarginBottom, currentPageNumber, canvasHigh, canvasMarginTop, canvasMarginBottom, yOffset, isBreakable, entity); - } catch (BigSizeElementException | PageOutOfBoundException e) { - throw new RuntimeException(entity.printInfo(), e); - } - - } - - /** - * Internal delegate method for {@link #definePositionOnPage}. - */ - public YPositionOnPage definePositionOnPage(double currentPositionY, - Entity entity, - int currentPageNumber, Canvas canvas, //Canvas Settings - Offset yOffset, - boolean isBreakable) { - log.debug("Defining position on page for {}", entity); - log.debug("currentPositionY: {}, currentPageNumber: {} , yOffset: {}, isBreakable: {}", currentPositionY, currentPageNumber, yOffset, isBreakable); - - var size = entity.getComponent(ContentSize.class).orElseThrow(); - var margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - - double objectHeight = size.height(); - double objectMarginTop = margin.top(); - double objectMarginBottom = margin.bottom(); - //canvas - double canvasHeight = canvas.height(); - double canvasMarginTop = canvas.margin().top(); - double canvasMarginBottom = canvas.margin().bottom(); - - try { - - return calculatePageCoordinates(currentPositionY, objectHeight, objectMarginTop, objectMarginBottom, currentPageNumber, canvasHeight, canvasMarginTop, canvasMarginBottom, yOffset, isBreakable, entity); - - } catch (BigSizeElementException e) { - log.error("{}{}", entity.printInfo(), e.getMessage()); - throw new RuntimeException(entity.printInfo(), e); - } catch (PageOutOfBoundException e) { - - throw new RuntimeException(e); - } - - } - - /** - * Core logic for calculating the position on a page. - */ - public YPositionOnPage calculatePageCoordinates(double currentPositionY, - double objectHeight, double objectMarginTop, double objectMarginBottom, //object Settings - int currentPageNumber, double canvasHeight, double canvasMarginTop, double canvasMarginBottom, //Canvas Settings - Offset yOffset, boolean isBreakable, Entity entity) throws BigSizeElementException, PageOutOfBoundException { - - if (!(canvasHeight > 0.0) || Double.isInfinite(canvasHeight)) { - throw new IllegalArgumentException("canvasHeight must be a finite positive number; was " + canvasHeight); - } - - if (log.isDebugEnabled()) { - debugCalculation(currentPositionY, objectHeight, objectMarginTop, objectMarginBottom, currentPageNumber, canvasHeight, canvasMarginTop, canvasMarginBottom, yOffset, isBreakable); - } - - int pageOffset = definePage(currentPositionY, canvasHeight); // may be negative, zero, or positive - int startPage = Math.addExact(currentPageNumber, pageOffset); - log.debug("startPage: {}, pageOffset: {}", startPage, pageOffset); - if (startPage < 0) { - log.error("Invalid page number {}", startPage); - throw new IllegalArgumentException("Page number is less than zero after pageOffset: " + startPage); - } - - - // Normalize y into [0, pageHeight) - double yInPage = positiveModulo(currentPositionY, canvasHeight); - double shift = 0.0; - - - int endPage = startPage; - if (isBreakable) { - double requireSpace = yInPage + objectHeight + objectMarginTop; - double currentSize = Math.min(yInPage + requireSpace, canvasHeight - canvasMarginTop); - if (currentSize < requireSpace) { - log.debug("Defining a new endPage: {}", endPage); - - while (requireSpace >= currentSize) { - currentSize += canvasHeight; - endPage--; - if (endPage < 0) { - endPage = 0; - log.error("Page out of bound, Current PageNumber {}, {}", endPage, entity.printInfo()); - //TODO посмотреть почему делает меньше 0 -// throw new PageOutOfBoundException( String.format("Page out of bound, Current PageNumber %d, %s", endPage, entity.printInfo())); - } - } - - } - } else { - log.trace("Element is not breakable {}", entity); - shift = downShift(yInPage, objectHeight, objectMarginBottom, objectMarginTop, canvasHeight, canvasMarginBottom, canvasMarginTop); - if (shift == 0) { - return new YPositionOnPage(yInPage, startPage, endPage); - } - yInPage += shift; - pageOffset = definePage(yInPage, canvasHeight); - yInPage = positiveModulo(yInPage, canvasHeight); - startPage += pageOffset; - log.debug("Shift: {}", shift); - endPage = startPage; - } - if (entity != null) { - ParentContainerUpdater.updateParentContainerSize(entity, entityManager, shift); - } - yOffset.incrementY(shift); - YPositionOnPage result = new YPositionOnPage(yInPage, startPage, endPage); - - - log.debug("Defined position on page: {}", result); - return result; - - } - -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/PaginationLayoutSystem.java b/src/main/java/com/demcha/compose/engine/pagination/PaginationLayoutSystem.java deleted file mode 100644 index 8c1a32f0a..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/PaginationLayoutSystem.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.core.SystemECS; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -/** - * Simple pagination fallback that converts resolved positions into single-page - * {@code Placement} components. - *

- * This implementation does not perform advanced page breaking. Instead it maps - * each entity's existing {@code ComputedPosition} and {@code ContentSize} to a - * placement on page {@code 0}. It is therefore mainly useful as a minimal - * placement bridge when the full page-breaker path is not used. - *

- */ -@Slf4j -public class PaginationLayoutSystem implements SystemECS { - private static final Logger LIFECYCLE_LOG = LoggerFactory.getLogger("com.demcha.compose.engine.pagination"); - - - /** - * Converts each positioned entity into a basic single-page placement. - * - * @param entityManager registry containing entities with computed positions - */ - @Override - public void process(EntityManager entityManager) { - long startNanos = System.nanoTime(); - LIFECYCLE_LOG.debug("pagination.fallback.start entities={}", entityManager.getEntities().size()); - entityManager.getEntities().forEach((id, entity) -> { - if (entity.has(ComputedPosition.class)) { - double x = entity.getComponent(ComputedPosition.class).get().x(); - double y = entity.getComponent(ComputedPosition.class).get().y(); - ContentSize contentSize = entity.getComponent(ContentSize.class).orElseThrow(); - Placement placement = new Placement(x, y, contentSize.width(), contentSize.height(), 0, 0); - entity.addComponent(placement); - } - }); - LIFECYCLE_LOG.debug( - "pagination.fallback.end entities={} durationMs={}", - entityManager.getEntities().size(), - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos)); - - } - -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/TextBlockProcessor.java b/src/main/java/com/demcha/compose/engine/pagination/TextBlockProcessor.java deleted file mode 100644 index 98e66af70..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/TextBlockProcessor.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.components.content.text.BlockTextData; -import com.demcha.compose.engine.components.content.text.BlockTextLineMetrics; -import com.demcha.compose.engine.components.content.text.LineTextData; -import com.demcha.compose.engine.components.content.text.TextStyle; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.layout.Align; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.RenderingPosition; -import com.demcha.compose.engine.components.renderable.BlockText; -import com.demcha.compose.engine.components.style.Padding; -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.exceptions.BigSizeElementException; -import com.demcha.compose.engine.measurement.TextMeasurementSystem; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@Slf4j -public class TextBlockProcessor { - private final PageLayoutCalculator pageLayoutCalculator; - - public TextBlockProcessor(EntityManager entityManager) { - this.pageLayoutCalculator = new PageLayoutCalculator(entityManager); - } - - private static boolean checkBlockCondition(Entity e) { - if (!e.hasAssignable(BlockTextData.class)) { - log.debug("Entity doesn't have TextComponent; skipping: {}", e); - return true; - } - - var positionOpt = RenderingPosition.from(e); - if (positionOpt.isEmpty()) { - log.warn("TextComponent has no RenderingPosition; skipping: {}", e); - return true; - } - return false; - } - - /** - * Calculates and assigns the position for each line of text within a {@link BlockText}. - * This method iterates through the lines, determines their position on the page, handles - * page breaks, and updates the entity's {@link BlockTextData} component with the new, - * positioned data. - * - * @param e The entity containing the {@link BlockText}. - * offsets caused by page breaks within the text block. - * @throws IOException if an error occurs while working with fonts. - * @throws IllegalStateException if the entity is missing required components. - */ - public void processLayoutSystemTextLines(Entity e) throws IOException, BigSizeElementException { - - if (checkBlockCondition(e)) { - return; - } - - var position = e.getComponent(ComputedPosition.class).orElseThrow(); - InnerBoxSize innerBoxSize = InnerBoxSize.from(e).orElseThrow(); - Padding padding = e.getComponent(Padding.class).orElse(Padding.zero()); - BlockText.ValidatedTextData validateText = getValidatedTextData(e); - BlockTextData blockTextComponent = validateText.textValue(); - var blockTextData = blockTextComponent.lines(); - - List lineMetrics = resolveLineMetrics(blockTextData, validateText.style()); - TextMeasurementSystem.LineMetrics baseMetrics = BlockTextLineMetrics.resolveStyleMetrics( - pageLayoutCalculator.getEntityManager(), - validateText.style()); - - double spacing = e.getComponent(Align.class).orElseGet(() -> { - log.warn("TextComponent has no Align; using default: {}", e); - return Align.defaultAlign(2); - }).spacing(); - - if (log.isDebugEnabled()) { - log.debug("Rendering textBlock '{}' at position ({}, {})", blockTextData, position.x(), position.y()); - log.debug("baseTextStyle={}", validateText.style()); - } - - double startX = position.x(); - double cursorTop = position.y() + padding.bottom() + innerBoxSize.height(); - List assignPositionTextData = new ArrayList<>(blockTextData.size()); - - for (int i = 0; i < blockTextData.size(); i++) { - LineTextData ltd = blockTextData.get(i); - TextMeasurementSystem.LineMetrics metrics = lineMetrics.get(i); - double baselineOffset = baselineOffset(ltd, metrics); - double currentX = ltd.x() + startX; - double baselineY = cursorTop - metrics.lineHeight() + baselineOffset; - - if (log.isDebugEnabled()) { - log.debug("line {} metrics={} baselineY={}", i, metrics, baselineY); - } - - // Preserve cached measurement payload and only rewrite resolved placement. - assignPositionTextData.add(new LineTextData(ltd, currentX, baselineY, 0)); - - cursorTop -= metrics.lineHeight(); - if (i < lineMetrics.size() - 1) { - cursorTop -= BlockTextLineMetrics.interLineGap( - metrics, - lineMetrics.get(i + 1), - baseMetrics, - spacing); - } - } - - e.addComponent(blockTextComponent.withLines(assignPositionTextData)); - } - - public Offset processPageBreakerBlockText(Entity entity, - EntityManager entityManager, - Canvas canvas, - @NonNull Offset yOffset) throws IOException, BigSizeElementException { - if (checkBlockCondition(entity)) { - return new Offset(); - } - - BlockText.ValidatedTextData validatedTextData = getValidatedTextData(entity); - BlockTextData blockTextComponent = validatedTextData.textValue(); - var blockTextData = blockTextComponent.lines(); - List lineMetrics = resolveLineMetrics(blockTextData, validatedTextData.style()); - - double spacing = entity.getComponent(Align.class).orElseGet(() -> { - log.warn("TextComponent has no Align; using default: {}", entity); - return Align.defaultAlign(2); - }).spacing(); - - final int currentPage = 0; - Offset entityYOffset = new Offset(); - List assignPositionTextData = new ArrayList<>(blockTextData.size()); - - for (int i = 0; i < blockTextData.size(); i++) { - LineTextData ltd = blockTextData.get(i); - TextMeasurementSystem.LineMetrics metrics = lineMetrics.get(i); - double baselineOffset = baselineOffset(ltd, metrics); - double currentBottomY = ltd.y() - baselineOffset + yOffset.y() + entityYOffset.y(); - - YPositionOnPage yPositionOnPage = definePositionOnPage( - currentBottomY, - metrics.lineHeight(), - currentPage, - canvas, - entityYOffset, - false, - entity); - - double baselineY = yPositionOnPage.yPosition() + baselineOffset; - // Preserve cached measurement payload and only rewrite page-local placement. - assignPositionTextData.add(new LineTextData(ltd, ltd.x(), baselineY, yPositionOnPage.startPage())); - } - - finalizePageBreakingAndDefinition(entity, blockTextComponent, yOffset, assignPositionTextData, entityYOffset); - return entityYOffset; - } - - private void finalizePageBreakingAndDefinition(Entity entity, - BlockTextData blockTextData, - Offset yOffset, - List assignPositionTextData, - Offset entityYOffset) { - BlockTextData newBlockTextData = blockTextData.withLines(assignPositionTextData); - - var size = entity.getComponent(ContentSize.class).orElseThrow(); - var newSize = new ContentSize(size.width(), size.height() + Math.abs(entityYOffset.y())); - entity.addComponent(newSize); - - yOffset.incrementY(entityYOffset); - log.debug("Returned Offset: {} , {}", yOffset, entity); - entity.addComponent(newBlockTextData); - } - - private YPositionOnPage definePositionOnPage(double currentY, - double textHeight, - int currentPage, - Canvas canvas, - Offset entityYOffset, - boolean isBreakable, - Entity entity) { - double canvasHeight = canvas.height(); - double canvasMarginTop = canvas.margin().top(); - double canvasMarginBottom = canvas.margin().bottom(); - try { - return pageLayoutCalculator.calculatePageCoordinates( - currentY, - textHeight, - 0.0, - 0.0, - currentPage, - canvasHeight, - canvasMarginTop, - canvasMarginBottom, - entityYOffset, - isBreakable, - entity); - } catch (BigSizeElementException ex) { - log.error("BigSizeElementException {}, {}", ex, entity); - throw new RuntimeException(String.format("BigSizeElementException %s, %s", ex, entity.printInfo())); - } - } - - private BlockText.ValidatedTextData getValidatedTextData(Entity e) { - var textValue = e.getComponent(BlockTextData.class).orElse(BlockTextData.empty()); - TextStyle style = e.getComponent(TextStyle.class).orElse(TextStyle.DEFAULT_STYLE); - return new BlockText.ValidatedTextData(style, textValue); - } - - private List resolveLineMetrics(List lines, TextStyle fallbackStyle) { - List metrics = new ArrayList<>(lines.size()); - for (LineTextData line : lines) { - metrics.add(line.hasCachedLineMetrics() - ? line.lineMetrics() - : BlockTextLineMetrics.resolveLineMetrics( - pageLayoutCalculator.getEntityManager(), - line, - fallbackStyle)); - } - return metrics; - } - - private double baselineOffset(LineTextData line, TextMeasurementSystem.LineMetrics metrics) { - if (line.hasCachedBaselineOffset()) { - return line.baselineOffset(); - } - return metrics.baselineOffsetFromBottom(); - } - - /** - * Functional interface to decouple TextProcessor from the main PageBreaker logic. - */ - @FunctionalInterface - public interface PagePositionCalculator { - YPositionOnPage calculate( - double currentY, - double height, - double marginTop, - double marginBottom, - int currentPage, - Canvas canvas, - Offset yOffset, - boolean isBreakable, - Entity entity - ) throws BigSizeElementException; - } -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/YPositionOnPage.java b/src/main/java/com/demcha/compose/engine/pagination/YPositionOnPage.java deleted file mode 100644 index 733514c46..000000000 --- a/src/main/java/com/demcha/compose/engine/pagination/YPositionOnPage.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.demcha.compose.engine.pagination; - -public record YPositionOnPage(double yPosition, int startPage, int endPage){ - public YPositionOnPage(double yPosition, int startPage) { - this(yPosition, startPage, startPage); - } - -} diff --git a/src/main/java/com/demcha/compose/engine/pagination/package-info.java b/src/main/java/com/demcha/compose/engine/pagination/package-info.java index 9f2097079..07d256192 100644 --- a/src/main/java/com/demcha/compose/engine/pagination/package-info.java +++ b/src/main/java/com/demcha/compose/engine/pagination/package-info.java @@ -1,19 +1,16 @@ /** * Pagination and page-placement helpers of the legacy ECS engine. * - *

This package paginates the resolved {@code Entity} graph for the original - * {@code Entity}-based pipeline ({@code PageBreaker} for the pagination walk, - * {@code PageLayoutCalculator} for page-coordinate math, {@code TextBlockProcessor} - * for block-text fragmentation, {@code ParentContainerUpdater} for upward - * size/position propagation). It is renderer-neutral, but it is part of the - * parallel legacy ECS engine: the canonical pipeline - * ({@code com.demcha.compose.document.layout}) does its own pagination and - * imports nothing here.

+ *

This package holds the {@code Entity}-based pagination helpers and markers + * ({@code ParentContainerUpdater} for upward size/position propagation, the + * {@code Breakable} marker, and {@code Offset}). It is renderer-neutral; the + * canonical pipeline ({@code com.demcha.compose.document.layout}) does its own + * pagination.

* - * @deprecated Legacy ECS engine, superseded by the canonical - * {@code com.demcha.compose.document.layout} pipeline. Not reachable from - * any public API; retained only for legacy regression tests and a candidate - * for removal. Do not extend it or spend optimization effort here. + * @deprecated Part of the legacy {@code Entity} engine, superseded by the + * canonical {@code com.demcha.compose.document.layout} pipeline; a candidate + * for removal once the {@code Entity} model is retired. Do not spend + * optimization effort here. */ @Deprecated package com.demcha.compose.engine.pagination; diff --git a/src/main/java/com/demcha/compose/engine/render/EntityRenderOrder.java b/src/main/java/com/demcha/compose/engine/render/EntityRenderOrder.java deleted file mode 100644 index 0b1975a4f..000000000 --- a/src/main/java/com/demcha/compose/engine/render/EntityRenderOrder.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.ComputedPosition; -import com.demcha.compose.engine.components.layout.coordinator.RenderingPosition; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.EntityManager; -import lombok.experimental.UtilityClass; - -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.UUID; - -/** - * Stable renderer-neutral ordering for already-laid-out entities. - * - *

- * This helper belongs to the rendering layer rather than pagination because it - * only answers "in what order should resolved entities be handed to a - * renderer?" - * It does not decide page-breaking or parent/child pagination rules. - *

- * - *

- * The ordering contract is: - *

- *
    - *
  1. higher {@link RenderingPosition#y()} first (top to bottom in engine - * coordinates)
  2. - *
  3. lower {@link RenderingPosition#x()} first for stable left-to-right - * ties
  4. - *
  5. original layer order when provided
  6. - *
  7. {@link UUID} as the final deterministic fallback
  8. - *
- */ -@UtilityClass -public class EntityRenderOrder { - - /** - * Resolves entities from the manager and returns them sorted by rendering - * position. - * - *

- * Duplicate or missing entity IDs are silently skipped. The returned map - * preserves the computed render order. - *

- * - * @param entityManager entity registry to resolve entities from - * @param entityUuids ordered list of entity IDs to include - * @return entities sorted by the {@linkplain #renderOrderComparator rendering - * order} - */ - public static LinkedHashMap sortByRenderingPosition(EntityManager entityManager, - List entityUuids) { - Objects.requireNonNull(entityManager, "entityManager must not be null"); - Objects.requireNonNull(entityUuids, "entityUuids must not be null"); - - LinkedHashMap entities = new LinkedHashMap<>(); - HashSet seen = new HashSet<>(); - - for (int i = 0; i < entityUuids.size(); i++) { - UUID entityId = entityUuids.get(i); - if (!seen.add(entityId)) { - continue; - } - Entity entity = entityManager.getEntity(entityId).orElse(null); - if (entity == null) { - continue; - } - entities.put(entityId, entity); - } - - if (entities.size() <= 1) { - return entities; - } - - List resolvedEntries = new java.util.ArrayList<>(entities.size()); - int originalIndex = 0; - for (var entry : entities.entrySet()) { - resolvedEntries.add(resolveRenderEntry(entry.getKey(), entry.getValue(), originalIndex++)); - } - - resolvedEntries.sort(renderOrderComparator()); - - LinkedHashMap ordered = new LinkedHashMap<>(resolvedEntries.size()); - for (RenderEntry entry : resolvedEntries) { - ordered.put(entry.id(), entry.entity()); - } - return ordered; - } - - /** - * Builds the render-order comparator using Y descending, X ascending, original - * layer index, and UUID string as the final deterministic fallback. - */ - private static Comparator renderOrderComparator() { - return Comparator - .comparingDouble(RenderEntry::y) - .reversed() - .thenComparingDouble(RenderEntry::x) - .thenComparingInt(RenderEntry::originalIndex) - .thenComparing(entry -> entry.id().toString()); - } - - /** - * Resolves the lightweight sort payload for one entity. - * - *

- * This precomputation keeps component lookups and margin fallback handling out - * of the comparator hot path. - *

- * - * @param entityId entity identifier - * @param entity resolved entity instance - * @param originalIndex original order inside the incoming layer - * @return immutable render-order entry - */ - private static RenderEntry resolveRenderEntry(UUID entityId, Entity entity, int originalIndex) { - ComputedPosition computedPosition = entity.getComponent(ComputedPosition.class) - .orElseThrow(() -> new IllegalStateException("Entity " + entity + " has no RenderingPosition")); - Margin margin = entity.getComponent(Margin.class).orElse(Margin.zero()); - double x = computedPosition.x() + margin.left(); - double y = computedPosition.y() + margin.bottom(); - return new RenderEntry(entityId, entity, x, y, originalIndex); - } - - /** - * Immutable sort payload used during one render-order computation. - */ - private record RenderEntry(UUID id, Entity entity, double x, double y, int originalIndex) { - } -} diff --git a/src/main/java/com/demcha/compose/engine/render/RenderingSystemBase.java b/src/main/java/com/demcha/compose/engine/render/RenderingSystemBase.java deleted file mode 100644 index 759409db3..000000000 --- a/src/main/java/com/demcha/compose/engine/render/RenderingSystemBase.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.demcha.compose.engine.render; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.content.shape.Side; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.render.guides.GuidLineSettings; -import com.demcha.compose.engine.render.guides.GuidesRenderer; -import lombok.Data; -import lombok.experimental.Accessors; - -import java.awt.*; -import java.io.IOException; -import java.util.Optional; -import java.util.Set; - -/** - * Abstract base class for rendering systems within the Entity-Component-System (ECS) architecture. - * This class provides common functionalities and abstract methods for rendering various graphical elements - * such as borders, rectangles, and circles onto a rendering stream. It integrates with a {@link Canvas}, - * {@link GuidLineSettings}, and a {@link RenderStream} to manage rendering operations. - * - * @param The type of the rendering stream, which must implement {@link AutoCloseable}. - */ -@Data -@Accessors(fluent = true) -public abstract class RenderingSystemBase implements RenderingSystemECS { - protected final Canvas canvas; - protected final GuidLineSettings guidLineSettings; - protected final RenderStream stream; - protected GuidesRenderer guidesRenderer; - protected final RenderHandlerRegistry renderHandlers = new RenderHandlerRegistry(); - protected RenderPassSession activeRenderSession; - - - /** - * Renders a border around a specified context. - * - * @param stream The rendering stream to draw on. - * @param context The rendering coordinate context defining the area for the border. - * @param lineDash A boolean indicating whether the border should be dashed. - * @param sides A set of {@link Side} enums indicating which sides of the context to render the border. - * @return {@code true} if the border was rendered successfully, {@code false} otherwise. - * @throws IOException If an I/O error occurs during rendering. - */ - public abstract boolean renderBorder(T stream, RenderCoordinateContext context, - boolean lineDash, - Set sides) throws IOException; - - /** - * Renders a rectangle on the rendering stream. - * - * @param stream The rendering stream to draw on. - * @param context The rendering coordinate context defining the rectangle's position and dimensions. - * @param lineDash A boolean indicating whether the rectangle's outline should be dashed. - * @return {@code true} if the rectangle was rendered successfully, {@code false} otherwise. - * @throws IOException If an I/O error occurs during rendering. - */ - @Override - public abstract boolean renderRectangle(T stream, RenderCoordinateContext context, boolean lineDash) throws IOException; - - /** - * Fills a circle with a specified color on the rendering stream. - * - * @param stream The rendering stream to draw on. - * @param cx The x-coordinate of the circle's center. - * @param cy The y-coordinate of the circle's center. - * @param r The radius of the circle. - * @param fill The {@link Color} to fill the circle with. - */ - public abstract void fillCircle(T stream, float cx, float cy, float r, Color fill) throws IOException; - - - /** - * Processes entities within the {@link EntityManager} for rendering. - * This method defines the core logic for how the rendering system interacts with entities - * to perform its rendering tasks. - * - * @param entityManager The entity manager containing the entities to be processed. - */ - public abstract void process(EntityManager entityManager); - - @Override - public Optional> activeRenderSession() { - return Optional.ofNullable(activeRenderSession); - } - - /** - * Initializes the {@link GuidesRenderer}. - *

- * Subclasses must implement this to provide a concrete, format-specific renderer (e.g., for PDF or images). - * This method is intended to be called in the constructor to ensure the renderer is immediately available. - * - *

{@code
-     * // Example implementation:
-     * public PdfRenderingSystemECS(PDDocument doc, Canvas canvas) {
-     * super(canvas, new GuidLineSettings(), new PdfStream(doc, canvas));
-     * // Init and set the guides renderer
-     * guidesRendererInitializer(new PdfGuidesRenderer(this));
-     * }
-     *
-     * @Override
-     * protected void guidesRendererInitializer(GuidesRenderer renderer) {
-     * guidesRenderer(renderer); // Call base setter
-     * }
-     * }
- * - * @param guidesRenderer The {@link GuidesRenderer} instance used to draw debug guides. - */ - protected abstract void guidesRendererInitializer(GuidesRenderer guidesRenderer); - -} - diff --git a/src/main/java/com/demcha/compose/engine/render/guides/impl/BoxRenderImpl.java b/src/main/java/com/demcha/compose/engine/render/guides/impl/BoxRenderImpl.java deleted file mode 100644 index f81035adf..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/impl/BoxRenderImpl.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.demcha.compose.engine.render.guides.impl; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.Placement; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.render.RenderingSystemECS; -import com.demcha.compose.engine.render.guides.BoxRender; -import lombok.Data; -import lombok.NonNull; -import lombok.ToString; -import lombok.experimental.Accessors; - -import java.io.IOException; -import java.util.Objects; - -@Accessors(fluent = true) -@Data -public final class BoxRenderImpl implements BoxRender { - @ToString.Exclude - private final RenderingSystemECS renderingSystem; - - public BoxRenderImpl(RenderingSystemECS renderingSystem) { - this.renderingSystem = renderingSystem; - } - - /** - * @param entity - * @param stream - * @return - */ - @Override - public boolean fromStream(@NonNull Entity entity, T stream) { - Placement placement = entity.getComponent(Placement.class).orElseThrow(); - var color = renderingSystem().guidLineSettings().BOX_COLOR(); - var stroke = renderingSystem().guidLineSettings().BOX_STROKE(); - RenderCoordinateContext context = new RenderCoordinateContext(placement.x(), placement.y(), placement.width(), placement.height(), placement.startPage(), placement.endPage(), stroke, color); - - try { - return renderingSystem().renderRectangle(stream, context, false); - } catch (IOException e) { - throw new RuntimeException(e); - } - - } - - @Override - public RenderingSystemECS renderingSystem() { - return renderingSystem; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (BoxRenderImpl) obj; - return Objects.equals(this.renderingSystem, that.renderingSystem); - } - - @Override - public int hashCode() { - return Objects.hash(renderingSystem); - } - - @Override - public String toString() { - return "BoxRenderImpl[" + - "renderingSystem=" + renderingSystem + ']'; - } - - -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/impl/MarginRenderImpl.java b/src/main/java/com/demcha/compose/engine/render/guides/impl/MarginRenderImpl.java deleted file mode 100644 index 8611d0698..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/impl/MarginRenderImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.demcha.compose.engine.render.guides.impl; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.render.RenderingSystemECS; -import com.demcha.compose.engine.render.guides.MarginRender; -import lombok.Data; -import lombok.NonNull; -import lombok.ToString; -import lombok.experimental.Accessors; - -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; - -/** - * - */ -@Accessors(fluent = true) -@Data -public final class MarginRenderImpl implements MarginRender { - @ToString.Exclude - private final RenderingSystemECS renderingSystem; - - /** - * @param renderingSystem - */ - public MarginRenderImpl( - RenderingSystemECS renderingSystem) { - this.renderingSystem = renderingSystem; - } - - /** - * @param entity - * @param stream - * @return - */ - @Override - public boolean fromStream(@NonNull Entity entity, T stream) { - Optional marginOpt = margin(entity); - if (marginOpt.isPresent()) { - try { - var margin = marginOpt.get(); - renderingSystem().renderRectangle(stream, margin, true); - markers(stream, margin); - return true; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return false; - - } - - @Override - public RenderingSystemECS renderingSystem() { - return renderingSystem; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (MarginRenderImpl) obj; - return Objects.equals(this.renderingSystem, that.renderingSystem); - } - - @Override - public int hashCode() { - return Objects.hash(renderingSystem); - } - - @Override - public String toString() { - return "MarginRenderImpl[" + - "renderingSystem=" + renderingSystem + ']'; - } - - -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/impl/PaddingRenderImpl.java b/src/main/java/com/demcha/compose/engine/render/guides/impl/PaddingRenderImpl.java deleted file mode 100644 index aba99035f..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/impl/PaddingRenderImpl.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.demcha.compose.engine.render.guides.impl; - -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.layout.coordinator.RenderCoordinateContext; -import com.demcha.compose.engine.render.RenderingSystemECS; -import com.demcha.compose.engine.render.guides.PaddingRender; -import lombok.Data; -import lombok.NonNull; -import lombok.ToString; -import lombok.experimental.Accessors; - -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; - -/** - * - */ -@Accessors(fluent = true) -@Data -public final class PaddingRenderImpl implements PaddingRender { - @ToString.Exclude - private final RenderingSystemECS renderingSystem; - - /** - * @param renderingSystem - */ - public PaddingRenderImpl( - RenderingSystemECS renderingSystem) { - this.renderingSystem = renderingSystem; - } - - /** - * @param entity - * @param stream - * @return - */ - @Override - public boolean fromStream(@NonNull Entity entity, T stream) { - Optional padding = padding(entity); - if (padding.isPresent()) { - try { - renderingSystem().renderRectangle(stream, padding.get(), true); - markers(stream, padding.get()); - return true; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - return false; - } - - @Override - public RenderingSystemECS renderingSystem() { - return renderingSystem; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (PaddingRenderImpl) obj; - return Objects.equals(this.renderingSystem, that.renderingSystem); - } - - @Override - public int hashCode() { - return Objects.hash(renderingSystem); - } - - @Override - public String toString() { - return "PaddingRenderImpl[" + - "renderingSystem=" + renderingSystem + ']'; - } - - -} diff --git a/src/main/java/com/demcha/compose/engine/render/guides/impl/package-info.java b/src/main/java/com/demcha/compose/engine/render/guides/impl/package-info.java deleted file mode 100644 index 6642a6f01..000000000 --- a/src/main/java/com/demcha/compose/engine/render/guides/impl/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Debug guide-line rendering contracts and implementations. - * - *

Ownership: Owned by render diagnostics and used only when guide overlays are enabled.

- *

Extension rules: Extend per backend without changing document authoring APIs.

- */ -package com.demcha.compose.engine.render.guides.impl; \ No newline at end of file diff --git a/src/main/java/com/demcha/compose/engine/utils/TextSanitizer.java b/src/main/java/com/demcha/compose/engine/utils/TextSanitizer.java deleted file mode 100644 index 2a0dea075..000000000 --- a/src/main/java/com/demcha/compose/engine/utils/TextSanitizer.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.demcha.compose.engine.utils; - -import java.text.Normalizer; -import java.util.Map; - -public final class TextSanitizer { - - private static final Map REPLACEMENTS = Map.ofEntries( - Map.entry(0x2192, "->"), // → right arrow - Map.entry(0x2190, "<-"), // ← left arrow - Map.entry(0x2022, "*"), // • bullet - Map.entry(0x2013, "-"), // – en dash - Map.entry(0x2014, "--"), // — em dash - Map.entry(0x2018, "'"), // ‘ - Map.entry(0x2019, "'"), // ’ - Map.entry(0x201C, "\""), // “ - Map.entry(0x201D, "\""), // ” - Map.entry(0x2026, "..."), // … ellipsis - Map.entry(0x00A0, " ") // NBSP - ); - - private TextSanitizer() {} - - /** Normalize + replace (no font checks). */ - public static String sanitize(String raw) { - if (raw == null || raw.isBlank()) return ""; - - String normalized = Normalizer.normalize(raw, Normalizer.Form.NFKC); - StringBuilder out = new StringBuilder(normalized.length()); - - for (int i = 0; i < normalized.length(); ) { - int cp = normalized.codePointAt(i); - i += Character.charCount(cp); - - // Keep common whitespace, remove other control chars - if (Character.isISOControl(cp) && cp != '\n' && cp != '\r' && cp != '\t') { - continue; - } - - String candidate = REPLACEMENTS.getOrDefault(cp, new String(Character.toChars(cp))); - out.append(candidate); // <-- IMPORTANT: append and continue - } - - return out.toString(); - } -} diff --git a/src/test/java/com/demcha/compose/engine/pagination/ContainerExpanderTest.java b/src/test/java/com/demcha/compose/engine/pagination/ContainerExpanderTest.java deleted file mode 100644 index bc7839f47..000000000 --- a/src/test/java/com/demcha/compose/engine/pagination/ContainerExpanderTest.java +++ /dev/null @@ -1,388 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.components.core.Component; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.geometry.InnerBoxSize; -import com.demcha.compose.engine.components.geometry.OuterBoxSize; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.exceptions.ContentSizeNotFoundException; -import com.demcha.compose.engine.components.geometry.Expendable; -import com.demcha.compose.engine.layout.container.ContainerExpander; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.within; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Unit tests for the {@link ContainerExpander} class. - * This class uses Mockito for mocking dependencies and AssertJ for fluent assertions. - * It assumes that InnerBoxSize and OuterBoxSize are retrieved via static .from() methods. - */ -@ExtendWith(MockitoExtension.class) -@DisplayName("ContainerExpander Tests") -class ContainerExpanderTest { - - // A small value for floating-point comparisons. - private static final double EPSILON = 1e-6; - - /** A concrete, named class for testing the Expendable marker interface. */ - private static class TestExpendable implements Expendable, Component {} - - /** - * Helper method to create a child entity. - * The OuterBoxSize is not added directly but should be mocked in tests. - * - * @return A new Entity representing a child. - */ - private Entity createChild() { - Entity child = new Entity(); - child.addComponent(Margin.zero()); // Add default margin - return child; - } - - @Nested - @DisplayName("expandContentSizeByChildren() Method") - class ExpandContentSizeByChildrenTests { - - private Entity parent; - private MockedStatic mockedInnerBoxSize; - private MockedStatic mockedOuterBoxSize; - - - @BeforeEach - void setUp() { - // Mock static .from() methods before each test - mockedInnerBoxSize = mockStatic(InnerBoxSize.class); - mockedOuterBoxSize = mockStatic(OuterBoxSize.class); - - parent = new Entity(); - parent.addComponent(new ContentSize(100, 80)); - - // Configure the mock for the parent's InnerBoxSize - mockedInnerBoxSize.when(() -> InnerBoxSize.from(eq(parent))) - .thenReturn(Optional.of(new InnerBoxSize(100, 80))); - } - - @AfterEach - void tearDown() { - // Close static mocks to prevent test pollution - mockedInnerBoxSize.close(); - mockedOuterBoxSize.close(); - } - - @Test - @DisplayName("Should throw NullPointerException if parent is null") - void expandContentSize_withNullParent_shouldThrowException() { - assertThrows(NullPointerException.class, - () -> ContainerExpander.expandContentSizeByChildren(null, Collections.emptySet()), - "parent cannot be null"); - } - - @Test - @DisplayName("Should throw NullPointerException if children set is null") - void expandContentSize_withNullChildren_shouldThrowException() { - assertThrows(NullPointerException.class, - () -> ContainerExpander.expandContentSizeByChildren(parent, null), - "children cannot be null"); - } - - @Test - @DisplayName("Should throw IllegalStateException if parent has no InnerBoxSize") - void expandContentSize_parentMissingInnerBoxSize_shouldThrowException() { - Entity parentWithoutInnerBox = new Entity(); - // Mock InnerBoxSize.from() to return empty for this specific entity - mockedInnerBoxSize.when(() -> InnerBoxSize.from(eq(parentWithoutInnerBox))).thenReturn(Optional.empty()); - - Set children = Set.of(createChild()); - - Exception exception = assertThrows(IllegalStateException.class, - () -> ContainerExpander.expandContentSizeByChildren(parentWithoutInnerBox, children)); - - assertThat(exception.getMessage()).contains("Parent is missing InnerBoxSize"); - } - - @Test - @DisplayName("Should throw ContentSizeNotFoundException if parent has no ContentSize") - void expandContentSize_parentMissingContentSize_shouldThrowException() { - Entity parentWithoutContentSize = new Entity(); - // Configure mock for its InnerBoxSize - mockedInnerBoxSize.when(() -> InnerBoxSize.from(eq(parentWithoutContentSize))) - .thenReturn(Optional.of(new InnerBoxSize(50, 50))); - - Entity child = createChild(); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(child))) - .thenReturn(Optional.of(new OuterBoxSize(100, 100))); - - Set children = Set.of(child); // Child is larger - - assertThrows(ContentSizeNotFoundException.class, - () -> ContainerExpander.expandContentSizeByChildren(parentWithoutContentSize, children)); - } - - @Test - @DisplayName("Should return false and not expand for an empty set of children") - void expandContentSize_withEmptyChildren_shouldReturnFalse() { - boolean result = ContainerExpander.expandContentSizeByChildren(parent, Collections.emptySet()); - - assertFalse(result); - ContentSize finalSize = parent.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isEqualTo(100); - assertThat(finalSize.height()).isEqualTo(80); - } - - @Test - @DisplayName("Should not expand if children fit within the parent's inner box") - void expandContentSize_whenChildrenFit_shouldNotExpand() { - Entity child1 = createChild(); - Entity child2 = createChild(); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(child1))).thenReturn(Optional.of(new OuterBoxSize(50, 40))); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(child2))).thenReturn(Optional.of(new OuterBoxSize(99, 79))); - - Set children = Set.of(child1, child2); - - boolean expanded = ContainerExpander.expandContentSizeByChildren(parent, children); - - assertFalse(expanded, "Should not expand as children fit"); - ContentSize finalSize = parent.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isEqualTo(100); - assertThat(finalSize.height()).isEqualTo(80); - } - - @Test - @DisplayName("Should expand width when a child is wider") - void expandContentSize_whenChildIsWider_shouldExpandWidth() { - Entity child = createChild(); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(child))) - .thenReturn(Optional.of(new OuterBoxSize(120, 70))); - Set children = Set.of(child); - - boolean expanded = ContainerExpander.expandContentSizeByChildren(parent, children); - - assertTrue(expanded, "Should expand because a child is wider"); - ContentSize finalSize = parent.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isCloseTo(120, within(EPSILON)); - assertThat(finalSize.height()).isCloseTo(80, within(EPSILON)); - } - - @Test - @DisplayName("Should expand height when a child is taller") - void expandContentSize_whenChildIsTaller_shouldExpandHeight() { - Entity child = createChild(); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(child))) - .thenReturn(Optional.of(new OuterBoxSize(90, 100))); - Set children = Set.of(child); - - boolean expanded = ContainerExpander.expandContentSizeByChildren(parent, children); - - assertTrue(expanded, "Should expand because a child is taller"); - ContentSize finalSize = parent.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isCloseTo(100, within(EPSILON)); - assertThat(finalSize.height()).isCloseTo(100, within(EPSILON)); - } - - @Test - @DisplayName("Should expand both width and height when a child is larger in both dimensions") - void expandContentSize_whenChildIsLarger_shouldExpandBothDimensions() { - Entity child = createChild(); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(child))) - .thenReturn(Optional.of(new OuterBoxSize(150, 120))); - Set children = Set.of(child); - - boolean expanded = ContainerExpander.expandContentSizeByChildren(parent, children); - - assertTrue(expanded, "Should expand because a child is larger"); - ContentSize finalSize = parent.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isCloseTo(150, within(EPSILON)); - assertThat(finalSize.height()).isCloseTo(120, within(EPSILON)); - } - - @Test - @DisplayName("Should expand to max width and height from different children") - void expandContentSize_withMultipleChildren_shouldExpandToMaxDimensions() { - Entity widestChild = createChild(); - Entity tallestChild = createChild(); - Entity smallestChild = createChild(); - - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(widestChild))).thenReturn(Optional.of(new OuterBoxSize(140, 70))); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(tallestChild))).thenReturn(Optional.of(new OuterBoxSize(90, 110))); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(smallestChild))).thenReturn(Optional.of(new OuterBoxSize(50, 50))); - - Set children = Set.of(widestChild, tallestChild, smallestChild); - - boolean expanded = ContainerExpander.expandContentSizeByChildren(parent, children); - - assertTrue(expanded, "Should expand to max dimensions from different children"); - ContentSize finalSize = parent.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isCloseTo(140, within(EPSILON)); - assertThat(finalSize.height()).isCloseTo(110, within(EPSILON)); - } - - @Test - @DisplayName("Should ignore children without an OuterBoxSize component") - void expandContentSize_shouldIgnoreChildWithoutOuterBoxSize() { - Entity childWithoutSize = createChild(); - Entity childWithSize = createChild(); - - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(childWithoutSize))).thenReturn(Optional.empty()); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(childWithSize))).thenReturn(Optional.of(new OuterBoxSize(50, 50))); - - Set children = Set.of(childWithoutSize, childWithSize); - - boolean expanded = ContainerExpander.expandContentSizeByChildren(parent, children); - - assertFalse(expanded, "Should not expand as the only sized child fits"); - } - } - - @Nested - @DisplayName("process() Method") - class ProcessMethodTests { - - @Mock - private EntityManager entityManager; - private MockedStatic mockedInnerBoxSize; - private MockedStatic mockedOuterBoxSize; - - private UUID parentUuid; - private Entity parentEntity; - private Map> childrenByParents; - - @BeforeEach - void setUp() { - mockedInnerBoxSize = mockStatic(InnerBoxSize.class); - mockedOuterBoxSize = mockStatic(OuterBoxSize.class); - - parentEntity = new Entity(); - // Assuming Entity has getUuid() to retrieve the auto-generated UUID - parentUuid = parentEntity.getUuid(); - - parentEntity.addComponent(new ContentSize(100, 100)); - mockedInnerBoxSize.when(() -> InnerBoxSize.from(eq(parentEntity))) - .thenReturn(Optional.of(new InnerBoxSize(100, 100))); - - childrenByParents = new LinkedHashMap<>(); // Use LinkedHashMap for predictable iteration - } - - @AfterEach - void tearDown() { - mockedInnerBoxSize.close(); - mockedOuterBoxSize.close(); - } - - @Test - @DisplayName("Should skip processing if parent entity is not found") - void process_whenParentNotFound_shouldSkip() { - when(entityManager.getEntity(parentUuid)).thenReturn(Optional.empty()); - childrenByParents.put(parentUuid, Collections.emptySet()); - - ContainerExpander.process(childrenByParents, entityManager); - - verify(entityManager, never()).getSetEntitiesFromUuids(any()); - } - - @Test - @DisplayName("Should skip parent that is not 'Expendable'") - void process_whenParentIsNotExpendable_shouldSkip() { - when(entityManager.getEntity(parentUuid)).thenReturn(Optional.of(parentEntity)); - childrenByParents.put(parentUuid, Collections.emptySet()); - - ContainerExpander.process(childrenByParents, entityManager); - - verify(entityManager, never()).getSetEntitiesFromUuids(any()); - ContentSize finalSize = parentEntity.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).as("Size should not change").isEqualTo(100); - } - - @Test - @DisplayName("Should process and expand an 'Expendable' parent") - void process_withExpendableParent_shouldExpand() { - // FIX: Added the Expendable component to ensure the parent is processed. - parentEntity.addComponent(new TestExpendable()); - - Entity childEntity1 = createChild(); - UUID childUuid1 = childEntity1.getUuid(); - Set childUuids = Set.of(childUuid1); - Set childEntities = Set.of(childEntity1); - - // FIX: Used the parent's UUID as the key, not the child's. - childrenByParents.put(parentUuid, childUuids); - - when(entityManager.getEntity(parentUuid)).thenReturn(Optional.of(parentEntity)); - when(entityManager.getSetEntitiesFromUuids(childUuids)).thenReturn(childEntities); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(childEntity1))) - .thenReturn(Optional.of(new OuterBoxSize(150, 80))); - - ContainerExpander.process(childrenByParents, entityManager); - - ContentSize finalSize = parentEntity.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize.width()).isCloseTo(150, within(EPSILON)); - assertThat(finalSize.height()).isCloseTo(100, within(EPSILON)); - } - - @Test - @DisplayName("Should handle multiple parents correctly") - void process_withMultipleParents_shouldProcessAll() { - // --- Parent 1 (Expendable, should expand) --- - // FIX: Added the Expendable component. - parentEntity.addComponent(new TestExpendable()); - Entity childEntity1 = createChild(); - UUID childUuid1 = childEntity1.getUuid(); - childrenByParents.put(parentUuid, Set.of(childUuid1)); - when(entityManager.getEntity(parentUuid)).thenReturn(Optional.of(parentEntity)); - when(entityManager.getSetEntitiesFromUuids(Set.of(childUuid1))).thenReturn(Set.of(childEntity1)); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(childEntity1))) - .thenReturn(Optional.of(new OuterBoxSize(120, 90))); - - // --- Parent 2 (Not Expendable, should be skipped) --- - Entity parent2Entity = new Entity(); - UUID parent2Uuid = parent2Entity.getUuid(); - parent2Entity.addComponent(new ContentSize(50, 50)); - mockedInnerBoxSize.when(() -> InnerBoxSize.from(eq(parent2Entity))) - .thenReturn(Optional.of(new InnerBoxSize(50, 50))); - childrenByParents.put(parent2Uuid, Collections.emptySet()); - when(entityManager.getEntity(parent2Uuid)).thenReturn(Optional.of(parent2Entity)); - - // --- Parent 3 (Expendable, but children fit, should not expand) --- - Entity parent3Entity = new Entity(); - UUID parent3Uuid = parent3Entity.getUuid(); - // FIX: Added the Expendable component. - parent3Entity.addComponent(new TestExpendable()); - parent3Entity.addComponent(new ContentSize(200, 200)); - mockedInnerBoxSize.when(() -> InnerBoxSize.from(eq(parent3Entity))) - .thenReturn(Optional.of(new InnerBoxSize(200, 200))); - UUID childUuid3 = UUID.randomUUID(); - Entity childEntity3 = createChild(); - childrenByParents.put(parent3Uuid, Set.of(childUuid3)); - when(entityManager.getEntity(parent3Uuid)).thenReturn(Optional.of(parent3Entity)); - when(entityManager.getSetEntitiesFromUuids(Set.of(childUuid3))).thenReturn(Set.of(childEntity3)); - mockedOuterBoxSize.when(() -> OuterBoxSize.from(eq(childEntity3))) - .thenReturn(Optional.of(new OuterBoxSize(10, 10))); - - - // Execute - ContainerExpander.process(childrenByParents, entityManager); - - // Verify Parent 1 expanded - ContentSize finalSize1 = parentEntity.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize1.width()).isCloseTo(120, within(EPSILON)); - - // Verify Parent 2 did not change - ContentSize finalSize2 = parent2Entity.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize2.width()).isEqualTo(50); - - // Verify Parent 3 did not change - ContentSize finalSize3 = parent3Entity.getComponent(ContentSize.class).orElseThrow(); - assertThat(finalSize3.width()).isEqualTo(200); - } - } -} diff --git a/src/test/java/com/demcha/compose/engine/pagination/PageBreakerTest.java b/src/test/java/com/demcha/compose/engine/pagination/PageBreakerTest.java deleted file mode 100644 index 35be44c69..000000000 --- a/src/test/java/com/demcha/compose/engine/pagination/PageBreakerTest.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.pagination.PageLayoutCalculator; -import com.demcha.compose.engine.pagination.YPositionOnPage; -import com.demcha.mock.FactoryPresets; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; - - -class PageBreakerTest { - @Mock - private EntityManager entityManager; - - @InjectMocks - private PageLayoutCalculator pageLayoutCalculator; - - static Stream provideScenarios() { - - return Stream.of( - // Сценарий 1: Стандартный А4, маленький объект - Arguments.of( - "Standard A4 Flow", // Название - 100.0, //Current positionY - 0,// Current page - FactoryPresets.OBJ_SMALL_BOX_100_50, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.MARGIN_STANDARD_20, - FactoryPresets.CANVAS_A4_STANDARD, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - 0.0, - new YPositionOnPage(100, 0, 0) // Ожидаемый результат - ), - Arguments.of( - "Full Width No Margins", // Название - 100.0, - 1,// Current Y - FactoryPresets.OBJ_FULL_WIDTH_500_100, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.MARGIN_ZERO, - FactoryPresets.CANVAS_A4_NO_MARGINS, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.OFFSET_DATA_ALL_2, // Отступы объекта - false, - 2, - new YPositionOnPage(100, 1, 1) // Ожидаемый результат - ), - Arguments.of( - "Full Width No Margins", // Название - 5.0, - 1,// Current Y - FactoryPresets.OBJ_SMALL_BOX_100_50, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.MARGIN_ZERO, - FactoryPresets.CANVAS_A4_NO_MARGINS, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - 0, - new YPositionOnPage(5, 1, 1) // Ожидаемый результат - ), - Arguments.of( - "Next page Position under Margin", // Название - 10.0, - 0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - -80.0, - new YPositionOnPage(30, 1, 1) // Ожидаемый результат - ), - Arguments.of( - "Next page Position under Margin 2", // Название - 10.0, - 0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - -80, - new YPositionOnPage(30, 1, 1) // Ожидаемый результат - ), - - Arguments.of( - "Next page negative position", // Название - -30, - 0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - -40, - new YPositionOnPage(30, 1, 1) // Ожидаемый результат - ) - - - - ); - } - - // --- 1. Data Provider (Сценарии) --- - static Stream provideDownShiftScenarios() { - return Stream.of( - // 1. Early Exit (yPos < MarginBottom) - Arguments.of( - "Early Exit", - 10.0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - -80.0 - ), - - // 2. Normal Fit - Arguments.of( - "Normal Fit", - 50.0, - FactoryPresets.OBJ_W100_H20, // H=20 - FactoryPresets.MARGIN_VERT_5, // T=5, B=5 - FactoryPresets.CANVAS_TEST_100_200_M_T10_B20, - 0.0 - ), - - // 3. Edge Case: Fits exactly - Arguments.of( - "Fits Exactly", - 30.0, - FactoryPresets.OBJ_W100_H60, // H=60 - FactoryPresets.MARGIN_VERT_10, - FactoryPresets.CANVAS_TEST_100_100_M_10, - -10.0 - ), - - // 4. Overflow with small Y - Arguments.of( - "Overflow Small Y", - 5.0, - FactoryPresets.OBJ_W100_H40, // H=40 - FactoryPresets.MARGIN_VERT_10, - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - -65.0 - ), - - // 5. Standard A4 - Arguments.of( - "Standard A4 Zero Shift", - 100.0, - FactoryPresets.OBJ_SMALL_BOX_100_50, - FactoryPresets.MARGIN_STANDARD_20, - FactoryPresets.CANVAS_A4_STANDARD, - 0.0 - ) - ); - } - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - - @MethodSource("provideScenarios") - @ParameterizedTest(name = "{0}") - void pageBreaker(){ - - } - - -} \ No newline at end of file diff --git a/src/test/java/com/demcha/compose/engine/pagination/PageLayoutCalculatorTest.java b/src/test/java/com/demcha/compose/engine/pagination/PageLayoutCalculatorTest.java deleted file mode 100644 index 8e0def1a4..000000000 --- a/src/test/java/com/demcha/compose/engine/pagination/PageLayoutCalculatorTest.java +++ /dev/null @@ -1,288 +0,0 @@ -package com.demcha.compose.engine.pagination; - -import com.demcha.compose.engine.core.Canvas; -import com.demcha.compose.engine.components.core.Entity; -import com.demcha.compose.engine.components.geometry.ContentSize; -import com.demcha.compose.engine.components.style.Margin; -import com.demcha.compose.engine.core.EntityManager; -import com.demcha.compose.engine.exceptions.BigSizeElementException; -import com.demcha.compose.engine.pagination.Offset; -import com.demcha.compose.engine.pagination.PageLayoutCalculator; -import com.demcha.compose.engine.pagination.YPositionOnPage; -import com.demcha.mock.FactoryClasses; -import com.demcha.mock.FactoryPresets; -import com.demcha.mock.data.CanvasData; -import com.demcha.mock.data.MarginData; -import com.demcha.mock.data.OffsetData; -import com.demcha.mock.data.SizeData; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -class PageLayoutCalculatorTest { - - @Mock - private EntityManager entityManager; - - @InjectMocks - private PageLayoutCalculator pageLayoutCalculator; - - static Stream provideScenarios() { - - return Stream.of( - // Сценарий 1: Стандартный А4, маленький объект - Arguments.of( - "Standard A4 Flow", // Название - 100.0, //Current positionY - 0,// Current page - FactoryPresets.OBJ_SMALL_BOX_100_50, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.MARGIN_STANDARD_20, - FactoryPresets.CANVAS_A4_STANDARD, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - 0.0, - new YPositionOnPage(100, 0, 0) // Ожидаемый результат - ), - Arguments.of( - "Full Width No Margins", // Название - 100.0, - 1,// Current Y - FactoryPresets.OBJ_FULL_WIDTH_500_100, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.MARGIN_ZERO, - FactoryPresets.CANVAS_A4_NO_MARGINS, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.OFFSET_DATA_ALL_2, // Отступы объекта - false, - 2, - new YPositionOnPage(100, 1, 1) // Ожидаемый результат - ), - Arguments.of( - "Full Width No Margins", // Название - 5.0, - 1,// Current Y - FactoryPresets.OBJ_SMALL_BOX_100_50, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.MARGIN_ZERO, - FactoryPresets.CANVAS_A4_NO_MARGINS, // <--- БЕРЕМ ГОТОВЫЙ ПРЕСЕТ - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - 0, - new YPositionOnPage(5, 1, 1) // Ожидаемый результат - ), - Arguments.of( - "Next page Position under Margin", // Название - 10.0, - 0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - -80.0, - new YPositionOnPage(30, 1, 1) // Ожидаемый результат - ), - Arguments.of( - "Next page Position under Margin 2", // Название - 10.0, - 0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - -80, - new YPositionOnPage(30, 1, 1) // Ожидаемый результат - ), - - Arguments.of( - "Next page negative position", // Название - -30, - 0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - FactoryPresets.OFFSET_DATA_ZERO, // Отступы объекта - false, - -40, - new YPositionOnPage(30, 1, 1) // Ожидаемый результат - ) - - - - ); - } - - // --- 1. Data Provider (Сценарии) --- - static Stream provideDownShiftScenarios() { - return Stream.of( - // 1. Early Exit (yPos < MarginBottom) - Arguments.of( - "Early Exit", - 10.0, - FactoryPresets.OBJ_SMALL_BOX_100_50, // H=50 - FactoryPresets.MARGIN_VERT_10, // T=10, B=10 - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - -80.0 - ), - - // 2. Normal Fit - Arguments.of( - "Normal Fit", - 50.0, - FactoryPresets.OBJ_W100_H20, // H=20 - FactoryPresets.MARGIN_VERT_5, // T=5, B=5 - FactoryPresets.CANVAS_TEST_100_200_M_T10_B20, - 0.0 - ), - - // 3. Edge Case: Fits exactly - Arguments.of( - "Fits Exactly", - 30.0, - FactoryPresets.OBJ_W100_H60, // H=60 - FactoryPresets.MARGIN_VERT_10, - FactoryPresets.CANVAS_TEST_100_100_M_10, - -10.0 - ), - - // 4. Overflow with small Y - Arguments.of( - "Overflow Small Y", - 5.0, - FactoryPresets.OBJ_W100_H40, // H=40 - FactoryPresets.MARGIN_VERT_10, - FactoryPresets.CANVAS_TEST_100_100_M_T10_B20, - -65.0 - ), - - // 5. Standard A4 - Arguments.of( - "Standard A4 Zero Shift", - 100.0, - FactoryPresets.OBJ_SMALL_BOX_100_50, - FactoryPresets.MARGIN_STANDARD_20, - FactoryPresets.CANVAS_A4_STANDARD, - 0.0 - ) - ); - } - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - @ParameterizedTest(name = "{0}") // Использует первый аргумент (String) как имя - @MethodSource("provideDownShiftScenarios") - void testDownShift_Refactored( - String description, - double yPosition, - SizeData elementSize, - MarginData elementMargin, - CanvasData canvas, - double expectedResult - ) throws BigSizeElementException { - // Мы "распаковываем" объекты обратно в примитивы для вызова старого метода - // Если ваш метод downShift принимает объекты, код станет еще чище! - - double result = PageLayoutCalculator.downShift( - yPosition, - elementSize.height(), - elementMargin.bottom(), - elementMargin.top(), - canvas.height(), - canvas.margin().bottom(), - canvas.margin().top() - ); - - assertThat(result) - .as("Scenario: " + description) - .isEqualTo(expectedResult); - } - - // 💡 Exception Handling Test - @Test - @DisplayName("Should throw BigSizeElementException when element is taller than canvas inner height") - void testDownShift_ThrowsException_WhenElementTooLarge() { - // Given - double elementHeight = 100.0; - double elementMarginBottom = 10.0; - double elementMarginTop = 10.0; - // Total Element = 120.0 - - double canvasHeight = 100.0; - double canvasMarginBottom = 10.0; - double canvasMarginTop = 10.0; - // Canvas Inner = 80.0 - - // When & Then - - assertThatThrownBy(() -> PageLayoutCalculator.downShift( - 50.0, elementHeight, elementMarginBottom, elementMarginTop, - canvasHeight, canvasMarginBottom, canvasMarginTop - )) - .isInstanceOf(BigSizeElementException.class) - .hasMessageContaining("Element is too large"); - } - - @MethodSource("provideScenarios") - @ParameterizedTest(name = "{0}") - void testLayoutCalculation( - String name, - double currentY, - int currentPage, - SizeData objectSizeData, // Принимаем Record - MarginData objectMarginData, - CanvasData canvasData, // Принимаем Record - OffsetData offsetData, - boolean isBreakable, - double expectedOffsetY, - YPositionOnPage expectedY - ) { - // 1. Создаем Моки через Фабрику, передавая Records - Canvas canvas = FactoryClasses.canvasMock(canvasData); - ContentSize size = FactoryClasses.mockContentSize(objectSizeData); - Margin objMargin = FactoryClasses.margin(objectMarginData); - - // Остальная подготовка... -// Entity entity = FactoryClasses.entityMock(size, objMargin); - Entity entity = FactoryClasses.entityMock(size, objMargin); - Offset offset = FactoryClasses.offsetReal(offsetData); - - - // 2. Выполняем тест - var result = pageLayoutCalculator.definePositionOnPage(currentY, entity, currentPage, canvas, offset, isBreakable); - - // 3. Проверка - assertAll("Проверка позиции на странице", // Заголовок - () -> assertThat(result.yPosition()) - .as("Y Position") - .isEqualTo(expectedY.yPosition()), - - () -> assertThat(result.startPage()) - .as("Start Page") - .isEqualTo(expectedY.startPage()), - - () -> assertThat(result.endPage()) - .as("End Page") - .isEqualTo(expectedY.endPage()) - ,() -> assertThat(expectedOffsetY) - .as("Offset") - .isEqualTo(offset.y()) - - ); - - } - - -} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index ea732eb0f..2adb12613 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -61,13 +61,6 @@
- - - - %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{36}) - %msg%n - - -