Only shown faces drawn, UI redo, new atlas script

This commit is contained in:
Lucas Dower 2022-01-15 22:39:52 +00:00
parent 97f2ae47fc
commit e2dce3f5a0
22 changed files with 1100 additions and 679 deletions

View File

@ -8,66 +8,18 @@
</head>
<body>
<div id="main">
<canvas id="canvas"></canvas>
<div class="toolbar-container">
<div class="toolbar-container glass pill-left pill-right">
<div class="toolbar-item pill-left toolbar-item-1">
<div class="input-group">
<button id="buttonChooseFile" class="input-group-item pill-left">
Choose file
</button>
<input id="inputFile" type="text"
class="input-group-item input-group-item-dominant pill-right text-field cursor-pointer" value=""
readonly></input>
</div>
</div>
<div class="toolbar-item toolbar-item-2">
<div id="groupVoxelise" class="input-group transparent">
<div class="input-group-item pill-left">Voxel size</div>
<input id="inputVoxelSize" type="number" class="input-group-item input-group-item-dominant text-field"
value="0.1" min="0.01" step="0.01" disabled>
</input>
<button id="buttonVoxelise" class="input-group-item pill-right bg-primary cursor-pointer" disabled>
Voxelise
</button>
</div>
</div>
<div class="toolbar-item pill-right toolbar-item-3">
<div id="groupExport" class="input-group transparent">
<button id="buttonSchematic" class="input-group-item input-group-item-dominant pill-left bg-secondary"
disabled>
Export .schematic
</button>
<button id="buttonLitematic" class="input-group-item input-group-item-dominant pill-right bg-secondary"
disabled>
Export .litematic
</button>
</button>
</div>
</div>
</div>
<div class="column-properties" id="properties">
</div>
</div>
<div id="toast" class="status-container pill-left pill-right glass toast hide"></div>
<div id="modal" class="pill-left pill-right glass modal" hidden>
<div class="content">
<div id="textModal" class="modalTop"></div>
<div class="modalBottom">
<button id="buttonModalAction" class="button pill bg-secondary"></button>
<button id="buttonModalClose" class="button pill bg-primary">Close</button>
</div>
<div class="column-canvas">
<canvas id="canvas"></canvas>
</div>
</div>
</body>
<script src="./src/vendor/jquery-3.6.0.min.js"></script>
<script src="./src/vendor/jquery-3.3.1.slim.min.js"></script>
<script src="./src/vendor/popper.min.js"></script>
<script>
require("./dist/client.js");
require("./dist/client.js");
</script>
</html>

View File

@ -25,10 +25,13 @@
"devDependencies": {
"@types/jquery": "^3.5.6",
"@types/pngjs": "^6.0.1",
"@types/prompt": "^1.1.2",
"adm-zip": "^0.5.9",
"chalk": "^4.1.2",
"electron": "^13.1.4",
"electron-packager": "^15.2.0",
"images": "^3.2.3",
"prompt": "^1.2.1",
"ts-node": "^10.1.0",
"typescript": "^4.3.5"
},

View File

@ -3168,9 +3168,9 @@
}
},
"colour": {
"r": 0.3836203022875817,
"g": 0.1345639297385621,
"b": 0.17588848039215685
"r": 0.3899111519607843,
"g": 0.1316840277777778,
"b": 0.16969975490196076
}
},
{
@ -3688,9 +3688,9 @@
}
},
"colour": {
"r": 0.2423355800653595,
"g": 0.1776858660130719,
"b": 0.0960375816993464
"r": 0.24634906045751634,
"g": 0.18068831699346408,
"b": 0.09762050653594771
}
},
{
@ -12320,9 +12320,9 @@
}
},
"colour": {
"r": 0.4506280637254902,
"g": 0.33268995098039217,
"b": 0.19045649509803922
"r": 0.4506587009803922,
"g": 0.3327205882352941,
"b": 0.19042585784313726
}
},
{
@ -12840,9 +12840,9 @@
}
},
"colour": {
"r": 0.3389297385620915,
"g": 0.2565921160130719,
"b": 0.15924734477124183
"r": 0.27654207516339874,
"g": 0.20654105392156863,
"b": 0.12396343954248368
}
},
{
@ -12892,9 +12892,9 @@
}
},
"colour": {
"r": 0.37919730392156864,
"g": 0.2981770833333333,
"b": 0.19424019607843138
"r": 0.28561580882352944,
"g": 0.22310049019607844,
"b": 0.14131433823529413
}
},
{
@ -13724,9 +13724,9 @@
}
},
"colour": {
"r": 0.2267391748366013,
"g": 0.28967626633986926,
"b": 0.33822406045751635
"r": 0.2215921160130719,
"g": 0.297172181372549,
"b": 0.34665951797385625
}
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -1,36 +1,30 @@
import { buildUI, registerUI, Group, Component, setEnabled } from "./ui/layout";
import { Exporter, Litematic, Schematic } from "./schematic";
import { VoxelManager } from "./voxel_manager";
import { Toast, ToastStyle } from "./ui/toast";
import { ToggleableGroup } from "./ui/group";
import { Renderer } from "./renderer";
import { Modal } from "./ui/modal";
import { Mesh } from "./mesh";
import { remote } from "electron";
import path from "path";
import { SliderElement } from "./ui/elements/slider";
import { ComboBoxElement } from "./ui/elements/combobox";
import { FileInputElement } from "./ui/elements/file_input";
import { remote } from "electron";
import fs from "fs";
import { ButtonElement } from "./ui/elements/button";
import { LabelElement } from "./ui/elements/label";
interface ReturnStatus {
message: string,
style: ToastStyle,
//style: ToastStyle,
error?: unknown
}
export enum Action {
Load,
Voxelise,
ExportSchematic,
ExportLitematic,
ConfirmExport
}
export class AppContext {
public ambientOcclusion: boolean;
private _voxelSize = 1.0;
private _gl: WebGLRenderingContext;
private _loadedMesh?: Mesh;
private _groupVoxelise: ToggleableGroup;
private _groupExport: ToggleableGroup;
private _actionMap = new Map<Action, (() => ReturnStatus | void)>();
private _ui: Group[];
private static _instance: AppContext;
@ -39,146 +33,186 @@ export class AppContext {
}
private constructor() {
const gl = (<HTMLCanvasElement>$("#canvas").get(0)).getContext("webgl");
this.ambientOcclusion = true;
this._ui = [
{
label: "Input",
components: [
{
label: new LabelElement("label1", "Wavefront .obj file"),
type: new FileInputElement("objFile", "obj")
},
{
label: new LabelElement("label2", "Material .mtl file"),
type: new FileInputElement("mtlFile", "mtl")
},
],
submitButton: new ButtonElement("loadMesh", "Load mesh", () => { this._load(); })
},
{
label: "Simplify",
components: [
{
label: new LabelElement("label3", "Ratio"),
type: new SliderElement("ratio", 0.0, 1.0, 0.01, 0.5) },
],
submitButton: new ButtonElement("simplifyMesh", "Simplify mesh", () => { })
},
{
label: "Build",
components: [
{
label: new LabelElement("label4", "Voxel size"),
type: new SliderElement("voxelSize", 0.01, 1.0, 0.01, 0.1)
},
{
label: new LabelElement("label5", "Ambient occlusion"),
type: new ComboBoxElement("ambientOcclusion", [
{ id: "on", displayText: "On (recommended)" },
{ id: "off", displayText: "Off (faster)" },
])
},
],
submitButton: new ButtonElement("voxeliseMesh", "Voxelise mesh", () => { this._voxelise(); })
},
{
label: "Palette",
components: [
{
label: new LabelElement("label6", "Block palette"),
type: new ComboBoxElement("blockPalette", [])
},
{
label: new LabelElement("label7", "Choice method"),
type: new ComboBoxElement("choiceMethod", [])
},
{
label: new LabelElement("label8", "Dithering"),
type: new ComboBoxElement("dithering", [])
},
],
submitButton: new ButtonElement("assignBlocks", "Assign blocks", () => { this._export(); })
},
{
label: "Export",
components: [
{
label: new LabelElement("label9", "File format"),
type: new ComboBoxElement("fileFormat",
[
{ id: "litematic", displayText: "Litematic" },
{ id: "schematic", displayText: "Schematic" },
])
},
],
submitButton: new ButtonElement("exportStructure", "Export structure", () => { this._export(); })
}
]
buildUI(this._ui);
registerUI(this._ui);
setEnabled(this._ui[1], false);
setEnabled(this._ui[2], false);
setEnabled(this._ui[3], false);
setEnabled(this._ui[4], false);
const gl = (<HTMLCanvasElement>document.getElementById('canvas')).getContext("webgl");
if (!gl) {
throw Error("Could not load WebGL context");
}
this._gl = gl;
this._actionMap.set(Action.Load, () => { return this._load(); });
this._actionMap.set(Action.Voxelise, () => { return this._voxelise(); });
this._actionMap.set(Action.ExportSchematic, () => { return this._preexport(new Schematic()); });
this._actionMap.set(Action.ExportLitematic, () => { return this._preexport(new Litematic()); });
this._groupVoxelise = new ToggleableGroup(["#inputVoxelSize", "#buttonVoxelise"], "#groupVoxelise", false);
this._groupExport = new ToggleableGroup(["#buttonSchematic", "#buttonLitematic"], "#groupExport", false);
}
public do(action: Action) {
const status = this._actionMap.get(action)!();
if (status) {
if (status.error) {
console.error(status.error);
}
Toast.show(status.message, status.style);
}
}
private _load(): ReturnStatus {
const files = remote.dialog.showOpenDialogSync({
title: "Load Waveform .obj file",
buttonLabel: "Load",
filters: [{
name: 'Waveform obj file',
extensions: ['obj']
}]
});
setEnabled(this._ui[2], false);
setEnabled(this._ui[4], false);
if (!files || files.length != 1) {
return { message: "A single .obj file must be selected to load", style: ToastStyle.Failure };
const objPath = (<FileInputElement>this._ui[0].components[0].type).getValue();
if (!fs.existsSync(objPath)) {
return { message: "Selected .obj cannot be found" };
}
const file = files[0];
if (!file.endsWith(".obj") && !file.endsWith(".OBJ")) {
return { message: "Files must be .obj format", style: ToastStyle.Failure };
const mtlPath = (<FileInputElement>this._ui[0].components[1].type).getValue();
if (!fs.existsSync(mtlPath)) {
return { message: "Selected .mtl cannot be found" };
}
const parsedPath = path.parse(file);
try {
this._loadedMesh = new Mesh(file, this._gl);
this._loadedMesh = new Mesh(objPath, this._gl);
this._loadedMesh.loadTextures(this._gl);
} catch (err: unknown) {
return { error: err, message: "Could not load mesh", style: ToastStyle.Failure };
return { error: err, message: "Could not load mesh" };
}
try {
const renderer = Renderer.Get;
renderer.clear();
renderer.registerMesh(this._loadedMesh);
renderer.compile();
} catch (err: unknown) {
return { message: "Could not render mesh", style: ToastStyle.Failure };
return { message: "Could not render mesh" };
}
$('#inputFile').prop("value", parsedPath.base);
this._groupVoxelise.setEnabled(true);
this._groupExport.setEnabled(false);
return { message: "Loaded successfully", style: ToastStyle.Success };
setEnabled(this._ui[2], true);
return { message: "Loaded successfully" };
}
private _voxelise(): ReturnStatus {
const newVoxelSize: number = parseFloat($("#inputVoxelSize").prop('value'));
if (newVoxelSize < 0.001) {
return { message: "Voxel size must be at least 0.001", style: ToastStyle.Failure };
}
this._voxelSize = newVoxelSize;
setEnabled(this._ui[4], false);
const voxelSize = (<SliderElement>this._ui[2].components[0].type).getValue();
const ambientOcclusion = (<ComboBoxElement>this._ui[2].components[1].type).getValue();
this.ambientOcclusion = ambientOcclusion === "on";
try {
const voxelManager = VoxelManager.Get;
voxelManager.setVoxelSize(this._voxelSize);
voxelManager.setVoxelSize(voxelSize);
voxelManager.voxeliseMesh(this._loadedMesh!);
const renderer = Renderer.Get;
renderer.clear();
renderer.registerVoxelMesh();
renderer.compile();
} catch (err: any) {
return { error: err, message: "Could not register voxel mesh", style: ToastStyle.Failure };
return { error: err, message: "Could not register voxel mesh" };//, style: ToastStyle.Failure };
}
this._groupExport.setEnabled(true);
return { message: "Voxelised successfully", style: ToastStyle.Success };
setEnabled(this._ui[4], true);
return { message: "Voxelised successfully" };//, style: ToastStyle.Success };
}
private _preexport(exporter: Exporter) {
const voxelManager = VoxelManager.Get;
console.log(voxelManager.min, voxelManager.max);
const schematicHeight = Math.ceil(voxelManager.max.z - voxelManager.min.z);
let message = "";
if (schematicHeight > 320) {
message += `Note, this structure is <b>${schematicHeight}</b> blocks tall, this is larger than the height of a Minecraft world (320 in 1.17, 256 in <=1.16). `;
}
const formatDisclaimer = exporter.getFormatDisclaimer();
if (formatDisclaimer) {
message += "\n" + formatDisclaimer;
}
if (message.length == 0) {
return this._export(exporter);
private _export(): ReturnStatus {
console.log(this._ui[4].components[0]);
const exportFormat = (<ComboBoxElement>this._ui[4].components[0].type).getValue();
let exporter: Exporter;
if (exportFormat === "schematic") {
exporter = new Schematic();
} else {
Modal.setButton("Export", () => { this._export(exporter); });
Modal.show(message);
exporter = new Litematic();
}
}
private _export(exporter: Exporter): ReturnStatus {
const filePath = remote.dialog.showSaveDialogSync({
title: "Save structure",
buttonLabel: "Save",
filters: [ exporter.getFormatFilter() ]
filters: [exporter.getFormatFilter()]
});
if (filePath === undefined) {
return { message: "Output cancelled", style: ToastStyle.Success };
return { message: "Output cancelled" };//, style: ToastStyle.Success };
}
try {
exporter.export(filePath);
} catch (err: unknown) {
return { error: err, message: "Failed to export", style: ToastStyle.Failure };
return { error: err, message: "Failed to export" };//, style: ToastStyle.Failure };
}
return { message: "Successfully exported", style: ToastStyle.Success };
return { message: "Successfully exported" };//, style: ToastStyle.Success };
}
public draw() {
Renderer.Get.draw();
}
}

View File

@ -59,7 +59,7 @@ export class SegmentedBuffer {
private _indicesInsertIndex: number = 0;
private _maxIndex: number = 0;
private _compiled: boolean = false;
private _sanityCheck: boolean = true;
private _sanityCheck: boolean = false;
constructor(bufferSize: number, attributes: Array<IndexedAttributed>) {
this._bufferSize = bufferSize;

View File

@ -1,5 +1,5 @@
import { Schematic, Litematic } from "./schematic";
import { Action, AppContext } from "./app_context";
import { AppContext } from "./app_context";
import { ArcballCamera } from "./camera";
import { MouseManager } from "./mouse";
@ -8,12 +8,13 @@ function AddEvent(htmlElementID: string, event: string, delegate: (e: any) => vo
}
// Register Events
const context = AppContext.Get;
/*
AddEvent("buttonChooseFile", "click", () => { context.do(Action.Load); });
AddEvent("inputFile", "click", () => { context.do(Action.Load); });
AddEvent("buttonVoxelise", "click", () => { context.do(Action.Voxelise); });
AddEvent("buttonSchematic", "click", async () => { context.do(Action.ExportSchematic); });
AddEvent("buttonLitematic", "click", async () => { context.do(Action.ExportLitematic); });
*/
const camera = ArcballCamera.Get;
AddEvent("canvas", "mousedown", (e) => { camera.onMouseDown(e); });
@ -25,6 +26,7 @@ AddEvent("canvas", "mousemove", (e) => { mouseManager.onMouseMove(e); });
// Begin draw loop
const context = AppContext.Get;
function render() {
context.draw();
requestAnimationFrame(render);

View File

@ -6,7 +6,7 @@ import url from "url";
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow: BrowserWindow;
function createWindow () {
function createWindow() {
// Create the browser window.
//const {width, height} = electron.screen.getPrimaryDisplay().workAreaSize;
const width = 1400;
@ -19,24 +19,24 @@ function createWindow () {
minWidth: 1280,
minHeight: 720,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
}
});
//mainWindow.removeMenu();
// Load index.html
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, '../index.html'),
protocol: 'file:',
slashes: true
}));
}));
// Open the DevTools.
//mainWindow.webContents.openDevTools();
// Emitted when the window is closed.
mainWindow.on('closed', function () {
app.quit();

View File

@ -13,6 +13,7 @@ import { Triangle } from "./triangle";
import { Mesh, FillMaterial, TextureMaterial, MaterialType } from "./mesh";
import { FaceInfo, BlockAtlas } from "./block_atlas";
import { AppConfig } from "./config"
import { AppContext } from "./app_context";
export class Renderer {
@ -42,7 +43,7 @@ export class Renderer {
}
private constructor() {
this._gl = (<HTMLCanvasElement>$("#canvas").get(0)).getContext("webgl")!;
this._gl = (<HTMLCanvasElement>document.getElementById('canvas')).getContext("webgl")!;
this._getNewBuffers();
this._setupOcclusions();
@ -139,9 +140,18 @@ export class Renderer {
return blankOcclusions;
}
private _registerVoxel(centre: Vector3, blockTexcoord: FaceInfo) {
private static readonly _faceNormals = [
new Vector3(1, 0, 0),
new Vector3(-1, 0, 0),
new Vector3(0, 1, 0),
new Vector3(0, -1, 0),
new Vector3(0, 0, 1),
new Vector3(0, 0, -1),
];
private _registerVoxel(centre: Vector3, blockTexcoord: FaceInfo) {
let occlusions: number[][];
if (AppConfig.AMBIENT_OCCLUSION_ENABLED) {
if (AppContext.Get.ambientOcclusion) {
occlusions = this._calculateOcclusions(centre);
} else {
occlusions = Renderer._getBlankOcclusions();
@ -168,7 +178,19 @@ export class Renderer {
}
}
this._registerVoxels.add(data);
for (let i = 0; i < 6; ++i)
{
if (!VoxelManager.Get.isVoxelAt(Vector3.add(centre, Renderer._faceNormals[i]))) {
this._registerVoxels.add({
position: data.position.slice(i * 12, (i+1) * 12),
occlusion: data.occlusion.slice(i * 16, (i+1) * 16),
normal: data.normal.slice(i * 12, (i+1) * 12),
indices: data.indices.slice(0, 6),
texcoord: data.texcoord.slice(i * 8, (i+1) * 8),
blockTexcoord: data.blockTexcoord.slice(i * 8, (i+1) * 8),
});
}
}
}
public registerTriangle(triangle: Triangle) {

46
src/ui/elements/button.ts Normal file
View File

@ -0,0 +1,46 @@
import { BaseUIElement } from "../layout";
import { assert } from "../../util";
export class ButtonElement extends BaseUIElement {
private _label: string;
private _onClick: () => void
public constructor(id: string, label: string, onClick: () => void) {
super(id);
this._label = label;
this._onClick = onClick;
this._isEnabled = true;
}
public generateHTML() {
return `
<div class="button" id="${this._id}">
${this._label}
</div>
`;
}
public registerEvents(): void {
const element = document.getElementById(this._id) as HTMLDivElement;
assert(element !== null);
element.addEventListener("click", () => {
if (this._isEnabled) {
this._onClick()
}
});
}
protected _onEnabledChanged() {
const element = document.getElementById(this._id) as HTMLDivElement;
assert(element !== null);
if (this._isEnabled) {
//element.classList.add("button");
element.classList.remove("button-disabled");
} else {
element.classList.add("button-disabled");
//element.classList.remove("button");
}
}
}

View File

@ -0,0 +1,45 @@
import { BaseUIElement } from "../layout";
import { assert } from "../../util";
export interface ComboBoxItem {
id: string;
displayText: string;
}
export class ComboBoxElement extends BaseUIElement {
private _items: ComboBoxItem[];
public constructor(id: string, items: ComboBoxItem[]) {
super(id);
this._items = items;
}
public generateHTML() {
let itemsHTML = "";
for (const item of this._items) {
itemsHTML += `<option value="${item.id}">${item.displayText}</option>`;
}
return `
<select name="${this._id}" id="${this._id}">
${itemsHTML}
</select>
`;
}
public registerEvents(): void {
}
public getValue() {
const element = document.getElementById(this._id) as HTMLSelectElement;
assert(element !== null);
return this._items[element.selectedIndex].id;
}
protected _onEnabledChanged() {
const element = document.getElementById(this._id) as HTMLSelectElement;
assert(element !== null);
element.disabled = !this._isEnabled;
}
}

View File

@ -0,0 +1,69 @@
import { BaseUIElement } from "../layout";
import { assert } from "../../util";
import { remote } from "electron";
import * as path from "path";
export class FileInputElement extends BaseUIElement {
private _fileExtension: string;
private _loadedFilePath: string;
public constructor(id: string, fileExtension: string) {
super(id);
this._fileExtension = fileExtension;
this._loadedFilePath = "";
}
public generateHTML() {
return `
<div class="input-text" id="${this._id}">
${this._loadedFilePath}
</div>
`;
}
public registerEvents(): void {
const element = document.getElementById(this._id) as HTMLDivElement;
assert(element !== null);
element.onclick = () => {
if (!this._isEnabled) {
return;
}
const files = remote.dialog.showOpenDialogSync({
title: "Load file",
buttonLabel: "Load",
filters: [{
name: 'Waveform obj file',
extensions: [`${this._fileExtension}`]
}]
});
if (files && files.length === 1) {
const filePath = files[0];
this._loadedFilePath = filePath;
} else {
this._loadedFilePath = "";
}
const parsedPath = path.parse(this._loadedFilePath);
element.innerHTML = parsedPath.name + parsedPath.ext;
};
}
public getValue() {
return this._loadedFilePath;
}
protected _onEnabledChanged() {
const element = document.getElementById(this._id) as HTMLDivElement;
assert(element !== null);
if (this._isEnabled) {
//element.classList.add("button");
element.classList.remove("input-text-disabled");
} else {
element.classList.add("input-text-disabled");
//element.classList.remove("button");
}
}
}

30
src/ui/elements/label.ts Normal file
View File

@ -0,0 +1,30 @@
import { assert } from "../../util";
export class LabelElement {
private _id: string;
private _text: string;
constructor(id: string, text: string) {
this._id = id;
this._text = text;
}
public generateHTML(): string {
return `
<div class="sub-left" id="${this._id}">
${this._text}
</div>
`;
}
public setEnabled(isEnabled: boolean) {
const element = document.getElementById(this._id) as HTMLDivElement;
assert(element !== null);
if (isEnabled) {
element.classList.remove("sub-left-disabled");
} else {
element.classList.add("sub-left-disabled");
}
}
}

89
src/ui/elements/slider.ts Normal file
View File

@ -0,0 +1,89 @@
import { BaseUIElement } from "../layout";
import { assert } from "../../util";
import { clamp } from "../../math";
export class SliderElement extends BaseUIElement {
private _min: number;
private _max: number;
private _step: number;
private _value: number;
private _dragging: boolean;
public constructor(id: string, min: number, max: number, step: number, value: number) {
super(id);
this._min = min;
this._max = max;
this._step = step;
this._value = value;
this._dragging = false;
}
public generateHTML() {
const norm = (this._value - this._min) / (this._max - this._min);
return `
<div class="new-slider" id="${this._id}">
<div class="new-slider-bar" id="${this._id}-bar"style="width: ${norm * 100}%;">
</div>
</div>
`;
}
public registerEvents() {
const element = document.getElementById(this._id) as HTMLDivElement;
assert(element !== null);
element.onmousedown = () => {
this._dragging = true;
};
document.addEventListener("mousemove", (e: any) => {
if (this._dragging) {
this._updateValue(e);
}
});
document.addEventListener("mouseup", (e: any) => {
if (this._dragging) {
this._updateValue(e);
}
this._dragging = false;
});
}
private _updateValue(e: MouseEvent) {
if (!this._isEnabled) {
return;
}
const element = document.getElementById(this._id) as HTMLDivElement;
const elementBar = document.getElementById(this._id + "-bar") as HTMLDivElement;
assert(element !== null && elementBar !== null);
const mouseEvent = e as MouseEvent;
const xOffset = mouseEvent.clientX - elementBar.getBoundingClientRect().x;
const width = element.clientWidth;
const norm = clamp(xOffset / width, 0.0, 1.0);
this._value = (norm * (this._max - this._min)) + this._min;
elementBar.style.width = `${norm * 100}%`;
}
public getValue() {
return this._value;
}
protected _onEnabledChanged() {
const element = document.getElementById(this._id) as HTMLDivElement;
const elementBar = document.getElementById(this._id + "-bar") as HTMLDivElement;
assert(element !== null && elementBar !== null);
if (this._isEnabled) {
//element.classList.add("button");
element.classList.remove("new-slider-disabled");
elementBar.classList.remove("new-slider-bar-disabled");
} else {
element.classList.add("new-slider-disabled");
elementBar.classList.add("new-slider-bar-disabled");
//element.classList.remove("button");
}
}
}

View File

@ -1,23 +0,0 @@
export class ToggleableGroup {
private _jqueryHtmlElements: Array<string>;
private _visibilityElement: string;
public constructor(jqueryHtmlElements: Array<string>, visibilityElement: string, enabledOnStart: boolean) {
this._jqueryHtmlElements = jqueryHtmlElements
this._visibilityElement = visibilityElement
this.setEnabled(enabledOnStart);
}
public setEnabled(isEnabled: boolean) {
this._jqueryHtmlElements.forEach(htmlElement => {
$(htmlElement).prop('disabled', !isEnabled);
});
if (isEnabled) {
$(this._visibilityElement).removeClass("transparent");
} else {
$(this._visibilityElement).addClass("transparent");
}
}
}

106
src/ui/layout.ts Normal file
View File

@ -0,0 +1,106 @@
import { ButtonElement } from "./elements/button";
import { LabelElement } from "./elements/label";
export interface Group {
label: string,
components: Component[]
submitButton: ButtonElement
}
export interface Component {
label : LabelElement,
type: BaseUIElement,
}
export abstract class BaseUIElement {
protected _id: string;
protected _isEnabled: boolean;
constructor(id: string) {
this._id = id;
this._isEnabled = true;
}
public setEnabled(isEnabled: boolean) {
this._isEnabled = isEnabled;
this._onEnabledChanged();
}
public abstract generateHTML(): string;
public abstract registerEvents(): void;
protected abstract _onEnabledChanged(): void;
}
function buildSubcomp(subcomp: Component) {
return `
<div class="item item-body">
${subcomp.label.generateHTML()}
<div class="divider"></div>
<div class="sub-right">
${subcomp.type.generateHTML()}
</div>
</div>
`;
}
function buildComponent(componentParams: Group) {
let innerHTML = "";
for (const subcomp of componentParams.components) {
innerHTML += buildSubcomp(subcomp);
}
return `
${innerHTML}
<div class="item item-body">
<div class="sub-right">
${componentParams.submitButton.generateHTML()}
</div>
</div>
<div class="item item-body">
<div class="sub-right">
<div class="item-body-sunken">
Nothing
</div>
</div>
</div>
`;
}
export function registerUI(uiGroups: Group[]) {
for (const group of uiGroups) {
for (const comp of group.components) {
comp.type.registerEvents();
}
group.submitButton.registerEvents();
}
}
export function buildUI(myItems: Group[]) {
let itemHTML = "";
for (const item of myItems) {
itemHTML += `
<div class="item item-body">
<div class="sub-left-alt">
${item.label.toUpperCase()}
</div>
<div class="sub-right">
<div class="h-div">
</div>
</div>
</div>
`;
itemHTML += buildComponent(item);
}
document.getElementById("properties")!.innerHTML = `<div class="menu"><div class="container">
` + itemHTML + `</div></div>`;
};
export function setEnabled(group: Group, isEnabled: boolean) {
for (const comp of group.components) {
comp.type.setEnabled(isEnabled);
comp.label.setEnabled(isEnabled);
}
group.submitButton.setEnabled(isEnabled);
}

View File

@ -1,26 +0,0 @@
export class Modal {
public static show(text: string) {
this._setText(text);
this._show();
}
public static setButton(text: string, onClick: (() => void)) {
$("#buttonModalAction").html(text);
$("#buttonModalAction").on("click", () => { this._hide(); onClick(); } );
$("#buttonModalClose").on("click", () => { this._hide(); });
}
private static _setText(text: string) {
$("#textModal").html(text);
}
private static _show() {
$("#modal").show()
}
private static _hide() {
$("#modal").hide();
}
}

View File

@ -1,38 +0,0 @@
export enum ToastStyle {
Success = "bg-success",
Failure = "bg-failure"
}
export class Toast {
private static current: ToastStyle = ToastStyle.Success;
private static autoHideDelay: number = 4000;
private static timeout: NodeJS.Timeout;
public static show(text: string, style: ToastStyle) {
this._setText(text);
this._setStyle(style);
this._show();
}
private static _setText(text: string) {
$("#toast").html(text);
}
private static _setStyle(style: ToastStyle) {
$("#toast").removeClass(Toast.current);
$("#toast").addClass(style);
Toast.current = style;
}
private static _show() {
clearTimeout(this.timeout);
$("#toast").removeClass("hide");
this.timeout = setTimeout(() => { this._hide(); }, this.autoHideDelay);
}
private static _hide() {
$("#toast").addClass("hide");
}
}

View File

@ -22,7 +22,7 @@ export class VoxelManager {
public voxels: Array<Block>;
public voxelTexcoords: Array<FaceInfo>;
public _voxelSize: number;
private voxelsHash: HashMap<Vector3, Block>;
private _blockMode!: MaterialType;
private _currentTexture!: Texture;

View File

@ -1,237 +1,276 @@
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@300;400&display=swap');
:root {
--properties-width: max(450px, 25%);
--canvas-width: calc(100% - var(--properties-width));
--pill-radius: 6px;
--prim-color: #008C74;
--prim-color-disabled: #004e41;
--prop-bg-color: #333333;
}
body {
margin: 0px;
background-color: rgb(25, 25, 25);
font-family: 'Lexend', sans-serif;
overflow: hidden;
user-select: none;
}
.column-properties {
background-color: var(--prop-bg-color);
position: absolute;
width: var(--properties-width);
height: 100%;
border-right: 1.5px solid rgb(35, 35, 35);
overflow: auto;
}
.column-canvas {
position: absolute;
margin-left: var(--properties-width);
padding-left: 1.5px;
width: 80%;
height: 100%;
}
canvas {
position: absolute;
width: 100%;
height: 100%;
}
body {
margin: 0px;
background-color: rgb(25, 25, 25);
background-size: 100%;
font-family: 'Lexend', sans-serif;
}
input {
font-family: 'Lexend', sans-serif;
font-size: medium;
}
input:enabled {
outline: none;
}
button {
font-family: 'Lexend', sans-serif;
font-size: medium;
}
button:hover:enabled {
cursor: pointer;
transition: 0.2s;
border-color: rgba(0, 0, 0, 0.3);
}
button:disabled {
cursor: default;
transition: 0.2s;
}
.toolbar-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: auto;
margin: auto;
}
.status-container {
position: fixed;
left: 50%;
bottom: 25px;
transform: translate(-50%, -50%);
margin: 0 auto;
}
.glass {
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(20px);
border: solid;
border-color: rgba(255, 255, 255, 0.1);
border-width: 1.5px;
margin-top: 25px;
}
.toast {
padding: 10px 50px !important;
font-weight: 300;
transition: 0.5s;
}
.hide {
opacity: 0;
transition: 0.5s;
}
.bg-success {
background-color: rgba(40, 167, 69, 0.5) !important;
color: white !important;
}
.bg-failure {
background-color: rgba(220, 53, 69, 0.5) !important;
color: white !important;
}
.transparent {
opacity: 0.5;
}
.toolbar-item {
padding: 10px;
cursor: default;
}
.input-group {
display: flex;
align-content: stretch;
}
.input-group-item {
background-color: #EFEFEF;
padding: 10px 14px;
border: solid;
border-color: rgba(0, 0, 0, 0.25);
border-width: 1.5px 0px;
}
.input-group-item-dominant {
flex-grow: 1;
}
.pill-left {
border-top-left-radius: 32px;
border-bottom-left-radius: 32px;
border-left-width: 1.5px;
border-right-width: 0px;
}
.pill-right {
border-top-right-radius: 32px;
border-bottom-right-radius: 32px;
border-right-width: 1.5px;
border-left-width: 0px;
}
.text-field {
background-color: white;
font-weight: 300;
cursor: text;
color: #808080;
}
.text-field:read-only {
cursor: pointer;
}
.text-field:disabled {
background-color: #EFEFEF;
cursor: default;
}
.bg-primary {
background-color: #007bff;
color: white;
}
.bg-secondary {
background-color: #dc3545;
color: white;
}
.bg-primary:disabled {
background-color: #5AAAFF;
}
.bg-secondary:disabled {
background-color: #E87C87;
}
.bg-primary:hover:enabled {
background-color: #0069D9;
}
.bg-secondary:hover:enabled {
background-color: #C82333;
}
@media (max-width: 1200px) {
.toolbar-container {
flex-direction: column;
align-content: center;
align-items: center;
}
.input-group-item-dominant {
flex-grow: 0;
}
.toolbar-item-1 {
border-top-right-radius: 200px;
border-bottom-right-radius: 200px;
border-right-width: 1.5px;
border-left-width: 0px;
}
.toolbar-item-2 {
border-top-left-radius: 200px;
border-bottom-left-radius: 200px;
border-top-right-radius: 200px;
border-bottom-right-radius: 200px;
border-right-width: 1.5px;
border-left-width: 1.5px;
}
.toolbar-item-3 {
border-top-left-radius: 200px;
border-bottom-left-radius: 200px;
border-right-width: 0px;
border-left-width: 1.5px;
}
}
.pill {
border-radius: 32px;
border-width: 1.5px;
border-style: solid;
border-color: rgba(0, 0, 0, 0.3);
}
.modal {
position: absolute;
padding: 40px;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400px;
}
div.content {
.menu {
display: flex;
flex-direction: column;
}
.modalTop {
color: white;
flex: 1;
font-weight: 300;
width: calc(100% - 20px);
padding: 10px;
}
.modalBottom {
flex: none;
padding-top: 50px;
.container {
display: flex;
flex-direction: column;
margin: 10px;
border-radius: 10px;
background-color: #1A1A1A;
box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 24px;
border: 2px solid rgba(255, 255, 255, 0.075);
padding: 8px 0px;
}
.item {
padding: 0px 0px;
color: white;
display: flex;
flex-direction: row;
align-items: center;
color: #A8A8A8;
}
.item-header {
background-color: #2F2F2F;
color: #C6C6C6;
font-weight: 500;
padding: 10px 20px;
}
.sub-left {
align-self: center;
padding: 0px 20px;
width: 125px;
}
.sub-left-disabled {
color: #535353 !important;
}
.sub-left-alt {
align-self: center;
padding: 0px 0px 0px 20px;
color: #303030;
font-weight: 400;
letter-spacing: 1px;
}
.sub-right {
flex-grow: 1;
padding: 0px 20px;
}
.item-body {
font-size: 85%;
margin: 0px 0px;
min-height: 30px;
padding: 10px 0px;
}
.item-body-sunken {
background-color: #141414;
box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 10px 0px inset;
border-radius: 5px;
color: #8C8C8C80;
font-weight: 300;
font-size: 90%;
padding: 12px 18px;
line-height: 180%;
border: 1px solid rgb(255, 255, 255, 0);
}
.round-top {
border-radius: 8.5px 8.5px 0px 0px;
}
.round-bottom {
border-radius: 0px 0px 10px 10px;
border-width: 0.5px 0px 0px 0px;
}
code {
border-radius: 3px;
padding: 0px 5px;
margin: 0px 5px;
background-color: rgb(15, 15, 15);
font-size: 110%;
}
select {
width: 100%;
height: 30px;
padding-left: 10px;
border-radius: 5px;
border: none;
font-family: 'Lexend', sans-serif;
font-weight: 300;
color: #A8A8A8;
background-color: #2F2F2F;
}
select:hover {
color: #C6C6C6;
background-color: #383838;
}
.file-input {
border-radius: 5px;
border: 1px solid rgb(255, 255, 255, 0);
font-family: 'Lexend', sans-serif;
font-weight: 300;
background-color: #2F2F2F;
padding: 0px 16px;
cursor: pointer;
height: 40px;
}
.file-input:hover {
border: 1px solid rgb(255, 255, 255, 0.1);
}
.new-slider {
border-radius: 5px;
font-family: 'Lexend', sans-serif;
font-weight: 300;
background-color: #2F2F2F;
cursor: ew-resize;
height: 30px;
font-size: 90%;
}
.new-slider-bar {
border-radius: 5px;
height: 28px;
background-color: var(--prim-color);
border: 1px solid rgb(255, 255, 255, 0.0);
}
.new-slider-bar:hover {
border: 1px solid rgb(255, 255, 255, 0.2);
background-color: #00A6D8;
}
.new-slider-disabled {
background-color: #242424;
cursor: default !important;
}
.new-slider-bar-disabled {
background-color: var(--prim-color-disabled) !important;
cursor: default !important;
border: 1px solid rgb(255, 255, 255, 0.0) !important;
}
.button {
padding: 8px 40px;
background-color: var(--prim-color);
border-radius: 5px;
height: 25px;
color: white;
text-align: center;
padding: 5px 0px 0px 0px;
border: 1px solid rgb(255, 255, 255, 0);
cursor: pointer;
}
.button:hover {
border: 1px solid rgb(255, 255, 255, 0.2);
background-color: #00A6D8;
cursor: pointer;
}
.button-disabled {
cursor: default !important;
background-color: var(--prim-color-disabled) !important;
border: 1px solid rgb(255, 255, 255, 0) !important;
color: #808080 !important;
}
.input-text {
background-color: #2F2F2F;
border-radius: 5px;
height: 24px;
font-weight: 300;
padding: 6px 0px 0px 10px;
border: 1px solid rgb(255, 255, 255, 0);
}
.input-text:hover {
border: 1px solid rgb(255, 255, 255, 0.2);
background-color: #383838;
cursor: pointer;
}
.input-text-disabled {
background-color: red !important;
}
.h-div {
height: 3px;
border-radius: 2px;
background-color: #252525;
}
.divider {
width: 2px;
border-radius: 1px;
background-color: #252525;
align-self: stretch;
}
.disabled {
opacity: 0.5;
cursor: default !important;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}

View File

@ -6,119 +6,189 @@ import { log, LogStyle } from "./logging";
import { PNG } from "pngjs";
import { isDirSetup, assert, getAverageColour } from "./misc";
import chalk from "chalk";
import prompt from "prompt";
const AdmZip = require("adm-zip");
// Check /blocks and /models is setup correctly
log(LogStyle.None, "Checking Minecraft assets are provided...");
const blocksDirSetup = isDirSetup("./blocks", "assets/minecraft/textures/block");
const modelsDirSetup = isDirSetup("./models", "assets/minecraft/models/block");
assert(blocksDirSetup && modelsDirSetup, "Folders not setup correctly");
log(LogStyle.Success, "Folders setup correctly\n")
// Load the ignore list
log(LogStyle.None, "Loading ignore list...")
let ignoreList: Array<string> = [];
const ignoreListPath = path.join(__dirname, "./ignore-list.txt");
const defaultIgnoreListPath = path.join(__dirname, "./default-ignore-list.txt");
if (fs.existsSync(ignoreListPath)) {
log(LogStyle.Success, "Found custom ignore list");
ignoreList = fs.readFileSync(ignoreListPath, "utf-8").replace(/\r/g, "").split("\n");
} else if (fs.existsSync(defaultIgnoreListPath)){
log(LogStyle.Success, "Found default ignore list");
ignoreList = fs.readFileSync(defaultIgnoreListPath, "utf-8").replace(/\r/g, "").split("\n");
} else {
log(LogStyle.Warning, "No ignore list found, looked for ignore-list.txt and default-ignore-list.txt");
}
log(LogStyle.Info, `${ignoreList.length} blocks found in ignore list\n`);
//
log(LogStyle.None, "Loading block models...")
enum parentModel {
Cube = "minecraft:block/cube",
CubeAll = "minecraft:block/cube_all",
CubeColumn = "minecraft:block/cube_column",
CubeColumnHorizontal = "minecraft:block/cube_column_horizontal",
TemplateSingleFace = "minecraft:block/template_single_face",
TemplateGlazedTerracotta = "minecraft:block/template_glazed_terracotta",
}
interface Model {
name: string,
colour?: RGB,
faces: {
[face: string]: Texture
prompt.start();
prompt.get([{
name: 'response',
description: 'Do you want to fetch textures and models? (Y/n)',
type: 'string',
required: true,
message: 'Respond with yes or no',
conform: (response) => {
return ["yes", "Y", "y", "no", "N", "n"].includes(response);
}
}
}], (err, res) => {
if (err) {
process.exit(1);
}
handleResponse(res);
});
interface Texture {
name: string,
texcoord?: UV,
colour?: RGB
}
const faces = ["north", "south", "up", "down", "east", "west"];
let allModels: Array<Model> = [];
let usedTextures: Set<string> = new Set();
fs.readdirSync(path.join(__dirname, "./models")).forEach(filename => {
if (path.extname(filename) !== ".json") {
return;
};
const filePath = path.join(__dirname, "./models", filename);
const fileData = fs.readFileSync(filePath, "utf8");
const modelData = JSON.parse(fileData);
const parsedPath = path.parse(filePath);
const modelName = parsedPath.name;
if (ignoreList.includes(filename)) {
return;
const handleResponse = (results: any) => {
const responseYes = ["yes", "Y", "y"].includes(results.response as any);
if (!responseYes) {
buildAtlas();
}
let faceData: {[face: string]: Texture} = {};
switch (modelData.parent) {
case parentModel.CubeAll:
faceData = {
up: { name: modelData.textures.all },
down: { name: modelData.textures.all },
north: { name: modelData.textures.all },
south: { name: modelData.textures.all },
east: { name: modelData.textures.all },
west: { name: modelData.textures.all }
const versionsDir = path.join(process.env.APPDATA!, "./.minecraft/versions");
if (!fs.existsSync(versionsDir)) {
log(LogStyle.Failure, "Could not fid .minecraft/versions\n");
process.exit(1);
}
log(LogStyle.Success, ".minecraft/versions found successfully\n");
const versions = fs.readdirSync(versionsDir)
.filter((file) => fs.lstatSync(path.join(versionsDir, file)).isDirectory())
.map((file) => ({ file, birthtime: fs.lstatSync(path.join(versionsDir, file)).birthtime }))
.sort((a, b) => b.birthtime.getTime() - a.birthtime.getTime());
for (let i = 0; i < versions.length; ++i) {
const versionName = versions[i].file
log(LogStyle.Info, `Searching in ${versionName} for ${versionName}.jar`);
const versionDir = path.join(versionsDir, versionName);
const versionFiles = fs.readdirSync(versionDir);
if (!versionFiles.includes(versionName + ".jar")) {
continue;
}
log(LogStyle.Success, `Up ${versionName}.jar successfully\n`);
const versionJarPath = path.join(versionDir, `${versionName}.jar`);
log(LogStyle.Info, `Upzipping ${versionName}.jar...`);
var zip = new AdmZip(versionJarPath);
const zipEntries = zip.getEntries();
zipEntries.forEach((zipEntry: any) => {
if (zipEntry.entryName.startsWith("assets/minecraft/textures/block")) {
zip.extractEntryTo(zipEntry.entryName, path.join(__dirname, "./blocks"), false, true);
} else if (zipEntry.entryName.startsWith("assets/minecraft/models/block")) {
zip.extractEntryTo(zipEntry.entryName, path.join(__dirname, "./models"), false, true);
}
break;
case parentModel.CubeColumn:
faceData = {
up: { name: modelData.textures.end },
down: { name: modelData.textures.end },
north: { name: modelData.textures.side },
south: { name: modelData.textures.side },
east: { name: modelData.textures.side },
west: { name: modelData.textures.side }
}
break;
case parentModel.Cube:
faceData = {
up: { name: modelData.textures.up },
down: { name: modelData.textures.down },
north: { name: modelData.textures.north },
south: { name: modelData.textures.south },
east: { name: modelData.textures.east },
west: { name: modelData.textures.west }
}
break;
case parentModel.TemplateSingleFace:
faceData = {
up: { name: modelData.textures.texture },
down: { name: modelData.textures.texture },
north: { name: modelData.textures.texture },
south: { name: modelData.textures.texture },
east: { name: modelData.textures.texture },
west: { name: modelData.textures.texture }
}
break;
case parentModel.TemplateGlazedTerracotta:
});
log(LogStyle.Success, `Extracted textures and models successfully\n`);
buildAtlas();
return;
}
};
const buildAtlas = () => {
// Check /blocks and /models is setup correctly
log(LogStyle.None, "Checking Minecraft assets are provided...");
const texturesDirSetup = isDirSetup("./models", "assets/minecraft/textures/block");
assert(texturesDirSetup, "/blocks is not setup correctly");
const modelsDirSetup = isDirSetup("./models", "assets/minecraft/models/block");
assert(modelsDirSetup, "/models is not setup correctly");
// Load the ignore list
log(LogStyle.None, "Loading ignore list...")
let ignoreList: Array<string> = [];
const ignoreListPath = path.join(__dirname, "./ignore-list.txt");
const defaultIgnoreListPath = path.join(__dirname, "./default-ignore-list.txt");
if (fs.existsSync(ignoreListPath)) {
log(LogStyle.Success, "Found custom ignore list");
ignoreList = fs.readFileSync(ignoreListPath, "utf-8").replace(/\r/g, "").split("\n");
} else if (fs.existsSync(defaultIgnoreListPath)) {
log(LogStyle.Success, "Found default ignore list");
ignoreList = fs.readFileSync(defaultIgnoreListPath, "utf-8").replace(/\r/g, "").split("\n");
} else {
log(LogStyle.Warning, "No ignore list found, looked for ignore-list.txt and default-ignore-list.txt");
}
log(LogStyle.Info, `${ignoreList.length} blocks found in ignore list\n`);
//
log(LogStyle.None, "Loading block models...")
enum parentModel {
Cube = "minecraft:block/cube",
CubeAll = "minecraft:block/cube_all",
CubeColumn = "minecraft:block/cube_column",
CubeColumnHorizontal = "minecraft:block/cube_column_horizontal",
TemplateSingleFace = "minecraft:block/template_single_face",
TemplateGlazedTerracotta = "minecraft:block/template_glazed_terracotta",
}
interface Model {
name: string,
colour?: RGB,
faces: {
[face: string]: Texture
}
}
interface Texture {
name: string,
texcoord?: UV,
colour?: RGB
}
const faces = ["north", "south", "up", "down", "east", "west"];
let allModels: Array<Model> = [];
let usedTextures: Set<string> = new Set();
fs.readdirSync(path.join(__dirname, "./models")).forEach(filename => {
if (path.extname(filename) !== ".json") {
return;
};
const filePath = path.join(__dirname, "./models", filename);
const fileData = fs.readFileSync(filePath, "utf8");
const modelData = JSON.parse(fileData);
const parsedPath = path.parse(filePath);
const modelName = parsedPath.name;
if (ignoreList.includes(filename)) {
return;
}
let faceData: { [face: string]: Texture } = {};
switch (modelData.parent) {
case parentModel.CubeAll:
faceData = {
up: { name: modelData.textures.all },
down: { name: modelData.textures.all },
north: { name: modelData.textures.all },
south: { name: modelData.textures.all },
east: { name: modelData.textures.all },
west: { name: modelData.textures.all }
}
break;
case parentModel.CubeColumn:
faceData = {
up: { name: modelData.textures.end },
down: { name: modelData.textures.end },
north: { name: modelData.textures.side },
south: { name: modelData.textures.side },
east: { name: modelData.textures.side },
west: { name: modelData.textures.side }
}
break;
case parentModel.Cube:
faceData = {
up: { name: modelData.textures.up },
down: { name: modelData.textures.down },
north: { name: modelData.textures.north },
south: { name: modelData.textures.south },
east: { name: modelData.textures.east },
west: { name: modelData.textures.west }
}
break;
case parentModel.TemplateSingleFace:
faceData = {
up: { name: modelData.textures.texture },
down: { name: modelData.textures.texture },
north: { name: modelData.textures.texture },
south: { name: modelData.textures.texture },
east: { name: modelData.textures.texture },
west: { name: modelData.textures.texture }
}
break;
case parentModel.TemplateGlazedTerracotta:
faceData = {
up: { name: modelData.textures.pattern },
down: { name: modelData.textures.pattern },
@ -128,83 +198,84 @@ fs.readdirSync(path.join(__dirname, "./models")).forEach(filename => {
west: { name: modelData.textures.pattern }
}
break;
default:
return;
}
for (const face of faces) {
usedTextures.add(faceData[face].name);
}
allModels.push({
name: modelName,
faces: faceData
});
});
log(LogStyle.Success, `${allModels.length} blocks loaded\n`);
const atlasSize = Math.ceil(Math.sqrt(usedTextures.size));
const atlasWidth = atlasSize * 16;
let offsetX = 0, offsetY = 0;
const outputImage = images(atlasWidth * 3, atlasWidth * 3);
let textureDetails: {[textureName: string]: {texcoord: UV, colour: RGB}} = {};
log(LogStyle.None, "Building blocks.png...");
usedTextures.forEach(textureName => {
const shortName = textureName.split("/")[1]; // Eww
const absolutePath = path.join(__dirname, "./blocks", shortName + ".png");
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
const image = images(absolutePath);
for (let x = 0; x < 3; ++x) {
for (let y = 0; y < 3; ++y) {
outputImage.draw(image, 16 * (3 * offsetX + x), 16 * (3 * offsetY + y));
default:
return;
}
}
textureDetails[textureName] = {
texcoord: {
u: 16 * (3 * offsetX + 1) / (atlasWidth * 3),
v: 16 * (3 * offsetY + 1) / (atlasWidth * 3)
},
colour: getAverageColour(pngData)
for (const face of faces) {
usedTextures.add(faceData[face].name);
}
allModels.push({
name: modelName,
faces: faceData
});
});
log(LogStyle.Success, `${allModels.length} blocks loaded\n`);
const atlasSize = Math.ceil(Math.sqrt(usedTextures.size));
const atlasWidth = atlasSize * 16;
let offsetX = 0, offsetY = 0;
const outputImage = images(atlasWidth * 3, atlasWidth * 3);
let textureDetails: { [textureName: string]: { texcoord: UV, colour: RGB } } = {};
log(LogStyle.None, "Building blocks.png...");
usedTextures.forEach(textureName => {
const shortName = textureName.split("/")[1]; // Eww
const absolutePath = path.join(__dirname, "./blocks", shortName + ".png");
const fileData = fs.readFileSync(absolutePath);
const pngData = PNG.sync.read(fileData);
const image = images(absolutePath);
for (let x = 0; x < 3; ++x) {
for (let y = 0; y < 3; ++y) {
outputImage.draw(image, 16 * (3 * offsetX + x), 16 * (3 * offsetY + y));
}
}
textureDetails[textureName] = {
texcoord: {
u: 16 * (3 * offsetX + 1) / (atlasWidth * 3),
v: 16 * (3 * offsetY + 1) / (atlasWidth * 3)
},
colour: getAverageColour(pngData)
}
++offsetX;
if (offsetX >= atlasSize) {
++offsetY;
offsetX = 0;
}
});
// Build up the output JSON
log(LogStyle.None, "Building blocks.json...\n");
for (const model of allModels) {
let blockColour = { r: 0, g: 0, b: 0 };
for (const face of faces) {
const faceTexture = textureDetails[model.faces[face].name];
const faceColour = faceTexture.colour;
blockColour.r += faceColour.r;
blockColour.g += faceColour.g;
blockColour.b += faceColour.b;
model.faces[face].texcoord = faceTexture.texcoord;
}
blockColour.r /= 6;
blockColour.g /= 6;
blockColour.b /= 6;
model.colour = blockColour;
}
++offsetX;
if (offsetX >= atlasSize) {
++offsetY;
offsetX = 0;
}
});
log(LogStyle.None, "Exporting...");
outputImage.save(path.join(__dirname, "../resources/blocks.png"));
log(LogStyle.Success, "blocks.png exported to /resources");
let outputJSON = { atlasSize: atlasSize, blocks: allModels };
fs.writeFileSync(path.join(__dirname, "../resources/blocks.json"), JSON.stringify(outputJSON, null, 4));
log(LogStyle.Success, "blocks.json exported to /resources\n");
// Build up the output JSON
log(LogStyle.None, "Building blocks.json...\n");
for (const model of allModels) {
let blockColour = {r: 0, g: 0, b: 0};
for (const face of faces) {
const faceTexture = textureDetails[model.faces[face].name];
const faceColour = faceTexture.colour;
blockColour.r += faceColour.r;
blockColour.g += faceColour.g;
blockColour.b += faceColour.b;
model.faces[face].texcoord = faceTexture.texcoord;
}
blockColour.r /= 6;
blockColour.g /= 6;
blockColour.b /= 6;
model.colour = blockColour;
}
log(LogStyle.None, "Exporting...");
outputImage.save(path.join(__dirname, "../resources/blocks.png"));
log(LogStyle.Success, "blocks.png exported to /resources");
let outputJSON = { atlasSize: atlasSize, blocks: allModels };
fs.writeFileSync(path.join(__dirname, "../resources/blocks.json"), JSON.stringify(outputJSON, null, 4));
log(LogStyle.Success, "blocks.json exported to /resources\n");
console.log(chalk.cyanBright(chalk.inverse("DONE") + " Now run " + chalk.inverse("npm start") + " and the new blocks will be used"));
console.log(chalk.cyanBright(chalk.inverse("DONE") + " Now run " + chalk.inverse(" npm start ") + " and the new blocks will be used"));
}

View File

@ -14,7 +14,7 @@ export function isDirSetup(relativePath: string, jarAssetDir: string) {
} else {
fs.mkdirSync(dir);
}
log(LogStyle.Warning, `Copy the contents of .minecraft/versions/<version>/<version>.jar/${jarAssetDir} from a Minecraft game files into ${relativePath}`);
log(LogStyle.Warning, `Copy the contents of .minecraft/versions/<version>/<version>.jar/${jarAssetDir} from a Minecraft game files into ${relativePath} or fetch them automatically`);
return false;
}