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