Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/editor/pane/mask.ts
1784 views
1
import { ComboBoxPropertyItemDataType, IObservable, makeObservable, ObservableValidator } from "@minecraft/server-editor";
2
import { Token } from "@modules/extern/tokenizr";
3
import { Vector, whenReady } from "@notbeer-api";
4
import { PaneBuilder, UIPane } from "./builder";
5
import {
6
BlockMaskNode,
7
ChainMaskNode,
8
ExistingMaskNode,
9
InputMaskNode,
10
IntersectMaskNode,
11
Mask,
12
MaskNode,
13
NegateMaskNode,
14
OffsetMaskNode,
15
PercentMaskNode,
16
ShadowMaskNode,
17
StateMaskNode,
18
SurfaceMaskNode,
19
TagMaskNode,
20
} from "@modules/mask";
21
import { BlockPermutation, BlockStates, BlockStateType, BlockTypes, RawMessage } from "@minecraft/server";
22
import { EventEmitter } from "library/classes/eventEmitter";
23
24
const dummyToken = new Token("", undefined, "");
25
const defaultBLock = "air";
26
const blockMaskNode = (block = defaultBLock) => new BlockMaskNode(dummyToken, { id: block });
27
const maskTypes = new Map<new (...args: any[]) => MaskNode, [string, () => MaskNode]>([
28
[BlockMaskNode, ["Block Mask", blockMaskNode]],
29
[ChainMaskNode, ["Chain Mask", () => new ChainMaskNode(dummyToken, [blockMaskNode()])]],
30
[IntersectMaskNode, ["Interset Mask", () => new IntersectMaskNode(dummyToken, [blockMaskNode()])]],
31
[StateMaskNode, ["Block State Mask", () => new StateMaskNode(dummyToken, new Map(), false)]],
32
[TagMaskNode, ["Tag Mask", () => new TagMaskNode(dummyToken, "wood")]],
33
[NegateMaskNode, ["Negate Mask", () => new NegateMaskNode(dummyToken, blockMaskNode())]],
34
[OffsetMaskNode, ["Offset Mask", () => new OffsetMaskNode(dummyToken, Vector.UP, blockMaskNode())]],
35
[SurfaceMaskNode, ["Surface Mask", () => new SurfaceMaskNode(dummyToken)]],
36
[ExistingMaskNode, ["Existing Mask", () => new ExistingMaskNode(dummyToken)]],
37
[ShadowMaskNode, ["Shadow Mask", () => new ShadowMaskNode(dummyToken)]],
38
[PercentMaskNode, ["Random Mask", () => new PercentMaskNode(dummyToken, 0.5)]],
39
[InputMaskNode, ["Input Mask", () => new InputMaskNode(dummyToken, defaultBLock)]],
40
]);
41
42
const blockTags: string[] = [];
43
whenReady(() => {
44
const set = new Set<string>();
45
BlockTypes.getAll().forEach(({ id }) =>
46
BlockPermutation.resolve(id)
47
.getTags()
48
.forEach((tag) => set.add(tag))
49
);
50
blockTags.push(...set.keys());
51
});
52
53
export class MaskUIBuilder extends EventEmitter<{ changed: [mask: Mask | undefined] }> implements PaneBuilder, IObservable<Mask | undefined> {
54
validator?: ObservableValidator<Mask | null>;
55
56
private node: MaskNode;
57
private enabled = makeObservable(false);
58
private pane?: UIPane;
59
60
constructor(mask?: Mask) {
61
super();
62
this.node = (mask ?? new Mask("air")).getRootNode();
63
this.enabled.set(!!mask);
64
}
65
66
get value(): Mask | undefined {
67
return this.enabled.value ? Mask.fromNode(this.node) : undefined;
68
}
69
70
set(newValue: Mask | undefined): boolean {
71
if (!newValue) return this.disable();
72
if (this.validator) newValue ??= this.validator.validate(newValue);
73
const changed = newValue?.toJSON() !== this.value?.toJSON();
74
this.node = newValue.getRootNode();
75
if (this.pane) this.build(this.pane);
76
if (!this.enabled.value) this.enabled.set(true);
77
else if (changed) this.emit("changed", this.value);
78
return changed;
79
}
80
81
disable() {
82
if (!this.enabled.value) return false;
83
this.enabled.set(false);
84
return true;
85
}
86
87
enable() {
88
if (this.enabled.value) return false;
89
this.enabled.set(true);
90
return true;
91
}
92
93
build(pane: UIPane) {
94
pane.changeItems([
95
{
96
type: "toggle",
97
title: "Enabled",
98
value: this.enabled,
99
onChange: (value) => {
100
const maskPane = pane.getSubPane(1);
101
maskPane.visible = value;
102
this.emit("changed", this.value);
103
},
104
},
105
{
106
type: "subpane",
107
hasExpander: false,
108
hasMargins: false,
109
items: [],
110
},
111
]);
112
pane.getSubPane(1).visible = this.enabled.value;
113
this.buildMaskUI(pane.getSubPane(1), this.node);
114
this.pane = pane;
115
}
116
117
private buildMaskUI(pane: UIPane, maskNode: MaskNode, parentNode?: MaskNode) {
118
let type = maskNode.constructor as new (...args: any[]) => MaskNode;
119
120
const build = () => {
121
pane.changeItems([
122
{
123
type: "dropdown",
124
title: "Type",
125
entries: Array.from(maskTypes.values()).map(([label], value) => ({ label, value })),
126
value: Array.from(maskTypes.keys()).indexOf(type),
127
onChange: (typeIndex) => {
128
const oldNode = maskNode;
129
type = Array.from(maskTypes.keys())[typeIndex];
130
maskNode = maskTypes.get(type)[1]();
131
if (!parentNode) this.node = maskNode;
132
build();
133
134
const siblings = parentNode?.nodes;
135
if (siblings && siblings.includes(oldNode)) {
136
const index = siblings.indexOf(oldNode);
137
siblings.splice(index, 1, maskNode);
138
}
139
this.emit("changed", this.value);
140
},
141
},
142
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
143
]);
144
145
const subPane = pane.getSubPane(1);
146
if (type === BlockMaskNode) this.buildBlockMaskUI(subPane, maskNode as BlockMaskNode);
147
else if (type === StateMaskNode) this.buildStateMaskUI(subPane, maskNode as StateMaskNode);
148
else if (type === SurfaceMaskNode) this.buildSurfaceMaskUI(subPane, maskNode as SurfaceMaskNode);
149
else if (type === TagMaskNode) this.buildTagMaskUI(subPane, maskNode as TagMaskNode);
150
else if (type === PercentMaskNode) this.buildPercentMaskUI(subPane, maskNode as PercentMaskNode);
151
else if (type === ChainMaskNode) this.buildChainOrIntersectMaskUI(subPane, maskNode as ChainMaskNode);
152
else if (type === IntersectMaskNode) this.buildChainOrIntersectMaskUI(subPane, maskNode as IntersectMaskNode);
153
else if (type === NegateMaskNode) this.buildNegateMaskUI(subPane, maskNode as NegateMaskNode);
154
else if (type === OffsetMaskNode) this.buildOffsetMaskUI(subPane, maskNode as OffsetMaskNode);
155
else if (type === InputMaskNode) this.buildInputMaskUI(subPane, maskNode as InputMaskNode);
156
};
157
build();
158
}
159
160
private buildBlockMaskUI(pane: UIPane, node: BlockMaskNode) {
161
let validNewStates: BlockStateType[] = [];
162
const statePanes = new Map<string, string>();
163
164
const addStateUI = (pane: UIPane, state: BlockStateType, defaultValue?: any) => {
165
const subPane = pane.addSubPane({
166
hasMargins: false,
167
hasExpander: false,
168
items: [
169
{
170
type: "dropdown",
171
title: state,
172
value: defaultValue !== undefined ? state.validValues.indexOf(defaultValue) : 0,
173
entries: state.validValues.map((v, i) => ({ label: `${v}`, value: i })),
174
onChange: (index) => {
175
node.block.states.set(state.id, state.validValues[index]);
176
this.emit("changed", this.value);
177
},
178
},
179
{
180
type: "button",
181
title: "Delete State",
182
pressed: () => {
183
pane.removeSubPane(subPane);
184
node.block.states.delete(state.id);
185
statePanes.delete(state.id);
186
updateStateEntries();
187
this.emit("changed", this.value);
188
},
189
},
190
],
191
});
192
statePanes.set(state.id, subPane);
193
};
194
195
const updateStateEntries = () => {
196
validNewStates = Object.keys(BlockPermutation.resolve(node.block.id).getAllStates())
197
.filter((state) => !node.block.states?.has(state))
198
.map((state) => BlockStates.get(state));
199
pane.setVisibility(2, !!validNewStates.length);
200
pane.updateEntries(2, [{ label: "Select State", value: -1 }, ...validNewStates.map((state, i) => ({ label: state.id, value: i }))]);
201
};
202
203
pane.changeItems([
204
{
205
type: "combo_box",
206
title: "Block",
207
dataType: ComboBoxPropertyItemDataType.Block,
208
showImage: true,
209
value: node.block.id,
210
onChange: (value) => {
211
node.block.id = value;
212
const validStates = BlockPermutation.resolve(node.block.id).getAllStates();
213
for (const state of node.block.states?.keys() ?? []) {
214
if (state in validStates) continue;
215
if (statePanes.has(state)) statePane.removeSubPane(statePanes.get(state));
216
node.block.states.delete(state);
217
}
218
updateStateEntries();
219
this.emit("changed", this.value);
220
},
221
},
222
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
223
{
224
type: "dropdown",
225
title: "New Block State",
226
entries: [],
227
value: -1,
228
onChange: (value) => {
229
if (value === -1) return;
230
const newState = validNewStates[value];
231
node.block.states ??= new Map();
232
node.block.states.set(newState.id, newState.validValues[0]);
233
addStateUI(pane.getSubPane(1), newState);
234
updateStateEntries();
235
pane.setValue(2, -1);
236
this.emit("changed", this.value);
237
},
238
},
239
]);
240
241
const statePane = pane.getSubPane(1);
242
for (const [state, value] of node.block.states?.entries() ?? []) addStateUI(statePane, BlockStates.get(state), value);
243
updateStateEntries();
244
}
245
246
private buildStateMaskUI(pane: UIPane, node: StateMaskNode) {
247
let validNewStates = BlockStates.getAll().filter((state) => !node.states.has(state.id));
248
249
const addStateUI = (pane: UIPane, state: BlockStateType, defaultValue?: any) => {
250
const subPane = pane.addSubPane({
251
hasMargins: false,
252
hasExpander: false,
253
items: [
254
{
255
type: "dropdown",
256
title: state,
257
value: defaultValue !== undefined ? state.validValues.indexOf(defaultValue) : 0,
258
entries: state.validValues.map((v, i) => ({ label: `${v}`, value: i })),
259
onChange: (index) => {
260
node.states.set(state.id, state.validValues[index]);
261
this.emit("changed", this.value);
262
},
263
},
264
{
265
type: "button",
266
title: "Delete State",
267
variant: 3,
268
pressed: () => {
269
pane.removeSubPane(subPane);
270
node.states.delete(state.id);
271
updateStateEntries();
272
this.emit("changed", this.value);
273
},
274
},
275
],
276
});
277
};
278
279
const updateStateEntries = () => {
280
validNewStates = BlockStates.getAll().filter((state) => !node.states.has(state.id));
281
pane.updateEntries(1, [{ label: "Select State", value: -1 }, ...validNewStates.map((state, i) => ({ label: state.id, value: i }))]);
282
};
283
284
pane.changeItems([
285
{
286
type: "toggle",
287
title: "Strict Mode",
288
value: node.strict,
289
onChange: (value) => {
290
node.strict = value;
291
this.emit("changed", this.value);
292
},
293
},
294
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
295
{
296
type: "dropdown",
297
title: "New Block State",
298
entries: [],
299
value: -1,
300
onChange: (value) => {
301
if (value === -1) return;
302
const newState = validNewStates[value];
303
node.states.set(newState.id, newState.validValues[0]);
304
addStateUI(statePane, newState);
305
updateStateEntries();
306
pane.setValue(1, -1);
307
this.emit("changed", this.value);
308
},
309
},
310
]);
311
312
const statePane = pane.getSubPane(1);
313
for (const [state, value] of node.states.entries()) addStateUI(statePane, BlockStates.get(state), value);
314
updateStateEntries();
315
}
316
317
private buildTagMaskUI(pane: UIPane, node: TagMaskNode) {
318
pane.changeItems([
319
{
320
type: "dropdown",
321
title: "Block Tag",
322
value: blockTags.indexOf(node.tag),
323
entries: blockTags.map((label, value) => ({ label, value })),
324
onChange: (value) => {
325
node.tag = blockTags[value];
326
this.emit("changed", this.value);
327
},
328
},
329
]);
330
}
331
332
private buildChainOrIntersectMaskUI(pane: UIPane, node: ChainMaskNode | IntersectMaskNode) {
333
const eachSubPane = (callback: (pane: UIPane, index: number) => void) => {
334
Object.values(maskPane.getAllSubPanes()).forEach((pane, index) => callback(pane, index));
335
};
336
337
const updateSubPanes = () => {
338
eachSubPane((pane, index) => {
339
pane.setVisibility(1, node.nodes.length > 1);
340
pane.title = `Sub-Mask ${index + 1}`;
341
});
342
};
343
344
const addMaskUI = (pane: UIPane, index: number, subNode: MaskNode) => {
345
const subPane = pane.addSubPane({
346
title: `Sub-Mask ${index + 1}`,
347
items: [
348
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
349
{
350
type: "button",
351
title: "Remove Mask",
352
variant: 3,
353
visible: node.nodes.length > 1,
354
pressed: () => {
355
pane.removeSubPane(subPane);
356
node.nodes.splice(index, 1);
357
updateSubPanes();
358
this.emit("changed", this.value);
359
},
360
},
361
],
362
});
363
this.buildMaskUI(pane.getSubPane(subPane).getSubPane(0), subNode, node);
364
};
365
366
pane.changeItems([
367
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
368
{
369
type: "button",
370
title: "Add Sub-Mask",
371
pressed: () => {
372
const newNode = blockMaskNode();
373
node.nodes.push(newNode);
374
addMaskUI(maskPane, node.nodes.length - 1, newNode);
375
updateSubPanes();
376
this.emit("changed", this.value);
377
},
378
},
379
]);
380
381
const maskPane = pane.getSubPane(0);
382
for (let i = 0; i < node.nodes.length; i++) addMaskUI(maskPane, i, node.nodes[i]);
383
updateSubPanes();
384
}
385
386
private buildNegateMaskUI(pane: UIPane, node: NegateMaskNode) {
387
pane.changeItems([{ type: "subpane", title: "Sub-Mask", items: [] }]);
388
this.buildMaskUI(pane.getSubPane(0), node.nodes[0], node);
389
}
390
391
private buildOffsetMaskUI(pane: UIPane, node: OffsetMaskNode) {
392
pane.changeItems([
393
{
394
type: "vector3",
395
title: "Offset",
396
value: node.offset,
397
onChange: (value) => {
398
node.offset = value;
399
this.emit("changed", this.value);
400
},
401
},
402
{ type: "subpane", title: "Sub-Mask", items: [] },
403
]);
404
this.buildMaskUI(pane.getSubPane(1), node.nodes[0], node);
405
}
406
407
private buildSurfaceMaskUI(pane: UIPane, node: SurfaceMaskNode) {
408
const range = { min: 0, max: 90 };
409
pane.changeItems([
410
{
411
type: "toggle",
412
title: "Filter Slope",
413
value: node.lowerAngle === undefined || node.upperAngle === undefined,
414
onChange: (value) => {
415
pane.setVisibility(1, value);
416
pane.setVisibility(2, value);
417
if (value) {
418
node.lowerAngle = 0;
419
node.upperAngle = 90;
420
pane.setValue(1, 0);
421
pane.setValue(2, 90);
422
} else {
423
node.lowerAngle = undefined;
424
node.upperAngle = undefined;
425
}
426
this.emit("changed", this.value);
427
},
428
},
429
{
430
type: "slider",
431
title: "Minimum Angle",
432
...range,
433
value: node.lowerAngle ?? 0,
434
onChange: (value) => {
435
node.lowerAngle = value;
436
this.emit("changed", this.value);
437
},
438
},
439
{
440
type: "slider",
441
title: "Maximum Angle",
442
...range,
443
value: node.upperAngle ?? 90,
444
onChange: (value) => {
445
node.upperAngle = value;
446
this.emit("changed", this.value);
447
},
448
},
449
]);
450
}
451
452
private buildPercentMaskUI(pane: UIPane, node: PercentMaskNode) {
453
pane.changeItems([
454
{
455
type: "slider",
456
title: "Chance",
457
min: 0,
458
max: 100,
459
value: node.percent * 100,
460
onChange: (value) => {
461
node.percent = value / 100;
462
this.emit("changed", this.value);
463
},
464
},
465
]);
466
}
467
468
private buildInputMaskUI(pane: UIPane, node: InputMaskNode) {
469
pane.changeItems([
470
{
471
type: "text_area",
472
title: "Mask",
473
value: node.input,
474
onChange: (value) => {
475
node.input = value;
476
try {
477
Mask.parseArgs([node.input]);
478
pane.setVisibility(1, false);
479
} catch (err) {
480
pane.setVisibility(1, true);
481
if ("isSyntaxError" in err) {
482
const { start, end } = err as { start: number; end: number };
483
pane.setValue(1, { id: "commands.generic.syntax", props: [value.slice(0, start), value.slice(start, end + 1), value.slice(end + 1)] });
484
} else if (err.rawtext?.[0].translate) {
485
const message = err.rawtext[0] as RawMessage;
486
pane.setValue(1, { id: message.translate, props: message.with as string[] });
487
}
488
}
489
this.emit("changed", this.value);
490
},
491
},
492
{ type: "label", visible: false, text: "" },
493
]);
494
}
495
}
496
497