Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sisilicon
GitHub Repository: sisilicon/worldedit-be
Path: blob/master/src/editor/control.ts
1782 views
1
import {
2
IModalTool,
3
IRootPropertyPane,
4
IPlayerUISession,
5
SupportedKeyboardActionTypes,
6
KeyBinding,
7
ActionTypes,
8
ContinuousActionState,
9
CursorControlMode,
10
CursorProperties,
11
CursorPropertiesChangeAfterEvent,
12
CursorTargetMode,
13
IBoolPropertyItem,
14
INumberPropertyItem,
15
IObservable,
16
ISubPanePropertyItem,
17
KeyboardKey,
18
makeObservable,
19
MouseInputType,
20
NumberPropertyItemVariant,
21
} from "@minecraft/server-editor";
22
import { PersistenceManager, RelativeDirection, getRotationCorrectedDirectionVector, getInputMarkup } from "./modules/brushes/util";
23
import { newLineMarkup } from "./util";
24
25
export class SharedControl {
26
public readonly localizationPrefix: string;
27
public readonly tool: IModalTool;
28
public readonly propertyPane: IRootPropertyPane;
29
public readonly controlName: string;
30
public readonly session: IPlayerUISession;
31
32
private active = false;
33
private initialized = false;
34
35
constructor(session: IPlayerUISession, parentTool: IModalTool, parentPropertyPane: IRootPropertyPane, controlName: string, localizationPrefix: string) {
36
this.session = session;
37
this.tool = parentTool;
38
this.propertyPane = parentPropertyPane;
39
this.controlName = controlName;
40
this.localizationPrefix = localizationPrefix;
41
}
42
43
get isActive() {
44
return this.active;
45
}
46
47
get isInitialized() {
48
return this.initialized;
49
}
50
51
initialize() {
52
this.initialized = true;
53
}
54
55
shutdown() {
56
this.initialized = false;
57
}
58
59
activateControl() {
60
if (!this.initialized) throw new Error("Control must be initialized before it can be activated");
61
if (this.active) throw new Error("Control is already active");
62
this.active = true;
63
}
64
65
deactivateControl() {
66
if (!this.active) throw new Error("Control is not active");
67
this.active = false;
68
}
69
70
registerToolKeyBinding(action: SupportedKeyboardActionTypes, binding: KeyBinding, tag: string) {
71
this.tool.registerKeyBinding(action, binding, {
72
uniqueId: this.getToolKeyBindingId(tag),
73
label: `${this.localizationPrefix}.keybinding.${tag}.title`,
74
tooltip: `${this.localizationPrefix}.keybinding.${tag}.tooltip`,
75
});
76
}
77
78
getToolKeyBindingId(tag: string) {
79
return `${this.tool.id}:${this.controlName}Keybinding:${tag}`;
80
}
81
82
localize(key: string) {
83
return `${this.localizationPrefix}.${key}`;
84
}
85
}
86
87
const CursorModeControl_PERSISTENCE_GROUP_NAME = "worldedit:cursor";
88
const PERSISTENCE_GROUPITEM_NAME = "cursor_settings";
89
const PROPERTY_CURSORMODECONTROL_NAME = "CursorModeControl";
90
const PROPERTY_CURSORMODECONTROL_LOCALIZATION_PREFIX = `resourcePack.editor.${PROPERTY_CURSORMODECONTROL_NAME}`;
91
const KEY_REPEAT_DELAY = 5;
92
const KEY_REPEAT_INTERVAL = 1;
93
94
export class CursorModeControl extends SharedControl {
95
public static readonly MIN_FIXED_DISTANCE = 1;
96
public static readonly MAX_FIXED_DISTANCE = 128;
97
98
private readonly overrideCursorProperties: CursorProperties;
99
private readonly mouseControlMode: IObservable<CursorControlMode>;
100
private readonly cursorTargetMode: IObservable<CursorTargetMode>;
101
private readonly projectThroughWater: IObservable<boolean>;
102
private readonly fixedDistanceCursor: IObservable<number>;
103
private readonly persistenceManager: PersistenceManager;
104
private readonly bindManualInput: boolean;
105
106
private controlRootPane: ISubPanePropertyItem;
107
private fixedDistanceSliderControl: INumberPropertyItem;
108
private projectThroughWaterCheckbox: IBoolPropertyItem;
109
private canMoveManually: () => boolean;
110
private cachedCursorProperties: CursorProperties;
111
private cursorPropertyEventSub: (ev: CursorPropertiesChangeAfterEvent) => void;
112
113
constructor(session: IPlayerUISession, parentTool: IModalTool, parentPropertyPane: IRootPropertyPane, bindManualInput = true, overrideCursorProperties: CursorProperties) {
114
super(session, parentTool, parentPropertyPane, PROPERTY_CURSORMODECONTROL_NAME, PROPERTY_CURSORMODECONTROL_LOCALIZATION_PREFIX);
115
this.canMoveManually = () => true;
116
this.persistenceManager = new PersistenceManager(session.extensionContext.player);
117
this.bindManualInput = bindManualInput;
118
const savedCursorProperties = this.loadSettings();
119
this.overrideCursorProperties = {
120
...overrideCursorProperties,
121
};
122
this.cachedCursorProperties = this.overrideCursorProperties;
123
if (savedCursorProperties) {
124
delete savedCursorProperties.projectThroughLiquid;
125
this.cachedCursorProperties = savedCursorProperties;
126
}
127
const currentCursorProperties = overrideCursorProperties ?? this.session.extensionContext.cursor.getDefaultProperties();
128
this.mouseControlMode = makeObservable(this.cachedCursorProperties.controlMode ?? CursorControlMode.KeyboardAndMouse);
129
this.cursorTargetMode = makeObservable(this.cachedCursorProperties.targetMode ?? CursorTargetMode.Block);
130
this.projectThroughWater = makeObservable(currentCursorProperties.projectThroughLiquid ?? true);
131
this.fixedDistanceCursor = makeObservable(this.cachedCursorProperties.fixedModeDistance ?? 5);
132
currentCursorProperties.visible = true;
133
}
134
135
get cursorProperties() {
136
const props: CursorProperties = {
137
...this.overrideCursorProperties,
138
controlMode: this.mouseControlMode.value,
139
targetMode: this.cursorTargetMode.value,
140
fixedModeDistance: this.fixedDistanceCursor.value,
141
};
142
return props;
143
}
144
145
initialize() {
146
super.initialize();
147
this.tool.onModalToolActivation.subscribe((eventData) => {
148
if (eventData.isActiveTool) {
149
const savedCursorProperties = this.cachedCursorProperties;
150
if (savedCursorProperties) {
151
if (savedCursorProperties.controlMode) this.mouseControlMode.set(savedCursorProperties.controlMode);
152
if (savedCursorProperties.targetMode) this.cursorTargetMode.set(savedCursorProperties.targetMode);
153
if (savedCursorProperties.fixedModeDistance) this.fixedDistanceCursor.set(savedCursorProperties.fixedModeDistance);
154
}
155
this.updateCursorProperties(this.session, true, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl, false);
156
} else {
157
this.session.extensionContext.cursor.popPropertiesById(this.tool.id);
158
}
159
});
160
if (this.bindManualInput) {
161
const moveBlockCursorManually = (session: IPlayerUISession, direction: RelativeDirection) => {
162
const rotationY = session.extensionContext.player.getRotation().y;
163
const rotationCorrectedVector = getRotationCorrectedDirectionVector(rotationY, direction);
164
session.extensionContext.cursor.moveBy(rotationCorrectedVector);
165
};
166
const keyUpAction = this.session.actionManager.createAction({
167
actionType: ActionTypes.ContinuousAction,
168
onExecute: (state) => {
169
if (state === ContinuousActionState.End) return;
170
if (this.canMoveManually()) this.session.extensionContext.cursor.moveBy({ x: 0, y: 1, z: 0 });
171
},
172
repeatInterval: KEY_REPEAT_INTERVAL,
173
repeatDelay: KEY_REPEAT_DELAY,
174
});
175
const keyDownAction = this.session.actionManager.createAction({
176
actionType: ActionTypes.ContinuousAction,
177
onExecute: (state) => {
178
if (state === ContinuousActionState.End) return;
179
if (this.canMoveManually()) this.session.extensionContext.cursor.moveBy({ x: 0, y: -1, z: 0 });
180
},
181
repeatInterval: KEY_REPEAT_INTERVAL,
182
repeatDelay: KEY_REPEAT_DELAY,
183
});
184
const keyLeftAction = this.session.actionManager.createAction({
185
actionType: ActionTypes.ContinuousAction,
186
onExecute: (state) => {
187
if (state === ContinuousActionState.End) return;
188
if (this.canMoveManually()) moveBlockCursorManually(this.session, RelativeDirection.Left);
189
},
190
repeatInterval: KEY_REPEAT_INTERVAL,
191
repeatDelay: KEY_REPEAT_DELAY,
192
});
193
const keyRightAction = this.session.actionManager.createAction({
194
actionType: ActionTypes.ContinuousAction,
195
onExecute: (state) => {
196
if (state === ContinuousActionState.End) return;
197
if (this.canMoveManually()) moveBlockCursorManually(this.session, RelativeDirection.Right);
198
},
199
repeatInterval: KEY_REPEAT_INTERVAL,
200
repeatDelay: KEY_REPEAT_DELAY,
201
});
202
const keyForwardAction = this.session.actionManager.createAction({
203
actionType: ActionTypes.ContinuousAction,
204
onExecute: (state) => {
205
if (state === ContinuousActionState.End) return;
206
if (this.canMoveManually()) moveBlockCursorManually(this.session, RelativeDirection.Forward);
207
},
208
repeatInterval: KEY_REPEAT_INTERVAL,
209
repeatDelay: KEY_REPEAT_DELAY,
210
});
211
const keyBackAction = this.session.actionManager.createAction({
212
actionType: ActionTypes.ContinuousAction,
213
onExecute: (state) => {
214
if (state === ContinuousActionState.End) return;
215
if (this.canMoveManually()) moveBlockCursorManually(this.session, RelativeDirection.Back);
216
},
217
repeatInterval: KEY_REPEAT_INTERVAL,
218
repeatDelay: KEY_REPEAT_DELAY,
219
});
220
this.registerToolKeyBinding(
221
keyForwardAction,
222
{
223
key: KeyboardKey.UP,
224
},
225
"moveCursorForward"
226
);
227
this.registerToolKeyBinding(
228
keyBackAction,
229
{
230
key: KeyboardKey.DOWN,
231
},
232
"moveCursorBack"
233
);
234
this.registerToolKeyBinding(
235
keyLeftAction,
236
{
237
key: KeyboardKey.LEFT,
238
},
239
"moveCursorLeft"
240
);
241
this.registerToolKeyBinding(
242
keyRightAction,
243
{
244
key: KeyboardKey.RIGHT,
245
},
246
"moveCursorRight"
247
);
248
this.registerToolKeyBinding(
249
keyUpAction,
250
{
251
key: KeyboardKey.PAGE_UP,
252
},
253
"moveCursorUp"
254
);
255
this.registerToolKeyBinding(
256
keyDownAction,
257
{
258
key: KeyboardKey.PAGE_DOWN,
259
},
260
"moveCursorDown"
261
);
262
{
263
const keyToggleMouseControlModeAction = this.session.actionManager.createAction({
264
actionType: ActionTypes.NoArgsAction,
265
onExecute: () => {
266
const currentMode = this.mouseControlMode.value;
267
let newMode = CursorControlMode.Fixed;
268
switch (currentMode) {
269
case CursorControlMode.KeyboardAndMouse:
270
newMode = CursorControlMode.Fixed;
271
break;
272
case CursorControlMode.Fixed:
273
newMode = CursorControlMode.Keyboard;
274
break;
275
case CursorControlMode.Keyboard:
276
default:
277
newMode = CursorControlMode.KeyboardAndMouse;
278
}
279
this.mouseControlMode.set(newMode);
280
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
281
},
282
});
283
this.registerToolKeyBinding(keyToggleMouseControlModeAction, { key: KeyboardKey.KEY_T }, "toggleMouseTracking");
284
}
285
const mouseWheelAction = this.session.actionManager.createAction({
286
actionType: ActionTypes.MouseRayCastAction,
287
onExecute: (_, mouseProps) => {
288
if (mouseProps.inputType === MouseInputType.WheelOut && mouseProps.modifiers.shift) {
289
if (this.mouseControlMode.value === CursorControlMode.Fixed) {
290
let currentDistance = this.fixedDistanceCursor.value;
291
if (mouseProps.modifiers.shift) currentDistance += 5;
292
else currentDistance += 1;
293
currentDistance = Math.min(currentDistance, CursorModeControl.MAX_FIXED_DISTANCE);
294
this.fixedDistanceCursor.set(currentDistance);
295
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
296
}
297
} else if (mouseProps.inputType === MouseInputType.WheelIn && mouseProps.modifiers.shift) {
298
if (this.mouseControlMode.value === CursorControlMode.Fixed) {
299
let currentDistance = this.fixedDistanceCursor.value;
300
if (mouseProps.modifiers.shift) currentDistance -= 5;
301
else currentDistance -= 1;
302
currentDistance = Math.max(currentDistance, CursorModeControl.MIN_FIXED_DISTANCE);
303
this.fixedDistanceCursor.set(currentDistance);
304
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
305
}
306
}
307
},
308
});
309
this.tool.registerMouseWheelBinding(mouseWheelAction);
310
}
311
{
312
const keyToggleTargetModeAction = this.session.actionManager.createAction({
313
actionType: ActionTypes.NoArgsAction,
314
onExecute: () => {
315
const currentMode = this.cursorTargetMode.value;
316
const newMode = currentMode === CursorTargetMode.Block ? CursorTargetMode.Face : CursorTargetMode.Block;
317
this.cursorTargetMode.set(newMode);
318
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
319
},
320
});
321
this.registerToolKeyBinding(keyToggleTargetModeAction, { key: KeyboardKey.KEY_B }, "toggleBlockTargetMode");
322
}
323
}
324
325
shutdown() {
326
super.shutdown();
327
if (this.cursorPropertyEventSub) {
328
this.session.extensionContext.afterEvents.cursorPropertyChange.unsubscribe(this.cursorPropertyEventSub);
329
}
330
}
331
332
activateControl() {
333
super.activateControl();
334
this.constructControlUI();
335
}
336
337
deactivateControl() {
338
super.deactivateControl();
339
this.destroyControlUI();
340
}
341
342
private destroyControlUI() {
343
if (this.controlRootPane) {
344
this.propertyPane.removeSubPane(this.controlRootPane);
345
this.controlRootPane = undefined;
346
}
347
}
348
349
private constructControlUI() {
350
if (this.controlRootPane) this.destroyControlUI();
351
352
this.controlRootPane = this.propertyPane.createSubPane({
353
title: this.localize("rootPane.title"),
354
infoTooltip: {
355
title: this.localize("rootPane.title"),
356
description: [this.localize("rootPane.tooltip")],
357
},
358
hasMargins: false,
359
});
360
{
361
this.controlRootPane.addDropdown(this.mouseControlMode, {
362
title: this.localize("mouseControlMode.title"),
363
tooltip: {
364
title: {
365
id: this.localize("mouseControlMode.tooltip.title"),
366
props: [getInputMarkup(this.getToolKeyBindingId("toggleMouseTracking"))],
367
},
368
description: {
369
id: this.localize("mouseControlMode.tooltip"),
370
props: [newLineMarkup + newLineMarkup, getInputMarkup(this.getToolKeyBindingId("toggleMouseTracking"))],
371
},
372
},
373
entries: [
374
{
375
label: this.localize("mouseControlMode.keyboard"),
376
value: CursorControlMode.Keyboard,
377
},
378
{
379
label: this.localize("mouseControlMode.keyboardAndMouse"),
380
value: CursorControlMode.KeyboardAndMouse,
381
},
382
{
383
label: this.localize("mouseControlMode.fixed"),
384
value: CursorControlMode.Fixed,
385
},
386
],
387
onChange: () => {
388
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
389
},
390
});
391
this.mouseControlMode.set(this.cachedCursorProperties.controlMode ?? CursorControlMode.KeyboardAndMouse);
392
this.cursorTargetMode.set(this.cachedCursorProperties.targetMode ?? CursorTargetMode.Block);
393
this.fixedDistanceCursor.set(this.cachedCursorProperties.fixedModeDistance ?? 5);
394
const fixedDistanceSliderVisible = this.cachedCursorProperties.controlMode === CursorControlMode.Fixed;
395
this.fixedDistanceSliderControl = this.controlRootPane.addNumber(this.fixedDistanceCursor, {
396
visible: fixedDistanceSliderVisible,
397
isInteger: true,
398
min: CursorModeControl.MIN_FIXED_DISTANCE,
399
max: CursorModeControl.MAX_FIXED_DISTANCE,
400
title: this.localize("fixedDistance.slider.title"),
401
tooltip: this.localize("fixedDistance.slider.tooltip"),
402
variant: NumberPropertyItemVariant.InputFieldAndSlider,
403
onChange: () => {
404
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
405
},
406
});
407
this.session.extensionContext.afterEvents.cursorPropertyChange.subscribe((_event) => {
408
if (_event.properties.fixedModeDistance !== undefined && _event.properties.fixedModeDistance !== this.fixedDistanceCursor.value) {
409
this.fixedDistanceCursor.set(_event.properties.fixedModeDistance);
410
}
411
});
412
}
413
{
414
this.controlRootPane.addToggleGroup(this.cursorTargetMode, {
415
title: this.localize("cursorTargetMode.title"),
416
tooltip: {
417
title: {
418
id: this.localize("cursorTargetMode.tooltip.title"),
419
props: [getInputMarkup(this.getToolKeyBindingId("toggleBlockTargetMode"))],
420
},
421
description: {
422
id: this.localize("cursorTargetMode.tooltip"),
423
props: [getInputMarkup(this.getToolKeyBindingId("toggleBlockTargetMode"))],
424
},
425
},
426
entries: [
427
{
428
tooltip: {
429
title: {
430
id: this.localize("cursorTargetMode.block"),
431
props: [getInputMarkup(this.getToolKeyBindingId("toggleBlockTargetMode"))],
432
},
433
description: {
434
id: this.localize("cursorTargetMode.block.tooltip"),
435
props: [newLineMarkup + newLineMarkup, getInputMarkup(this.getToolKeyBindingId("toggleBlockTargetMode"))],
436
},
437
},
438
value: CursorTargetMode.Block,
439
icon: "pack://textures/editor/block-mode.png",
440
},
441
{
442
tooltip: {
443
title: {
444
id: this.localize("cursorTargetMode.face"),
445
props: [getInputMarkup(this.getToolKeyBindingId("toggleBlockTargetMode"))],
446
},
447
description: {
448
id: this.localize("cursorTargetMode.face.tooltip"),
449
props: [newLineMarkup + newLineMarkup, getInputMarkup(this.getToolKeyBindingId("toggleBlockTargetMode"))],
450
},
451
},
452
value: CursorTargetMode.Face,
453
icon: "pack://textures/editor/face-mode.png",
454
},
455
],
456
onChange: () => {
457
this.updateCursorProperties(this.session, false, this.mouseControlMode.value, this.cursorTargetMode.value, this.fixedDistanceCursor.value, this.fixedDistanceSliderControl);
458
},
459
});
460
}
461
{
462
this.projectThroughWaterCheckbox = this.controlRootPane.addBool(this.projectThroughWater, {
463
title: this.localize("projectThroughWater.title"),
464
tooltip: this.localize("projectThroughWater.tooltip"),
465
visible: this.mouseControlMode.value === CursorControlMode.Mouse || this.mouseControlMode.value === CursorControlMode.KeyboardAndMouse,
466
onChange: () => {
467
const cursorProperties = {
468
projectThroughLiquid: this.projectThroughWater.value,
469
};
470
this.session.extensionContext.cursor.updatePropertiesById(cursorProperties, this.tool.id);
471
},
472
});
473
this.cursorPropertyEventSub = this.session.extensionContext.afterEvents.cursorPropertyChange.subscribe((event) => {
474
if (event.properties.projectThroughLiquid !== undefined) {
475
this.projectThroughWater.set(event.properties.projectThroughLiquid);
476
}
477
});
478
}
479
}
480
481
private loadSettings() {
482
const group = this.persistenceManager.getGroup(CursorModeControl_PERSISTENCE_GROUP_NAME);
483
if (group) {
484
const key = `${this.tool.id}_${PERSISTENCE_GROUPITEM_NAME}`;
485
const storeItem = group.fetchItem<CursorProperties>(key);
486
if (storeItem && storeItem.value) {
487
return storeItem.value;
488
}
489
group.dispose();
490
}
491
return undefined;
492
}
493
494
private saveSettings(settings: CursorProperties) {
495
const group = this.persistenceManager.getOrCreateGroup(CursorModeControl_PERSISTENCE_GROUP_NAME);
496
if (group) {
497
const key = `${this.tool.id}_${PERSISTENCE_GROUPITEM_NAME}`;
498
const storeItem = group.getOrCreateItem(key, settings);
499
if (storeItem) {
500
storeItem.commit();
501
}
502
group.dispose();
503
return;
504
}
505
}
506
507
private updateCursorProperties(
508
session: IPlayerUISession,
509
isActivationUpdate: boolean,
510
cursorControlMode: CursorControlMode,
511
cursorTargetMode: CursorTargetMode,
512
fixedDistanceValue: number,
513
fixedDistanceSliderControl: INumberPropertyItem,
514
isSaveSettings = true
515
) {
516
const cursorProperties = {
517
...this.overrideCursorProperties,
518
controlMode: cursorControlMode,
519
targetMode: cursorTargetMode,
520
fixedModeDistance: fixedDistanceValue,
521
};
522
if (fixedDistanceSliderControl) {
523
fixedDistanceSliderControl.visible = cursorControlMode === CursorControlMode.Fixed;
524
}
525
if (cursorControlMode === CursorControlMode.Keyboard) {
526
this.session.toolRail.focusToolInputContext();
527
}
528
if (this.projectThroughWaterCheckbox) {
529
this.projectThroughWaterCheckbox.visible = cursorControlMode === CursorControlMode.Mouse || cursorControlMode === CursorControlMode.KeyboardAndMouse;
530
}
531
if (isActivationUpdate) {
532
session.extensionContext.cursor.pushPropertiesById(cursorProperties, this.tool.id);
533
} else {
534
session.extensionContext.cursor.updatePropertiesById(cursorProperties, this.tool.id);
535
}
536
this.cachedCursorProperties = cursorProperties;
537
if (isSaveSettings) this.saveSettings(cursorProperties);
538
}
539
}
540
541