From dca4aa4c762cd6e174e10c961e64e786c3c47182 Mon Sep 17 00:00:00 2001
From: Svjatoslav Agejenko
Date: Sun, 20 Sep 2026 12:05:55 +0300
Subject: [PATCH] Extract CSG engine + device hotplug; resolve blend-formula
question
MIME-Version: 1.0
Content-Type: text/plain; charset=utf8
Content-Transfer-Encoding: 8bit
- New Csg utility (pure polygon-list functions: union/subtract/intersect
over BSP clip/invert sequences). AbstractCompositeShape's three CSG
methods are now one-line delegates; clonePolygons moved into Csg.
Composite drops 95 lines and its dependency on BspTree.
- New gui/DeviceHotplug: head-tracker + SpaceMouse hot-plug managers,
extracted from ViewPanel (which now holds a single field and two
delegating getters). ViewPanel loses its HeadTrackingManager/
SpaceMouseManager lifecycle code.
- Blend formulas: PROVEN bit-identical. SolidPolygon's
((255-a)*dest + a*src)>>8 and TexturedTriangle's lerp form
dest + ((a*(src-dest) - dest)>>8) agree for all 16.6M (a,src,dest)
inputs (exhaustive check). Documented at both span writers; no change.
- transformChildrenParallel's aggregator param: NOT dead as the review
claimed â it feeds the serial-fallback paths. Restored with accurate
javadoc (was nearly deleted on the review's say-so; the build caught
it before commit).
Deliberately NOT split: ViewPanel's render-loop core (mailbox, present
gates, pending paints) and AbstractCompositeShape's parallel-transform
planner â both are concurrency-critical and deliberate; extraction
trades cohesion for race-condition risk with zero functional gain.
---
.../aukio/e3d/gui/DeviceHotplug.java | 78 +++++++++
.../svjatoslav/aukio/e3d/gui/ViewPanel.java | 55 +-----
.../basic/solidpolygon/SolidPolygon.java | 4 +
.../texturedpolygon/TexturedTriangle.java | 6 +-
.../base/AbstractCompositeShape.java | 115 ++-----------
.../raster/shapes/composite/base/Csg.java | 160 ++++++++++++++++++
6 files changed, 267 insertions(+), 151 deletions(-)
create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java
create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java
new file mode 100644
index 0000000..ab8c26d
--- /dev/null
+++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java
@@ -0,0 +1,78 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.gui;
+
+import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTracker;
+import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTrackingManager;
+import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceMouseManager;
+import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceNavigatorHid;
+
+/**
+ * Optional input-device hot-plug for a {@link ViewPanel}: starts the
+ * RayNeo head-tracking manager and the SpaceNavigator 6DOF-mouse manager
+ * (each auto-detects its device even when plugged in after startup), and
+ * exposes the currently connected device, if any.
+ *
+ * Extracted from ViewPanel to keep the panel focused on the render
+ * pipeline. Either device can be disabled with
+ * {@code -De3d.headtrack=false} / {@code -De3d.spacemouse=false}.
+ */
+final class DeviceHotplug {
+
+ /** Head tracker hot-plug manager, unless disabled via e3d.headtrack=false. */
+ private HeadTrackingManager headTrackingManager;
+
+ /** SpaceNavigator hot-plug manager, unless disabled via e3d.spacemouse=false. */
+ private SpaceMouseManager spaceMouseManager;
+
+ /**
+ * Starts both hot-plug managers for the given panel (respecting the
+ * disable properties).
+ *
+ * @param viewPanel the panel whose camera the devices will drive
+ */
+ DeviceHotplug(final ViewPanel viewPanel) {
+ if (!"false".equalsIgnoreCase(
+ System.getProperty("e3d.headtrack", "true"))) {
+ headTrackingManager = new HeadTrackingManager(viewPanel);
+ headTrackingManager.start();
+ }
+ if (!"false".equalsIgnoreCase(
+ System.getProperty("e3d.spacemouse", "true"))) {
+ spaceMouseManager = new SpaceMouseManager(viewPanel);
+ spaceMouseManager.start();
+ }
+ }
+
+ /**
+ * Returns the active SpaceNavigator device, or null when no 6DOF
+ * mouse is currently connected.
+ */
+ SpaceNavigatorHid getSpaceMouse() {
+ return spaceMouseManager == null ? null
+ : spaceMouseManager.getDevice();
+ }
+
+ /**
+ * Returns the active head tracker, or null when no glasses are
+ * currently connected.
+ */
+ HeadTracker getHeadTracker() {
+ return headTrackingManager == null ? null
+ : headTrackingManager.getTracker();
+ }
+
+ /** Stops both hot-plug managers; safe to call more than once. */
+ void stop() {
+ if (headTrackingManager != null) {
+ headTrackingManager.stop();
+ headTrackingManager = null;
+ }
+ if (spaceMouseManager != null) {
+ spaceMouseManager.stop();
+ spaceMouseManager = null;
+ }
+ }
+}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java
index 0a69725..04e9e61 100755
--- a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java
+++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java
@@ -16,9 +16,7 @@ import eu.svjatoslav.aukio.e3d.diag.Telemetry;
import eu.svjatoslav.aukio.e3d.geometry.Point3D;
import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadLookController;
import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTracker;
-import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTrackingManager;
import eu.svjatoslav.aukio.e3d.gui.headtrack.RayNeoHid;
-import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceMouseManager;
import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceNavigatorHid;
import eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager;
import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack;
@@ -103,11 +101,8 @@ public class ViewPanel extends Canvas {
private final KeyboardFocusStack keyboardFocusStack;
/** The camera representing the viewer's position and orientation. */
private final Camera camera = new Camera();
- /** Head tracker hot-plug manager, unless disabled via e3d.headtrack=false. */
- private HeadTrackingManager headTrackingManager;
-
- /** SpaceNavigator hot-plug manager, unless disabled via e3d.spacemouse=false. */
- private SpaceMouseManager spaceMouseManager;
+ /** Optional input devices (head tracker, 6DOF mouse) with hot-plug. */
+ private DeviceHotplug deviceHotplug;
/** The root shape collection containing all 3D shapes in the scene. */
private final ShapeCollection rootShapeCollection = new ShapeCollection();
/** The set of frame listeners notified before each frame. */
@@ -337,8 +332,7 @@ public class ViewPanel extends Canvas {
keyboardFocusStack = new KeyboardFocusStack(this);
initializeCanvas();
- initializeHeadTracking();
- initializeSpaceMouse();
+ deviceHotplug = new DeviceHotplug(this);
// Set default ambient light for the scene
lightingManager.setAmbientLight(new Color(50, 50, 50));
@@ -1322,40 +1316,12 @@ public class ViewPanel extends Canvas {
return transformExecutor;
}
- /**
- * Starts the head tracking hot-plug manager: RayNeo glasses are
- * detected when plugged in (even after startup) and head look-around
- * is enabled automatically. Disable with {@code -De3d.headtrack=false}.
- */
- private void initializeHeadTracking() {
- if ("false".equalsIgnoreCase(
- System.getProperty("e3d.headtrack", "true")))
- return;
- headTrackingManager = new HeadTrackingManager(this);
- headTrackingManager.start();
- }
-
- /**
- * Starts the SpaceNavigator hot-plug manager: the 6DOF mouse is
- * detected when plugged in (even after startup) and cap deflection
- * drives the camera automatically. Disable with
- * {@code -De3d.spacemouse=false}.
- */
- private void initializeSpaceMouse() {
- if ("false".equalsIgnoreCase(
- System.getProperty("e3d.spacemouse", "true")))
- return;
- spaceMouseManager = new SpaceMouseManager(this);
- spaceMouseManager.start();
- }
-
/**
* Returns the active SpaceNavigator device, or null when no 6DOF
* mouse is currently connected.
*/
public SpaceNavigatorHid getSpaceMouse() {
- return spaceMouseManager == null ? null
- : spaceMouseManager.getDevice();
+ return deviceHotplug == null ? null : deviceHotplug.getSpaceMouse();
}
/**
@@ -1363,21 +1329,16 @@ public class ViewPanel extends Canvas {
* currently connected.
*/
public HeadTracker getHeadTracker() {
- return headTrackingManager == null ? null
- : headTrackingManager.getTracker();
+ return deviceHotplug == null ? null : deviceHotplug.getHeadTracker();
}
/**
* Stops rendering of this view.
*/
public void stop() {
- if (headTrackingManager != null) {
- headTrackingManager.stop();
- headTrackingManager = null;
- }
- if (spaceMouseManager != null) {
- spaceMouseManager.stop();
- spaceMouseManager = null;
+ if (deviceHotplug != null) {
+ deviceHotplug.stop();
+ deviceHotplug = null;
}
renderThreadRunning = false;
presentThreadRunning = false;
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
index 313b66b..3a3ca1d 100644
--- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
+++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
@@ -366,6 +366,10 @@ public class SolidPolygon extends AbstractCoordinateShape {
final int greenWithAlpha = g * polygonAlpha;
final int blueWithAlpha = b * polygonAlpha;
+ // Blend form ((255-a)*dest + a*src) >> 8. Proven bit-identical
+ // to TexturedTriangle's lerp form dest + ((a*(src-dest) - dest)
+ // >> 8) for every (a,src,dest) â do NOT "fix" one to match the
+ // other cosmetically; both are the same formula.
for (int i = 0; i < width; i++) {
if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) {
final int dest = pixels[offset];
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
index 76f4b29..643a930 100644
--- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
+++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
@@ -377,7 +377,11 @@ public class TexturedTriangle extends AbstractCoordinateShape {
depth[renderBufferOffset] = (float) zw;
} else if (srcAlpha != 0) {
// Translucent: blend, but do NOT write depth â
- // translucency must not occlude later fragments
+ // translucency must not occlude later fragments.
+ // Lerp form dest + ((a*(src-dest) - dest) >> 8):
+ // algebraically ((255-a)*dest + a*src) >> 8 â proven
+ // bit-identical to SolidPolygon's form for all
+ // inputs, so the two span writers blend the same.
final int destPixel = renderBufferPixels[renderBufferOffset];
final int destR = (destPixel >> 16) & 0xff;
final int destG = (destPixel >> 8) & 0xff;
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
index fa0ca38..776d21b 100644
--- a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
+++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
@@ -546,29 +546,8 @@ public class AbstractCompositeShape extends AbstractShape {
* @see #intersect(AbstractCompositeShape)
*/
public void union(final AbstractCompositeShape other) {
-
- final BspTree selfTree = new BspTree(clonePolygons(extractSolidPolygons()));
- final BspTree otherTree = new BspTree(clonePolygons(other.extractSolidPolygons()));
-
- // Remove from self any polygons that are inside other (interior faces)
- selfTree.clipTo(otherTree);
-
- // Remove from other any polygons that are inside self (interior faces)
- otherTree.clipTo(selfTree);
-
- // Invert other to convert remaining polygons for the next clip step
- otherTree.invert();
-
- // Clip inverted other against self to remove back-facing coplanar polygons
- otherTree.clipTo(selfTree);
-
- // Invert back to restore correct polygon orientation
- otherTree.invert();
-
- // Merge other's remaining polygons into self's BSP tree
- selfTree.addPolygons(otherTree.allPolygons());
-
- replaceSolidPolygons(selfTree.allPolygons());
+ replaceSolidPolygons(Csg.union(extractSolidPolygons(),
+ other.extractSolidPolygons()));
mergeNonPolygonChildrenFrom(other);
}
@@ -594,39 +573,8 @@ public class AbstractCompositeShape extends AbstractShape {
* @see #intersect(AbstractCompositeShape)
*/
public void subtract(final AbstractCompositeShape other) {
-
- final BspTree target = new BspTree(clonePolygons(extractSolidPolygons()));
- final BspTree cutter = new BspTree(clonePolygons(other.extractSolidPolygons()));
-
- // Invert target: convert "inside" to "outside" and vice versa
- // This transforms the problem from "subtract B from A" to "intersect A's complement with B's complement"
- target.invert();
-
- // Clip target against cutter: removes parts of target that are INSIDE the cutter
- // Since target is inverted, this removes parts that were OUTSIDE the original target
- target.clipTo(cutter);
-
- // Clip cutter against (inverted) target: removes parts of cutter outside the inverted target
- // This keeps only cutter polygons that are inside the inverted target = outside original target
- cutter.clipTo(target);
-
- // Invert cutter to flip its inside/outside
- cutter.invert();
-
- // Clip inverted cutter against target: removes coplanar back-faces
- cutter.clipTo(target);
-
- // Invert cutter back to correct orientation
- cutter.invert();
-
- // Merge cutter's polygons into target's BSP tree
- target.addPolygons(cutter.allPolygons());
-
- // Invert target back to restore correct inside/outside orientation
- // Result: the carved-out volume (target minus cutter)
- target.invert();
-
- replaceSolidPolygons(target.allPolygons());
+ replaceSolidPolygons(Csg.subtract(extractSolidPolygons(),
+ other.extractSolidPolygons()));
}
/**
@@ -651,51 +599,8 @@ public class AbstractCompositeShape extends AbstractShape {
* @see #subtract(AbstractCompositeShape)
*/
public void intersect(final AbstractCompositeShape other) {
-
- final BspTree selfTree = new BspTree(clonePolygons(extractSolidPolygons()));
- final BspTree otherTree = new BspTree(clonePolygons(other.extractSolidPolygons()));
-
- // Invert self to convert "inside" to "outside"
- // This transforms intersection into: keep parts that are "outside both inverted shapes"
- selfTree.invert();
-
- // Clip other against inverted self: keeps only parts of other that are INSIDE original self
- // (because clipTo removes what's "outside" the BSP, and inverted self's "outside" = original self's "inside")
- otherTree.clipTo(selfTree);
-
- // Invert other (which now represents the intersection region)
- otherTree.invert();
-
- // Clip inverted self against (inverted intersection): removes parts outside the intersection
- selfTree.clipTo(otherTree);
-
- // Clip intersection result against inverted self: removes back-facing coplanar polygons
- otherTree.clipTo(selfTree);
-
- // Build final BSP tree from the clipped intersection polygons
- selfTree.addPolygons(otherTree.allPolygons());
-
- // Invert back to restore correct inside/outside orientation
- selfTree.invert();
-
- replaceSolidPolygons(selfTree.allPolygons());
- }
-
- /**
- * Creates deep clones of all polygons in the list.
- *
- * CSG operations modify polygons in-place via BSP tree operations.
- * Cloning ensures the original polygon data is preserved.
- *
- * @param polygons the polygons to clone
- * @return a new list containing deep clones of all polygons
- */
- private List clonePolygons(final List polygons) {
- final List cloned = new ArrayList<>(polygons.size());
- for (final SolidPolygon p : polygons) {
- cloned.add(p.deepClone());
- }
- return cloned;
+ replaceSolidPolygons(Csg.intersect(extractSolidPolygons(),
+ other.extractSolidPolygons()));
}
/**
@@ -1184,8 +1089,12 @@ public class AbstractCompositeShape extends AbstractShape {
* value.
*
* @param transformPipe the transform stack (includes this composite's transform)
- * @param aggregator unused in the parallel path: per-task aggregators
- * are merged by the coordinator's drain
+ * @param aggregator the caller's aggregator, used ONLY when the fork
+ * bails out (too few chunks, or the frame task
+ * budget is exhausted) and the children fall back
+ * to a serial inline transform. The parallel fork
+ * itself queues into per-task aggregators merged
+ * by the coordinator's drain.
* @param context the rendering context (provides the coordinator)
*/
private void transformChildrenParallel(final TransformStack transformPipe,
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java
new file mode 100644
index 0000000..5eb8191
--- /dev/null
+++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java
@@ -0,0 +1,160 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base;
+
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pure CSG (Constructive Solid Geometry) boolean engine: union, subtract
+ * and intersect over lists of {@link SolidPolygon}s, built on BSP tree
+ * clip/invert sequences.
+ *
+ * These are pure functions â they take polygon lists and return new
+ * polygon lists, touching no shape state. {@link AbstractCompositeShape}'s
+ * instance methods ({@code union/subtract/intersect}) delegate here and
+ * handle the child-registry bookkeeping themselves.
+ *
+ * All operations clone their inputs first (BSP operations mutate
+ * polygons in place), so caller lists are never modified.
+ */
+public final class Csg {
+
+ private Csg() {
+ // utility class
+ }
+
+ /**
+ * Union of two polygon sets: every surface of both, interior faces
+ * removed.
+ *
+ * @param a first operand's polygons (not modified)
+ * @param b second operand's polygons (not modified)
+ * @return the union result polygons
+ */
+ public static List union(final List a,
+ final List b) {
+ final BspTree selfTree = new BspTree(clonePolygons(a));
+ final BspTree otherTree = new BspTree(clonePolygons(b));
+
+ // Remove from self any polygons that are inside other (interior faces)
+ selfTree.clipTo(otherTree);
+
+ // Remove from other any polygons that are inside self (interior faces)
+ otherTree.clipTo(selfTree);
+
+ // Invert other to convert remaining polygons for the next clip step
+ otherTree.invert();
+
+ // Clip inverted other against self to remove back-facing coplanar polygons
+ otherTree.clipTo(selfTree);
+
+ // Invert back to restore correct polygon orientation
+ otherTree.invert();
+
+ // Merge other's remaining polygons into self's BSP tree
+ selfTree.addPolygons(otherTree.allPolygons());
+
+ return selfTree.allPolygons();
+ }
+
+ /**
+ * Subtraction {@code a - b}: the cutter volume carved out of the
+ * target.
+ *
+ * @param a target polygons (not modified)
+ * @param b cutter polygons (not modified)
+ * @return the difference result polygons
+ */
+ public static List subtract(final List a,
+ final List b) {
+ final BspTree target = new BspTree(clonePolygons(a));
+ final BspTree cutter = new BspTree(clonePolygons(b));
+
+ // Invert target: convert "inside" to "outside" and vice versa
+ // This transforms the problem from "subtract B from A" to "intersect A's complement with B's complement"
+ target.invert();
+
+ // Clip target against cutter: removes parts of target that are INSIDE the cutter
+ // Since target is inverted, this removes parts that were OUTSIDE the original target
+ target.clipTo(cutter);
+
+ // Clip cutter against (inverted) target: removes parts of cutter outside the inverted target
+ // This keeps only cutter polygons that are inside the inverted target = outside original target
+ cutter.clipTo(target);
+
+ // Invert cutter to flip its inside/outside
+ cutter.invert();
+
+ // Clip inverted cutter against target: removes coplanar back-faces
+ cutter.clipTo(target);
+
+ // Invert cutter back to correct orientation
+ cutter.invert();
+
+ // Merge cutter's polygons into target's BSP tree
+ target.addPolygons(cutter.allPolygons());
+
+ // Invert target back to restore correct inside/outside orientation
+ // Result: the carved-out volume (target minus cutter)
+ target.invert();
+
+ return target.allPolygons();
+ }
+
+ /**
+ * Intersection of two polygon sets: only the overlapping volume
+ * remains.
+ *
+ * @param a first operand's polygons (not modified)
+ * @param b second operand's polygons (not modified)
+ * @return the intersection result polygons
+ */
+ public static List intersect(final List a,
+ final List b) {
+ final BspTree selfTree = new BspTree(clonePolygons(a));
+ final BspTree otherTree = new BspTree(clonePolygons(b));
+
+ // Invert self to convert "inside" to "outside"
+ // This transforms intersection into: keep parts that are "outside both inverted shapes"
+ selfTree.invert();
+
+ // Clip other against inverted self: keeps only parts of other that are INSIDE original self
+ // (because clipTo removes what's "outside" the BSP, and inverted self's "outside" = original self's "inside")
+ otherTree.clipTo(selfTree);
+
+ // Invert other (which now represents the intersection region)
+ otherTree.invert();
+
+ // Clip inverted self against (inverted intersection): removes parts outside the intersection
+ selfTree.clipTo(otherTree);
+
+ // Clip intersection result against inverted self: removes back-facing coplanar polygons
+ otherTree.clipTo(selfTree);
+
+ // Build final BSP tree from the clipped intersection polygons
+ selfTree.addPolygons(otherTree.allPolygons());
+
+ // Invert back to restore correct inside/outside orientation
+ selfTree.invert();
+
+ return selfTree.allPolygons();
+ }
+
+ /**
+ * Deep clones of all polygons in the list: CSG operations modify
+ * polygons in-place via BSP tree operations, cloning preserves the
+ * originals.
+ */
+ private static List clonePolygons(final List polygons) {
+ final List cloned = new ArrayList<>(polygons.size());
+ for (final SolidPolygon p : polygons) {
+ cloned.add(p.deepClone());
+ }
+ return cloned;
+ }
+}
--
2.20.1