forked from mirror/ObjToSchematic
Action output message, more options to toggle
This commit is contained in:
parent
e2dce3f5a0
commit
e4bec861c0
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,4 +5,5 @@ ObjToSchematic-win32-x64
|
||||
/resources/*.mtl
|
||||
/dist
|
||||
/tools/blocks
|
||||
/tools/models
|
||||
/tools/models
|
||||
notes.md
|
@ -12,19 +12,35 @@ import { remote } from "electron";
|
||||
import fs from "fs";
|
||||
import { ButtonElement } from "./ui/elements/button";
|
||||
import { LabelElement } from "./ui/elements/label";
|
||||
import { OutputElement } from "./ui/elements/output";
|
||||
|
||||
export enum ActionReturnType {
|
||||
Success,
|
||||
Warning,
|
||||
Failure
|
||||
}
|
||||
interface ReturnStatus {
|
||||
message: string,
|
||||
//style: ToastStyle,
|
||||
type: ActionReturnType,
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export enum Action {
|
||||
Load = 0,
|
||||
Simplify = 1,
|
||||
Voxelise = 2,
|
||||
Palette = 3,
|
||||
Export = 4,
|
||||
}
|
||||
|
||||
export class AppContext {
|
||||
public ambientOcclusion: boolean;
|
||||
public dithering: boolean;
|
||||
|
||||
private _gl: WebGLRenderingContext;
|
||||
private _loadedMesh?: Mesh;
|
||||
private _ui: Group[];
|
||||
private _actionMap = new Map<Action, (() => ReturnStatus | void)>();
|
||||
|
||||
private static _instance: AppContext;
|
||||
|
||||
@ -34,6 +50,13 @@ export class AppContext {
|
||||
|
||||
private constructor() {
|
||||
this.ambientOcclusion = true;
|
||||
this.dithering = true;
|
||||
|
||||
this._actionMap.set(Action.Load, () => { return this._load(); });
|
||||
this._actionMap.set(Action.Simplify, () => {});
|
||||
this._actionMap.set(Action.Voxelise, () => { return this._voxelise(); });
|
||||
this._actionMap.set(Action.Palette, () => { return this._palette(); } );
|
||||
this._actionMap.set(Action.Export, () => { return this._export(); });
|
||||
|
||||
this._ui = [
|
||||
{
|
||||
@ -48,7 +71,8 @@ export class AppContext {
|
||||
type: new FileInputElement("mtlFile", "mtl")
|
||||
},
|
||||
],
|
||||
submitButton: new ButtonElement("loadMesh", "Load mesh", () => { this._load(); })
|
||||
submitButton: new ButtonElement("loadMesh", "Load mesh", () => { this.do(Action.Load); }),
|
||||
output: new OutputElement("output1")
|
||||
},
|
||||
{
|
||||
label: "Simplify",
|
||||
@ -57,7 +81,8 @@ export class AppContext {
|
||||
label: new LabelElement("label3", "Ratio"),
|
||||
type: new SliderElement("ratio", 0.0, 1.0, 0.01, 0.5) },
|
||||
],
|
||||
submitButton: new ButtonElement("simplifyMesh", "Simplify mesh", () => { })
|
||||
submitButton: new ButtonElement("simplifyMesh", "Simplify mesh", () => { }),
|
||||
output: new OutputElement("output2")
|
||||
},
|
||||
{
|
||||
label: "Build",
|
||||
@ -74,25 +99,34 @@ export class AppContext {
|
||||
])
|
||||
},
|
||||
],
|
||||
submitButton: new ButtonElement("voxeliseMesh", "Voxelise mesh", () => { this._voxelise(); })
|
||||
submitButton: new ButtonElement("voxeliseMesh", "Voxelise mesh", () => { this.do(Action.Voxelise); }),
|
||||
output: new OutputElement("output3")
|
||||
},
|
||||
{
|
||||
label: "Palette",
|
||||
components: [
|
||||
{
|
||||
label: new LabelElement("label6", "Block palette"),
|
||||
type: new ComboBoxElement("blockPalette", [])
|
||||
type: new ComboBoxElement("blockPalette", [
|
||||
{ id: "default", displayText: "Default" }
|
||||
])
|
||||
},
|
||||
{
|
||||
label: new LabelElement("label7", "Choice method"),
|
||||
type: new ComboBoxElement("choiceMethod", [])
|
||||
type: new ComboBoxElement("choiceMethod", [
|
||||
{ id: "euclidian", displayText: "Euclidian distance" }
|
||||
])
|
||||
},
|
||||
{
|
||||
label: new LabelElement("label8", "Dithering"),
|
||||
type: new ComboBoxElement("dithering", [])
|
||||
type: new ComboBoxElement("dithering", [
|
||||
{ id: "on", displayText: "On (recommended)" },
|
||||
{ id: "off", displayText: "Off" },
|
||||
])
|
||||
},
|
||||
],
|
||||
submitButton: new ButtonElement("assignBlocks", "Assign blocks", () => { this._export(); })
|
||||
submitButton: new ButtonElement("assignBlocks", "Assign blocks", () => { this.do(Action.Palette); }),
|
||||
output: new OutputElement("output4")
|
||||
},
|
||||
{
|
||||
label: "Export",
|
||||
@ -106,7 +140,8 @@ export class AppContext {
|
||||
])
|
||||
},
|
||||
],
|
||||
submitButton: new ButtonElement("exportStructure", "Export structure", () => { this._export(); })
|
||||
submitButton: new ButtonElement("exportStructure", "Export structure", () => { this.do(Action.Export); }),
|
||||
output: new OutputElement("output5")
|
||||
}
|
||||
]
|
||||
|
||||
@ -124,25 +159,35 @@ export class AppContext {
|
||||
this._gl = gl;
|
||||
}
|
||||
|
||||
public do(action: Action) {
|
||||
const status = this._actionMap.get(action)!();
|
||||
if (status) {
|
||||
this._ui[action].output.setMessage(status.message, status.type);
|
||||
if (status.error) {
|
||||
console.error(status.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _load(): ReturnStatus {
|
||||
setEnabled(this._ui[2], false);
|
||||
setEnabled(this._ui[4], false);
|
||||
|
||||
const objPath = (<FileInputElement>this._ui[0].components[0].type).getValue();
|
||||
if (!fs.existsSync(objPath)) {
|
||||
return { message: "Selected .obj cannot be found" };
|
||||
return { message: "Selected .obj cannot be found", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
const mtlPath = (<FileInputElement>this._ui[0].components[1].type).getValue();
|
||||
if (!fs.existsSync(mtlPath)) {
|
||||
return { message: "Selected .mtl cannot be found" };
|
||||
return { message: "Selected .mtl cannot be found", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
try {
|
||||
this._loadedMesh = new Mesh(objPath, this._gl);
|
||||
this._loadedMesh.loadTextures(this._gl);
|
||||
} catch (err: unknown) {
|
||||
return { error: err, message: "Could not load mesh" };
|
||||
return { error: err, message: "Could not load mesh", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
try {
|
||||
@ -151,19 +196,19 @@ export class AppContext {
|
||||
renderer.registerMesh(this._loadedMesh);
|
||||
renderer.compile();
|
||||
} catch (err: unknown) {
|
||||
return { message: "Could not render mesh" };
|
||||
return { message: "Could not render mesh", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
setEnabled(this._ui[2], true);
|
||||
return { message: "Loaded successfully" };
|
||||
return { message: "Loaded successfully", type: ActionReturnType.Success };
|
||||
}
|
||||
|
||||
private _voxelise(): ReturnStatus {
|
||||
setEnabled(this._ui[3], false);
|
||||
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 {
|
||||
@ -176,15 +221,36 @@ export class AppContext {
|
||||
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", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
setEnabled(this._ui[3], true);
|
||||
return { message: "Voxelised successfully", type: ActionReturnType.Success };
|
||||
}
|
||||
|
||||
private _palette(): ReturnStatus {
|
||||
setEnabled(this._ui[4], false);
|
||||
|
||||
const dithering = (<ComboBoxElement>this._ui[3].components[2].type).getValue();
|
||||
this.dithering = dithering === "on";
|
||||
|
||||
try {
|
||||
const voxelManager = VoxelManager.Get;
|
||||
voxelManager.assignBlocks();
|
||||
|
||||
const renderer = Renderer.Get;
|
||||
renderer.clear();
|
||||
renderer.registerVoxelMesh();
|
||||
renderer.compile();
|
||||
} catch (err: any) {
|
||||
return { error: err, message: "Could not register voxel mesh", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
setEnabled(this._ui[4], true);
|
||||
return { message: "Voxelised successfully" };//, style: ToastStyle.Success };
|
||||
return { message: "Blocks assigned successfully", type: ActionReturnType.Success };
|
||||
}
|
||||
|
||||
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") {
|
||||
@ -200,16 +266,16 @@ export class AppContext {
|
||||
});
|
||||
|
||||
if (filePath === undefined) {
|
||||
return { message: "Output cancelled" };//, style: ToastStyle.Success };
|
||||
return { message: "Output cancelled", type: ActionReturnType.Warning };
|
||||
}
|
||||
|
||||
try {
|
||||
exporter.export(filePath);
|
||||
} catch (err: unknown) {
|
||||
return { error: err, message: "Failed to export" };//, style: ToastStyle.Failure };
|
||||
return { error: err, message: "Failed to export", type: ActionReturnType.Failure };
|
||||
}
|
||||
|
||||
return { message: "Successfully exported" };//, style: ToastStyle.Success };
|
||||
return { message: "Successfully exported", type: ActionReturnType.Success };
|
||||
}
|
||||
|
||||
public draw() {
|
||||
|
@ -3,7 +3,7 @@ import { assert } from "../../util";
|
||||
|
||||
export class ButtonElement extends BaseUIElement {
|
||||
private _label: string;
|
||||
private _onClick: () => void
|
||||
private _onClick: () => void;
|
||||
|
||||
public constructor(id: string, label: string, onClick: () => void) {
|
||||
super(id);
|
||||
@ -26,7 +26,7 @@ export class ButtonElement extends BaseUIElement {
|
||||
|
||||
element.addEventListener("click", () => {
|
||||
if (this._isEnabled) {
|
||||
this._onClick()
|
||||
this._onClick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
32
src/ui/elements/output.ts
Normal file
32
src/ui/elements/output.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { BaseUIElement } from "../layout";
|
||||
import { assert } from "../../util";
|
||||
import { ActionReturnType } from "../../app_context";
|
||||
|
||||
export class OutputElement {
|
||||
private _id: string;
|
||||
|
||||
public constructor(id: string) {
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
public generateHTML() {
|
||||
return `
|
||||
<div class="item-body-sunken" id="${this._id}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public setMessage(message: string, returnType: ActionReturnType) {
|
||||
const element = document.getElementById(this._id) as HTMLDivElement;
|
||||
assert(element !== null);
|
||||
|
||||
element.innerHTML = message;
|
||||
element.classList.remove("border-warning");
|
||||
element.classList.remove("border-failure");
|
||||
if (returnType === ActionReturnType.Warning) {
|
||||
element.classList.add("border-warning");
|
||||
} else if (returnType === ActionReturnType.Failure) {
|
||||
element.classList.add("border-error");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
import { ButtonElement } from "./elements/button";
|
||||
import { LabelElement } from "./elements/label";
|
||||
import { OutputElement } from "./elements/output";
|
||||
|
||||
export interface Group {
|
||||
label: string,
|
||||
components: Component[]
|
||||
submitButton: ButtonElement
|
||||
label: string;
|
||||
components: Component[];
|
||||
submitButton: ButtonElement;
|
||||
output: OutputElement;
|
||||
}
|
||||
|
||||
export interface Component {
|
||||
label : LabelElement,
|
||||
type: BaseUIElement,
|
||||
label: LabelElement;
|
||||
type: BaseUIElement;
|
||||
}
|
||||
|
||||
export abstract class BaseUIElement {
|
||||
@ -59,9 +61,7 @@ function buildComponent(componentParams: Group) {
|
||||
</div>
|
||||
<div class="item item-body">
|
||||
<div class="sub-right">
|
||||
<div class="item-body-sunken">
|
||||
Nothing
|
||||
</div>
|
||||
${componentParams.output.generateHTML()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -8,7 +8,7 @@ import { Mesh, MaterialType } from "./mesh";
|
||||
import { triangleArea } from "./math";
|
||||
import { Axes, generateRays, rayIntersectTriangle } from "./ray";
|
||||
import { BasicBlockAssigner, OrderedDitheringBlockAssigner } from "./block_assigner.js";
|
||||
import { AppConfig } from "./config.js";
|
||||
import { AppContext } from "./app_context.js";
|
||||
|
||||
interface Block {
|
||||
position: Vector3;
|
||||
@ -77,6 +77,7 @@ export class VoxelManager {
|
||||
|
||||
public assignBlocks() {
|
||||
this.blockPalette = [];
|
||||
this.voxelTexcoords = [];
|
||||
let meanSquaredError = 0.0;
|
||||
|
||||
for (let i = 0; i < this.voxels.length; ++i) {
|
||||
@ -84,7 +85,8 @@ export class VoxelManager {
|
||||
|
||||
const averageColour = getAverageColour(voxel.colours!);
|
||||
|
||||
const blockAssigner = AppConfig.DITHERING_ENABLED ? new OrderedDitheringBlockAssigner() : new BasicBlockAssigner();
|
||||
const ditheringEnabled = AppContext.Get.dithering;
|
||||
const blockAssigner = ditheringEnabled ? new OrderedDitheringBlockAssigner() : new BasicBlockAssigner();
|
||||
const block = blockAssigner.assignBlock(averageColour, voxel.position);
|
||||
|
||||
const squaredError = Math.pow(255 * (block.colour.r - averageColour.r), 2) + Math.pow(255 * (block.colour.g - averageColour.g), 2) + Math.pow(255 * (block.colour.b - averageColour.b), 2);
|
||||
@ -97,6 +99,17 @@ export class VoxelManager {
|
||||
console.log("Mean Squared Error:", meanSquaredError);
|
||||
}
|
||||
|
||||
public assignBlankBlocks() {
|
||||
this.blockPalette = [];
|
||||
|
||||
const whiteColour = { r: 1.0, g: 1.0, b: 1.0 };
|
||||
const block = new BasicBlockAssigner().assignBlock(whiteColour, new Vector3(0, 0, 0));
|
||||
|
||||
for (let i = 0; i < this.voxels.length; ++i) {
|
||||
this._assignBlock(i, block);
|
||||
}
|
||||
}
|
||||
|
||||
public addVoxel(pos: Vector3, block: BlockInfo) {
|
||||
// Is there already a voxel in this position?
|
||||
let voxel = this.voxelsHash.get(pos);
|
||||
@ -176,6 +189,10 @@ export class VoxelManager {
|
||||
});
|
||||
}
|
||||
|
||||
public paintMesh() {
|
||||
this.assignBlocks();
|
||||
}
|
||||
|
||||
voxeliseMesh(mesh: Mesh) {
|
||||
this._clearVoxels();
|
||||
|
||||
@ -194,7 +211,8 @@ export class VoxelManager {
|
||||
});
|
||||
});
|
||||
|
||||
this.assignBlocks();
|
||||
this.assignBlankBlocks();
|
||||
//this.assignBlocks();
|
||||
}
|
||||
|
||||
}
|
18
styles.css
18
styles.css
@ -5,6 +5,7 @@
|
||||
--canvas-width: calc(100% - var(--properties-width));
|
||||
--pill-radius: 6px;
|
||||
--prim-color: #008C74;
|
||||
--prim-color-hover: #009e84;
|
||||
--prim-color-disabled: #004e41;
|
||||
--prop-bg-color: #333333;
|
||||
}
|
||||
@ -116,7 +117,7 @@ canvas {
|
||||
font-size: 90%;
|
||||
padding: 12px 18px;
|
||||
line-height: 180%;
|
||||
border: 1px solid rgb(255, 255, 255, 0);
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.round-top {
|
||||
@ -147,11 +148,14 @@ select {
|
||||
color: #A8A8A8;
|
||||
background-color: #2F2F2F;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
color: #C6C6C6;
|
||||
background-color: #383838;
|
||||
}
|
||||
select:disabled {
|
||||
background-color: #282828 !important;
|
||||
color: #535353;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
border-radius: 5px;
|
||||
@ -209,7 +213,7 @@ select:hover {
|
||||
}
|
||||
.button:hover {
|
||||
border: 1px solid rgb(255, 255, 255, 0.2);
|
||||
background-color: #00A6D8;
|
||||
background-color: var(--prim-color-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
.button-disabled {
|
||||
@ -273,4 +277,12 @@ select:hover {
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.border-warning {
|
||||
border: 1.5px solid orange;
|
||||
}
|
||||
|
||||
.border-error {
|
||||
border: 1.5px solid red;
|
||||
}
|
Loading…
Reference in New Issue
Block a user