Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/editor/pane/pattern.ts
1784 views
1
import { BlockPermutation, BlockStates, BlockStateType, RawMessage } from "@minecraft/server";
2
import { ComboBoxPropertyItemDataType, IObservable, ObservableValidator } from "@minecraft/server-editor";
3
import { Token } from "@modules/extern/tokenizr";
4
import {
5
PatternNode,
6
VoidPatternNode,
7
BlockPatternNode,
8
ChainPatternNode,
9
TypePatternNode,
10
StatePatternNode,
11
RandStatePatternNode,
12
ClipboardPatternNode,
13
BlobPatternNode,
14
Pattern,
15
InputPatternNode,
16
GradientPatternNode,
17
} from "@modules/pattern";
18
import { Vector } from "@notbeer-api";
19
import { PaneBuilder, UIPane } from "./builder";
20
import { EventEmitter } from "library/classes/eventEmitter";
21
import { Cardinal, CardinalDirection } from "@modules/directions";
22
23
const dummyToken = new Token("", undefined, "");
24
const defaultBLock = "stone";
25
const blockPatternNode = (block = defaultBLock) => new BlockPatternNode(dummyToken, BlockPermutation.resolve(block));
26
const patternTypes = new Map<new (...args: any[]) => PatternNode, [string, () => PatternNode]>([
27
[VoidPatternNode, ["Void Pattern", () => new VoidPatternNode(dummyToken)]],
28
[BlockPatternNode, ["Block Pattern", blockPatternNode]],
29
[ChainPatternNode, ["Chain Pattern", () => new ChainPatternNode(dummyToken, [blockPatternNode()])]],
30
[TypePatternNode, ["Block Type Pattern", () => new TypePatternNode(dummyToken, defaultBLock)]],
31
[StatePatternNode, ["Block State Pattern", () => new StatePatternNode(dummyToken, new Map())]],
32
[RandStatePatternNode, ["Random Block State Pattern", () => new RandStatePatternNode(dummyToken, defaultBLock)]],
33
// [ClipboardPatternNode, ["Clipboard Pattern", () => new ClipboardPatternNode(dummyToken, Vector.ZERO)]],
34
// [HandPatternNode, ["Hotbar Pattern", () => new HandPatternNode(dummyToken)]],
35
[GradientPatternNode, ["Gradient Pattern", () => new GradientPatternNode(dummyToken, "")]],
36
[BlobPatternNode, ["Blob Pattern", () => new BlobPatternNode(dummyToken, 3, new ChainPatternNode(dummyToken, [blockPatternNode(defaultBLock), blockPatternNode("cobblestone")]))]],
37
[InputPatternNode, ["Input Pattern", () => new InputPatternNode(dummyToken, defaultBLock)]],
38
]);
39
40
export class PatternUIBuilder extends EventEmitter<{ changed: [pattern: Pattern] }> implements PaneBuilder, IObservable<Pattern> {
41
validator?: ObservableValidator<Pattern>;
42
43
private node?: PatternNode;
44
private gradientListener?: (list: string[]) => void;
45
private pane?: UIPane;
46
47
constructor(pattern: Pattern) {
48
super();
49
this.node = pattern.getRootNode();
50
}
51
52
get value() {
53
return Pattern.fromNode(this.node);
54
}
55
56
set(newValue: Pattern) {
57
if (this.validator) newValue ??= this.validator.validate(newValue);
58
const changed = newValue.toJSON() !== this.value.toJSON();
59
this.node = newValue.getRootNode();
60
if (this.pane) this.build(this.pane);
61
if (changed) this.emit("changed", this.value);
62
return changed;
63
}
64
65
build(pane: UIPane) {
66
this.buildPatternUI(pane, this.node);
67
this.pane = pane;
68
}
69
70
destroy() {
71
if (this.gradientListener) this.pane.worldedit.off("gradientListUpdated", this.gradientListener);
72
}
73
74
private buildPatternUI(pane: UIPane, patternNode: PatternNode, parentNode?: PatternNode) {
75
let type = patternNode.constructor as new (...args: any[]) => PatternNode;
76
77
const build = () => {
78
pane.changeItems([
79
{
80
type: "dropdown",
81
title: "Type",
82
entries: Array.from(patternTypes.values()).map(([label], value) => ({ label, value })),
83
value: Array.from(patternTypes.keys()).indexOf(type),
84
onChange: (typeIndex) => {
85
const oldNode = patternNode;
86
type = Array.from(patternTypes.keys())[typeIndex];
87
patternNode = patternTypes.get(type)[1]();
88
if (!parentNode) this.node = patternNode;
89
build();
90
91
const siblings = parentNode?.nodes;
92
if (siblings && siblings.includes(oldNode)) {
93
const index = siblings.indexOf(oldNode);
94
siblings.splice(index, 1, patternNode);
95
}
96
this.emit("changed", this.value);
97
},
98
},
99
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
100
]);
101
102
const subPane = pane.getSubPane(1);
103
if (type === BlockPatternNode) this.buildBlockPatternUI(subPane, patternNode as BlockPatternNode);
104
else if (type === ChainPatternNode) this.buildChainPatternUI(subPane, patternNode as ChainPatternNode);
105
else if (type === TypePatternNode) this.buildTypeOrRandStatePatternUI(subPane, patternNode as TypePatternNode);
106
else if (type === StatePatternNode) this.buildStatePatternUI(subPane, patternNode as StatePatternNode);
107
else if (type === RandStatePatternNode) this.buildTypeOrRandStatePatternUI(subPane, patternNode as RandStatePatternNode);
108
// else if (type === ClipboardPatternNode) this.buildClipboardPatternUI(subPane, patternNode as ClipboardPatternNode);
109
// else if (type === HandPatternNode) this.buildTypeOrRandStatePatternUI(subPane, patternNode.node as RandStatePatternNode);
110
else if (type === GradientPatternNode) this.buildGradientPatternUI(subPane, patternNode as GradientPatternNode);
111
else if (type === BlobPatternNode) this.buildBlobPatternUI(subPane, patternNode as BlobPatternNode);
112
else if (type === InputPatternNode) this.buildInputPatternUI(subPane, patternNode as InputPatternNode);
113
};
114
build();
115
}
116
117
private buildBlockPatternUI(pane: UIPane, node: BlockPatternNode) {
118
const buildBlockProperties = () => {
119
pane.getSubPane(1).changeItems(
120
Object.entries(node.permutation.getAllStates()).map(([key, value]) => {
121
const validValues = BlockStates.get(key).validValues;
122
return {
123
type: "dropdown",
124
title: key,
125
value: validValues.indexOf(value),
126
entries: validValues.map((v, i) => ({ label: `${v}`, value: i })),
127
onChange: (index) => {
128
node.permutation = node.permutation.withState(key as any, validValues[index]);
129
this.emit("changed", this.value);
130
},
131
};
132
})
133
);
134
};
135
136
pane.changeItems([
137
{
138
type: "combo_box",
139
title: "Block",
140
dataType: ComboBoxPropertyItemDataType.Block,
141
showImage: true,
142
value: node.permutation.type.id,
143
onChange: (value) => {
144
node.permutation = BlockPermutation.resolve(value);
145
buildBlockProperties();
146
this.emit("changed", this.value);
147
},
148
},
149
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
150
]);
151
buildBlockProperties();
152
}
153
154
private buildChainPatternUI(pane: UIPane, node: ChainPatternNode) {
155
const eachSubPane = (callback: (pane: UIPane, index: number) => void) => {
156
Object.values(patternPane.getAllSubPanes()).forEach((pane, index) => callback(pane, index));
157
};
158
159
const updateSubPanes = () => {
160
eachSubPane((pane, index) => {
161
pane.setVisibility(2, node.nodes.length > 1);
162
pane.setValue(0, node.getWeight(index) * 100);
163
pane.title = `Sub-Pattern ${index + 1}`;
164
});
165
};
166
167
const addPatternUI = (pane: UIPane, index: number, subNode: PatternNode) => {
168
const subPane = pane.addSubPane({
169
title: `Sub-Pattern ${index + 1}`,
170
items: [
171
{
172
type: "slider",
173
title: "Weight",
174
value: node.getWeight(index) * 100,
175
min: 0,
176
max: 100,
177
isInteger: true,
178
visible: !node.evenDistribution,
179
onChange: (value) => {
180
node.setWeight(index, value / 100);
181
this.emit("changed", this.value);
182
},
183
},
184
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
185
{
186
type: "button",
187
title: "Remove Pattern",
188
variant: 3,
189
visible: node.nodes.length > 1,
190
pressed: () => {
191
pane.removeSubPane(subPane);
192
node.nodes.splice(index, 1);
193
node.removeWeight(index);
194
updateSubPanes();
195
this.emit("changed", this.value);
196
},
197
},
198
],
199
});
200
this.buildPatternUI(pane.getSubPane(subPane).getSubPane(1), subNode, node);
201
};
202
203
pane.changeItems([
204
{
205
type: "toggle",
206
title: "Even Distribution",
207
value: node.evenDistribution,
208
onChange: (value) => {
209
node.evenDistribution = value;
210
eachSubPane((pane) => pane.setVisibility(0, !node.evenDistribution));
211
this.emit("changed", this.value);
212
},
213
},
214
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
215
{
216
type: "button",
217
title: "Add Sub-Pattern",
218
pressed: () => {
219
const newNode = blockPatternNode();
220
node.nodes.push(newNode);
221
addPatternUI(patternPane, node.nodes.length - 1, newNode);
222
updateSubPanes();
223
this.emit("changed", this.value);
224
},
225
},
226
]);
227
228
const patternPane = pane.getSubPane(1);
229
for (let i = 0; i < node.nodes.length; i++) addPatternUI(patternPane, i, node.nodes[i]);
230
updateSubPanes();
231
}
232
233
private buildTypeOrRandStatePatternUI(pane: UIPane, node: TypePatternNode | RandStatePatternNode) {
234
pane.changeItems([
235
{
236
type: "combo_box",
237
title: "Block",
238
dataType: ComboBoxPropertyItemDataType.Block,
239
showImage: true,
240
value: node.type,
241
onChange: (value) => {
242
node.type = value;
243
this.emit("changed", this.value);
244
},
245
},
246
]);
247
}
248
249
private buildStatePatternUI(pane: UIPane, node: StatePatternNode) {
250
let validNewStates = BlockStates.getAll().filter((state) => !node.states.has(state.id));
251
252
const addStateUI = (pane: UIPane, state: BlockStateType, defaultValue?: any) => {
253
const subPane = pane.addSubPane({
254
hasMargins: false,
255
hasExpander: false,
256
items: [
257
{
258
type: "dropdown",
259
title: state,
260
value: defaultValue !== undefined ? state.validValues.indexOf(defaultValue) : 0,
261
entries: state.validValues.map((v, i) => ({ label: `${v}`, value: i })),
262
onChange: (index) => {
263
node.states.set(state.id, state.validValues[index]);
264
this.emit("changed", this.value);
265
},
266
},
267
{
268
type: "button",
269
title: "Delete State",
270
variant: 3,
271
pressed: () => {
272
pane.removeSubPane(subPane);
273
node.states.delete(state.id);
274
updateStateEntries();
275
this.emit("changed", this.value);
276
},
277
},
278
],
279
});
280
};
281
282
const updateStateEntries = () => {
283
validNewStates = BlockStates.getAll().filter((state) => !node.states.has(state.id));
284
pane.updateEntries(1, [{ label: "Select State", value: -1 }, ...validNewStates.map((state, i) => ({ label: state.id, value: i }))]);
285
};
286
287
pane.changeItems([
288
{ type: "subpane", hasExpander: false, hasMargins: false, items: [] },
289
{
290
type: "dropdown",
291
title: "New Block State",
292
entries: [],
293
value: -1,
294
onChange: (value) => {
295
if (value === -1) return;
296
const newState = validNewStates[value];
297
node.states.set(newState.id, newState.validValues[0]);
298
addStateUI(statePane, newState);
299
updateStateEntries();
300
pane.setValue(1, -1);
301
this.emit("changed", this.value);
302
},
303
},
304
]);
305
306
const statePane = pane.getSubPane(0);
307
for (const [state, value] of node.states.entries()) addStateUI(statePane, BlockStates.get(state), value);
308
updateStateEntries();
309
}
310
311
private buildClipboardPatternUI(pane: UIPane, node: ClipboardPatternNode) {
312
pane.changeItems([
313
{
314
type: "vector3",
315
title: "Offset",
316
value: { x: node.offset.x, y: node.offset.y, z: node.offset.z },
317
onChange: (value) => {
318
node.offset = new Vector(value.x, value.y, value.z);
319
this.emit("changed", this.value);
320
},
321
},
322
]);
323
}
324
325
private buildInputPatternUI(pane: UIPane, node: InputPatternNode) {
326
pane.changeItems([
327
{
328
type: "text_area",
329
title: "Pattern",
330
value: node.input,
331
onChange: (value) => {
332
node.input = value;
333
try {
334
Pattern.parseArgs([node.input]);
335
pane.setVisibility(1, false);
336
} catch (err) {
337
pane.setVisibility(1, true);
338
if ("isSyntaxError" in err) {
339
const { start, end } = err as { start: number; end: number };
340
pane.setValue(1, { id: "commands.generic.syntax", props: [value.slice(0, start), value.slice(start, end + 1), value.slice(end + 1)] });
341
} else if (err.rawtext?.[0].translate) {
342
const message = err.rawtext[0] as RawMessage;
343
pane.setValue(1, { id: message.translate, props: message.with as string[] });
344
}
345
}
346
this.emit("changed", this.value);
347
},
348
},
349
{ type: "label", visible: false, text: "" },
350
]);
351
}
352
353
private buildBlobPatternUI(pane: UIPane, node: BlobPatternNode) {
354
pane.changeItems([
355
{
356
type: "slider",
357
title: "Blob Size",
358
value: node.size,
359
onChange: (value) => {
360
node.size = value;
361
this.emit("changed", this.value);
362
},
363
},
364
{ type: "subpane", title: "Sub-Pattern", items: [] },
365
]);
366
this.buildPatternUI(pane.getSubPane(1), node.nodes[0], node);
367
}
368
369
private buildGradientPatternUI(pane: UIPane, node: GradientPatternNode) {
370
const cardinals = Object.values(CardinalDirection) as string[];
371
const directions = [...cardinals, "radial", "light"];
372
let gradients = pane.worldedit.getGradientNames();
373
pane.changeItems([
374
{
375
type: "dropdown",
376
title: "Gradient ID",
377
value: gradients.indexOf(node.gradientId),
378
entries: gradients.map((id, i) => ({ label: id, value: i })),
379
onChange: (value) => {
380
node.gradientId = gradients[value];
381
this.emit("changed", this.value);
382
},
383
},
384
{
385
type: "dropdown",
386
title: "Direction",
387
value: directions.indexOf(node.cardinal instanceof Cardinal ? node.cardinal.cardinal : node.cardinal),
388
entries: directions.map((direction, index) => ({ value: index, label: direction })),
389
onChange: (valueIdx) => {
390
const value = directions[valueIdx];
391
node.cardinal = cardinals.includes(value) ? new Cardinal(value as CardinalDirection) : (value as "radial" | "light");
392
this.emit("changed", this.value);
393
},
394
},
395
]);
396
397
if (this.gradientListener) pane.worldedit.off("gradientListUpdated", this.gradientListener);
398
this.gradientListener = (newList) => {
399
const currentGradient = gradients[pane.getValue(0) as number];
400
gradients = newList;
401
pane.updateEntries(
402
0,
403
gradients.map((id, i) => ({ label: id, value: i }))
404
);
405
pane.setValue(0, gradients.indexOf(currentGradient));
406
};
407
pane.worldedit.on("gradientListUpdated", this.gradientListener);
408
}
409
}
410
411