<!-- SOLIDIVISM · part 358 -->
o's
		// complement.
		const isInside = fillRule === 'nonzero'
			? ( w => w !== 0 )
			: ( w => ( w & 1 ) !== 0 );

		// Build an entry per usable subpath. Self-winding follows the standard
		// convention used by ShapeUtils: counter-clockwise (signed area > 0)
		// contributes +1 to the winding number at an interior point,
		// clockwise contributes -1.
		const entries = [];

		for ( const subPath of this.subPaths ) {

			const points = subPath.getPoints();
			if ( points.length < 3 ) continue;

			const area = ShapeUtils.area( points );
			if ( area === 0 ) continue;

			const boundingBox = new Box2();
			for ( let i = 0; i < points.length; i ++ ) boundingBox.expandByPoint( points[ i ] );

			entries.push( {
				subPath: subPath,
				points: points,
				boundingBox: boundingBox,
				interiorPoint: getInteriorPoint( points, boundingBox ),
				absArea: Math.abs( area ),
				winding: area < 0 ? -1 : 1,
				container: null,
				exclude: false,
				role: null
			} );

		}

		// Sort by area descending. This guarantees that any subpath that could
		// contain `entries[i]` is located at a smaller index and has already
		// been processed when it's entries[i]'s turn. Port of paper.js'
		// reorientPaths() algorithm.
		entries.sort( ( a, b ) => b.absArea - a.absArea );

		// Walk already-processed entries from closest-in-size to largest,
		// stopping at the innermost container. Accumulate the container's
		// cumulative winding into this entry's winding so that the final value
		// equals the winding number at this entry's interior point.
		//
		// A subpath only contributes to the fill boundary when crossing it
		// actually flips the "insideness" per the fill rule; otherwise it's a
		// redundant overlap and gets excluded to avoid double-counting.
		for ( let i = 0; i < entries.length; i ++ ) {

			const entry = entries[ i ];
			let containerWinding = 0;

			for ( let j = i - 1; j >= 0; j -- ) {

				const candidate = entries[ j ];

				if ( ! candidate.boundingBox.containsBox( entry.boundingBox ) ) continue;
				if ( ! pointInPolygon( entry.interiorPoint, candidate.points ) ) continue;

				entry.container = candidate.exclude ? candidate.container : candidate;
				containerWinding = candidate.winding;
				entry.winding += containerWinding;
				break;

			}

			if ( isInside( entry.winding ) === isInside( containerWinding ) ) {

				entry.exclude = true;

			}

		}

		// Classify retained entries. An entry is an outer shape if it has no
		// container or if its container is itself a hole (a solid nested inside
		// a hole becomes a new top-level shape); otherwise it's a hole in its
		// container. Entries were already sorted outermost-first, so each
		// container's role is known by the time we look at it.
		for ( const entry of entries ) {

			if ( entry.exclude ) continue;
			entry.role = ( entry.container === null || entry.container.role === 'hole' ) ? 'outer' : 'hole';

		}

		// Build Shapes for outers first, then attach holes to their container's
		// Shape.
		const shapes = [];
		const shapeByEntry = new Map();

		for ( const entry of entries ) {

			if ( entry.exclude || entry.role !== 'outer' ) continue;

			const shape = new Shape();
			shape.curves = entry.subPath.curves;
			shapes.push( shape );
			shapeByEntry.set( entry, shape );

		}

		for ( const entry of entries ) {

			if ( entry.exclude || entry.role !== 'hole' ) continue;

			const shape = shapeByEntry.get( entry.container );
			if ( ! shape ) continue;

			const hole = new Path();
			hole.curves = entry.subPath.curves;
			shape.holes.push( hole );

		}

		return shapes;


	}

}

/**
 * Abstract base class for controls.
 *
 * @abstract
 * @augments EventDispatcher
 */
class Controls extends EventDispatcher {

	/**
	 * Constructs a new controls instance.
	 *
	 * @param {Object3D} object - The object that is managed by the controls.
	 * @param {?HTMLElement} domElement - The HTML element used for event listeners.
	 */
	constructor( object, domElement = null ) {

		super();

		/**
		 * The object that is managed by the controls.
		 *
		 * @type {Object3D}
		 */
		this.object = object;

		/**
		 * The HTML element used for event listeners.
		 *
		 * @type {?HTMLElement}
		 * @default null
		 */
		this.domElement = domElement;

		/**
		 * Whether the controls responds to user input or not.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.enabled = true;

		/**
		 * The internal state of the controls.
		 *
		 * @type {number}
		 * @default -1
		 */
		this.state = -1;

		/**
		 * This object defines the keyboard input of the controls.
		 *
		 * @type {Object}
		 */
		this.keys = {};

		/**
		 * This object defines what type of actions are assigned to the available mouse buttons.
		 * It depends on the control implementation what kind of mouse buttons and actions are supported.
		 *
		 * @type {{LEFT: ?number, MIDDLE: ?number, RIGHT: ?number}}
		 */
		this.mouseButtons = { LEFT: null, MIDDLE: null, RIGHT: 