Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/editor/modules/region_operations.ts
1784 views
1
/* eslint-disable prefer-const */
2
import { IPlayerUISession, ProgressIndicatorPropertyItemVariant, Widget, WidgetComponentClipboard, WidgetGroupSelectionMode } from "@minecraft/server-editor";
3
import { UIPane } from "editor/pane/builder";
4
import { EditorModule } from "./base";
5
import { Pattern } from "@modules/pattern";
6
import { regionSize, Server, Thread, Vector } from "@notbeer-api";
7
import { PatternUIBuilder } from "editor/pane/pattern";
8
import { MaskUIBuilder } from "editor/pane/mask";
9
import { Mask } from "@modules/mask";
10
import { Cardinal, CardinalDirection } from "@modules/directions";
11
import { getSession } from "server/sessions";
12
import { system } from "@minecraft/server";
13
import { Jobs } from "@modules/jobs";
14
15
enum RegionOperatorMode {
16
Fill,
17
Outline,
18
Wall,
19
Stack,
20
Move,
21
Smooth,
22
}
23
24
const directions = Object.values(CardinalDirection);
25
26
export class RegionOpModule extends EditorModule {
27
private pane: UIPane;
28
private widget: Widget;
29
private widgetComponents: WidgetComponentClipboard[] = [];
30
31
private tickId: number;
32
private thread?: Thread;
33
34
private direction = CardinalDirection.Forward;
35
private distance = 5;
36
private stackCount = 1;
37
private iterations = 1;
38
private mode = RegionOperatorMode.Fill;
39
40
private readonly patternUIBuilder = new PatternUIBuilder(new Pattern("stone"));
41
private readonly maskUIBuilder = new MaskUIBuilder(new Mask("air"));
42
private readonly heightMaskUIBuilder = new MaskUIBuilder(new Mask("#surface"));
43
44
constructor(session: IPlayerUISession) {
45
super(session);
46
const tool = session.toolRail.addTool("worldedit:region_operations", { title: "WorldEdit Region Operations", icon: "pack://textures/editor/region_operations_tool.png" });
47
const widgetGroup = session.extensionContext.widgetManager.createGroup({ groupSelectionMode: WidgetGroupSelectionMode.None, visible: true });
48
49
this.widget = widgetGroup.createWidget(Vector.ZERO, { visible: true });
50
51
this.pane = new UIPane(this.session, {
52
title: "Region Operations",
53
items: [
54
{
55
type: "dropdown",
56
title: "Mode",
57
value: this.mode,
58
entries: [
59
{ label: "Fill Selection", value: RegionOperatorMode.Fill },
60
{ label: "Outline Selection", value: RegionOperatorMode.Outline },
61
{ label: "Wall Selection", value: RegionOperatorMode.Wall },
62
{ label: "Stack Selection", value: RegionOperatorMode.Stack },
63
{ label: "Move Selection", value: RegionOperatorMode.Move },
64
{ label: "Smooth Selection", value: RegionOperatorMode.Smooth },
65
],
66
onChange: (value) => {
67
this.mode = value;
68
this.updatePane();
69
},
70
},
71
{
72
type: "button",
73
title: "Execute Operation",
74
enable: this.canOperate(),
75
pressed: this.performOperation.bind(this),
76
},
77
{ type: "progress", uniqueId: "operationProgress", variant: ProgressIndicatorPropertyItemVariant.ProgressBar, visible: false },
78
{ type: "divider" },
79
{
80
type: "dropdown",
81
title: "Direction",
82
uniqueId: "direction",
83
entries: directions.map((dir, index) => ({ label: dir, value: index })),
84
value: directions.indexOf(CardinalDirection.Forward),
85
onChange: (value) => {
86
this.direction = directions[value];
87
this.updateWidgets();
88
},
89
},
90
{
91
type: "slider",
92
title: "Distance",
93
uniqueId: "distance",
94
min: 1,
95
value: 5,
96
isInteger: true,
97
onChange: (value) => {
98
this.distance = value;
99
this.updateWidgets();
100
},
101
},
102
{
103
type: "slider",
104
title: "Stack Count",
105
uniqueId: "stackCount",
106
min: 1,
107
value: 1,
108
isInteger: true,
109
onChange: (value) => {
110
this.stackCount = value;
111
this.updateWidgets();
112
},
113
},
114
{
115
type: "slider",
116
title: "Iterations",
117
uniqueId: "iterations",
118
min: 1,
119
value: 1,
120
isInteger: true,
121
onChange: (value) => {
122
this.iterations = value;
123
},
124
},
125
{
126
type: "subpane",
127
title: "Pattern",
128
uniqueId: "pattern",
129
items: this.patternUIBuilder,
130
},
131
{
132
type: "subpane",
133
title: "Mask",
134
uniqueId: "mask",
135
items: this.maskUIBuilder,
136
},
137
{
138
type: "subpane",
139
title: "Height Mask",
140
uniqueId: "heightMask",
141
items: this.heightMaskUIBuilder,
142
},
143
],
144
});
145
this.pane.bindToTool(tool);
146
this.session.extensionContext.afterEvents.SelectionChange.subscribe(this.onSelectionChange);
147
this.updatePane();
148
149
let lastCardinal = new Cardinal(this.direction).getDirection(this.player);
150
this.tickId = system.runInterval(() => {
151
const cardinal = new Cardinal(this.direction).getDirection(this.player);
152
if (!lastCardinal.equals(cardinal)) {
153
lastCardinal = cardinal;
154
this.updateWidgets();
155
}
156
157
if (this.thread && !this.thread.isActive) this.thread = undefined;
158
const job = this.thread ? Jobs.getJobsForThread(this.thread)[0] : undefined;
159
if (job) {
160
this.pane.setVisibility("operationProgress", true);
161
this.pane.setValue("operationProgress", Jobs.getProgress(job));
162
} else {
163
this.pane.setVisibility("operationProgress", false);
164
}
165
}, 2);
166
}
167
168
teardown() {
169
system.clearRun(this.tickId);
170
}
171
172
private performOperation() {
173
if (this.usesPatternAndMask()) {
174
const args = new Map<string, any>([
175
["pattern", this.patternUIBuilder.value],
176
["mask", this.maskUIBuilder.value],
177
]);
178
const command = {
179
[RegionOperatorMode.Fill]: "replace",
180
[RegionOperatorMode.Outline]: "faces",
181
[RegionOperatorMode.Wall]: "walls",
182
}[this.mode];
183
this.thread = Server.command.getRegistration(command).callback(this.player, "editor-callback", args);
184
} else if (this.mode === RegionOperatorMode.Stack) {
185
Server.command.getRegistration("stack").callback(
186
this.player,
187
"editor-callback",
188
new Map(
189
Object.entries({
190
a: true,
191
count: this.stackCount,
192
offset: new Cardinal(this.direction),
193
})
194
)
195
);
196
} else if (this.mode === RegionOperatorMode.Move) {
197
this.thread = Server.command.getRegistration("move").callback(
198
this.player,
199
"editor-callback",
200
new Map(
201
Object.entries({
202
a: true,
203
amount: this.distance,
204
offset: new Cardinal(this.direction),
205
})
206
)
207
);
208
} else if (this.mode === RegionOperatorMode.Smooth) {
209
this.thread = Server.command.getRegistration("smooth").callback(
210
this.player,
211
"editor-callback",
212
new Map(
213
Object.entries({
214
iterations: this.iterations,
215
mask: this.heightMaskUIBuilder.value,
216
})
217
)
218
);
219
}
220
}
221
222
private updatePane() {
223
this.pane.setVisibility("direction", this.mode === RegionOperatorMode.Stack || this.mode === RegionOperatorMode.Move);
224
this.pane.setVisibility("distance", this.mode === RegionOperatorMode.Move);
225
this.pane.setVisibility("stackCount", this.mode === RegionOperatorMode.Stack);
226
this.pane.setVisibility("iterations", this.mode === RegionOperatorMode.Smooth);
227
this.pane.getSubPane("pattern").visible = this.usesPatternAndMask();
228
this.pane.getSubPane("mask").visible = this.usesPatternAndMask();
229
this.pane.getSubPane("heightMask").visible = this.mode === RegionOperatorMode.Smooth;
230
this.updateWidgets();
231
}
232
233
private updateWidgets() {
234
const relativeOffsets: Vector[] = [];
235
const direction = new Cardinal(this.direction).getDirection(this.player);
236
const selection = getSession(this.player).selection;
237
238
if (selection) {
239
if (this.mode === RegionOperatorMode.Move) {
240
relativeOffsets.push(direction.mul(this.distance));
241
} else if (this.mode === RegionOperatorMode.Stack) {
242
const size = regionSize(...getSession(this.player).selection.getRange());
243
for (let i = 0; i < this.stackCount; i++) {
244
relativeOffsets.push(direction.mul(i + 1).mul(size));
245
}
246
}
247
}
248
249
for (let i = 0; i < relativeOffsets.length; i++) {
250
if (!this.widgetComponents[i]) {
251
const selection = this.session.extensionContext.selectionManager.volume;
252
const clipboard = this.session.extensionContext.clipboardManager.create();
253
clipboard.readFromWorld(selection!.get());
254
this.widgetComponents.push(
255
this.widget.addClipboardComponent("region-op-preview" + i, clipboard, {
256
normalizedOrigin: new Vector(-1, -1, -1),
257
showOutline: true,
258
visible: true,
259
})
260
);
261
}
262
this.widgetComponents[i].offset = relativeOffsets[i].add(selection.getRange()[0]);
263
}
264
265
while (this.widgetComponents.length > relativeOffsets.length) {
266
const component = this.widgetComponents.pop();
267
component.delete();
268
}
269
270
this.widget.visible = !!this.widgetComponents.length;
271
}
272
273
private clearWidgets() {
274
while (this.widgetComponents.length) {
275
const component = this.widgetComponents.pop();
276
component.delete();
277
}
278
}
279
280
private usesPatternAndMask() {
281
return this.mode === RegionOperatorMode.Fill || this.mode === RegionOperatorMode.Outline || this.mode === RegionOperatorMode.Wall;
282
}
283
284
private canOperate() {
285
return !this.session.extensionContext.selectionManager.volume.isEmpty;
286
}
287
288
private onSelectionChange = () => {
289
this.pane.setEnabled(1, this.canOperate());
290
this.clearWidgets();
291
this.updateWidgets();
292
};
293
}
294
295