@@ -0,0 +1,74 @@
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
|
||||
export const MaterialType = Object.freeze({
|
||||
OPAQUE: "opaque",
|
||||
BLEND: "blend"
|
||||
});
|
||||
|
||||
// An incorrect block is drawn slightly outside its cell so it wraps the real
|
||||
// block instead of z-fighting with it.
|
||||
const OVERLAY_SCALE = 1.01;
|
||||
|
||||
// A missing block is drawn slightly inside its cell so a run of them reads as one mass,
|
||||
// but when a block is placed there is no z-fighting. This inset is also the one thing
|
||||
// keeping neighbor culling from being exactly invisible: two missing blocks
|
||||
// side by side stop short of their shared boundary, so culling the faces they
|
||||
// present to each other opens a seam visible at a grazing angle. Setting this
|
||||
// to 1.00 makes every cull exact, but causes z-fighting for placed blocks.
|
||||
const MISSING_SCALE = 0.995;
|
||||
|
||||
// A missing block in a mode that draws no preview (the Classic look): a small see-through cube
|
||||
// floating in the middle of its cell.
|
||||
const MARKER_SCALE = 0.90;
|
||||
|
||||
const PARTICLE_STYLES = Object.freeze({
|
||||
[BlockVerificationLevel.NoMatch]: Object.freeze({
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 0.2 }, scale: OVERLAY_SCALE, material: MaterialType.BLEND
|
||||
}),
|
||||
[BlockVerificationLevel.TypeMatch]: Object.freeze({
|
||||
color: { red: 1, green: 1, blue: 0, alpha: 0.2 }, scale: OVERLAY_SCALE, material: MaterialType.BLEND
|
||||
}),
|
||||
[BlockVerificationLevel.Missing]: Object.freeze({
|
||||
color: { red: 0.55, green: 0.8, blue: 1, alpha: 1 }, scale: MISSING_SCALE, material: MaterialType.OPAQUE
|
||||
})
|
||||
});
|
||||
|
||||
const MARKER_STYLE = Object.freeze({
|
||||
color: { red: 0, green: 0, blue: 1, alpha: 0.2 }, scale: MARKER_SCALE, material: MaterialType.BLEND
|
||||
});
|
||||
|
||||
const BOX_STYLES = Object.freeze({
|
||||
[BlockVerificationLevel.NoMatch]: Object.freeze({
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 }, scale: OVERLAY_SCALE
|
||||
}),
|
||||
[BlockVerificationLevel.TypeMatch]: Object.freeze({
|
||||
color: { red: 1, green: 1, blue: 0, alpha: 1 }, scale: OVERLAY_SCALE
|
||||
}),
|
||||
[BlockVerificationLevel.Missing]: Object.freeze({
|
||||
color: { red: 0.3, green: 0.57, blue: 0.87, alpha: 1 }, scale: 1.00
|
||||
})
|
||||
});
|
||||
|
||||
// A face the pipeline couldn't resolve.
|
||||
export const UNRESOLVED_FACE_STYLE = Object.freeze({
|
||||
color: { red: 0.3, green: 0.57, blue: 0.87, alpha: 0.2 }, material: MaterialType.BLEND
|
||||
});
|
||||
|
||||
// Water is the one block whose texture is authored see-through - Java draws it
|
||||
// translucent and block/water_still carries an alpha of 180/255, which the
|
||||
// atlas keeps. The preview's usual material alpha-tests rather than blending,
|
||||
// which forced every partly-transparent texel solid.
|
||||
export const WATER_MATERIAL = MaterialType.BLEND;
|
||||
|
||||
// Undefined for levels that draw no particle at all.
|
||||
export function particleStyleOf(verificationLevel, showsBlockPreview) {
|
||||
if (verificationLevel === BlockVerificationLevel.Missing && !showsBlockPreview)
|
||||
return MARKER_STYLE;
|
||||
return PARTICLE_STYLES[verificationLevel];
|
||||
}
|
||||
|
||||
// Undefined for levels that get no box. That is what makes a cell going to
|
||||
// Match or Air a removal rather than a recolour.
|
||||
export function boxStyleOf(verificationLevel) {
|
||||
return BOX_STYLES[verificationLevel];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { blockKeySpecs, blockModels } from "../../../blockModels";
|
||||
|
||||
export class BlockModelKeyResolver {
|
||||
lookupFaceRefs(blockId, states) {
|
||||
const keySpec = blockKeySpecs[blockId];
|
||||
if (keySpec === void 0)
|
||||
return void 0;
|
||||
for (let shapeIndex = 0; shapeIndex < keySpec.props.length; shapeIndex++) {
|
||||
const key = this.buildStateKey(blockId, keySpec.props[shapeIndex], shapeIndex, states);
|
||||
if (key === void 0)
|
||||
continue;
|
||||
const faceReferences = blockModels[key];
|
||||
if (faceReferences !== void 0)
|
||||
return faceReferences;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
faceRefsForKey(key) {
|
||||
return blockModels[key];
|
||||
}
|
||||
|
||||
buildStateKey(blockId, shapeProperties, shapeIndex, states) {
|
||||
let key = `${blockId}[${shapeIndex}|`;
|
||||
for (let propertyIndex = 0; propertyIndex < shapeProperties.length; propertyIndex++) {
|
||||
const propertyName = shapeProperties[propertyIndex];
|
||||
const value = states[propertyName];
|
||||
if (value === void 0)
|
||||
return void 0;
|
||||
if (propertyIndex > 0)
|
||||
key += ",";
|
||||
key += typeof value === "boolean" ? (value ? 1 : 0) : value;
|
||||
}
|
||||
return `${key}]`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { FaceTable } from "./FaceTable";
|
||||
import { CubeFaceLibrary } from "./CubeFaceLibrary";
|
||||
import { BlockModelKeyResolver } from "./BlockModelKeyResolver";
|
||||
import { CellFlags } from "../../Verifier/CellFlags";
|
||||
|
||||
class BlockModelResolver {
|
||||
#waterBlockIds = new Set(["minecraft:water", "minecraft:flowing_water"]);
|
||||
#waterloggedWaterState = "minecraft:water[0|0]";
|
||||
|
||||
constructor({ keyResolver, faceReader, unknownFaces, sideMaskPacker }) {
|
||||
this.keyResolver = keyResolver;
|
||||
this.faceReader = faceReader;
|
||||
this.unknownFaces = unknownFaces;
|
||||
this.sideMaskPacker = sideMaskPacker;
|
||||
this.resolvedByBlock = new WeakMap();
|
||||
this.waterloggedFacesCache = void 0;
|
||||
this.overlaySideMasksCache = void 0;
|
||||
}
|
||||
|
||||
facesOf(block) {
|
||||
return this.resolve(block).faces;
|
||||
}
|
||||
|
||||
sideMasksOf(block) {
|
||||
return this.resolve(block).sideMasks;
|
||||
}
|
||||
|
||||
overlaySideMasks() {
|
||||
if (this.overlaySideMasksCache === void 0)
|
||||
this.overlaySideMasksCache = this.computeSideMasks(this.unknownFaces);
|
||||
return this.overlaySideMasksCache;
|
||||
}
|
||||
|
||||
waterloggedFaces() {
|
||||
if (this.waterloggedFacesCache === void 0) {
|
||||
const faceRefs = this.keyResolver.faceRefsForKey(this.#waterloggedWaterState) ?? [];
|
||||
this.waterloggedFacesCache = faceRefs.map(this.faceReader);
|
||||
}
|
||||
return this.waterloggedFacesCache;
|
||||
}
|
||||
|
||||
isWater(block) {
|
||||
return this.#waterBlockIds.has(this.blockIdOf(block));
|
||||
}
|
||||
|
||||
resolve(block) {
|
||||
const cached = this.resolvedByBlock.get(block);
|
||||
if (cached !== void 0)
|
||||
return cached;
|
||||
|
||||
const faces = this.lookupFaces(block);
|
||||
const resolved = {
|
||||
faces,
|
||||
sideMasks: this.computeSideMasks(faces)
|
||||
};
|
||||
|
||||
this.resolvedByBlock.set(block, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
lookupFaces(block) {
|
||||
const blockId = this.blockIdOf(block);
|
||||
const states = block.states ?? block.getAllStates();
|
||||
const faceReferences = this.keyResolver.lookupFaceRefs(blockId, states);
|
||||
if (faceReferences === void 0)
|
||||
return this.unknownFaces;
|
||||
return faceReferences.map(this.faceReader);
|
||||
}
|
||||
|
||||
computeSideMasks(faces) {
|
||||
let markerMask = 0;
|
||||
let opaqueMask = 0;
|
||||
|
||||
for (const face of faces) {
|
||||
if (!face.covers)
|
||||
continue;
|
||||
if (face.missing)
|
||||
markerMask |= 1 << face.cull;
|
||||
else if (face.opaque)
|
||||
opaqueMask |= 1 << face.cull;
|
||||
}
|
||||
|
||||
return this.sideMaskPacker(markerMask, opaqueMask);
|
||||
}
|
||||
|
||||
blockIdOf(block) {
|
||||
return block.typeId ?? block.type.id;
|
||||
}
|
||||
}
|
||||
|
||||
export const blockModelResolver = new BlockModelResolver({
|
||||
keyResolver: new BlockModelKeyResolver(),
|
||||
faceReader: FaceTable.faceAt,
|
||||
unknownFaces: CubeFaceLibrary.createPlainUnknownCubeFaces(),
|
||||
sideMaskPacker: CellFlags.pack
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { whiteUvRect } from "../../../blockAtlas";
|
||||
|
||||
export class CubeFaceLibrary {
|
||||
static createPlainCubeFaces() {
|
||||
return [
|
||||
{ center: [8, 16, 8], width: 16, height: 16, facing: [0, -1, 0], roll: 180, tintindex: -1, uv: whiteUvRect, cull: 1, covers: true },
|
||||
{ center: [8, 0, 8], width: 16, height: 16, facing: [0, 1, 0], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 0, covers: true },
|
||||
{ center: [8, 8, 0], width: 16, height: 16, facing: [0, 0, -1], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 2, covers: true },
|
||||
{ center: [8, 8, 16], width: 16, height: 16, facing: [0, 0, 1], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 3, covers: true },
|
||||
{ center: [16, 8, 8], width: 16, height: 16, facing: [1, 0, 0], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 5, covers: true },
|
||||
{ center: [0, 8, 8], width: 16, height: 16, facing: [-1, 0, 0], roll: 0, tintindex: -1, uv: whiteUvRect, cull: 4, covers: true }
|
||||
];
|
||||
}
|
||||
|
||||
static createUnknownCubeFaces(baseFaces) {
|
||||
return baseFaces.map((face) => ({ ...face, missing: true }));
|
||||
}
|
||||
|
||||
static createPlainUnknownCubeFaces() {
|
||||
const plainFaces = CubeFaceLibrary.createPlainCubeFaces();
|
||||
return CubeFaceLibrary.createUnknownCubeFaces(plainFaces);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { blockFaceData, blockMissingFaces } from "../../../blockModels";
|
||||
|
||||
export class FaceTable {
|
||||
static FACE_CULL = 0;
|
||||
static FACE_FACING = 1;
|
||||
static FACE_CENTER = 4;
|
||||
static FACE_WIDTH = 7;
|
||||
static FACE_HEIGHT = 8;
|
||||
static FACE_ROLL = 9;
|
||||
static FACE_TINTINDEX = 10;
|
||||
static FACE_UV = 11;
|
||||
static FACE_ROW_WIDTH = 15;
|
||||
|
||||
static NO_CULL = -1;
|
||||
|
||||
// The cull column carries two extra bits above the direction: whether the face spans its whole side of the block, and whether it does so opaquely.
|
||||
static FACE_CULL_MASK = 0b111;
|
||||
static FACE_COVERS_SIDE = 0b1000;
|
||||
static FACE_OPAQUE_SIDE = 0b10000;
|
||||
|
||||
static #faceCount = blockFaceData.length / FaceTable.FACE_ROW_WIDTH;
|
||||
static #missingFaces = new Set(blockMissingFaces);
|
||||
static #materialized = new Array(FaceTable.#faceCount);
|
||||
|
||||
static faceAt(index) {
|
||||
const cachedFace = FaceTable.#materialized[index];
|
||||
if (cachedFace !== void 0)
|
||||
return cachedFace;
|
||||
|
||||
const face = FaceTable.readFaceRow(index * FaceTable.FACE_ROW_WIDTH);
|
||||
if (FaceTable.#missingFaces.has(index))
|
||||
face.missing = true;
|
||||
|
||||
FaceTable.#materialized[index] = face;
|
||||
return face;
|
||||
}
|
||||
|
||||
static readFaceRow(offset) {
|
||||
const face = {
|
||||
center: [
|
||||
blockFaceData[offset + FaceTable.FACE_CENTER],
|
||||
blockFaceData[offset + FaceTable.FACE_CENTER + 1],
|
||||
blockFaceData[offset + FaceTable.FACE_CENTER + 2]
|
||||
],
|
||||
width: blockFaceData[offset + FaceTable.FACE_WIDTH],
|
||||
height: blockFaceData[offset + FaceTable.FACE_HEIGHT],
|
||||
facing: [
|
||||
blockFaceData[offset + FaceTable.FACE_FACING],
|
||||
blockFaceData[offset + FaceTable.FACE_FACING + 1],
|
||||
blockFaceData[offset + FaceTable.FACE_FACING + 2]
|
||||
],
|
||||
roll: blockFaceData[offset + FaceTable.FACE_ROLL],
|
||||
tintindex: blockFaceData[offset + FaceTable.FACE_TINTINDEX],
|
||||
uv: {
|
||||
x: blockFaceData[offset + FaceTable.FACE_UV],
|
||||
y: blockFaceData[offset + FaceTable.FACE_UV + 1],
|
||||
w: blockFaceData[offset + FaceTable.FACE_UV + 2],
|
||||
h: blockFaceData[offset + FaceTable.FACE_UV + 3]
|
||||
}
|
||||
};
|
||||
|
||||
FaceTable.applyCullFlags(face, blockFaceData[offset + FaceTable.FACE_CULL]);
|
||||
return face;
|
||||
}
|
||||
|
||||
static applyCullFlags(face, cullData) {
|
||||
if (cullData === FaceTable.NO_CULL)
|
||||
return;
|
||||
|
||||
face.cull = cullData & FaceTable.FACE_CULL_MASK;
|
||||
if (cullData & FaceTable.FACE_COVERS_SIDE)
|
||||
face.covers = true;
|
||||
if (cullData & FaceTable.FACE_OPAQUE_SIDE)
|
||||
face.opaque = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Vector } from "../../../lib/Vector.js";
|
||||
|
||||
export class Cuboid {
|
||||
static #edges = [
|
||||
[0, 1], [0, 2], [0, 4], [1, 3],
|
||||
[1, 5], [2, 3], [2, 6], [3, 7],
|
||||
[4, 5], [4, 6], [5, 7], [6, 7]
|
||||
];
|
||||
static #maxSegmentsPerEdge = 16;
|
||||
|
||||
constructor(min, max) {
|
||||
this.min = Vector.from(min);
|
||||
this.max = Vector.from(max);
|
||||
this.corners = Cuboid.cornersOf(this.min, this.max);
|
||||
}
|
||||
|
||||
static cornersOf(min, max) {
|
||||
return [
|
||||
new Vector(min.x, min.y, min.z),
|
||||
new Vector(max.x, min.y, min.z),
|
||||
new Vector(min.x, max.y, min.z),
|
||||
new Vector(max.x, max.y, min.z),
|
||||
new Vector(min.x, min.y, max.z),
|
||||
new Vector(max.x, min.y, max.z),
|
||||
new Vector(min.x, max.y, max.z),
|
||||
new Vector(max.x, max.y, max.z)
|
||||
];
|
||||
}
|
||||
|
||||
static segmentCount(start, end) {
|
||||
return Math.min(Math.floor(end.distance(start)), Cuboid.#maxSegmentsPerEdge);
|
||||
}
|
||||
|
||||
static *edgePoints(corners) {
|
||||
for (const [startIndex, endIndex] of Cuboid.#edges) {
|
||||
const start = corners[startIndex];
|
||||
const end = corners[endIndex];
|
||||
const segments = Cuboid.segmentCount(start, end);
|
||||
for (let i = 1; i < segments; i++)
|
||||
yield start.lerp(end, i / segments);
|
||||
}
|
||||
}
|
||||
|
||||
static *edgeSegments(corners) {
|
||||
for (const [startIndex, endIndex] of Cuboid.#edges) {
|
||||
const start = corners[startIndex];
|
||||
const end = corners[endIndex];
|
||||
const segments = Cuboid.segmentCount(start, end);
|
||||
for (let i = 0; i < segments; i++)
|
||||
yield [start.lerp(end, i / segments), start.lerp(end, (i + 1) / segments)];
|
||||
}
|
||||
}
|
||||
|
||||
matches(min, max) {
|
||||
return this.min.x === min.x && this.min.y === min.y && this.min.z === min.z
|
||||
&& this.max.x === max.x && this.max.y === max.y && this.max.z === max.z;
|
||||
}
|
||||
|
||||
edgePoints() {
|
||||
return Cuboid.edgePoints(this.corners);
|
||||
}
|
||||
|
||||
edgeSegments() {
|
||||
return Cuboid.edgeSegments(this.corners);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { DebugLine, DebugSphere, debugDrawer } from "@minecraft/debug-utilities";
|
||||
import { OutlineRenderer } from "./OutlineRenderer.js";
|
||||
|
||||
export class DebugOutlineRenderer extends OutlineRenderer {
|
||||
CORNER_SPHERE_SCALE = 0.1;
|
||||
#shapes = [];
|
||||
#isDrawing = false;
|
||||
|
||||
start() {
|
||||
this.stop();
|
||||
this.#isDrawing = true;
|
||||
this.#draw();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#isDrawing = false;
|
||||
for (const shape of this.#shapes)
|
||||
shape.remove();
|
||||
this.#shapes.length = 0;
|
||||
}
|
||||
|
||||
setBounds(dimension, min, max) {
|
||||
const changed = super.setBounds(dimension, min, max);
|
||||
if (changed && this.#isDrawing)
|
||||
this.start();
|
||||
return changed;
|
||||
}
|
||||
|
||||
#draw() {
|
||||
if (this.drawCorners) {
|
||||
for (const corner of this.corners) {
|
||||
const cornerSphereShape = this.#cornerSphere(corner);
|
||||
this.#add(cornerSphereShape, this.cornerColor);
|
||||
}
|
||||
}
|
||||
if (this.drawEdges) {
|
||||
let index = 0;
|
||||
for (const [start, end] of this.cuboid.edgeSegments()) {
|
||||
const edgeShape = new DebugLine(start, end);
|
||||
const color = this.edgeColors[index++ % this.edgeColors.length];
|
||||
this.#add(edgeShape, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#add(shape, color) {
|
||||
shape.color = color;
|
||||
this.#shapes.push(shape);
|
||||
debugDrawer.addShape(shape);
|
||||
}
|
||||
|
||||
#cornerSphere(corner) {
|
||||
const sphere = new DebugSphere({ dimension: this.dimension, x: corner.x, y: corner.y, z: corner.z });
|
||||
sphere.scale = this.CORNER_SPHERE_SCALE;
|
||||
return sphere;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { OutlineRenderer } from "./OutlineRenderer";
|
||||
import { DebugOutlineRenderer } from "./DebugOutlineRenderer";
|
||||
import { ParticleOutlineRenderer } from "./ParticleOutlineRenderer";
|
||||
|
||||
export class HybridOutlineRenderer extends OutlineRenderer {
|
||||
#edgeRenderer;
|
||||
#cornerRenderer;
|
||||
|
||||
constructor(dimension, min, max, particleTiming) {
|
||||
super(dimension, min, max);
|
||||
this.#edgeRenderer = new DebugOutlineRenderer(dimension, min, max);
|
||||
this.#edgeRenderer.drawCorners = false;
|
||||
this.#cornerRenderer = new ParticleOutlineRenderer(dimension, min, max, particleTiming);
|
||||
this.#cornerRenderer.drawEdges = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#edgeRenderer.start();
|
||||
this.#cornerRenderer.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.#edgeRenderer.stop();
|
||||
this.#cornerRenderer.stop();
|
||||
}
|
||||
|
||||
setBounds(dimension, min, max) {
|
||||
const changed = super.setBounds(dimension, min, max);
|
||||
this.#edgeRenderer.setBounds(dimension, min, max);
|
||||
this.#cornerRenderer.setBounds(dimension, min, max);
|
||||
return changed;
|
||||
}
|
||||
|
||||
addStandaloneCorners(locations) {
|
||||
super.addStandaloneCorners(locations);
|
||||
this.#edgeRenderer.addStandaloneCorners(locations);
|
||||
this.#cornerRenderer.addStandaloneCorners(locations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Vector } from "../../../lib/Vector.js";
|
||||
import { Cuboid } from "./Cuboid.js";
|
||||
|
||||
export class OutlineRenderer {
|
||||
dimension;
|
||||
corners;
|
||||
cuboid;
|
||||
drawCorners = true;
|
||||
drawEdges = true;
|
||||
#standaloneCorners = [];
|
||||
|
||||
cornerColor = Object.freeze({ red: 1, green: 1, blue: 1, alpha: 1 });
|
||||
edgeColors = Object.freeze([
|
||||
Object.freeze({ red: 0.93333333, green: 0.77647059, blue: 0.13333333, alpha: 1 }),
|
||||
Object.freeze({ red: 0.09019608, green: 0.09019608, blue: 0.09019608, alpha: 1 })
|
||||
]);
|
||||
|
||||
constructor(dimension, min, max) {
|
||||
this.#assignBounds(dimension, min, max);
|
||||
}
|
||||
|
||||
setBounds(dimension, min, max) {
|
||||
if (dimension === this.dimension && this.cuboid.matches(min, max))
|
||||
return false;
|
||||
this.#assignBounds(dimension, min, max);
|
||||
return true;
|
||||
}
|
||||
|
||||
addStandaloneCorners(locations) {
|
||||
for (const location of locations)
|
||||
this.#standaloneCorners.push(Vector.from(location));
|
||||
this.#collectCorners();
|
||||
}
|
||||
|
||||
start() {
|
||||
throw new Error(`${this.constructor.name} must implement start().`);
|
||||
}
|
||||
|
||||
stop() {
|
||||
throw new Error(`${this.constructor.name} must implement stop().`);
|
||||
}
|
||||
|
||||
#assignBounds(dimension, min, max) {
|
||||
this.dimension = dimension;
|
||||
this.cuboid = new Cuboid(min, max);
|
||||
this.#collectCorners();
|
||||
}
|
||||
|
||||
#collectCorners() {
|
||||
this.corners = this.#standaloneCorners.length === 0
|
||||
? this.cuboid.corners
|
||||
: [...this.cuboid.corners, ...this.#standaloneCorners];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { MolangVariableMap, system, TicksPerSecond } from "@minecraft/server";
|
||||
import { OutlineRenderer } from "./OutlineRenderer.js";
|
||||
|
||||
const DEFAULT_TIMING = Object.freeze({ drawIntervalTicks: 10, lifetimeTicks: 20 });
|
||||
|
||||
export class ParticleOutlineRenderer extends OutlineRenderer {
|
||||
#outlineParticle = "construct:outline";
|
||||
|
||||
#drawIntervalTicks;
|
||||
#lifetimeSeconds;
|
||||
#runner;
|
||||
|
||||
constructor(dimension, min, max, timing) {
|
||||
super(dimension, min, max);
|
||||
const { drawIntervalTicks, lifetimeTicks } = { ...DEFAULT_TIMING, ...timing };
|
||||
this.#drawIntervalTicks = drawIntervalTicks;
|
||||
this.#lifetimeSeconds = lifetimeTicks / TicksPerSecond;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#runner = system.runInterval(() => this.#draw(), this.#drawIntervalTicks);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.#runner)
|
||||
return;
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
}
|
||||
|
||||
#draw() {
|
||||
if (this.drawCorners) {
|
||||
for (const corner of this.corners)
|
||||
this.#spawn(corner, this.cornerColor);
|
||||
}
|
||||
if (this.drawEdges) {
|
||||
let index = 0;
|
||||
for (const point of this.cuboid.edgePoints()) {
|
||||
const color = this.edgeColors[index++ % this.edgeColors.length];
|
||||
this.#spawn(point, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spawn(location, color) {
|
||||
const molang = new MolangVariableMap();
|
||||
molang.setColorRGBA("dot_color", color);
|
||||
molang.setFloat("lifetime", this.#lifetimeSeconds);
|
||||
try {
|
||||
this.dimension.spawnParticle(this.#outlineParticle, location, molang);
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { StructureNotFoundError } from "../../Errors/StructureNotFoundError";
|
||||
import { Cuboid } from "./Cuboid";
|
||||
import { createOutlineRenderer } from "./createOutlineRenderer.js";
|
||||
|
||||
export class StructureOutliner {
|
||||
instance;
|
||||
#renderer;
|
||||
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.#renderer?.stop();
|
||||
this.#renderer = void 0;
|
||||
if (!this.instance.isEnabled())
|
||||
return;
|
||||
const view = this.#readView();
|
||||
if (!view)
|
||||
return;
|
||||
this.#renderer = this.#createRenderer(view);
|
||||
if (view.wholeStructureCorners)
|
||||
this.#renderer.addStandaloneCorners(view.wholeStructureCorners);
|
||||
this.#renderer.start();
|
||||
}
|
||||
|
||||
#readView() {
|
||||
try {
|
||||
const dimension = this.instance.getDimension();
|
||||
const bounds = this.instance.getBounds();
|
||||
const min = this.instance.toGlobalCoords(bounds.min);
|
||||
const max = this.instance.toGlobalCoords(bounds.max);
|
||||
if (!this.instance.hasLayerSelected())
|
||||
return { dimension, min, max };
|
||||
const layer = this.instance.getLayerBounds(this.instance.getLayer());
|
||||
return {
|
||||
dimension,
|
||||
min: this.instance.toGlobalCoords(layer.min),
|
||||
max: this.instance.toGlobalCoords(layer.max),
|
||||
wholeStructureCorners: Cuboid.cornersOf(min, max)
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof StructureNotFoundError)
|
||||
return void 0;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
#createRenderer(view) {
|
||||
return createOutlineRenderer(this.instance.getRenderMode(), view.dimension, view.min, view.max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { renderProfileOf } from "../../Enums/RenderMode";
|
||||
import { HybridOutlineRenderer } from "./HybridOutlineRenderer.js";
|
||||
import { ParticleOutlineRenderer } from "./ParticleOutlineRenderer.js";
|
||||
|
||||
export function createOutlineRenderer(renderMode, dimension, min, max, particleTiming) {
|
||||
const profile = renderProfileOf(renderMode);
|
||||
const Renderer = profile.hybridOutline ? HybridOutlineRenderer : ParticleOutlineRenderer;
|
||||
return new Renderer(dimension, min, max, particleTiming);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { system } from "@minecraft/server";
|
||||
import { BlockVerificationLevel } from "../../Enums/BlockVerificationLevel";
|
||||
import { renderProfileOf } from "../../Enums/RenderMode";
|
||||
import { RefreshRate } from "../../Verifier/RefreshRate";
|
||||
import { Vector } from "../../../lib/Vector";
|
||||
import { DebugBoxLayer } from "./DebugBoxLayer";
|
||||
import { PreviewParticleLayer } from "./PreviewParticleLayer";
|
||||
import { PreviewSweep } from "./PreviewSweep";
|
||||
|
||||
const RENDER_INTERVAL_TICKS = 1;
|
||||
|
||||
export class BlockPreviewRenderer {
|
||||
instance;
|
||||
|
||||
#profile;
|
||||
#sweep = new PreviewSweep();
|
||||
#boxes = new DebugBoxLayer();
|
||||
#particles;
|
||||
#runner;
|
||||
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
this.#applyRenderMode();
|
||||
}
|
||||
|
||||
refresh(isPriority = false) {
|
||||
this.#stop();
|
||||
this.#applyRenderMode();
|
||||
if (!this.instance.isEnabled() || !this.instance.options.verifier.isEnabled)
|
||||
return;
|
||||
if (isPriority)
|
||||
this.#sweep.arm();
|
||||
this.#runner = system.runInterval(() => this.#renderNextSlice(), RENDER_INTERVAL_TICKS);
|
||||
}
|
||||
|
||||
renderBlockAt(location) {
|
||||
if (!this.#runner)
|
||||
return;
|
||||
const frame = this.#openFrame();
|
||||
if (frame)
|
||||
this.#drawCell(frame, location);
|
||||
}
|
||||
|
||||
#applyRenderMode() {
|
||||
this.#profile = renderProfileOf(this.instance.options.renderMode);
|
||||
this.#particles = new PreviewParticleLayer(this.#profile.blockPreview);
|
||||
}
|
||||
|
||||
#stop() {
|
||||
this.#boxes.clear();
|
||||
if (!this.#runner)
|
||||
return;
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
this.#sweep.reset();
|
||||
}
|
||||
|
||||
#renderNextSlice() {
|
||||
const frame = this.#openFrame();
|
||||
if (!frame)
|
||||
return;
|
||||
const refreshSeconds = this.instance.options.verifier.refreshSeconds;
|
||||
for (const location of this.#sweep.locations(frame.bounds, frame.volume, refreshSeconds))
|
||||
this.#drawCell(frame, location);
|
||||
}
|
||||
|
||||
#openFrame() {
|
||||
const bounds = this.instance.getActiveBounds();
|
||||
const volume = Vector.volume(bounds.min, bounds.max);
|
||||
if (volume <= 0)
|
||||
return void 0;
|
||||
const grid = this.instance.verifier.getCompletedGrid();
|
||||
if (!grid?.matchesBounds(bounds))
|
||||
return void 0;
|
||||
this.#boxes.retarget(bounds);
|
||||
return {
|
||||
bounds,
|
||||
volume,
|
||||
grid,
|
||||
dimension: this.instance.getDimension(),
|
||||
lifetimeSeconds: RefreshRate.cycleSeconds(volume, this.instance.options.verifier.refreshSeconds)
|
||||
};
|
||||
}
|
||||
|
||||
#drawCell(frame, location) {
|
||||
const verificationLevel = frame.grid.get(location);
|
||||
const origin = this.instance.toGlobalCoords(location);
|
||||
if (this.#profile.debugMarkers) {
|
||||
const index = frame.grid.indexOf(location);
|
||||
this.#boxes.draw(index, frame.dimension, origin, verificationLevel);
|
||||
}
|
||||
if (!this.#profile.particleOverlays)
|
||||
return;
|
||||
if (verificationLevel === BlockVerificationLevel.Unknown || verificationLevel === BlockVerificationLevel.Air)
|
||||
return;
|
||||
this.#particles.draw({
|
||||
dimension: frame.dimension,
|
||||
origin,
|
||||
block: this.instance.getBlock(location),
|
||||
verificationLevel,
|
||||
lifetimeSeconds: frame.lifetimeSeconds,
|
||||
occlusionMask: frame.grid.occlusionMaskAt(location)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { DebugBox, debugDrawer } from "@minecraft/debug-utilities";
|
||||
import { boxStyleOf } from "../VerificationStyle";
|
||||
|
||||
export class DebugBoxLayer {
|
||||
#boxes = new Map();
|
||||
#bounds;
|
||||
|
||||
retarget(bounds) {
|
||||
if (this.#matchesBounds(bounds))
|
||||
return;
|
||||
this.clear();
|
||||
this.#bounds = {
|
||||
min: { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z },
|
||||
max: { x: bounds.max.x, y: bounds.max.y, z: bounds.max.z }
|
||||
};
|
||||
}
|
||||
|
||||
draw(index, dimension, origin, verificationLevel) {
|
||||
if (index === -1)
|
||||
return;
|
||||
const style = boxStyleOf(verificationLevel);
|
||||
const existing = this.#boxes.get(index);
|
||||
if (!style)
|
||||
this.#hide(existing);
|
||||
else if (existing)
|
||||
this.#restyle(existing, style);
|
||||
else
|
||||
this.#boxes.set(index, this.#create(dimension, origin, style));
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const entry of this.#boxes.values())
|
||||
this.#hide(entry);
|
||||
this.#boxes.clear();
|
||||
this.#bounds = void 0;
|
||||
}
|
||||
|
||||
#matchesBounds(bounds) {
|
||||
return this.#bounds !== void 0
|
||||
&& this.#bounds.min.x === bounds.min.x && this.#bounds.max.x === bounds.max.x
|
||||
&& this.#bounds.min.y === bounds.min.y && this.#bounds.max.y === bounds.max.y
|
||||
&& this.#bounds.min.z === bounds.min.z && this.#bounds.max.z === bounds.max.z;
|
||||
}
|
||||
|
||||
#hide(entry) {
|
||||
if (!entry?.isVisible)
|
||||
return;
|
||||
entry.box.remove();
|
||||
entry.isVisible = false;
|
||||
}
|
||||
|
||||
#restyle(entry, style) {
|
||||
entry.box.color = style.color;
|
||||
entry.box.scale = style.scale;
|
||||
if (entry.isVisible)
|
||||
return;
|
||||
debugDrawer.addShape(entry.box);
|
||||
entry.isVisible = true;
|
||||
}
|
||||
|
||||
#create(dimension, origin, style) {
|
||||
const box = new DebugBox({
|
||||
dimension,
|
||||
x: origin.x + 0.5,
|
||||
y: origin.y + 0.5,
|
||||
z: origin.z + 0.5
|
||||
});
|
||||
box.color = style.color;
|
||||
box.scale = style.scale;
|
||||
debugDrawer.addShape(box);
|
||||
return { box, isVisible: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DebugBox, debugDrawer } from "@minecraft/debug-utilities";
|
||||
import { boxStyleOf } from "../VerificationStyle";
|
||||
|
||||
export function drawExpiringDebugBox(dimension, origin, verificationLevel, lifetimeSeconds) {
|
||||
const style = boxStyleOf(verificationLevel);
|
||||
if (!style)
|
||||
return;
|
||||
const box = new DebugBox({
|
||||
dimension,
|
||||
x: origin.x + 0.5,
|
||||
y: origin.y + 0.5,
|
||||
z: origin.z + 0.5
|
||||
});
|
||||
box.color = style.color;
|
||||
box.scale = style.scale;
|
||||
box.timeLeft = lifetimeSeconds;
|
||||
debugDrawer.addShape(box);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { MolangVariableMap } from "@minecraft/server";
|
||||
import { DEBUG_CONFIG } from "../../../consts";
|
||||
|
||||
const BLOCK_CENTER = 0.5;
|
||||
const PIXELS_PER_BLOCK = 16;
|
||||
|
||||
export class FaceParticleSpawner {
|
||||
#molang = new MolangVariableMap();
|
||||
#location = { x: 0, y: 0, z: 0 };
|
||||
#normal = { x: 0, y: 0, z: 0 };
|
||||
#size = { x: 0, y: 0, z: 0 };
|
||||
#uv = { x: 0, y: 0, z: 0 };
|
||||
#uvSize = { x: 0, y: 0, z: 0 };
|
||||
|
||||
#dimension;
|
||||
#origin;
|
||||
#color;
|
||||
|
||||
startBlock(dimension, origin, lifetimeSeconds) {
|
||||
this.#dimension = dimension;
|
||||
this.#origin = origin;
|
||||
this.#color = void 0;
|
||||
this.#molang.setFloat("lifetime", lifetimeSeconds);
|
||||
}
|
||||
|
||||
spawnFace(face, color, material, scale) {
|
||||
this.#setColor(color);
|
||||
this.#setGeometry(face, scale);
|
||||
this.#setUv(face);
|
||||
try {
|
||||
this.#dimension.spawnParticle(`construct:block_face_${material}`, this.#location, this.#molang);
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
|
||||
#setColor(color) {
|
||||
if (color === this.#color)
|
||||
return;
|
||||
this.#molang.setColorRGBA("face_color", color);
|
||||
this.#color = color;
|
||||
}
|
||||
|
||||
#setGeometry(face, scale) {
|
||||
this.#location.x = this.#origin.x + BLOCK_CENTER + (face.center[0] / PIXELS_PER_BLOCK - BLOCK_CENTER) * scale;
|
||||
this.#location.y = this.#origin.y + BLOCK_CENTER + (face.center[1] / PIXELS_PER_BLOCK - BLOCK_CENTER) * scale;
|
||||
this.#location.z = this.#origin.z + BLOCK_CENTER + (face.center[2] / PIXELS_PER_BLOCK - BLOCK_CENTER) * scale;
|
||||
|
||||
this.#normal.x = face.facing[0];
|
||||
this.#normal.y = face.facing[1];
|
||||
this.#normal.z = face.facing[2];
|
||||
|
||||
this.#size.x = (face.width / PIXELS_PER_BLOCK) * 0.5 * scale;
|
||||
this.#size.y = (face.height / PIXELS_PER_BLOCK) * 0.5 * scale;
|
||||
this.#size.z = face.roll;
|
||||
|
||||
this.#molang.setVector3("normal", this.#normal);
|
||||
this.#molang.setVector3("size", this.#size);
|
||||
}
|
||||
|
||||
#setUv(face) {
|
||||
this.#uv.x = DEBUG_CONFIG.enable ? DEBUG_CONFIG.render_all_textures_as.u : face.uv.x;
|
||||
this.#uv.y = DEBUG_CONFIG.enable ? DEBUG_CONFIG.render_all_textures_as.v : face.uv.y;
|
||||
this.#uvSize.x = face.uv.w;
|
||||
this.#uvSize.y = face.uv.h;
|
||||
this.#molang.setVector3("uv", this.#uv);
|
||||
this.#molang.setVector3("uv_size", this.#uvSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { BlockVerificationLevel } from "../../Enums/BlockVerificationLevel";
|
||||
import { CellFlags } from "../../Verifier/CellFlags";
|
||||
import { CubeFaceLibrary } from "../model/CubeFaceLibrary";
|
||||
import { blockModelResolver } from "../model/BlockModelResolver";
|
||||
import { UNRESOLVED_FACE_STYLE, WATER_MATERIAL, particleStyleOf } from "../VerificationStyle";
|
||||
import { FaceParticleSpawner } from "./FaceParticleSpawner";
|
||||
|
||||
export class PreviewParticleLayer {
|
||||
#showsBlockPreview;
|
||||
#spawner = new FaceParticleSpawner();
|
||||
|
||||
constructor(showsBlockPreview) {
|
||||
this.#showsBlockPreview = showsBlockPreview;
|
||||
}
|
||||
|
||||
draw(cell) {
|
||||
const style = particleStyleOf(cell.verificationLevel, this.#showsBlockPreview);
|
||||
if (!style || !cell.block)
|
||||
return;
|
||||
const culls = this.#cullMasks(cell);
|
||||
this.#spawner.startBlock(cell.dimension, cell.origin, cell.lifetimeSeconds);
|
||||
for (const layer of this.#faceLayers(cell, style.material))
|
||||
this.#drawLayer(cell, layer, style, culls);
|
||||
}
|
||||
|
||||
#cullMasks(cell) {
|
||||
if (this.#isMarkerCube(cell.verificationLevel))
|
||||
return { opaque: 0, marker: 0 };
|
||||
const opaque = CellFlags.opaqueMask(cell.occlusionMask);
|
||||
return { opaque, marker: opaque | CellFlags.markerMask(cell.occlusionMask) };
|
||||
}
|
||||
|
||||
#faceLayers(cell, material) {
|
||||
if (!this.#showsBlockPreview || cell.verificationLevel !== BlockVerificationLevel.Missing)
|
||||
return [{ faces: CubeFaceLibrary.createPlainCubeFaces(), material, isMarker: true }];
|
||||
const layers = [{
|
||||
faces: blockModelResolver.facesOf(cell.block),
|
||||
material: blockModelResolver.isWater(cell.block) ? WATER_MATERIAL : material
|
||||
}];
|
||||
if (cell.block.isWaterlogged)
|
||||
layers.push({ faces: blockModelResolver.waterloggedFaces(), material: WATER_MATERIAL });
|
||||
return layers;
|
||||
}
|
||||
|
||||
#drawLayer(cell, layer, style, culls) {
|
||||
const layerCull = layer.isMarker ? culls.marker : culls.opaque;
|
||||
for (const face of layer.faces) {
|
||||
const unresolved = face.missing === true;
|
||||
if (this.#isCulled(face, unresolved ? culls.marker : layerCull))
|
||||
continue;
|
||||
try {
|
||||
if (unresolved)
|
||||
this.#spawner.spawnFace(face, UNRESOLVED_FACE_STYLE.color, UNRESOLVED_FACE_STYLE.material, style.scale);
|
||||
else
|
||||
this.#spawner.spawnFace(face, style.color, layer.material, style.scale);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to render face for block ${cell.block.typeId} at ${JSON.stringify(cell.origin)} with verification level ${cell.verificationLevel}:`, error, error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#isCulled(face, cullMask) {
|
||||
return face.cull !== void 0 && ((cullMask >> face.cull) & 1) === 1;
|
||||
}
|
||||
|
||||
#isMarkerCube(verificationLevel) {
|
||||
return verificationLevel === BlockVerificationLevel.Missing && !this.#showsBlockPreview;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { BlockBudget } from "../../Verifier/BlockBudget";
|
||||
import { PriorityPass } from "../../Verifier/PriorityPass";
|
||||
import { RefreshRate } from "../../Verifier/RefreshRate";
|
||||
|
||||
export class PreviewSweep {
|
||||
#cursor = 0;
|
||||
#budget = new BlockBudget();
|
||||
#priorityPass = new PriorityPass();
|
||||
|
||||
arm() {
|
||||
this.#priorityPass.arm();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.#cursor = 0;
|
||||
this.#budget.clear();
|
||||
}
|
||||
|
||||
*locations(bounds, volume, refreshSeconds) {
|
||||
this.#budget.credit(this.#blocksPerTick(volume, refreshSeconds));
|
||||
if (this.#budget.isExhausted())
|
||||
return;
|
||||
if (this.#cursor >= volume)
|
||||
this.#cursor = 0;
|
||||
while (this.#cursor < volume && !this.#budget.isExhausted()) {
|
||||
yield locationAt(bounds, this.#cursor);
|
||||
this.#budget.spend(1);
|
||||
this.#cursor++;
|
||||
}
|
||||
if (this.#cursor >= volume) {
|
||||
this.#cursor = 0;
|
||||
this.#priorityPass.disarm();
|
||||
}
|
||||
}
|
||||
|
||||
#blocksPerTick(volume, refreshSeconds) {
|
||||
if (this.#priorityPass.isArmed())
|
||||
return RefreshRate.priorityBlocksPerTick();
|
||||
return RefreshRate.blocksPerTick(volume, refreshSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
function locationAt(bounds, index) {
|
||||
const width = bounds.max.x - bounds.min.x;
|
||||
const depth = bounds.max.z - bounds.min.z;
|
||||
const layerArea = width * depth;
|
||||
const y = bounds.min.y + Math.floor(index / layerArea);
|
||||
const withinLayer = index % layerArea;
|
||||
if (bounds.max.x < bounds.max.z) {
|
||||
return {
|
||||
x: bounds.min.x + (withinLayer % width),
|
||||
y,
|
||||
z: bounds.min.z + Math.floor(withinLayer / width)
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: bounds.min.x + Math.floor(withinLayer / depth),
|
||||
y,
|
||||
z: bounds.min.z + (withinLayer % depth)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user