Skip to content

Commit 0431f63

Browse files
committed
feat(api): expose clipToBounds on the layer-stack DSL
LayerStackNode.clipToBounds — added with the block SVG viewBox clip — was only reachable by constructing the node directly. LayerStackBuilder now offers clipToBounds() / clipToBounds(boolean) so DSL-built stacks can opt into the same behaviour: the overflow: hidden of a stacking box. The flag defaults off, so existing stacks are unchanged. LayerStackBuilderTest covers both: clipToBounds() emits a balanced clip pair around the layers, and the default (and explicit false) emits none.
1 parent 3862687 commit 0431f63

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ PDF `GoTo` actions. External links are unchanged.
101101
the icon box: `LayerStackNode` gains an opt-in `clipToBounds` (`@since 1.9.0`,
102102
default off so existing stacks stay byte-identical) and `SvgIcon.node(...)`
103103
sets it. It reuses the `ShapeContainer` clip pipeline — one paired
104-
begin/end marker per icon — so it matches the inline fix above.
104+
begin/end marker per icon — so it matches the inline fix above. The same
105+
flag is exposed to the DSL as `LayerStackBuilder.clipToBounds()` — the
106+
`overflow: hidden` of a stacking box for any layer stack.
105107

106108
### Documentation
107109

src/main/java/com/demcha/compose/document/dsl/LayerStackBuilder.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public final class LayerStackBuilder {
2626
private String name = "";
2727
private DocumentInsets padding = DocumentInsets.zero();
2828
private DocumentInsets margin = DocumentInsets.zero();
29+
private boolean clipToBounds = false;
2930

3031
/**
3132
* Creates a layer stack builder.
@@ -265,12 +266,37 @@ public LayerStackBuilder margin(double margin) {
265266
return margin(DocumentInsets.of(margin));
266267
}
267268

269+
/**
270+
* Clips the layers to the stack box — the {@code overflow: hidden} of a
271+
* stacking box. Anything a layer paints outside the box (after padding) is
272+
* cut away. Off by default, so layers may overflow.
273+
*
274+
* @return this builder
275+
* @since 1.9.0
276+
*/
277+
public LayerStackBuilder clipToBounds() {
278+
return clipToBounds(true);
279+
}
280+
281+
/**
282+
* Sets whether the layers are clipped to the stack box.
283+
*
284+
* @param clip {@code true} to clip layers to the box, {@code false} to let
285+
* them overflow (the default)
286+
* @return this builder
287+
* @since 1.9.0
288+
*/
289+
public LayerStackBuilder clipToBounds(boolean clip) {
290+
this.clipToBounds = clip;
291+
return this;
292+
}
293+
268294
/**
269295
* Builds the layer stack node.
270296
*
271297
* @return immutable layer stack node
272298
*/
273299
public LayerStackNode build() {
274-
return new LayerStackNode(name, List.copyOf(layers), padding, margin);
300+
return new LayerStackNode(name, List.copyOf(layers), padding, margin, clipToBounds);
275301
}
276302
}

src/test/java/com/demcha/compose/document/dsl/LayerStackBuilderTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import com.demcha.compose.GraphCompose;
44
import com.demcha.compose.document.api.DocumentSession;
55
import com.demcha.compose.document.layout.LayoutGraph;
6+
import com.demcha.compose.document.layout.PlacedFragment;
67
import com.demcha.compose.document.layout.PlacedNode;
8+
import com.demcha.compose.document.layout.payloads.ShapeClipBeginPayload;
9+
import com.demcha.compose.document.layout.payloads.ShapeClipEndPayload;
710
import com.demcha.compose.document.node.LayerAlign;
811
import com.demcha.compose.document.node.LayerStackNode;
912
import com.demcha.compose.document.node.SpacerNode;
@@ -424,6 +427,51 @@ void stackPaddingShrinksLayerBoundingBox() {
424427
}
425428
}
426429

430+
@Test
431+
void clipToBoundsEmitsBalancedClipMarkersAroundLayers() {
432+
try (DocumentSession session = GraphCompose.document()
433+
.pageSize(420, 320)
434+
.margin(DocumentInsets.of(20))
435+
.create()) {
436+
437+
LayerStackNode clipped = new LayerStackBuilder()
438+
.name("Clipped")
439+
.layer(new SpacerNode("Inner", 80.0, 50.0,
440+
DocumentInsets.zero(), DocumentInsets.zero()))
441+
.clipToBounds()
442+
.build();
443+
444+
assertThat(clipped.clipToBounds()).isTrue();
445+
session.add(clipped);
446+
447+
List<PlacedFragment> fragments = session.layoutGraph().fragments();
448+
assertThat(fragments)
449+
.filteredOn(f -> f.payload() instanceof ShapeClipBeginPayload)
450+
.as("clipToBounds() opens exactly one bounds clip")
451+
.hasSize(1);
452+
assertThat(fragments)
453+
.filteredOn(f -> f.payload() instanceof ShapeClipEndPayload)
454+
.as("clipToBounds() closes exactly one bounds clip")
455+
.hasSize(1);
456+
} catch (Exception e) {
457+
throw new RuntimeException(e);
458+
}
459+
}
460+
461+
@Test
462+
void defaultStackDoesNotClipAndExplicitFalseStaysOff() {
463+
SpacerNode inner = new SpacerNode("Inner", 40.0, 30.0,
464+
DocumentInsets.zero(), DocumentInsets.zero());
465+
466+
// Default: no clip, byte-identical to pre-1.9 stacks.
467+
assertThat(new LayerStackBuilder().name("Default").layer(inner).build().clipToBounds())
468+
.isFalse();
469+
// Explicit false is a no-op opt-out.
470+
assertThat(new LayerStackBuilder().name("Off").layer(inner).clipToBounds(false).build()
471+
.clipToBounds())
472+
.isFalse();
473+
}
474+
427475
private static PlacedNode nodeWithSemanticName(LayoutGraph graph, String name) {
428476
return graph.nodes().stream()
429477
.filter(n -> name.equals(n.semanticName()))

0 commit comments

Comments
 (0)