Skip to content

Commit fce3950

Browse files
committed
Add BooleanGroup Phase 1: Weeks 1 + 2
Introduces the BooleanGroup class to Two.js, enabling boolean operations ('union', 'subtract', 'intersect', 'exclude') on groups of shapes. Adds the makeBooleanGroup factory method, updates type definitions, and provides initial structure and tests for BooleanGroup functionality (Phase 1: computation not yet implemented).
1 parent f7bfa3e commit fce3950

File tree

5 files changed

+781
-1
lines changed

5 files changed

+781
-1
lines changed

src/boolean-group.js

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { Group } from './group.js';
2+
3+
/**
4+
* @name Two.BooleanGroup
5+
* @class
6+
* @extends Two.Group
7+
* @param {Two.Shape[]} [children] - A list of {@link Two.Shape} objects for boolean operations.
8+
* @param {String} [operation='union'] - The boolean operation to apply: 'union', 'subtract', 'intersect', or 'exclude'.
9+
* @description A {@link Two.Group} that applies boolean operations to its children. The result is cached and recomputed only when the operation or children change.
10+
*/
11+
export class BooleanGroup extends Group {
12+
/**
13+
* @name Two.BooleanGroup#_flagOperation
14+
* @private
15+
* @property {Boolean} - Determines whether the {@link Two.BooleanGroup#operation} needs updating.
16+
*/
17+
_flagOperation = false;
18+
19+
/**
20+
* @name Two.BooleanGroup#_operation
21+
* @private
22+
* @property {String} - The boolean operation type.
23+
* @see {@link Two.BooleanGroup#operation}
24+
*/
25+
_operation = 'union';
26+
27+
/**
28+
* @name Two.BooleanGroup#_resultPath
29+
* @private
30+
* @property {Two.Path} - Cached result path from the boolean operation.
31+
*/
32+
_resultPath = null;
33+
34+
constructor(children, operation) {
35+
super(children);
36+
37+
for (let prop in proto) {
38+
Object.defineProperty(this, prop, proto[prop]);
39+
}
40+
41+
this._renderer.type = 'boolean-group';
42+
43+
/**
44+
* @name Two.BooleanGroup#operation
45+
* @property {String} - The boolean operation to apply to children: 'union', 'subtract', 'intersect', or 'exclude'.
46+
*/
47+
if (operation) {
48+
this.operation = operation;
49+
}
50+
}
51+
52+
/**
53+
* @name Two.BooleanGroup.Properties
54+
* @property {String[]} - A list of properties that are on every {@link Two.BooleanGroup}.
55+
*/
56+
static Properties = ['operation'];
57+
58+
/**
59+
* @name Two.BooleanGroup.Operations
60+
* @property {Object} - Object of possible boolean operations to perform
61+
*/
62+
static Operations = {
63+
union: 'union',
64+
subtract: 'subtract',
65+
intersect: 'intersect',
66+
exclude: 'exclude',
67+
};
68+
69+
/**
70+
* @name Two.BooleanGroup#getResultPath
71+
* @function
72+
* @returns {Two.Path} - The computed result path of the boolean operation.
73+
* @description Returns the cached result path if available, otherwise computes and caches it.
74+
* @nota-bene In Phase 1, this returns null as the computation algorithm will be implemented in later phases.
75+
*/
76+
getResultPath() {
77+
if (this._flagOperation || !this._resultPath) {
78+
// Phase 1: Return null - computation will be implemented in later phases
79+
// The infrastructure is in place to cache and recompute as needed
80+
this._resultPath = null;
81+
this._flagOperation = false;
82+
}
83+
84+
return this._resultPath;
85+
}
86+
87+
/**
88+
* @name Two.BooleanGroup#flatten
89+
* @function
90+
* @returns {Two.Path} - A new permanent path representing the boolean operation result.
91+
* @description Converts the boolean group to a permanent path. The returned path is not cached and represents a snapshot of the current operation result.
92+
* @nota-bene In Phase 1, this returns null as the computation algorithm will be implemented in later phases.
93+
*/
94+
flatten() {
95+
const resultPath = this.getResultPath();
96+
97+
if (!resultPath) {
98+
// Phase 1: Return null - computation will be implemented in later phases
99+
return null;
100+
}
101+
102+
// Clone the result path to create a permanent copy
103+
const permanentPath = resultPath.clone();
104+
105+
// Copy transformation from the boolean group to the permanent path
106+
permanentPath.translation.copy(this.translation);
107+
permanentPath.rotation = this.rotation;
108+
permanentPath.scale = this.scale;
109+
110+
if (this.matrix.manual) {
111+
permanentPath.matrix.copy(this.matrix);
112+
}
113+
114+
return permanentPath;
115+
}
116+
117+
/**
118+
* @name Two.BooleanGroup#_update
119+
* @function
120+
* @private
121+
* @param {Boolean} [bubbles=false] - Force the parent to `_update` as well.
122+
* @description This is called before rendering happens by the renderer. If the operation changed or children were modified, it triggers recomputation of the result path.
123+
* @nota-bene Try not to call this method more than once a frame.
124+
*/
125+
_update() {
126+
// Check if we need to recompute the boolean operation result
127+
if (
128+
this._flagOperation ||
129+
this._flagAdditions ||
130+
this._flagSubtractions ||
131+
this._flagOrder
132+
) {
133+
// Mark that the result needs recomputation
134+
this._flagOperation = true;
135+
136+
// Get the result path (will trigger recomputation if needed)
137+
const resultPath = this.getResultPath();
138+
139+
// If we have a result path, apply group styling to it
140+
if (resultPath) {
141+
resultPath.fill = this.fill;
142+
resultPath.stroke = this.stroke;
143+
resultPath.linewidth = this.linewidth;
144+
resultPath.opacity = this.opacity;
145+
resultPath.visible = this.visible;
146+
resultPath.cap = this.cap;
147+
resultPath.join = this.join;
148+
resultPath.miter = this.miter;
149+
}
150+
}
151+
152+
// Call parent update
153+
return super._update.apply(this, arguments);
154+
}
155+
156+
/**
157+
* @name Two.BooleanGroup#flagReset
158+
* @function
159+
* @private
160+
* @description Called internally to reset all flags. Ensures that only properties that change are updated before being sent to the renderer.
161+
*/
162+
flagReset() {
163+
this._flagOperation = false;
164+
165+
super.flagReset.call(this);
166+
167+
return this;
168+
}
169+
170+
/**
171+
* @name Two.BooleanGroup#clone
172+
* @function
173+
* @param {Two.Group} [parent] - The parent group or scene to add the clone to.
174+
* @returns {Two.BooleanGroup}
175+
* @description Create a new instance of {@link Two.BooleanGroup} with the same properties of the current group.
176+
*/
177+
clone(parent) {
178+
const children = this.children.map(function (child) {
179+
return child.clone();
180+
});
181+
182+
const clone = new BooleanGroup(children, this.operation);
183+
184+
clone.opacity = this.opacity;
185+
186+
if (this.mask) {
187+
clone.mask = this.mask;
188+
}
189+
190+
clone.translation.copy(this.translation);
191+
clone.rotation = this.rotation;
192+
clone.scale = this.scale;
193+
clone.className = this.className;
194+
195+
if (this.matrix.manual) {
196+
clone.matrix.copy(this.matrix);
197+
}
198+
199+
if (parent) {
200+
parent.add(clone);
201+
}
202+
203+
return clone._update();
204+
}
205+
206+
/**
207+
* @name Two.BooleanGroup#toObject
208+
* @function
209+
* @returns {Object}
210+
* @description Return a JSON compatible plain object that represents the boolean group.
211+
*/
212+
toObject() {
213+
const result = super.toObject.call(this);
214+
215+
result.renderer.type = 'boolean-group';
216+
result.operation = this.operation;
217+
218+
return result;
219+
}
220+
}
221+
222+
const proto = {
223+
operation: {
224+
enumerable: true,
225+
get: function () {
226+
return this._operation;
227+
},
228+
set: function (v) {
229+
const validOperations = Object.values(BooleanGroup.Operations);
230+
if (validOperations.indexOf(v) === -1) {
231+
console.warn(
232+
`Two.BooleanGroup: Invalid operation "${v}". Valid operations are: ${validOperations.join(
233+
', '
234+
)}`
235+
);
236+
return;
237+
}
238+
this._flagOperation = this._operation !== v || this._flagOperation;
239+
this._operation = v;
240+
},
241+
},
242+
};

src/two.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Anchor } from './anchor.js';
1717
import { Collection } from './collection.js';
1818
import { Events } from './events.js';
1919
import { Group } from './group.js';
20+
import { BooleanGroup } from './boolean-group.js';
2021
import { Matrix } from './matrix.js';
2122
import { Path } from './path.js';
2223
import { Registry } from './registry.js';
@@ -329,6 +330,7 @@ export default class Two {
329330
static Collection = Collection;
330331
static Events = Events;
331332
static Group = Group;
333+
static BooleanGroup = BooleanGroup;
332334
static Matrix = Matrix;
333335
static Path = Path;
334336
static Registry = Registry;
@@ -1148,6 +1150,32 @@ export default class Two {
11481150
return group;
11491151
}
11501152

1153+
/**
1154+
* @name Two#makeBooleanGroup
1155+
* @function
1156+
* @param {(Two.Shape[]|...Two.Shape)} [objects] - Two.js objects to be added to the boolean group in the form of an array or as individual arguments.
1157+
* @param {String} [operation='union'] - The boolean operation to apply: 'union', 'subtract', 'intersect', or 'exclude'.
1158+
* @returns {Two.BooleanGroup}
1159+
* @description Creates a Two.js boolean group object and adds it to the scene.
1160+
*/
1161+
makeBooleanGroup(objects, operation) {
1162+
if (!(objects instanceof Array)) {
1163+
objects = Array.prototype.slice.call(arguments);
1164+
// If operation was passed as second argument when using varargs,
1165+
// check if last argument is a string (operation)
1166+
const lastArg = objects[objects.length - 1];
1167+
if (typeof lastArg === 'string') {
1168+
operation = lastArg;
1169+
objects = objects.slice(0, -1);
1170+
}
1171+
}
1172+
1173+
const group = new BooleanGroup(objects, operation);
1174+
this.scene.add(group);
1175+
1176+
return group;
1177+
}
1178+
11511179
/**
11521180
* @name Two#interpret
11531181
* @function

0 commit comments

Comments
 (0)