Path: blob/master/src/editor/modules/brushes/manager.ts
1785 views
import { BlockVolumeBase, Player, system, Vector3 } from "@minecraft/server";1import { IPlayerUISession, PaintCompletionState, RelativeVolumeListBlockVolume, Widget, WidgetComponentVolumeOutline } from "@minecraft/server-editor";2import { Jobs } from "@modules/jobs";3import { Mask } from "@modules/mask";4import { Pattern } from "@modules/pattern";5import { Thread, Vector, VectorSet } from "@notbeer-api";6import { shapeToBlockVolume } from "editor/util";7import { Brush } from "server/brushes/base_brush";8import { SphereBrush } from "server/brushes/sphere_brush";9import { getSession } from "server/sessions";1011const managers = new WeakMap<IPlayerUISession, EditorBrushManager>();1213class EditorBrushManager {14private readonly session: IPlayerUISession;15private readonly player: Player;1617private widget?: Widget;18private volumeOutline?: WidgetComponentVolumeOutline;1920private previewWidget?: Widget;21private previewVolumeOutline?: WidgetComponentVolumeOutline;2223private readonly volumeUpdator = shapeToBlockVolume();24private volume: BlockVolumeBase;25private volumeOffset: Vector3 = Vector.ZERO;2627private locations = new VectorSet();28private paintingHandler?: number;29private onComplete?: (state: PaintCompletionState) => void;3031private mask = new Mask();32private brush: Brush;3334constructor(session: IPlayerUISession) {35this.session = session;36this.player = session.extensionContext.player;37this.brush = new SphereBrush(3, new Pattern("stone"), false);38this.updateVolume();39}4041getBrushShapeOffset(): Vector3 {42return (this.player.getDynamicProperty("brushShapeOffset") as Vector3) ?? Vector.ZERO;43}4445setBrushShapeOffset(offset: Vector3): void {46if (this.volumeOutline) this.volumeOutline.offset = offset;47this.player.setDynamicProperty("brushShapeOffset", offset);48}4950activateBrushTool(): void {51if (!this.widget) {52const widgetGroup = this.session.extensionContext.widgetManager.createGroup();53this.widget ??= widgetGroup.createWidget(Vector.ZERO, { bindPositionToBlockCursor: true, selectable: false });54this.volumeOutline ??= this.widget.addVolumeOutline("shape", this.volume, {55volumeOffset: Vector.add(this.volumeOffset, this.volume?.getMin() ?? Vector.ZERO),56offset: this.getBrushShapeOffset(),57showOutline: false,58});5960this.previewWidget ??= widgetGroup.createWidget(Vector.ZERO, { selectable: false });61this.previewVolumeOutline ??= this.previewWidget.addVolumeOutline("preview", new RelativeVolumeListBlockVolume(), { showOutline: false });62}63this.widget.visible = true;64}6566deactivateBrushTool(): void {67if (this.widget) this.widget.visible = false;68this.endPainting(true);69}7071setBrushMask(mask: Mask): void {72this.mask = mask;73}7475setBrush(brush: Brush): void {76this.brush = brush;77this.updateVolume();78}7980singlePaint(onComplete: (state: PaintCompletionState) => void): void {81this.applyBrush([this.widget!.location], (success) => onComplete(success ? PaintCompletionState.Success : PaintCompletionState.Failed));82}8384beginPainting(onComplete: (state: PaintCompletionState) => void): void {85if (this.paintingHandler !== undefined) return;86this.locations.clear();87this.paintingHandler = system.runInterval(() => {88const nextLocation = this.widget?.location;89if (!nextLocation || this.locations.has(nextLocation)) return;9091this.locations.add(nextLocation);92const previewVolume = this.previewVolumeOutline?.getVolume() as RelativeVolumeListBlockVolume;93if (previewVolume && this.volumeOutline) {94const volume = this.volumeOutline.getVolume();95const offset = Vector.add(nextLocation, this.getBrushShapeOffset()).add(this.volumeOffset);96volume.translate(offset);97previewVolume.add(volume);98volume.translate(Vector.mul(offset, -1));99this.previewWidget.location = previewVolume.getMin();100}101});102this.onComplete = onComplete;103}104105endPainting(discard: boolean): void {106if (this.paintingHandler === undefined) return;107system.clearRun(this.paintingHandler);108this.paintingHandler = undefined;109if (!discard) {110this.applyBrush(Array.from(this.locations.values()), (success) => {111this.onComplete?.(success ? PaintCompletionState.Success : PaintCompletionState.Failed);112this.previewVolumeOutline?.getVolume().clear();113});114} else {115this.onComplete?.(PaintCompletionState.Canceled);116this.previewVolumeOutline?.getVolume().clear();117}118this.onComplete = undefined;119}120121isBrushPaintBusy(): boolean {122return this.paintingHandler !== undefined;123}124125private updateVolume() {126const [shape, location] = this.brush.getOutline();127this.volumeUpdator.update(shape, (volume) => {128this.volume = volume;129this.volumeOffset = location;130if (this.volumeOutline) {131this.volumeOutline.setVolume(this.volume);132this.volumeOutline.volumeOffset = Vector.add(this.volumeOffset, this.volume.getMin());133}134});135}136137private applyBrush(locations: Vector3[], onFinish?: (success: boolean) => void) {138const session = getSession(this.player);139const offsetLocations = locations.map((loc) => Vector.add(loc, this.getBrushShapeOffset()));140new Thread().start(141function* (this: EditorBrushManager) {142try {143yield* Jobs.run(session, -1, this.brush.apply(offsetLocations, session, this.mask));144onFinish?.(true);145} catch (err) {146onFinish?.(false);147throw err;148}149}.bind(this)150);151}152}153154export function getEditorBrushManager(session: IPlayerUISession) {155if (!managers.has(session)) managers.set(session, new EditorBrushManager(session));156return managers.get(session)!;157}158159160