Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/editor/modules/brushes/control.ts
1785 views
1
import { Vector3, system } from "@minecraft/server";
2
import {
3
IObservable,
4
ISubPanePropertyItem,
5
IPlayerUISession,
6
IModalTool,
7
IRootPropertyPane,
8
makeObservable,
9
ActionTypes,
10
KeyboardKey,
11
InputModifier,
12
NumberPropertyItemVariant,
13
ImageResourceType,
14
} from "@minecraft/server-editor";
15
import { Easing } from "@modules/easing";
16
import easingsFunctions from "@modules/extern/easingFunctions";
17
import { Mask } from "@modules/mask";
18
import { Pattern } from "@modules/pattern";
19
import { Databases, Vector } from "@notbeer-api";
20
import config from "config";
21
import { SharedControl } from "editor/control";
22
import { PaneItem, UIPane } from "editor/pane/builder";
23
import { MaskUIBuilder } from "editor/pane/mask";
24
import { PatternUIBuilder } from "editor/pane/pattern";
25
import { Database } from "library/@types/classes/databaseBuilder";
26
import { Brush, brushTypes } from "server/brushes/base_brush";
27
import { ErosionType } from "server/brushes/erosion_brush";
28
import { getEditorBrushManager } from "./manager";
29
import { RelativeDirection, getRotationCorrectedDirectionVector } from "./util";
30
31
const PROPERTY_BRUSHPAINTCONTROL_NAME = "BrushPaintControl";
32
const PROPERTY_BRUSHPAINTCONTROL_LOCALIZATION_PREFIX = `resourcePack.editor.${PROPERTY_BRUSHPAINTCONTROL_NAME}`;
33
34
enum BrushPaintControlStringKeys {
35
RootPaneTitle = "rootPane.title",
36
RootPaneTooltip = "brushSettings.tooltip",
37
BrushShapeSelectionTitle = "brush.title",
38
BrushShapeSelectionTooltip = "brush.tooltip",
39
OffsetTitle = "offset.title",
40
OffsetTooltip = "offset.tooltip",
41
BrushShapeSettingsTitle = "shapeSettings.title",
42
BrushShapeSettingsTooltip = "shapeSettings.tooltip",
43
FillConstraintsTitle = "fillConstraints.title",
44
FillConstraintsTooltip = "fillConstraints.tooltip",
45
MaskModeTitle = "fillConstraints.maskMode.title",
46
MaskModeTooltip = "fillConstraints.maskMode.tooltip",
47
}
48
49
export class BrushPaintSharedControl extends SharedControl {
50
static readonly MIN_OFFSET = { x: -100, y: -100, z: -100 };
51
static readonly MAX_OFFSET = { x: 100, y: 100, z: 100 };
52
53
private readonly brushTypes: string[];
54
private readonly selectedBrushIndex: IObservable<number>;
55
private readonly brushShapeOffset: IObservable<{ x: number; y: number; z: number }>;
56
private readonly brushSettings: {
57
radius: IObservable<number>;
58
height: IObservable<number>;
59
depth: IObservable<number>;
60
iterations: IObservable<number>;
61
erosionType: IObservable<number>;
62
smoothness: IObservable<number>;
63
growPercent: IObservable<number>;
64
falloffAmount: IObservable<number>;
65
falloffType: IObservable<string>;
66
pattern: PatternUIBuilder;
67
heightMask: MaskUIBuilder;
68
surfaceMask: MaskUIBuilder;
69
};
70
71
private brushControlRootPane: ISubPanePropertyItem;
72
private brushSettingsSubPane: UIPane;
73
private mask: MaskUIBuilder;
74
private brushSettingsUpdateHandler: number;
75
private settingsDatabase: Database;
76
77
private brush: Brush;
78
79
constructor(session: IPlayerUISession, parentTool: IModalTool, parentPropertyPane: IRootPropertyPane, brushTypes: string[]) {
80
super(session, parentTool, parentPropertyPane, PROPERTY_BRUSHPAINTCONTROL_NAME, PROPERTY_BRUSHPAINTCONTROL_LOCALIZATION_PREFIX);
81
this.brushTypes = brushTypes;
82
this.selectedBrushIndex = makeObservable(0);
83
this.brushShapeOffset = makeObservable({ x: 0, y: 0, z: 0 });
84
this.brushSettings = {
85
radius: makeObservable(3),
86
height: makeObservable(3),
87
depth: makeObservable(1),
88
iterations: makeObservable(1),
89
erosionType: makeObservable(ErosionType.DEFAULT),
90
smoothness: makeObservable(0),
91
growPercent: makeObservable(50),
92
falloffAmount: makeObservable(0),
93
falloffType: makeObservable("linear"),
94
pattern: new PatternUIBuilder(new Pattern("stone")),
95
heightMask: new MaskUIBuilder(),
96
surfaceMask: new MaskUIBuilder(),
97
};
98
this.mask = new MaskUIBuilder();
99
this.settingsDatabase = Databases.load("editor_brush_settings", session.extensionContext.player);
100
this.loadBrushSettings();
101
}
102
103
initialize() {
104
super.initialize();
105
if (!this.tool) throw new Error("SharedControl tool is not set");
106
107
this.brushSettings.pattern.on("changed", () => this.updateBrushSettings());
108
this.brushSettings.heightMask.on("changed", () => this.updateBrushSettings());
109
this.brushSettings.surfaceMask.on("changed", () => this.updateBrushSettings());
110
this.mask.on("changed", () => this.updateBrushMask());
111
112
const offsetNudgeUpAction = this.session.actionManager.createAction({
113
actionType: ActionTypes.NoArgsAction,
114
onExecute: () => this.nudgeOffset({ x: 0, y: 1, z: 0 }),
115
});
116
const offsetNudgeDownAction = this.session.actionManager.createAction({
117
actionType: ActionTypes.NoArgsAction,
118
onExecute: () => this.nudgeOffset({ x: 0, y: -1, z: 0 }),
119
});
120
const offsetNudgeForwardAction = this.session.actionManager.createAction({
121
actionType: ActionTypes.NoArgsAction,
122
onExecute: () => this.nudgeOffset(this.getRelativeNudgeDirection(RelativeDirection.Forward)),
123
});
124
const offsetNudgeBackAction = this.session.actionManager.createAction({
125
actionType: ActionTypes.NoArgsAction,
126
onExecute: () => this.nudgeOffset(this.getRelativeNudgeDirection(RelativeDirection.Back)),
127
});
128
const offsetNudgeLeftAction = this.session.actionManager.createAction({
129
actionType: ActionTypes.NoArgsAction,
130
onExecute: () => this.nudgeOffset(this.getRelativeNudgeDirection(RelativeDirection.Left)),
131
});
132
const offsetNudgeRightAction = this.session.actionManager.createAction({
133
actionType: ActionTypes.NoArgsAction,
134
onExecute: () => this.nudgeOffset(this.getRelativeNudgeDirection(RelativeDirection.Right)),
135
});
136
this.registerToolKeyBinding(offsetNudgeUpAction, { key: KeyboardKey.PAGE_UP, modifier: InputModifier.Control | InputModifier.Shift }, "nudgeOffsetUp");
137
this.registerToolKeyBinding(offsetNudgeDownAction, { key: KeyboardKey.PAGE_DOWN, modifier: InputModifier.Control | InputModifier.Shift }, "nudgeOffsetDown");
138
this.registerToolKeyBinding(offsetNudgeForwardAction, { key: KeyboardKey.UP, modifier: InputModifier.Control | InputModifier.Shift }, "nudgeOffsetForward");
139
this.registerToolKeyBinding(offsetNudgeBackAction, { key: KeyboardKey.DOWN, modifier: InputModifier.Control | InputModifier.Shift }, "nudgeOffsetBack");
140
this.registerToolKeyBinding(offsetNudgeLeftAction, { key: KeyboardKey.LEFT, modifier: InputModifier.Control | InputModifier.Shift }, "nudgeOffsetLeft");
141
this.registerToolKeyBinding(offsetNudgeRightAction, { key: KeyboardKey.RIGHT, modifier: InputModifier.Control | InputModifier.Shift }, "nudgeOffsetRight");
142
const toggleMask = this.session.actionManager.createAction({
143
actionType: ActionTypes.NoArgsAction,
144
onExecute: () => {
145
if (!this.mask.value) this.mask.enable();
146
else this.mask.disable();
147
},
148
});
149
this.registerToolKeyBinding(toggleMask, { key: KeyboardKey.KEY_M }, "toggleMask");
150
}
151
152
activateControl() {
153
if (this.isActive) {
154
this.session.log.error("Cannot activate already active Brush Control");
155
return;
156
}
157
super.activateControl();
158
this.brushShapeOffset.set(getEditorBrushManager(this.session).getBrushShapeOffset());
159
this.constructControlUI();
160
this.brushControlRootPane?.show();
161
getEditorBrushManager(this.session).activateBrushTool();
162
this.setBrushType();
163
this.updateBrushMask();
164
}
165
166
deactivateControl() {
167
if (!this.isActive) {
168
this.session.log.error("Cannot deactivate inactive Brush Control");
169
return;
170
}
171
super.deactivateControl();
172
getEditorBrushManager(this.session).deactivateBrushTool();
173
this.destroyControlUI();
174
this.brushControlRootPane?.hide();
175
}
176
177
private destroyControlUI() {
178
if (this.brushControlRootPane) {
179
this.propertyPane.removeSubPane(this.brushControlRootPane);
180
this.brushControlRootPane = undefined;
181
}
182
}
183
184
private constructControlUI() {
185
if (this.brushControlRootPane) this.destroyControlUI();
186
187
this.brushShapeOffset.set(getEditorBrushManager(this.session).getBrushShapeOffset());
188
const settingsPane = new UIPane(
189
this.session,
190
{
191
items: [
192
{
193
type: "dropdown",
194
title: this.localize(BrushPaintControlStringKeys.BrushShapeSelectionTitle),
195
tooltip: this.localize(BrushPaintControlStringKeys.BrushShapeSelectionTooltip),
196
entries: this.getBrushShapeDropdownEntries(),
197
value: this.selectedBrushIndex,
198
onChange: () => this.setBrushType(),
199
},
200
{
201
type: "vector3",
202
title: this.localize(BrushPaintControlStringKeys.OffsetTitle),
203
tooltip: this.localize(BrushPaintControlStringKeys.OffsetTooltip),
204
value: this.brushShapeOffset,
205
isInteger: true,
206
min: BrushPaintSharedControl.MIN_OFFSET,
207
max: BrushPaintSharedControl.MAX_OFFSET,
208
onChange: (newValue) => getEditorBrushManager(this.session).setBrushShapeOffset(newValue),
209
},
210
{
211
type: "subpane",
212
uniqueId: "settings",
213
title: this.localize(BrushPaintControlStringKeys.BrushShapeSettingsTitle),
214
infoTooltip: {
215
title: this.localize(BrushPaintControlStringKeys.BrushShapeSettingsTitle),
216
description: [this.localize(BrushPaintControlStringKeys.BrushShapeSettingsTooltip)],
217
},
218
items: this.getBrushSettingsItems(),
219
},
220
],
221
},
222
(this.brushControlRootPane = this.propertyPane.createSubPane({
223
title: this.localize(BrushPaintControlStringKeys.RootPaneTitle),
224
infoTooltip: {
225
title: this.localize(BrushPaintControlStringKeys.RootPaneTitle),
226
description: [this.localize(BrushPaintControlStringKeys.RootPaneTooltip)],
227
},
228
hasMargins: false,
229
}))
230
);
231
232
this.brushSettingsSubPane = settingsPane.getSubPane("settings");
233
this.setBrushType();
234
235
new UIPane(this.session, { items: this.mask }, this.brushControlRootPane.createSubPane({ title: "Mask" }));
236
}
237
238
private updateBrushMask() {
239
getEditorBrushManager(this.session).setBrushMask(this.mask.value ?? new Mask());
240
}
241
242
private getSelectedBrushType() {
243
const currentBrushIndex = this.selectedBrushIndex.value;
244
if (currentBrushIndex < 0 || currentBrushIndex >= this.brushTypes.length) {
245
throw new Error("Invalid brush index");
246
}
247
return this.brushTypes[currentBrushIndex];
248
}
249
250
private setBrushType() {
251
const brushType = brushTypes.get(this.getSelectedBrushType());
252
const settings = Object.fromEntries(Object.entries(this.brushSettings).map(([key, observable]) => [key, observable.value]));
253
this.brush = new brushType(...brushType.parseJSON(JSON.parse(JSON.stringify(settings))));
254
getEditorBrushManager(this.session).setBrush(this.brush);
255
this.updateBrushSettings();
256
this.updateSettingsSubPane();
257
}
258
259
private updateSettingsSubPane() {
260
if (!this.brushSettingsSubPane) return;
261
this.brushSettingsSubPane.setVisibility("radius", "radius" in this.brush);
262
this.brushSettingsSubPane.setVisibility("height", "height" in this.brush);
263
this.brushSettingsSubPane.setVisibility("depth", "depth" in this.brush);
264
this.brushSettingsSubPane.setVisibility("iterations", "iterations" in this.brush);
265
this.brushSettingsSubPane.setVisibility("erosionType", "erosionType" in this.brush);
266
this.brushSettingsSubPane.setVisibility("smoothness", "smoothness" in this.brush);
267
this.brushSettingsSubPane.setVisibility("growPercent", "growPercent" in this.brush);
268
this.brushSettingsSubPane.setVisibility("falloffAmount", "falloffAmount" in this.brush);
269
this.brushSettingsSubPane.setVisibility("falloffType", "falloffType" in this.brush);
270
this.brushSettingsSubPane.getSubPane("pattern").visible = "pattern" in this.brush;
271
this.brushSettingsSubPane.getSubPane("heightMask").visible = "heightMask" in this.brush;
272
this.brushSettingsSubPane.getSubPane("surfaceMask").visible = "surfaceMask" in this.brush;
273
this.updateBrushSettings();
274
}
275
276
private getRelativeNudgeDirection(direction: RelativeDirection) {
277
const rotationY = this.session.extensionContext.player.getRotation().y;
278
const rotationCorrectedVector = getRotationCorrectedDirectionVector(rotationY, direction);
279
return rotationCorrectedVector;
280
}
281
282
private nudgeOffset(nudgeVector: Vector3) {
283
let update = Vector.add(this.brushShapeOffset.value, nudgeVector);
284
update = Vector.min(Vector.max(update, BrushPaintSharedControl.MIN_OFFSET), BrushPaintSharedControl.MAX_OFFSET);
285
this.brushShapeOffset.set(update);
286
getEditorBrushManager(this.session).setBrushShapeOffset(update);
287
}
288
289
private getBrushShapeDropdownEntries() {
290
return this.brushTypes.map((brush, index) => {
291
const item = {
292
label: `worldedit.config.brush.${brush.replace("_brush", "")}`,
293
value: index,
294
imageData: {
295
path: `pack://textures/ui/${brush}.png`,
296
type: ImageResourceType.Icon,
297
},
298
};
299
return item;
300
});
301
}
302
303
private getBrushSettingsItems(): PaneItem[] {
304
return [
305
{
306
type: "slider",
307
uniqueId: "radius",
308
title: "Radius",
309
...{ min: 1, max: config.maxBrushRadius },
310
isInteger: true,
311
value: this.brushSettings.radius,
312
onChange: () => this.updateBrushSettings(),
313
},
314
{
315
type: "slider",
316
uniqueId: "height",
317
title: "Height",
318
isInteger: true,
319
value: this.brushSettings.height,
320
onChange: () => this.updateBrushSettings(),
321
},
322
{
323
type: "slider",
324
uniqueId: "depth",
325
title: "Depth",
326
isInteger: true,
327
value: this.brushSettings.depth,
328
onChange: () => this.updateBrushSettings(),
329
},
330
{
331
type: "slider",
332
uniqueId: "iterations",
333
title: "Iterations",
334
isInteger: true,
335
value: this.brushSettings.iterations,
336
onChange: () => this.updateBrushSettings(),
337
},
338
{
339
type: "dropdown",
340
uniqueId: "erosionType",
341
title: "Erosion Type",
342
value: this.brushSettings.erosionType,
343
entries: [
344
{ label: "Erode", value: ErosionType.DEFAULT },
345
{ label: "Melt", value: ErosionType.MELT },
346
{ label: "Fill", value: ErosionType.FILL },
347
{ label: "Lift", value: ErosionType.LIFT },
348
{ label: "Smooth", value: ErosionType.SMOOTH },
349
],
350
onChange: () => this.updateBrushSettings(),
351
},
352
{
353
type: "slider",
354
uniqueId: "smoothness",
355
title: "Smoothness",
356
...{ min: 0, max: 6 },
357
isInteger: true,
358
value: this.brushSettings.smoothness,
359
onChange: () => this.updateBrushSettings(),
360
},
361
{
362
type: "slider",
363
uniqueId: "growPercent",
364
title: "Growth Percent",
365
...{ min: 0, max: 100 },
366
variant: NumberPropertyItemVariant.InputFieldAndSlider,
367
value: this.brushSettings.growPercent,
368
onChange: () => this.updateBrushSettings(),
369
},
370
{
371
type: "slider",
372
uniqueId: "falloffAmount",
373
title: "Falloff Amount",
374
...{ min: 0, max: 1 },
375
variant: NumberPropertyItemVariant.InputFieldAndSlider,
376
value: this.brushSettings.falloffAmount,
377
onChange: () => this.updateBrushSettings(),
378
},
379
{
380
type: "combo_box",
381
uniqueId: "falloffType",
382
title: "Falloff Type",
383
value: this.brushSettings.falloffType,
384
entries: Object.keys(easingsFunctions).map((type) => ({ label: type, value: type })),
385
onChange: () => this.updateBrushSettings(),
386
},
387
{
388
type: "subpane",
389
uniqueId: "pattern",
390
title: "Pattern",
391
items: this.brushSettings.pattern,
392
},
393
{
394
type: "subpane",
395
uniqueId: "heightMask",
396
title: "Height Mask",
397
items: this.brushSettings.heightMask,
398
},
399
{
400
type: "subpane",
401
uniqueId: "surfaceMask",
402
title: "Surface Mask",
403
items: this.brushSettings.surfaceMask,
404
},
405
];
406
}
407
408
private updateBrushSettings() {
409
if (this.brushSettingsUpdateHandler) system.clearRun(this.brushSettingsUpdateHandler);
410
this.brushSettingsUpdateHandler = system.runTimeout(() => {
411
for (const property in this.brushSettings) {
412
if (!(property in this.brush)) continue;
413
if (property === "falloffType") {
414
this.brush[property] = new Easing(this.brushSettings[property].value);
415
} else {
416
this.brush[property] = this.brushSettings[property].value;
417
}
418
}
419
getEditorBrushManager(this.session).setBrush(this.brush);
420
this.settingsDatabase.data = this.brush.toJSON();
421
this.settingsDatabase.save();
422
}, 5);
423
}
424
425
private loadBrushSettings() {
426
const savedSettings = this.settingsDatabase.data;
427
const brushType = brushTypes.get(savedSettings.id);
428
if (!brushType) return;
429
430
this.brush = new brushType(...brushType.parseJSON(savedSettings));
431
this.selectedBrushIndex.set(this.brushTypes.indexOf(savedSettings.id));
432
for (const property in savedSettings) {
433
if (!(property in this.brushSettings)) continue;
434
if (property === "falloffType") {
435
this.brushSettings[property].set((this.brush[property] as Easing).type);
436
} else {
437
this.brushSettings[property].set(this.brush[property]);
438
}
439
}
440
}
441
}
442
443