Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ node_modules/
dist/
cache/
build/
__pycache__/
*.pyc

# OS & Editor Artifacts
.DS_Store
Expand Down
6 changes: 6 additions & 0 deletions textures/block/water_flow.png.mcmeta
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"animation": {
"frametime": 1,
"interpolate": true
}
}
696 changes: 696 additions & 0 deletions textures/block/water_flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions textures/block/water_still.png.mcmeta
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"animation": {
"frametime": 2,
"interpolate": true
}
}
54 changes: 54 additions & 0 deletions textures/block/water_still.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions tools/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@ export async function buildResourcePack(targetRes = 512, options = {}) {
const relLog = path.join(relDir === "." ? "" : relDir, stem + ".png").replace(/\\/g, "/");

const isOverlay = stem.endsWith("_overlay") || file.relPath.includes("_overlay");
if (pbr && !isOverlay) {
const isFluid = stem.startsWith("water_") || stem.startsWith("lava_") || stem === "water" || stem === "lava";
if (pbr && !isOverlay && !isFluid) {
const pbrMaps = generatePbrMaps(stem, rendered.pixels, targetRes, targetRes);
fs.writeFileSync(path.join(targetDir, stem + "_n.png"), pbrMaps.normalMap);
fs.writeFileSync(path.join(targetDir, stem + "_s.png"), pbrMaps.specularMap);
Expand Down Expand Up @@ -336,7 +337,7 @@ export async function buildResourcePack(targetRes = 512, options = {}) {
aliasesToCopy.push({ src: destPng, dest: path.join(targetDir, "grass_2.png"), label: grassRel, stem });
}

const pbrSuffix = pbr && !isOverlay ? " (+LabPBR _n/_s)" : "";
const pbrSuffix = pbr && !isOverlay && !isFluid ? " (+LabPBR _n/_s)" : "";
return " ✓ " + relLog + pbrSuffix;
} else {
// Direct mirror for non-SVG texture assets (.mcmeta, .png, .json)
Expand Down
15 changes: 13 additions & 2 deletions tools/lib/vector-linter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,19 @@ export function lintSvgContent(svgContent, filePath = "<inline>") {
} else {
const rawViewBox = viewBoxMatch[2];
const normalizedViewBox = rawViewBox.trim().replace(/\s+/g, " ");
if (normalizedViewBox !== REQUIRED_VIEWBOX) {
errors.push(`${filePath}: Invalid viewBox "${rawViewBox}" (must be exactly "${REQUIRED_VIEWBOX}")`);
const vbParts = normalizedViewBox.split(" ");
const isValidMultiFrame =
vbParts.length === 4 &&
vbParts[0] === "0" &&
vbParts[1] === "0" &&
vbParts[2] === "512" &&
!isNaN(Number(vbParts[3])) &&
Number.isInteger(Number(vbParts[3])) &&
Number(vbParts[3]) > 0 &&
Number(vbParts[3]) % 512 === 0;

if (!isValidMultiFrame) {
errors.push(`${filePath}: Invalid viewBox "${rawViewBox}" (must be exactly "${REQUIRED_VIEWBOX}" or a vertical multi-frame strip "0 0 512 (512 * N)")`);
}
}

Expand Down
3 changes: 2 additions & 1 deletion tools/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,11 @@ def expected_assets(resolution, pbr=False):
if source.suffix == ".svg":
stem = source.stem
stems = [f"{dye}_{stem.removesuffix('.template')}" for dye in dyes] if stem.endswith(".template") else [stem]
is_fluid = stem.startswith(("water", "lava"))
for output in stems:
name = prefix + output + ".png"
textures[name] = svg_dimensions(source, resolution)
if pbr and (stem.endswith(".template") or "_overlay" not in relative.as_posix()):
if pbr and not is_fluid and (stem.endswith(".template") or "_overlay" not in relative.as_posix()):
for suffix in ("_n.png", "_s.png"):
textures[prefix + output + suffix] = (resolution, resolution)
if not stem.endswith(".template"):
Expand Down
51 changes: 30 additions & 21 deletions tools/test-tiling.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -317,28 +317,37 @@ export function analyzeSeams(renderedImage, testAxes = ["x", "y"], tolerance = 0
}
}

// 2. Vertical Seam (Y-axis wrap: Row 0 vs Row H-1)
// 2. Vertical Seam (Y-axis wrap: per-frame top vs bottom for multi-frame strips)
if (testAxes.includes("y")) {
for (let x = 0; x < W; x++) {
const idx0 = (0 * W + x) * 4;
const idx1 = ((H - 1) * W + x) * 4;

const dr = Math.abs(px[idx0 + 0] - px[idx1 + 0]);
const dg = Math.abs(px[idx0 + 1] - px[idx1 + 1]);
const db = Math.abs(px[idx0 + 2] - px[idx1 + 2]);
const da = Math.abs(px[idx0 + 3] - px[idx1 + 3]);
const delta = Math.max(dr, dg, db, da);

if (delta > tolerance) {
errors.push({
axis: "Y",
coord: x,
delta,
color0: toHex(px[idx0], px[idx0 + 1], px[idx0 + 2], px[idx0 + 3]),
color1: toHex(px[idx1], px[idx1 + 1], px[idx1 + 2], px[idx1 + 3]),
c0Raw: [px[idx0], px[idx0 + 1], px[idx0 + 2], px[idx0 + 3]],
c1Raw: [px[idx1], px[idx1 + 1], px[idx1 + 2], px[idx1 + 3]]
});
const frameHeight = (H > W && H % W === 0) ? W : H;
const frameCount = Math.floor(H / frameHeight);

for (let f = 0; f < frameCount; f++) {
const topRow = f * frameHeight;
const bottomRow = (f + 1) * frameHeight - 1;

for (let x = 0; x < W; x++) {
const idx0 = (topRow * W + x) * 4;
const idx1 = (bottomRow * W + x) * 4;

const dr = Math.abs(px[idx0 + 0] - px[idx1 + 0]);
const dg = Math.abs(px[idx0 + 1] - px[idx1 + 1]);
const db = Math.abs(px[idx0 + 2] - px[idx1 + 2]);
const da = Math.abs(px[idx0 + 3] - px[idx1 + 3]);
const delta = Math.max(dr, dg, db, da);

if (delta > tolerance) {
errors.push({
axis: "Y",
frame: f,
coord: x,
delta,
color0: toHex(px[idx0], px[idx0 + 1], px[idx0 + 2], px[idx0 + 3]),
color1: toHex(px[idx1], px[idx1 + 1], px[idx1 + 2], px[idx1 + 3]),
c0Raw: [px[idx0], px[idx0 + 1], px[idx0 + 2], px[idx0 + 3]],
c1Raw: [px[idx1], px[idx1 + 1], px[idx1 + 2], px[idx1 + 3]]
});
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions tools/test/vector-linter.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ console.log("\n[Suite 2] ViewBox Invariant Verification");
assertEqual(validRes.ok, true, 'Valid viewBox "0 0 512 512" passes');
assertEqual(validRes.errors.length, 0, "No errors on valid viewBox");

const validMultiFrame16 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 8192" width="512" height="8192"><rect width="512" height="8192" fill="#000"/></svg>`;
const resMulti16 = lintSvgContent(validMultiFrame16, "water_still.svg");
assertEqual(resMulti16.ok, true, 'Multi-frame vertical strip viewBox "0 0 512 8192" passes');
assertEqual(resMulti16.errors.length, 0, "No errors on multi-frame 16x strip viewBox");

const validMultiFrame3 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 1536" width="512" height="1536"><rect width="512" height="1536" fill="#000"/></svg>`;
const resMulti3 = lintSvgContent(validMultiFrame3, "water.svg");
assertEqual(resMulti3.ok, true, 'Multi-frame vertical strip viewBox "0 0 512 1536" passes');
assertEqual(resMulti3.errors.length, 0, "No errors on multi-frame 3x strip viewBox");

const missingViewBox = `<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512"><rect width="512" height="512" fill="#000"/></svg>`;
const missingRes = lintSvgContent(missingViewBox, "missing_viewbox.svg");
assertEqual(missingRes.ok, false, "Missing viewBox is rejected");
Expand Down
Loading