2 * Sixth 3D engine. Author: Svjatoslav Agejenko.
3 * This project is released under Creative Commons Zero (CC0) license.
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
7 import eu.svjatoslav.sixth.e3d.geometry.Box;
8 import eu.svjatoslav.sixth.e3d.geometry.BspTree;
9 import eu.svjatoslav.sixth.e3d.geometry.Frustum;
10 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
11 import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
12 import eu.svjatoslav.sixth.e3d.gui.ViewSpaceTracker;
13 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
14 import eu.svjatoslav.sixth.e3d.math.Transform;
15 import eu.svjatoslav.sixth.e3d.math.TransformStack;
16 import eu.svjatoslav.sixth.e3d.math.Vertex;
17 import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
18 import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
19 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
20 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
21 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
22 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
23 import eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TexturedPolygonTessellator;
25 import java.util.ArrayList;
26 import java.util.Iterator;
27 import java.util.List;
30 * A composite shape that groups multiple sub-shapes into a single logical unit.
32 * <p>Use {@code AbstractCompositeShape} to build complex 3D objects by combining
33 * primitive shapes (lines, polygons, textured polygons) into a group that can be
34 * positioned, rotated, and manipulated as one entity. Sub-shapes can be organized
35 * into named groups for selective visibility toggling.</p>
37 * <p><b>Usage example - creating a custom composite shape:</b></p>
39 * // Create a composite shape at position (0, 0, 200)
40 * AbstractCompositeShape myObject = new AbstractCompositeShape(
41 * new Point3D(0, 0, 200)
45 * myObject.addShape(new Line(
46 * new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
50 * // Add shapes to a named group for toggling visibility
51 * myObject.addShape(labelShape, "labels");
52 * myObject.hideGroup("labels"); // hide all shapes in "labels" group
53 * myObject.showGroup("labels"); // show them again
56 * viewPanel.getRootShapeCollection().addShape(myObject);
59 * <p><b>Level-of-detail tessellation:</b></p>
60 * <p>Textured polygons within the composite shape are automatically tessellated into smaller
61 * triangles based on distance from the viewer. This provides perspective-correct texture
62 * mapping without requiring hardware support. The tessellation factor adapts dynamically.</p>
64 * <p><b>Extending this class:</b></p>
65 * <p>Override {@link #beforeTransformHook} to customize shape appearance or behavior
66 * on each frame (e.g., animations, dynamic geometry updates).</p>
68 * @see SubShape wrapper for individual sub-shapes with group and visibility support
69 * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape the base shape class
70 * @see eu.svjatoslav.sixth.e3d.renderer.raster.tessellation.TexturedPolygonTessellator the level-of-detail polygon tessellator
72 public class AbstractCompositeShape extends AbstractShape {
74 * Source-of-truth registry of all sub-shapes added to this composite.
76 * <p>Each sub-shape is wrapped with its group identifier and visibility state.
77 * Shapes are stored in insertion order and remain in this collection even when
78 * hidden (visibility state toggles instead of removal).</p>
80 * <p><b>Performance note:</b> This list is NOT processed for every frame.
81 * Instead, it serves as the authoritative source from which {@link #cachedRenderList}
82 * is compiled whenever the cache becomes invalid (see {@link #cacheNeedsRebuild}).
83 * Only modifications to this registry (add/remove/show/hide) trigger cache rebuild.</p>
85 * @see #cachedRenderList the frame-optimized cache derived from this registry
86 * @see #cacheNeedsRebuild the flag controlling when the cache is rebuilt
88 private final List<SubShape> subShapesRegistry = new ArrayList<>();
91 * Tracks the distance and angle between the camera and this shape to compute
92 * an appropriate tessellation factor for level-of-detail adjustments.
94 private final ViewSpaceTracker viewSpaceTracker;
97 * The current tessellation factor used for tessellating textured polygons into smaller
98 * triangles for perspective-correct rendering. Higher values produce more triangles
99 * for distant objects; lower values for nearby objects. Updated dynamically based
100 * on view-space analysis.
102 * TODO: move this to TexturedTriangle. LOD must be computed per textured triangle, not per-shape.
104 double currentTessellationFactor = 5;
107 * Frame-optimized cache of shapes ready for rendering, derived from {@link #subShapesRegistry}.
109 * <p>This list is processed during every frame in the {@link #transform} method.
112 * <li>Non-textured shapes (Line, SolidPolygon) - passed through directly</li>
113 * <li>Textured polygons - tessellated into smaller triangles based on current LOD factor</li>
116 * <p><b>Caching strategy:</b> Regenerating this list involves texture tessellation which
117 * is expensive. The list is rebuilt only when {@link #cacheNeedsRebuild} is true,
118 * avoiding per-frame reconstruction overhead.</p>
120 * @see #subShapesRegistry the source registry this cache is derived from
121 * @see #cacheNeedsRebuild the flag that triggers cache regeneration
123 private List<AbstractShape> cachedRenderList = new ArrayList<>();
126 * Flag indicating whether {@link #cachedRenderList} needs to be rebuilt from {@link #subShapesRegistry}.
128 * <p>Set to {@code true} when:</p>
130 * <li>A shape is added via {@link #addShape}</li>
131 * <li>A shape is removed via {@link #removeGroup}</li>
132 * <li>Group visibility changes via {@link #showGroup} or {@link #hideGroup}</li>
133 * <li>The tessellation factor changes significantly (determined by {@link #isRetessellationNeeded})</li>
136 * <p>Set to {@code false} after {@link #retessellate} completes the cache rebuild.</p>
138 * <p>This flag enables the performance optimization of avoiding per-frame list
139 * reconstruction - the registry is only re-processed when something actually changed.</p>
141 * @see #subShapesRegistry the source data that may need reprocessing
142 * @see #cachedRenderList the cache that gets rebuilt when this flag is true
144 private boolean cacheNeedsRebuild = true;
147 * Flag indicating this composite is the root scene container (ShapeCollection's root).
149 * <p>Root composites have different behavior for LOD-based tessellation:</p>
151 * <li>Root position equals camera position, so distance to camera is always 0</li>
152 * <li>ViewSpaceTracker cannot compute meaningful tessellation factor for root</li>
153 * <li>Root uses fixed {@link #currentTessellationFactor} and skips LOD-based retessellation checks</li>
154 * <li>Root still performs N-gon triangulation when {@link #cacheNeedsRebuild} is true</li>
157 * <p>Set via {@link #setRootComposite(boolean)} by ShapeCollection.</p>
159 private boolean isRootComposite = false;
162 * The position and orientation transform for this composite shape.
163 * Applied to all sub-shapes during the rendering transform pass.
165 private Transform transform;
168 * Creates a composite shape at the world origin with no rotation.
170 public AbstractCompositeShape() {
171 this(new Transform());
175 * Creates a composite shape at the specified location with no rotation.
177 * @param location the position in world space
179 public AbstractCompositeShape(final Point3D location) {
180 this(new Transform(location));
184 * Creates a composite shape with the specified transform (position and orientation).
186 * @param transform the initial transform defining position and rotation
188 public AbstractCompositeShape(final Transform transform) {
189 this.transform = transform;
190 viewSpaceTracker = new ViewSpaceTracker();
194 * Adds a sub-shape to this composite shape without a group identifier.
196 * @param shape the shape to add
198 public void addShape(final AbstractShape shape) {
199 addShape(shape, null);
203 * Adds a sub-shape to this composite shape with an optional group identifier.
205 * <p>Grouped shapes can be shown, hidden, or removed together using
206 * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.</p>
208 * @param shape the shape to add
209 * @param groupId the group identifier, or {@code null} for ungrouped shapes
211 public void addShape(final AbstractShape shape, final String groupId) {
212 subShapesRegistry.add(new SubShape(shape, groupId, true));
213 cacheNeedsRebuild = true;
217 * This method should be overridden by anyone wanting to customize the shape
218 * before it is rendered.
220 * @param transformPipe the current transform stack
221 * @param context the rendering context for the current frame
223 public void beforeTransformHook(final TransformStack transformPipe,
224 final RenderingContext context) {
228 * Returns the world-space position of this composite shape.
230 * @return the translation component of this shape's transform
232 public Point3D getLocation() {
233 return transform.getTranslation();
237 * Returns the axis-aligned bounding box encompassing all sub-shapes.
239 * <p>The bounding box is computed by aggregating the bounds of all visible
240 * sub-shapes, then transforming the result by this composite's own transform.</p>
242 * <p><b>Caching:</b> The bounding box is recomputed whenever
243 * {@link #cacheNeedsRebuild} is true (shapes added/removed/visibility changed).
244 * For nested composites, the bounds include their local transform offset.</p>
246 * @return the axis-aligned bounding box in this composite's local coordinates
249 public Box getBoundingBox() {
250 if (cachedBoundingBox == null || cacheNeedsRebuild) {
251 if (subShapesRegistry.isEmpty()) {
252 return super.getBoundingBox();
255 double minX = Double.MAX_VALUE;
256 double maxX = Double.MIN_VALUE;
257 double minY = Double.MAX_VALUE;
258 double maxY = Double.MIN_VALUE;
259 double minZ = Double.MAX_VALUE;
260 double maxZ = Double.MIN_VALUE;
262 for (final SubShape subShape : subShapesRegistry) {
263 if (!subShape.isVisible()) {
267 final AbstractShape shape = subShape.getShape();
268 final Box shapeBounds = shape.getBoundingBox();
270 // Get bounds and apply sub-shape's transform if it's a composite
271 Point3D shapeMin = new Point3D(shapeBounds.getMinX(), shapeBounds.getMinY(), shapeBounds.getMinZ());
272 Point3D shapeMax = new Point3D(shapeBounds.getMaxX(), shapeBounds.getMaxY(), shapeBounds.getMaxZ());
274 // If sub-shape is a composite, apply its transform to the bounds
275 if (shape instanceof AbstractCompositeShape) {
276 final Transform subTransform = ((AbstractCompositeShape) shape).getTransform();
277 final Point3D subTranslation = subTransform.getTranslation();
278 shapeMin.add(subTranslation);
279 shapeMax.add(subTranslation);
282 minX = Math.min(minX, shapeMin.x);
283 maxX = Math.max(maxX, shapeMax.x);
284 minY = Math.min(minY, shapeMin.y);
285 maxY = Math.max(maxY, shapeMax.y);
286 minZ = Math.min(minZ, shapeMin.z);
287 maxZ = Math.max(maxZ, shapeMax.z);
290 if (minX == Double.MAX_VALUE) {
292 return super.getBoundingBox();
295 cachedBoundingBox = new Box(
296 new Point3D(minX, minY, minZ),
297 new Point3D(maxX, maxY, maxZ)
300 return cachedBoundingBox;
304 * Returns the sub-shapes registry (source of truth for all sub-shapes).
306 * <p>This is the authoritative list of all sub-shapes including hidden ones.
307 * For per-frame rendering, use {@link #cachedRenderList} instead (accessed internally).</p>
309 * @return the registry list of all sub-shapes with their group and visibility metadata
310 * @see #cachedRenderList the frame-optimized cache derived from this registry
312 public List<SubShape> getSubShapesRegistry() {
313 return subShapesRegistry;
317 * Extracts all SolidPolygon instances from this composite shape.
319 * <p>Recursively traverses the shape hierarchy and collects all
320 * SolidPolygon instances. Used for CSG operations where polygons
321 * are needed directly without conversion.</p>
323 * @return list of SolidPolygon instances from this shape hierarchy
325 public List<SolidPolygon> extractSolidPolygons() {
326 final List<SolidPolygon> result = new ArrayList<>();
327 for (final SubShape subShape : subShapesRegistry) {
328 final AbstractShape shape = subShape.getShape();
329 if (shape instanceof SolidPolygon) {
330 result.add((SolidPolygon) shape);
331 } else if (shape instanceof AbstractCompositeShape) {
332 result.addAll(((AbstractCompositeShape) shape).extractSolidPolygons());
339 * Returns the view-space tracker that monitors the distance
340 * and angle between the camera and this shape for level-of-detail adjustments.
342 * @return the view-space tracker for this shape
344 public ViewSpaceTracker getViewSpaceTracker() {
345 return viewSpaceTracker;
349 * Hides all sub-shapes belonging to the specified group.
350 * Hidden shapes are not rendered but remain in the collection.
352 * @param groupIdentifier the group to hide
353 * @see #showGroup(String)
354 * @see #removeGroup(String)
356 public void hideGroup(final String groupIdentifier) {
357 for (final SubShape subShape : subShapesRegistry) {
358 if (subShape.matchesGroup(groupIdentifier)) {
359 subShape.setVisible(false);
360 cacheNeedsRebuild = true;
366 * Determines whether textured polygons need to be re-tessellated based on tessellation factor change.
368 * Re-tessellation is needed if the tessellation state is marked outdated, or if the ratio between
369 * the larger and smaller tessellation factor exceeds 1.5x. This threshold prevents frequent
370 * re-tessellation for minor view changes while ensuring significant LOD changes trigger updates.
372 * @param proposedNewTessellationFactor the tessellation factor computed from current view distance
373 * @param currentTessellationFactor the tessellation factor currently in use
374 * @return {@code true} if re-tessellation should be performed
376 private boolean isRetessellationNeeded(final double proposedNewTessellationFactor, final double currentTessellationFactor) {
378 if (cacheNeedsRebuild)
381 // retessellate if there is significant difference between proposed and current tessellation factor
382 final double larger = Math.max(proposedNewTessellationFactor, currentTessellationFactor);
383 final double smaller = Math.min(proposedNewTessellationFactor, currentTessellationFactor);
385 return (larger / smaller) > 1.5d;
389 * Permanently removes all sub-shapes belonging to the specified group.
391 * @param groupIdentifier the group to remove
392 * @see #hideGroup(String)
394 public void removeGroup(final String groupIdentifier) {
395 final java.util.Iterator<SubShape> iterator = subShapesRegistry
398 while (iterator.hasNext()) {
399 final SubShape subShape = iterator.next();
400 if (subShape.matchesGroup(groupIdentifier)) {
402 cacheNeedsRebuild = true;
408 * Returns all sub-shapes belonging to the specified group.
410 * @param groupIdentifier the group identifier to match
411 * @return list of matching sub-shapes
413 public List<SubShape> getGroup(final String groupIdentifier) {
414 final List<SubShape> result = new ArrayList<>();
415 for (int i = 0; i < subShapesRegistry.size(); i++) {
416 final SubShape subShape = subShapesRegistry.get(i);
417 if (subShape.matchesGroup(groupIdentifier))
418 result.add(subShape);
424 * Checks if re-slicing is needed and performs it if so.
426 * <p>For root composites, skips LOD-based checks since distance to camera is 0.
427 * Only retessellates when {@link #cacheNeedsRebuild} is true (shapes added/removed/visibility changed).</p>
429 * <p>For normal composites, checks both cache validity and LOD-based tessellation factor changes.</p>
431 * @param context the rendering context for logging
433 private void retessellateIfNeeded(final RenderingContext context) {
435 if (isRootComposite) {
436 if (cacheNeedsRebuild)
437 retessellate(context);
441 final double proposedTessellationFactor = viewSpaceTracker.proposeTessellationFactor();
443 if (isRetessellationNeeded(proposedTessellationFactor, currentTessellationFactor)) {
444 currentTessellationFactor = proposedTessellationFactor;
445 retessellate(context);
450 * Paint solid elements of this composite shape into given color.
452 * <p>Applies recursively to nested {@code AbstractCompositeShape} sub-shapes.</p>
454 * @param color the color to apply to all solid sub-shapes
456 public void setColor(final Color color) {
457 for (final SubShape subShape : getSubShapesRegistry()) {
458 final AbstractShape shape = subShape.getShape();
460 if (shape instanceof SolidPolygon) {
461 ((SolidPolygon) shape).setColor(color);
462 } else if (shape instanceof Line) {
463 ((Line) shape).color = color;
464 } else if (shape instanceof AbstractCompositeShape) {
465 ((AbstractCompositeShape) shape).setColor(color);
471 * Assigns a group identifier to all sub-shapes that currently have no group.
473 * @param groupIdentifier the group to assign to ungrouped shapes
475 public void setGroupForUngrouped(final String groupIdentifier) {
476 for (final SubShape subShape : subShapesRegistry)
477 if (subShape.isUngrouped())
478 subShape.setGroup(groupIdentifier);
482 public void setMouseInteractionController(
483 final MouseInteractionController mouseInteractionController) {
484 super.setMouseInteractionController(mouseInteractionController);
486 for (final SubShape subShape : subShapesRegistry)
487 subShape.getShape().setMouseInteractionController(
488 mouseInteractionController);
490 cacheNeedsRebuild = true;
494 * Marks this composite as the root scene container.
496 * <p>Root composites skip LOD-based tessellation factor checks since their position
497 * equals the camera position (distance = 0). They use a fixed tessellation factor
498 * and only retessellate when shapes are added, removed, or visibility changes.</p>
500 * <p>Called by {@code ShapeCollection} to configure its root composite.</p>
502 * @param isRoot {@code true} if this is the root composite, {@code false} otherwise
504 public void setRootComposite(final boolean isRoot) {
505 this.isRootComposite = isRoot;
509 * Returns this composite's transform (position and orientation).
511 * @return the transform object
513 public Transform getTransform() {
518 * Sets the transform for this composite shape.
520 * @param transform the new transform
521 * @return this composite shape (for chaining)
523 public AbstractCompositeShape setTransform(final Transform transform) {
524 this.transform = transform;
529 * Sets the cache rebuild flag, forcing {@link #cachedRenderList} to be regenerated.
531 * <p>Used by {@code ShapeCollection} to trigger retessellate when clearing the scene
532 * or for other advanced use cases.</p>
534 * @param needsRebuild {@code true} to force cache rebuild on next frame
536 public void setCacheNeedsRebuild(final boolean needsRebuild) {
537 this.cacheNeedsRebuild = needsRebuild;
541 * Enables or disables shading for all SolidTriangle and SolidPolygon sub-shapes.
542 * When enabled, shapes use the global lighting manager from the rendering
543 * context to calculate flat shading based on light sources.
545 * <p>Applies recursively to nested {@code AbstractCompositeShape} sub-shapes.</p>
547 * @param shadingEnabled {@code true} to enable shading, {@code false} to disable
548 * @return this composite shape (for chaining)
550 public AbstractCompositeShape setShadingEnabled(final boolean shadingEnabled) {
551 for (final SubShape subShape : getSubShapesRegistry()) {
552 final AbstractShape shape = subShape.getShape();
553 if (shape instanceof SolidPolygon) {
554 ((SolidPolygon) shape).setShadingEnabled(shadingEnabled);
555 } else if (shape instanceof AbstractCompositeShape) {
556 ((AbstractCompositeShape) shape).setShadingEnabled(shadingEnabled);
563 * Enables or disables backface culling for all SolidPolygon and TexturedTriangle sub-shapes.
565 * <p>Applies recursively to nested {@code AbstractCompositeShape} sub-shapes.</p>
567 * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
568 * @return this composite shape (for chaining)
570 public AbstractCompositeShape setBackfaceCulling(final boolean backfaceCulling) {
571 for (final SubShape subShape : getSubShapesRegistry()) {
572 final AbstractShape shape = subShape.getShape();
573 if (shape instanceof SolidPolygon) {
574 ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling);
575 } else if (shape instanceof TexturedTriangle) {
576 ((TexturedTriangle) shape).setBackfaceCulling(backfaceCulling);
577 } else if (shape instanceof AbstractCompositeShape) {
578 ((AbstractCompositeShape) shape).setBackfaceCulling(backfaceCulling);
585 * Performs an in-place union with another composite shape.
587 * <p>This shape's SolidPolygon children are replaced with the union result.
588 * Non-SolidPolygon children from both shapes are preserved and combined.</p>
590 * <p><b>CSG Operation:</b> Union combines two shapes into one, keeping all
591 * geometry from both. Uses BSP tree algorithms for robust boolean operations.</p>
593 * <p><b>Child handling:</b></p>
595 * <li>SolidPolygon children from both shapes → replaced with union result</li>
596 * <li>Non-SolidPolygon children from this shape → preserved</li>
597 * <li>Non-SolidPolygon children from other shape → added to this shape</li>
598 * <li>Nested AbstractCompositeShape children → preserved unchanged (not recursively processed)</li>
601 * @param other the shape to union with
602 * @see #subtract(AbstractCompositeShape)
603 * @see #intersect(AbstractCompositeShape)
605 public void union(final AbstractCompositeShape other) {
607 final BspTree selfTree = new BspTree(clonePolygons(extractSolidPolygons()));
608 final BspTree otherTree = new BspTree(clonePolygons(other.extractSolidPolygons()));
610 // Remove from self any polygons that are inside other (interior faces)
611 selfTree.clipTo(otherTree);
613 // Remove from other any polygons that are inside self (interior faces)
614 otherTree.clipTo(selfTree);
616 // Invert other to convert remaining polygons for the next clip step
619 // Clip inverted other against self to remove back-facing coplanar polygons
620 otherTree.clipTo(selfTree);
622 // Invert back to restore correct polygon orientation
625 // Merge other's remaining polygons into self's BSP tree
626 selfTree.addPolygons(otherTree.allPolygons());
628 replaceSolidPolygons(selfTree.allPolygons());
629 mergeNonPolygonChildrenFrom(other);
633 * Performs an in-place subtraction with another composite shape.
635 * <p>This shape's SolidPolygon children are replaced with the difference result.
636 * The other shape acts as a "cutter" that carves out volume from this shape.</p>
638 * <p><b>CSG Operation:</b> Subtract removes the volume of the second shape
639 * from the first shape. Useful for creating holes, cavities, and cutouts.</p>
641 * <p><b>Child handling:</b></p>
643 * <li>SolidPolygon children from this shape → replaced with difference result</li>
644 * <li>Non-SolidPolygon children from this shape → preserved</li>
645 * <li>All children from other shape → discarded (other is just a cutter)</li>
646 * <li>Nested AbstractCompositeShape children → preserved unchanged</li>
649 * @param other the shape to subtract (the cutter)
650 * @see #union(AbstractCompositeShape)
651 * @see #intersect(AbstractCompositeShape)
653 public void subtract(final AbstractCompositeShape other) {
655 final BspTree target = new BspTree(clonePolygons(extractSolidPolygons()));
656 final BspTree cutter = new BspTree(clonePolygons(other.extractSolidPolygons()));
658 // Invert target: convert "inside" to "outside" and vice versa
659 // This transforms the problem from "subtract B from A" to "intersect A's complement with B's complement"
662 // Clip target against cutter: removes parts of target that are INSIDE the cutter
663 // Since target is inverted, this removes parts that were OUTSIDE the original target
664 target.clipTo(cutter);
666 // Clip cutter against (inverted) target: removes parts of cutter outside the inverted target
667 // This keeps only cutter polygons that are inside the inverted target = outside original target
668 cutter.clipTo(target);
670 // Invert cutter to flip its inside/outside
673 // Clip inverted cutter against target: removes coplanar back-faces
674 cutter.clipTo(target);
676 // Invert cutter back to correct orientation
679 // Merge cutter's polygons into target's BSP tree
680 target.addPolygons(cutter.allPolygons());
682 // Invert target back to restore correct inside/outside orientation
683 // Result: the carved-out volume (target minus cutter)
686 replaceSolidPolygons(target.allPolygons());
690 * Performs an in-place intersection with another composite shape.
692 * <p>This shape's SolidPolygon children are replaced with the intersection result.
693 * Only the overlapping volume between the two shapes remains.</p>
695 * <p><b>CSG Operation:</b> Intersect keeps only the volume where both shapes
696 * overlap. Useful for creating shapes constrained by multiple boundaries.</p>
698 * <p><b>Child handling:</b></p>
700 * <li>SolidPolygon children from this shape → replaced with intersection result</li>
701 * <li>Non-SolidPolygon children from this shape → preserved</li>
702 * <li>All children from other shape → discarded</li>
703 * <li>Nested AbstractCompositeShape children → preserved unchanged</li>
706 * @param other the shape to intersect with
707 * @see #union(AbstractCompositeShape)
708 * @see #subtract(AbstractCompositeShape)
710 public void intersect(final AbstractCompositeShape other) {
712 final BspTree selfTree = new BspTree(clonePolygons(extractSolidPolygons()));
713 final BspTree otherTree = new BspTree(clonePolygons(other.extractSolidPolygons()));
715 // Invert self to convert "inside" to "outside"
716 // This transforms intersection into: keep parts that are "outside both inverted shapes"
719 // Clip other against inverted self: keeps only parts of other that are INSIDE original self
720 // (because clipTo removes what's "outside" the BSP, and inverted self's "outside" = original self's "inside")
721 otherTree.clipTo(selfTree);
723 // Invert other (which now represents the intersection region)
726 // Clip inverted self against (inverted intersection): removes parts outside the intersection
727 selfTree.clipTo(otherTree);
729 // Clip intersection result against inverted self: removes back-facing coplanar polygons
730 otherTree.clipTo(selfTree);
732 // Build final BSP tree from the clipped intersection polygons
733 selfTree.addPolygons(otherTree.allPolygons());
735 // Invert back to restore correct inside/outside orientation
738 replaceSolidPolygons(selfTree.allPolygons());
742 * Creates deep clones of all polygons in the list.
744 * <p>CSG operations modify polygons in-place via BSP tree operations.
745 * Cloning ensures the original polygon data is preserved.</p>
747 * @param polygons the polygons to clone
748 * @return a new list containing deep clones of all polygons
750 private List<SolidPolygon> clonePolygons(final List<SolidPolygon> polygons) {
751 final List<SolidPolygon> cloned = new ArrayList<>(polygons.size());
752 for (final SolidPolygon p : polygons) {
753 cloned.add(p.deepClone());
759 * Replaces this shape's SolidPolygon children with new polygons.
761 * <p>Preserves all non-SolidPolygon children (Lines, nested composites, etc.).</p>
763 * @param newPolygons the polygons to replace with
765 private void replaceSolidPolygons(final List<SolidPolygon> newPolygons) {
766 // Remove all direct SolidPolygon children from this shape
767 final Iterator<SubShape> iterator = subShapesRegistry.iterator();
768 while (iterator.hasNext()) {
769 final SubShape subShape = iterator.next();
770 if (subShape.getShape() instanceof SolidPolygon) {
775 // Add all result polygons as new children
776 for (final SolidPolygon polygon : newPolygons) {
780 cacheNeedsRebuild = true;
784 * Merges non-SolidPolygon children from another shape into this shape.
786 * <p>Copies all non-SolidPolygon children (Lines, nested composites, etc.)
787 * from the other shape, preserving their group identifiers.</p>
789 * @param other the shape to merge non-polygon children from
791 private void mergeNonPolygonChildrenFrom(final AbstractCompositeShape other) {
796 for (final SubShape otherSubShape : other.subShapesRegistry) {
797 final AbstractShape otherShape = otherSubShape.getShape();
798 if (!(otherShape instanceof SolidPolygon)) {
799 addShape(otherShape, otherSubShape.getGroupIdentifier());
803 cacheNeedsRebuild = true;
807 * Makes all sub-shapes belonging to the specified group visible.
809 * @param groupIdentifier the group to show
810 * @see #hideGroup(String)
812 public void showGroup(final String groupIdentifier) {
813 for (int i = 0; i < subShapesRegistry.size(); i++) {
814 final SubShape subShape = subShapesRegistry.get(i);
815 if (subShape.matchesGroup(groupIdentifier)) {
816 subShape.setVisible(true);
817 cacheNeedsRebuild = true;
823 * Retessellates all textured polygons, triangulates N-vertex solid polygons,
824 * and rebuilds the cached render list.
825 * Logs the operation to the debug log buffer if available.
827 * @param context the rendering context for logging, may be {@code null}
829 private void retessellate(final RenderingContext context) {
830 cacheNeedsRebuild = false;
832 final List<AbstractShape> result = new ArrayList<>();
834 final TexturedPolygonTessellator tessellator = new TexturedPolygonTessellator(currentTessellationFactor);
835 int texturedPolygonCount = 0;
836 int solidPolygonCount = 0;
837 int triangulatedPolygonCount = 0;
838 int otherShapeCount = 0;
840 for (int i = 0; i < subShapesRegistry.size(); i++) {
841 final SubShape subShape = subShapesRegistry.get(i);
842 if (!subShape.isVisible())
845 final AbstractShape shape = subShape.getShape();
847 if (shape instanceof TexturedTriangle) {
848 tessellator.tessellate((TexturedTriangle) shape);
849 texturedPolygonCount++;
850 } else if (shape instanceof SolidPolygon polygon) {
851 final int vertexCount = polygon.getVertexCount();
853 if (vertexCount == 3) {
857 triangulateSolidPolygon(polygon, result);
858 triangulatedPolygonCount++;
866 result.addAll(tessellator.getResult());
868 cachedRenderList = result;
870 if (context != null && context.debugLogBuffer != null) {
871 context.debugLogBuffer.log("retessellate: " + getClass().getSimpleName()
872 + " tessellationFactor=" + String.format("%.2f", currentTessellationFactor)
873 + " texturedPolygons=" + texturedPolygonCount
874 + " solidPolygons=" + solidPolygonCount
875 + " triangulatedPolygons=" + triangulatedPolygonCount
876 + " otherShapes=" + otherShapeCount
877 + " resultingTexturedPolygons=" + tessellator.getResult().size());
882 * Triangulates a convex solid polygon using fan triangulation.
884 * <p>Fan triangulation creates N-2 triangles from an N-vertex polygon by using
885 * vertex 0 as the anchor and connecting it to each adjacent pair of vertices.</p>
887 * <p>Properties (color, shading, backface culling, mouse interaction) are
888 * propagated to each resulting triangle to ensure consistent behavior.</p>
890 * @param polygon the polygon to triangulate (must have at least 4 vertices)
891 * @param result the list to add the resulting triangles to
893 private void triangulateSolidPolygon(final SolidPolygon polygon,
894 final List<AbstractShape> result) {
896 final Color color = polygon.getColor();
897 final boolean shadingEnabled = polygon.isShadingEnabled();
898 final boolean backfaceCulling = polygon.isBackfaceCullingEnabled();
899 final MouseInteractionController mouseController = polygon.mouseInteractionController;
901 final List<Vertex> vertices = polygon.vertices;
902 final Vertex v0 = vertices.get(0);
904 for (int i = 1; i < vertices.size() - 1; i++) {
905 final Vertex v1 = vertices.get(i);
906 final Vertex v2 = vertices.get(i + 1);
908 final SolidPolygon triangle = new SolidPolygon(
909 v0.coordinate, v1.coordinate, v2.coordinate, color);
911 triangle.setShadingEnabled(shadingEnabled);
912 triangle.setBackfaceCulling(backfaceCulling);
913 triangle.setMouseInteractionController(mouseController);
915 result.add(triangle);
920 public void transform(final TransformStack transformPipe,
921 final RenderAggregator aggregator, final RenderingContext context) {
923 // Add the current composite shape transform to the end of the transform
925 transformPipe.addTransform(transform);
927 // FRUSTUM CULLING: Check if this composite's bounds are visible
928 // Root composite skips this check (its bounds are always the full scene)
929 // Non-root composites check their aggregated bounds against the frustum
930 if (context.frustum != null && !isRootComposite) {
931 // Count this composite for culling statistics (before frustum test)
932 if (context.cullingStatistics != null) {
933 context.cullingStatistics.totalComposites++;
936 final Box localBounds = getBoundingBox();
938 // Transform all 8 corners of the bounding box to view space
939 final double minX = localBounds.getMinX();
940 final double maxX = localBounds.getMaxX();
941 final double minY = localBounds.getMinY();
942 final double maxY = localBounds.getMaxY();
943 final double minZ = localBounds.getMinZ();
944 final double maxZ = localBounds.getMaxZ();
946 final double[] xs = {minX, maxX};
947 final double[] ys = {minY, maxY};
948 final double[] zs = {minZ, maxZ};
950 double viewMinX = Double.MAX_VALUE;
951 double viewMaxX = -Double.MAX_VALUE;
952 double viewMinY = Double.MAX_VALUE;
953 double viewMaxY = -Double.MAX_VALUE;
954 double viewMinZ = Double.MAX_VALUE;
955 double viewMaxZ = -Double.MAX_VALUE;
957 for (int i = 0; i < 8; i++) {
958 final double x = xs[(i & 1)];
959 final double y = ys[(i >> 1) & 1];
960 final double z = zs[(i >> 2) & 1];
962 final Point3D corner = transformPointToViewSpace(x, y, z, transformPipe);
964 viewMinX = Math.min(viewMinX, corner.x);
965 viewMaxX = Math.max(viewMaxX, corner.x);
966 viewMinY = Math.min(viewMinY, corner.y);
967 viewMaxY = Math.max(viewMaxY, corner.y);
968 viewMinZ = Math.min(viewMinZ, corner.z);
969 viewMaxZ = Math.max(viewMaxZ, corner.z);
972 final Box viewSpaceBounds = new Box(
973 new Point3D(viewMinX, viewMinY, viewMinZ),
974 new Point3D(viewMaxX, viewMaxY, viewMaxZ)
977 final Frustum frustum = context.frustum;
978 final boolean visible = frustum.intersectsAABB(viewSpaceBounds);
981 // Entire composite outside frustum - skip processing all children
982 if (context.cullingStatistics != null) {
983 context.cullingStatistics.culledComposites++;
985 transformPipe.dropTransform();
990 viewSpaceTracker.analyze(transformPipe, context);
992 beforeTransformHook(transformPipe, context);
994 retessellateIfNeeded(context);
996 // transform rendered subshapes
997 for (final AbstractShape shape : cachedRenderList)
998 shape.transform(transformPipe, aggregator, context);
1000 transformPipe.dropTransform();
1004 * Transforms a point to view space using the current transform stack.
1005 * Helper method for frustum culling that transforms bounding box corners.
1007 * @param x the X coordinate in local space
1008 * @param y the Y coordinate in local space
1009 * @param z the Z coordinate in local space
1010 * @param transformPipe the current transform stack
1011 * @return the transformed point in view space
1013 private Point3D transformPointToViewSpace(final double x, final double y, final double z,
1014 final TransformStack transformPipe) {
1015 final Point3D input = new Point3D(x, y, z);
1016 final Point3D result = new Point3D();
1017 transformPipe.transform(input, result);