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