Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/editor/modules/brushes/manager.ts
1785 views
1
import { BlockVolumeBase, Player, system, Vector3 } from "@minecraft/server";
2
import { IPlayerUISession, PaintCompletionState, RelativeVolumeListBlockVolume, Widget, WidgetComponentVolumeOutline } from "@minecraft/server-editor";
3
import { Jobs } from "@modules/jobs";
4
import { Mask } from "@modules/mask";
5
import { Pattern } from "@modules/pattern";
6
import { Thread, Vector, VectorSet } from "@notbeer-api";
7
import { shapeToBlockVolume } from "editor/util";
8
import { Brush } from "server/brushes/base_brush";
9
import { SphereBrush } from "server/brushes/sphere_brush";
10
import { getSession } from "server/sessions";
11
12
const managers = new WeakMap<IPlayerUISession, EditorBrushManager>();
13
14
class EditorBrushManager {
15
private readonly session: IPlayerUISession;
16
private readonly player: Player;
17
18
private widget?: Widget;
19
private volumeOutline?: WidgetComponentVolumeOutline;
20
21
private previewWidget?: Widget;
22
private previewVolumeOutline?: WidgetComponentVolumeOutline;
23
24
private readonly volumeUpdator = shapeToBlockVolume();
25
private volume: BlockVolumeBase;
26
private volumeOffset: Vector3 = Vector.ZERO;
27
28
private locations = new VectorSet();
29
private paintingHandler?: number;
30
private onComplete?: (state: PaintCompletionState) => void;
31
32
private mask = new Mask();
33
private brush: Brush;
34
35
constructor(session: IPlayerUISession) {
36
this.session = session;
37
this.player = session.extensionContext.player;
38
this.brush = new SphereBrush(3, new Pattern("stone"), false);
39
this.updateVolume();
40
}
41
42
getBrushShapeOffset(): Vector3 {
43
return (this.player.getDynamicProperty("brushShapeOffset") as Vector3) ?? Vector.ZERO;
44
}
45
46
setBrushShapeOffset(offset: Vector3): void {
47
if (this.volumeOutline) this.volumeOutline.offset = offset;
48
this.player.setDynamicProperty("brushShapeOffset", offset);
49
}
50
51
activateBrushTool(): void {
52
if (!this.widget) {
53
const widgetGroup = this.session.extensionContext.widgetManager.createGroup();
54
this.widget ??= widgetGroup.createWidget(Vector.ZERO, { bindPositionToBlockCursor: true, selectable: false });
55
this.volumeOutline ??= this.widget.addVolumeOutline("shape", this.volume, {
56
volumeOffset: Vector.add(this.volumeOffset, this.volume?.getMin() ?? Vector.ZERO),
57
offset: this.getBrushShapeOffset(),
58
showOutline: false,
59
});
60
61
this.previewWidget ??= widgetGroup.createWidget(Vector.ZERO, { selectable: false });
62
this.previewVolumeOutline ??= this.previewWidget.addVolumeOutline("preview", new RelativeVolumeListBlockVolume(), { showOutline: false });
63
}
64
this.widget.visible = true;
65
}
66
67
deactivateBrushTool(): void {
68
if (this.widget) this.widget.visible = false;
69
this.endPainting(true);
70
}
71
72
setBrushMask(mask: Mask): void {
73
this.mask = mask;
74
}
75
76
setBrush(brush: Brush): void {
77
this.brush = brush;
78
this.updateVolume();
79
}
80
81
singlePaint(onComplete: (state: PaintCompletionState) => void): void {
82
this.applyBrush([this.widget!.location], (success) => onComplete(success ? PaintCompletionState.Success : PaintCompletionState.Failed));
83
}
84
85
beginPainting(onComplete: (state: PaintCompletionState) => void): void {
86
if (this.paintingHandler !== undefined) return;
87
this.locations.clear();
88
this.paintingHandler = system.runInterval(() => {
89
const nextLocation = this.widget?.location;
90
if (!nextLocation || this.locations.has(nextLocation)) return;
91
92
this.locations.add(nextLocation);
93
const previewVolume = this.previewVolumeOutline?.getVolume() as RelativeVolumeListBlockVolume;
94
if (previewVolume && this.volumeOutline) {
95
const volume = this.volumeOutline.getVolume();
96
const offset = Vector.add(nextLocation, this.getBrushShapeOffset()).add(this.volumeOffset);
97
volume.translate(offset);
98
previewVolume.add(volume);
99
volume.translate(Vector.mul(offset, -1));
100
this.previewWidget.location = previewVolume.getMin();
101
}
102
});
103
this.onComplete = onComplete;
104
}
105
106
endPainting(discard: boolean): void {
107
if (this.paintingHandler === undefined) return;
108
system.clearRun(this.paintingHandler);
109
this.paintingHandler = undefined;
110
if (!discard) {
111
this.applyBrush(Array.from(this.locations.values()), (success) => {
112
this.onComplete?.(success ? PaintCompletionState.Success : PaintCompletionState.Failed);
113
this.previewVolumeOutline?.getVolume().clear();
114
});
115
} else {
116
this.onComplete?.(PaintCompletionState.Canceled);
117
this.previewVolumeOutline?.getVolume().clear();
118
}
119
this.onComplete = undefined;
120
}
121
122
isBrushPaintBusy(): boolean {
123
return this.paintingHandler !== undefined;
124
}
125
126
private updateVolume() {
127
const [shape, location] = this.brush.getOutline();
128
this.volumeUpdator.update(shape, (volume) => {
129
this.volume = volume;
130
this.volumeOffset = location;
131
if (this.volumeOutline) {
132
this.volumeOutline.setVolume(this.volume);
133
this.volumeOutline.volumeOffset = Vector.add(this.volumeOffset, this.volume.getMin());
134
}
135
});
136
}
137
138
private applyBrush(locations: Vector3[], onFinish?: (success: boolean) => void) {
139
const session = getSession(this.player);
140
const offsetLocations = locations.map((loc) => Vector.add(loc, this.getBrushShapeOffset()));
141
new Thread().start(
142
function* (this: EditorBrushManager) {
143
try {
144
yield* Jobs.run(session, -1, this.brush.apply(offsetLocations, session, this.mask));
145
onFinish?.(true);
146
} catch (err) {
147
onFinish?.(false);
148
throw err;
149
}
150
}.bind(this)
151
);
152
}
153
}
154
155
export function getEditorBrushManager(session: IPlayerUISession) {
156
if (!managers.has(session)) managers.set(session, new EditorBrushManager(session));
157
return managers.get(session)!;
158
}
159
160