mirror of
https://github.com/LucasDower/ObjToSchematic.git
synced 2025-02-11 13:29:29 +08:00
Only shown faces drawn, UI redo, new atlas script
This commit is contained in:
parent
97f2ae47fc
commit
e2dce3f5a0
58
index.html
58
index.html
@ -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>
|
@ -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"
|
||||
},
|
||||
|
@ -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 |
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
18
src/main.ts
18
src/main.ts
@ -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();
|
||||
|
@ -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
46
src/ui/elements/button.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
45
src/ui/elements/combobox.ts
Normal file
45
src/ui/elements/combobox.ts
Normal 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;
|
||||
}
|
||||
}
|
69
src/ui/elements/file_input.ts
Normal file
69
src/ui/elements/file_input.ts
Normal 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
30
src/ui/elements/label.ts
Normal 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
89
src/ui/elements/slider.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
106
src/ui/layout.ts
Normal 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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
473
styles.css
473
styles.css
@ -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);
|
||||
}
|
@ -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"));
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user