Start working at 1.2.0
/ build (push) Has been cancelled

This commit is contained in:
2026-08-26 09:54:39 +08:00 Unverified
parent 36f2dd0685
commit 136d5ffa3b
159 changed files with 60793 additions and 617 deletions
@@ -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)
};
}