Merge pull request #96 from LucasDower/0.7-material-action

0.7 material action
This commit is contained in:
Lucas Dower 2023-01-20 20:44:35 +00:00 committed by GitHub
commit 8379ff51b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1693 additions and 1174 deletions

4
res/static/folder.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round" id="folder-svg">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2" />
</svg>

After

Width:  |  Height:  |  Size: 397 B

7
res/static/switch.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-switch-3" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round" id="switch-svg">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M3 17h2.397a5 5 0 0 0 4.096 -2.133l.177 -.253m3.66 -5.227l.177 -.254a5 5 0 0 1 4.096 -2.133h3.397" />
<path d="M18 4l3 3l-3 3" />
<path d="M3 7h2.397a5 5 0 0 1 4.096 2.133l4.014 5.734a5 5 0 0 0 4.096 2.133h3.397" />
<path d="M18 20l3 -3l-3 -3" />
</svg>

After

Width:  |  Height:  |  Size: 565 B

6
res/static/upload.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-upload" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round" id="upload-svg">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" />
<polyline points="7 9 12 4 17 9" />
<line x1="12" y1="4" x2="12" y2="16" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

View File

@ -3,37 +3,37 @@ import path from 'path';
import { FallableBehaviour } from './block_mesh';
import { ArcballCamera } from './camera';
import { RGBA, RGBAUtil } from './colour';
import { AppConfig } from './config';
import { EAppEvent, EventManager } from './event';
import { IExporter } from './exporters/base_exporter';
import { ExporterFactory, TExporters } from './exporters/exporters';
import { MaterialMap, MaterialType, SolidMaterial, TexturedMaterial } from './mesh';
import { Renderer } from './renderer';
import { MaterialMapManager } from './material-map';
import { MaterialType } from './mesh';
import { MeshType, Renderer } from './renderer';
import { StatusHandler, StatusMessage } from './status';
import { TextureFiltering } from './texture';
import { SolidMaterialUIElement, TextureMaterialUIElement } from './ui/elements/material';
import { OutputStyle } from './ui/elements/output';
import { SolidMaterialElement } from './ui/elements/solid_material_element';
import { TexturedMaterialElement } from './ui/elements/textured_material_element';
import { UI } from './ui/layout';
import { UIMessageBuilder, UITreeBuilder } from './ui/misc';
import { UIMessageBuilder } from './ui/misc';
import { ColourSpace, EAction } from './util';
import { ASSERT } from './util/error_util';
import { FileUtil } from './util/file_util';
import { LOG_ERROR, Logger } from './util/log_util';
import { AppPaths, PathUtil } from './util/path_util';
import { AppPaths } from './util/path_util';
import { Vector3 } from './vector';
import { TWorkerJob, WorkerController } from './worker_controller';
import { SetMaterialsParams, TFromWorkerMessage, TToWorkerMessage } from './worker_types';
import { TFromWorkerMessage, TToWorkerMessage } from './worker_types';
export class AppContext {
private _ui: UI;
private _workerController: WorkerController;
private _lastAction?: EAction;
public maxConstraint?: Vector3;
private _materialMap: MaterialMap;
private _materialManager: MaterialMapManager;
public constructor() {
this._materialMap = {};
this._materialManager = new MaterialMapManager(new Map());
Logger.Get.enableLogToFile();
Logger.Get.initLogFile('client');
@ -53,7 +53,7 @@ export class AppContext {
this._ui = new UI(this);
this._ui.build();
this._ui.registerEvents();
this._ui.disable(EAction.Voxelise);
this._ui.disable(EAction.Materials);
this._workerController = new WorkerController(path.resolve(__dirname, 'worker_interface.js'));
this._workerController.addJob({ id: 'init', payload: { action: 'Init', params: {} } });
@ -166,6 +166,8 @@ export class AppContext {
switch (action) {
case EAction.Import:
return this._import();
case EAction.Materials:
return this._materials();
case EAction.Voxelise:
return this._voxelise();
case EAction.Assign:
@ -185,7 +187,7 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'Import',
params: {
filepath: uiElements.input.getCachedValue(),
filepath: uiElements.input.getValue(),
},
};
@ -202,8 +204,7 @@ export class AppContext {
);
dimensions.mulScalar(AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT / 8.0).floor();
this.maxConstraint = dimensions;
this._materialMap = payload.result.materials;
this._onMaterialMapChanged();
this._materialManager = new MaterialMapManager(payload.result.materials);
if (payload.result.triangleCount < AppConfig.Get.RENDER_TRIANGLE_THRESHOLD) {
outputElement.setTaskInProgress('render', '[Renderer]: Processing...');
@ -212,186 +213,77 @@ export class AppContext {
const message = `Will not render mesh as its over ${AppConfig.Get.RENDER_TRIANGLE_THRESHOLD.toLocaleString()} triangles.`;
outputElement.setTaskComplete('render', '[Renderer]: Stopped', [message], 'warning');
}
this._updateMaterialsAction();
};
callback.bind(this);
return { id: 'Import', payload: payload, callback: callback };
}
private _materials(): TWorkerJob {
this._ui.getActionOutput(EAction.Materials)
.setTaskInProgress('action', '[Materials]: Loading...');
const payload: TToWorkerMessage = {
action: 'SetMaterials',
params: {
materials: this._materialManager.materials,
},
};
const callback = (payload: TFromWorkerMessage) => {
// This callback is managed through `AppContext::do`, therefore
// this callback is only called if the job is successful.
ASSERT(payload.action === 'SetMaterials');
const outputElement = this._ui.getActionOutput(EAction.Materials);
outputElement.setTaskComplete('action', '[Materials]: Updated', [], 'success');
// The material map shouldn't need updating because the materials
// returned from the worker **should** be the same as the materials
// sent.
{
//this._materialMap = payload.result.materials;
//this._onMaterialMapChanged();
}
payload.result.materialsChanged.forEach((materialName) => {
const material = this._materialManager.materials.get(materialName);
ASSERT(material !== undefined);
Renderer.Get.recreateMaterialBuffer(materialName, material);
Renderer.Get.setModelToUse(MeshType.TriangleMesh);
});
this._ui.enableTo(EAction.Voxelise);
};
return { id: 'Import', payload: payload, callback: callback };
}
private _sendMaterialsToWorker(callback: (result: SetMaterialsParams.Output) => void) {
const payload: TToWorkerMessage = {
action: 'SetMaterials',
params: {
materials: this._materialMap,
},
};
const job: TWorkerJob = {
id: 'SetMaterial',
payload: payload,
callback: (result: TFromWorkerMessage) => {
ASSERT(result.action === 'SetMaterials');
// TODO: Check the action didn't fail
this._materialMap = result.result.materials;
this._onMaterialMapChanged();
this._ui.enableTo(EAction.Voxelise);
callback(result.result);
},
};
this._workerController.addJob(job);
this._ui.disableAll();
}
public onMaterialTypeSwitched(materialName: string) {
const oldMaterial = this._materialMap[materialName];
if (oldMaterial.type == MaterialType.textured) {
this._materialMap[materialName] = {
type: MaterialType.solid,
colour: RGBAUtil.random(),
edited: true,
canBeTextured: oldMaterial.canBeTextured,
set: false,
open: true,
};
} else {
this._materialMap[materialName] = {
type: MaterialType.textured,
alphaFactor: 1.0,
path: PathUtil.join(AppPaths.Get.static, 'debug.png'),
edited: true,
canBeTextured: oldMaterial.canBeTextured,
extension: 'repeat',
interpolation: 'linear',
open: true,
};
}
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
// TODO: Check the action didn't fail
Renderer.Get.recreateMaterialBuffer(materialName, result.materials[materialName]);
});
}
public onMaterialExtensionChanged(materialName: string) {
const oldMaterial = this._materialMap[materialName];
ASSERT(oldMaterial.type === MaterialType.textured);
this._materialMap[materialName] = {
type: MaterialType.textured,
alphaFactor: oldMaterial.alphaFactor,
alphaPath: oldMaterial.alphaPath,
path: oldMaterial.path,
edited: true,
canBeTextured: oldMaterial.canBeTextured,
extension: oldMaterial.extension === 'clamp' ? 'repeat' : 'clamp',
interpolation: oldMaterial.interpolation,
open: true,
};
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
// TODO: Check the action didn't fail
Renderer.Get.recreateMaterialBuffer(materialName, result.materials[materialName]);
});
}
public onMaterialInterpolationChanged(materialName: string) {
const oldMaterial = this._materialMap[materialName];
ASSERT(oldMaterial.type === MaterialType.textured);
this._materialMap[materialName] = {
type: MaterialType.textured,
alphaFactor: oldMaterial.alphaFactor,
alphaPath: oldMaterial.alphaPath,
path: oldMaterial.path,
edited: true,
canBeTextured: oldMaterial.canBeTextured,
extension: oldMaterial.extension,
interpolation: oldMaterial.interpolation === 'linear' ? 'nearest' : 'linear',
open: true,
};
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
// TODO: Check the action didn't fail
Renderer.Get.recreateMaterialBuffer(materialName, result.materials[materialName]);
});
}
public onMaterialTextureReplace(materialName: string, newTexturePath: string) {
const oldMaterial = this._materialMap[materialName];
ASSERT(oldMaterial.type === MaterialType.textured);
this._materialMap[materialName] = {
type: MaterialType.textured,
alphaFactor: oldMaterial.alphaFactor,
alphaPath: oldMaterial.alphaPath,
path: newTexturePath,
edited: true,
canBeTextured: oldMaterial.canBeTextured,
extension: oldMaterial.extension,
interpolation: oldMaterial.interpolation,
open: true,
};
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
Renderer.Get.updateMeshMaterialTexture(materialName, result.materials[materialName] as TexturedMaterial);
});
}
public onMaterialColourChanged(materialName: string, newColour: RGBA) {
ASSERT(this._materialMap[materialName].type === MaterialType.solid);
const oldMaterial = this._materialMap[materialName] as TexturedMaterial;
this._materialMap[materialName] = {
type: MaterialType.solid,
colour: newColour,
edited: true,
canBeTextured: oldMaterial.canBeTextured,
set: true,
open: true,
};
this._sendMaterialsToWorker((result: SetMaterialsParams.Output) => {
Renderer.Get.recreateMaterialBuffer(materialName, result.materials[materialName] as SolidMaterial);
});
}
private _onMaterialMapChanged() {
// Add material information to the output log
const outputElement = this._ui.getActionOutput(EAction.Import);
const messageBuilder = outputElement.getMessage();
const tree = UITreeBuilder.create('Materials');
tree.toggleIsOpen();
for (const [materialName, material] of Object.entries(this._materialMap)) {
if (materialName === 'DEFAULT_UNASSIGNED') {
continue;
}
const subTree = UITreeBuilder.create(material.edited ? `<i>${materialName}*</i>` : materialName);
if (material.open) {
subTree.toggleIsOpen();
}
private _updateMaterialsAction() {
this._ui.layoutDull['materials'].elements = {};
this._ui.layoutDull['materials'].elementsOrder = [];
this._materialManager.materials.forEach((material, materialName) => {
if (material.type === MaterialType.solid) {
const uiElement = new SolidMaterialUIElement(materialName, this, material);
subTree.addChild({ html: uiElement.buildHTML(), warning: uiElement.hasWarning()}, () => {
uiElement.registerEvents();
});
this._ui.layoutDull['materials'].elements[`mat_${materialName}`] = new SolidMaterialElement(materialName, material)
.setLabel(materialName)
.onChangeTypeDelegate(() => {
this._materialManager.changeMaterialType(materialName, MaterialType.textured);
this._updateMaterialsAction();
});
} else {
const uiElement = new TextureMaterialUIElement(materialName, this, material);
subTree.addChild({ html: uiElement.buildHTML(), warning: uiElement.hasWarning()}, () => {
uiElement.registerEvents();
});
this._ui.layoutDull['materials'].elements[`mat_${materialName}`] = new TexturedMaterialElement(materialName, material)
.setLabel(materialName)
.onChangeTypeDelegate(() => {
this._materialManager.changeMaterialType(materialName, MaterialType.solid);
this._updateMaterialsAction();
});
}
tree.addChild(subTree);
}
messageBuilder.setTree('materials', tree);
outputElement.updateMessage();
this._ui.layoutDull['materials'].elementsOrder.push(`mat_${materialName}`);
});
this._ui.refreshSubcomponents(this._ui.layoutDull['materials']);
}
private _renderMesh(): TWorkerJob {
@ -403,7 +295,7 @@ export class AppContext {
const callback = (payload: TFromWorkerMessage) => {
// This callback is not managed through `AppContext::do`, therefore
// we need to check the payload is not an error
this._ui.enableTo(EAction.Voxelise);
this._ui.enableTo(EAction.Materials);
switch (payload.action) {
case 'KnownError':
@ -443,12 +335,12 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'Voxelise',
params: {
constraintAxis: uiElements.constraintAxis.getCachedValue(),
voxeliser: uiElements.voxeliser.getCachedValue(),
size: uiElements.size.getCachedValue(),
useMultisampleColouring: uiElements.multisampleColouring.getCachedValue(),
enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue(),
voxelOverlapRule: uiElements.voxelOverlapRule.getCachedValue(),
constraintAxis: uiElements.constraintAxis.getValue(),
voxeliser: uiElements.voxeliser.getValue(),
size: uiElements.size.getValue(),
useMultisampleColouring: uiElements.multisampleColouring.getValue(),
enableAmbientOcclusion: uiElements.ambientOcclusion.getValue(),
voxelOverlapRule: uiElements.voxelOverlapRule.getValue(),
},
};
@ -471,8 +363,8 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'RenderNextVoxelMeshChunk',
params: {
enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue(),
desiredHeight: uiElements.size.getCachedValue(),
enableAmbientOcclusion: uiElements.ambientOcclusion.getValue(),
desiredHeight: uiElements.size.getValue(),
},
};
@ -522,21 +414,21 @@ export class AppContext {
this._ui.getActionOutput(EAction.Assign)
.setTaskInProgress('action', '[Block Mesh]: Loading...');
Renderer.Get.setLightingAvailable(uiElements.calculateLighting.getCachedValue());
Renderer.Get.setLightingAvailable(uiElements.calculateLighting.getValue());
const payload: TToWorkerMessage = {
action: 'Assign',
params: {
textureAtlas: uiElements.textureAtlas.getCachedValue(),
blockPalette: uiElements.blockPalette.getCachedValue(),
dithering: uiElements.dithering.getCachedValue(),
textureAtlas: uiElements.textureAtlas.getValue(),
blockPalette: uiElements.blockPalette.getValue(),
dithering: uiElements.dithering.getValue(),
colourSpace: ColourSpace.RGB,
fallable: uiElements.fallable.getCachedValue() as FallableBehaviour,
resolution: Math.pow(2, uiElements.colourAccuracy.getCachedValue()),
calculateLighting: uiElements.calculateLighting.getCachedValue(),
lightThreshold: uiElements.lightThreshold.getCachedValue(),
contextualAveraging: uiElements.contextualAveraging.getCachedValue(),
errorWeight: uiElements.errorWeight.getCachedValue() / 10,
fallable: uiElements.fallable.getValue() as FallableBehaviour,
resolution: Math.pow(2, uiElements.colourAccuracy.getValue()),
calculateLighting: uiElements.calculateLighting.getValue(),
lightThreshold: uiElements.lightThreshold.getValue(),
contextualAveraging: uiElements.contextualAveraging.getValue(),
errorWeight: uiElements.errorWeight.getValue() / 10,
},
};
@ -560,7 +452,7 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'RenderNextBlockMeshChunk',
params: {
textureAtlas: uiElements.textureAtlas.getCachedValue(),
textureAtlas: uiElements.textureAtlas.getValue(),
},
};
@ -605,7 +497,7 @@ export class AppContext {
}
private _export(): (TWorkerJob | undefined) {
const exporterID: TExporters = this._ui.layout.export.elements.export.getCachedValue();
const exporterID: TExporters = this._ui.layout.export.elements.export.getValue();
const exporter: IExporter = ExporterFactory.GetExporter(exporterID);
const filepath = remote.dialog.showSaveDialogSync({

View File

@ -243,6 +243,8 @@ export class BufferGenerator {
}
const material = mesh.getMaterialByName(materialName);
ASSERT(material !== undefined);
materialBuffers.push({
buffer: materialBuffer,
material: material,

View File

@ -21,9 +21,19 @@ export class ObjImporter extends IImporter {
private _uvs: UV[] = [];
private _tris: Tri[] = [];
private _materials: { [key: string]: (SolidMaterial | TexturedMaterial) } = {
'DEFAULT_UNASSIGNED': { type: MaterialType.solid, colour: RGBAColours.WHITE, edited: true, canBeTextured: false, set: true, open: false },
};
private _materials: Map<string, SolidMaterial | TexturedMaterial>;
public constructor() {
super();
this._materials = new Map();
this._materials.set('DEFAULT_UNASSIGNED', {
type: MaterialType.solid,
colour: RGBAColours.WHITE,
canBeTextured: false,
needsAttention: false,
});
}
private _mtlLibs: string[] = [];
private _currentMaterialName: string = 'DEFAULT_UNASSIGNED';
@ -276,8 +286,6 @@ export class ObjImporter extends IImporter {
];
override parseFile(filePath: string) {
ASSERT(path.isAbsolute(filePath), `ObjImporter: ${filePath} not absolute`);
this._objPath = path.parse(filePath);
this._parseOBJ(filePath);
@ -302,8 +310,11 @@ export class ObjImporter extends IImporter {
}
private _parseOBJ(path: string) {
if (path === '') {
throw new AppError(`No filepath given`);
}
if (!fs.existsSync(path)) {
throw new AppError(`Could not find ${path}`);
throw new AppError(`Could not find '${path}'`);
}
const fileContents = fs.readFileSync(path, 'utf8');
if (fileContents.includes('<27>')) {
@ -391,20 +402,19 @@ export class ObjImporter extends IImporter {
private _addCurrentMaterial() {
if (this._materialReady && this._currentMaterialName !== '') {
if (this._currentTexture !== '') {
this._materials[this._currentMaterialName] = {
this._materials.set(this._currentMaterialName, {
type: MaterialType.textured,
path: this._currentTexture,
alphaPath: this._currentTransparencyTexture === '' ? undefined : this._currentTransparencyTexture,
alphaFactor: this._currentAlpha,
edited: false,
canBeTextured: true,
extension: 'repeat',
interpolation: 'linear',
open: false,
};
needsAttention: false,
});
this._currentTransparencyTexture = '';
} else {
this._materials[this._currentMaterialName] = {
this._materials.set(this._currentMaterialName, {
type: MaterialType.solid,
colour: {
r: this._currentColour.r,
@ -412,11 +422,9 @@ export class ObjImporter extends IImporter {
b: this._currentColour.b,
a: this._currentAlpha,
},
edited: false,
canBeTextured: false,
set: true,
open: false,
};
needsAttention: false,
});
}
this._currentAlpha = 1.0;
}

48
src/material-map.ts Normal file
View File

@ -0,0 +1,48 @@
import { RGBAColours, RGBAUtil } from './colour';
import { MaterialMap, MaterialType } from './mesh';
import { ASSERT } from './util/error_util';
import { AppPaths, PathUtil } from './util/path_util';
export class MaterialMapManager {
public materials: MaterialMap;
public constructor(materials: MaterialMap) {
this.materials = materials;
}
/**
* Convert a material to a new type, i.e. textured to solid.
* Will return if the material is already the given type.
*/
public changeMaterialType(materialName: string, newMaterialType: MaterialType) {
const currentMaterial = this.materials.get(materialName);
ASSERT(currentMaterial !== undefined, 'Cannot change material type of non-existent material');
if (currentMaterial.type === newMaterialType) {
return;
}
switch (newMaterialType) {
case MaterialType.solid:
this.materials.set(materialName, {
type: MaterialType.solid,
colour: RGBAColours.MAGENTA,
canBeTextured: currentMaterial.canBeTextured,
needsAttention: true,
});
break;
case MaterialType.textured:
this.materials.set(materialName, {
type: MaterialType.textured,
alphaFactor: 1.0,
alphaPath: undefined,
canBeTextured: currentMaterial.canBeTextured,
extension: 'repeat',
interpolation: 'linear',
needsAttention: true,
path: PathUtil.join(AppPaths.Get.static, 'debug.png'),
});
break;
}
}
}

View File

@ -29,15 +29,13 @@ export interface Tri {
export enum MaterialType { solid, textured }
/* eslint-enable */
type BaseMaterial = {
edited: boolean,
canBeTextured: boolean,
open: boolean, // TODO: Refactor, this is UI specific, shouldn't exist here
needsAttention: boolean, // True if the user should make edits to this material
}
export type SolidMaterial = BaseMaterial & {
type: MaterialType.solid,
colour: RGBA,
set: boolean,
}
export type TexturedMaterial = BaseMaterial & {
type: MaterialType.textured,
@ -47,7 +45,7 @@ export type TexturedMaterial = BaseMaterial & {
interpolation: TTexelInterpolation,
extension: TTexelExtension,
}
export type MaterialMap = { [key: string]: (SolidMaterial | TexturedMaterial) };
export type MaterialMap = Map<string, SolidMaterial | TexturedMaterial>;
export class Mesh {
public readonly id: string;
@ -159,7 +157,7 @@ export class Mesh {
}
private _checkMaterials() {
if (Object.keys(this._materials).length === 0) {
if (this._materials.size === 0) {
throw new AppError('Loaded mesh has no materials');
}
@ -168,30 +166,26 @@ export class Mesh {
const usedMaterials = new Set<string>();
const missingMaterials = new Set<string>();
for (const tri of this._tris) {
if (!(tri.material in this._materials)) {
if (!this._materials.has(tri.material)) {
// This triangle makes use of a material we don't have info about
// Try infer details about this material and add it to our materials
if (tri.texcoordIndices === undefined) {
// No texcoords are defined, therefore make a solid material
this._materials[tri.material] = {
this._materials.set(tri.material, {
type: MaterialType.solid,
colour: RGBAColours.MAGENTA,
edited: true,
canBeTextured: false,
set: false,
open: false,
};
needsAttention: true,
});
} else {
// Texcoords exist
this._materials[tri.material] = {
this._materials.set(tri.material, {
type: MaterialType.solid,
colour: RGBAUtil.random(),
edited: true,
canBeTextured: true,
set: false,
open: false,
};
needsAttention: true,
});
}
missingMaterials.add(tri.material);
@ -201,14 +195,15 @@ export class Mesh {
}
const materialsToRemove = new Set<string>();
for (const materialName in this._materials) {
this._materials.forEach((material, materialName) => {
if (!usedMaterials.has(materialName)) {
LOG_WARN(`'${materialName}' is not used by any triangles, removing...`);
materialsToRemove.add(materialName);
}
}
});
materialsToRemove.forEach((materialName) => {
delete this._materials[materialName];
this._materials.delete(materialName);
});
if (missingMaterials.size > 0) {
@ -216,20 +211,17 @@ export class Mesh {
}
// Check texture paths are absolute and exist
for (const materialName in this._materials) {
const material = this._materials[materialName];
this._materials.forEach((material, materialName) => {
if (material.type === MaterialType.textured) {
ASSERT(path.isAbsolute(material.path), 'Material texture path not absolute');
if (!fs.existsSync(material.path)) {
LOG_WARN(`Could not find ${material.path} for material ${materialName}, changing to solid-white material`);
this._materials[materialName] = {
LOG_WARN(`Could not find ${material.path} for material ${materialName}, changing to solid material`);
this._materials.set(materialName, {
type: MaterialType.solid,
colour: RGBAColours.WHITE,
edited: true,
colour: RGBAColours.MAGENTA,
canBeTextured: true,
set: false,
open: false,
};
needsAttention: true,
});
} else {
const parsedPath = path.parse(material.path);
if (parsedPath.ext === '.tga') {
@ -237,7 +229,7 @@ export class Mesh {
}
}
}
}
});
}
private _centreMesh() {
@ -266,7 +258,9 @@ export class Mesh {
private _loadTextures() {
this._loadedTextures = {};
for (const tri of this._tris) {
const material = this._materials[tri.material];
const material = this._materials.get(tri.material);
ASSERT(material !== undefined, 'Triangle uses a material that doesn\'t exist in the material map');
if (material.type == MaterialType.textured) {
if (!(tri.material in this._loadedTextures)) {
this._loadedTextures[tri.material] = new Texture(material.path, material.alphaPath);
@ -348,7 +342,7 @@ export class Mesh {
}
public getMaterialByName(materialName: string) {
return this._materials[materialName];
return this._materials.get(materialName);
}
public setMaterials(materialMap: MaterialMap) {
@ -361,9 +355,8 @@ export class Mesh {
}
public sampleTextureMaterial(materialName: string, uv: UV): RGBA {
ASSERT(materialName in this._materials, `Sampling material that does not exist: ${materialName}`);
const material = this._materials[materialName];
const material = this._materials.get(materialName);
ASSERT(material !== undefined, `Sampling material that does not exist: ${materialName}`);
ASSERT(material.type === MaterialType.textured, 'Sampling texture material of non-texture material');
ASSERT(materialName in this._loadedTextures, 'Sampling texture that is not loaded');

View File

@ -198,7 +198,12 @@ export class Renderer {
if (material.type === MaterialType.solid) {
this._materialBuffers.set(materialName, {
buffer: oldBuffer.buffer,
material: material,
material: {
type: MaterialType.solid,
colour: RGBAUtil.copy(material.colour),
needsAttention: material.needsAttention,
canBeTextured: material.canBeTextured,
},
numElements: oldBuffer.numElements,
materialName: materialName,
});
@ -208,7 +213,6 @@ export class Renderer {
material: {
type: MaterialType.textured,
path: material.path,
edited: material.edited,
canBeTextured: material.canBeTextured,
interpolation: material.interpolation,
extension: material.extension,
@ -226,7 +230,7 @@ export class Renderer {
wrap: material.extension === 'clamp' ? this._gl.CLAMP_TO_EDGE : this._gl.REPEAT,
}) : undefined,
useAlphaChannel: material.alphaPath ? new Texture(material.path, material.alphaPath)._useAlphaChannel() : undefined,
open: material.open,
needsAttention: material.needsAttention,
},
numElements: oldBuffer.numElements,
materialName: materialName,
@ -240,7 +244,6 @@ export class Renderer {
buffer.material = {
type: MaterialType.textured,
path: material.path,
edited: material.edited,
interpolation: material.interpolation,
extension: material.extension,
canBeTextured: material.canBeTextured,
@ -258,7 +261,7 @@ export class Renderer {
wrap: material.extension === 'clamp' ? this._gl.CLAMP_TO_EDGE : this._gl.REPEAT,
}) : undefined,
useAlphaChannel: material.alphaPath ? new Texture(material.path, material.alphaPath)._useAlphaChannel() : undefined,
open: material.open,
needsAttention: material.needsAttention,
};
return;
}
@ -281,7 +284,6 @@ export class Renderer {
this._materialBuffers.set(materialName, {
buffer: twgl.createBufferInfoFromArrays(this._gl, buffer),
material: {
edited: material.edited,
canBeTextured: material.canBeTextured,
type: MaterialType.textured,
interpolation: material.interpolation,
@ -301,7 +303,7 @@ export class Renderer {
wrap: material.extension === 'clamp' ? this._gl.CLAMP_TO_EDGE : this._gl.REPEAT,
}) : undefined,
useAlphaChannel: material.alphaPath ? new Texture(material.path, material.alphaPath)._useAlphaChannel() : undefined,
open: material.open,
needsAttention: material.needsAttention,
},
numElements: numElements,
materialName: materialName,

View File

@ -60,6 +60,8 @@ export class StatusHandler {
switch (action) {
case EAction.Import:
return '[Importer]: Loaded';
case EAction.Materials:
return '[Materials]: Updated';
case EAction.Voxelise:
return '[Voxeliser]: Succeeded';
case EAction.Assign:
@ -67,7 +69,7 @@ export class StatusHandler {
case EAction.Export:
return '[Exporter]: Saved';
default:
ASSERT(false);
ASSERT(false, 'Unknown action');
}
}
@ -75,6 +77,8 @@ export class StatusHandler {
switch (action) {
case EAction.Import:
return '[Importer]: Failed';
case EAction.Materials:
return '[Materials]: Failed';
case EAction.Voxelise:
return '[Voxeliser]: Failed';
case EAction.Assign:
@ -82,7 +86,7 @@ export class StatusHandler {
case EAction.Export:
return '[Exporter]: Failed';
default:
ASSERT(false);
ASSERT(false, 'Unknown action');
}
}

View File

@ -1,49 +0,0 @@
import { ASSERT } from '../../util/error_util';
export abstract class BaseUIElement<Type> {
protected _id: string;
protected _label: string;
protected _isEnabled: boolean;
protected _value?: Type;
protected _cachedValue?: any;
constructor(label: string) {
this._id = '_' + Math.random().toString(16);
this._label = label;
this._isEnabled = true;
}
public setEnabled(isEnabled: boolean, isGroupEnable: boolean = true) {
if (isEnabled && isGroupEnable && !this._obeyGroupEnables) {
return;
}
this._isEnabled = isEnabled;
this._onEnabledChanged();
}
public getCachedValue(): Type {
ASSERT(this._cachedValue !== undefined, 'Attempting to access value before cached');
return this._cachedValue as Type;
}
protected getValue(): Type {
ASSERT(this._value !== undefined);
return this._value;
}
public cacheValue() {
this._cachedValue = this.getValue();
}
public abstract generateHTML(): string;
public abstract registerEvents(): void;
protected abstract _onEnabledChanged(): void;
private _obeyGroupEnables: boolean = true;
public setObeyGroupEnables(shouldListen: boolean) {
this._obeyGroupEnables = shouldListen;
return this;
}
}

View File

@ -0,0 +1,85 @@
import { getRandomID } from '../../util';
import { UIUtil } from '../../util/ui_util';
/**
* The base UI class from which user interactable DOM elements are built from.
* Each `BaseUIElement` can be enabled/disabled.
*/
export abstract class BaseUIElement<T> {
private _id: string;
private _isEnabled: boolean;
private _obeyGroupEnables: boolean;
public constructor() {
this._id = getRandomID();
this._isEnabled = true;
this._obeyGroupEnables = true;
}
/**
* Get whether or not this UI element is interactable.
*/
public getEnabled() {
return this._isEnabled;
}
/**
* Set whether or not this UI element is interactable.
*/
public setEnabled(isEnabled: boolean, isGroupEnable: boolean = true) {
if (isGroupEnable && !this._obeyGroupEnables) {
return;
}
this._isEnabled = isEnabled;
this._onEnabledChanged();
}
/**
* Sets whether or not this element should be enabled when the group
* is it apart of becomes enabled. This is useful if an element should
* only be enabled if another element has a particular value. If this is
* false then there needs to be a some event added to manually enable this
* element.
*/
public setShouldObeyGroupEnables(obey: boolean) {
this._obeyGroupEnables = obey;
}
/**
* The actual HTML that represents this UI element. It is recommended to
* give the outermost element that ID generated for this BaseUIElement so
* that `getElement()` returns all elements created here.
*/
public abstract generateHTML(): string;
/**
* A delegate that is called after the UI element has been added to the DOM.
* Calls to `addEventListener` should be placed here.
*/
public abstract registerEvents(): void;
public finalise(): void {
this._onEnabledChanged();
}
/**
* Returns the actual DOM element that this BaseUIElement refers to.
* Calling this before the element is created (i.e. before `generateHTML`)
* is called will throw an error.
*/
protected _getElement() {
return UIUtil.getElementById(this._id) as T;
}
/**
* Each BaseUIElement is assignd an ID that can be used a DOM element with.
*/
protected _getId() {
return this._id;
}
/**
* A delegate that is called when the enabled status is changed.
*/
protected abstract _onEnabledChanged(): void;
}

View File

@ -1,59 +1,53 @@
import { ASSERT } from '../../util/error_util';
import { BaseUIElement } from './base';
import { UIUtil } from '../../util/ui_util';
import { BaseUIElement } from './base_element';
export class ButtonElement extends BaseUIElement<any> {
export class ButtonElement extends BaseUIElement<HTMLDivElement> {
private _label: string;
private _onClick: () => void;
public constructor(label: string, onClick: () => void) {
super(label);
this._onClick = onClick;
this._isEnabled = true;
public constructor() {
super();
this._label = 'Unknown';
this._onClick = () => { };
}
public generateHTML() {
return `
<div class="button" id="${this._id}">
<div class="button-label">${this._label}</div>
<div class="button-progress" id="${this._id}-progress"></div>
</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.remove('button-disabled');
} else {
element.classList.add('button-disabled');
}
}
public setLabelOverride(label: string) {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null, 'Updating label override of element that does not exist');
element.innerHTML = label;
/**
* Sets the delegate that is called when this button is clicked.
*/
public setOnClick(delegate: () => void) {
this._onClick = delegate;
return this;
}
public removeLabelOverride() {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null, 'Removing label override of element that does not exist');
/**
* Sets the label of this button.
*/
public setLabel(label: string) {
this._label = label;
return this;
}
element.innerHTML = this._label;
/**
* Override the current label with a new value.
*/
public setLabelOverride(label: string) {
this._getElement().innerHTML = label;
return this;
}
/**
* Remove the label override and set the label back to its default
*/
public removeLabelOverride() {
this._getElement().innerHTML = this._label;
return this;
}
/**
* Start the loading animation
*/
public startLoading() {
this._getElement().classList.add('button-loading');
return this;
}
@ -62,26 +56,48 @@ export class ButtonElement extends BaseUIElement<any> {
* @param progress A number between 0.0 and 1.0 inclusive.
*/
public setProgress(progress: number) {
const element = document.getElementById(this._id + '-progress') as HTMLDivElement;
ASSERT(element !== null);
element.style.width = `${progress * 100}%`;
return this;
}
public startLoading() {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
element.classList.add('button-loading');
const progressBarElement = UIUtil.getElementById(this._getProgressBarId());
progressBarElement.style.width = `${progress * 100}%`;
return this;
}
/**
* Stop the loading animation
*/
public stopLoading() {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
element.classList.remove('button-loading');
this._getElement().classList.remove('button-loading');
return this;
}
public override generateHTML() {
return `
<div class="button" id="${this._getId()}">
<div class="button-label">${this._label}</div>
<div class="button-progress" id="${this._getProgressBarId()}"></div>
</div>
`;
}
public override registerEvents(): void {
this._getElement().addEventListener('click', () => {
if (this.getEnabled()) {
this._onClick?.();
}
});
}
protected override _onEnabledChanged() {
if (this.getEnabled()) {
this._getElement().classList.remove('button-disabled');
} else {
this._getElement().classList.add('button-disabled');
}
}
/**
* Gets the ID of the DOM element for the button's progress bar.
*/
private _getProgressBarId() {
return this._getId() + '-progress';
}
}

View File

@ -1,95 +1,86 @@
import { getRandomID } from '../../util';
import { ASSERT } from '../../util/error_util';
import { LabelledElement } from './labelled_element';
import { UIUtil } from '../../util/ui_util';
import { ConfigUIElement } from './config_element';
export class CheckboxElement extends LabelledElement<boolean> {
private _checkboxId: string;
private _checkboxPipId: string;
private _checkboxTextId: string;
private _onText: string;
private _offText: string;
export class CheckboxElement extends ConfigUIElement<boolean, HTMLSelectElement> {
private _labelChecked: string;
private _labelUnchecked: string;
public constructor(label: string, value: boolean, onText: string, offText: string) {
super(label);
this._checkboxId = getRandomID();
this._checkboxPipId = getRandomID();
this._checkboxTextId = getRandomID();
this._value = value;
this._onText = onText;
this._offText = offText;
public constructor() {
super(false);
this._labelChecked = 'On';
this._labelUnchecked = 'Off';
}
protected generateInnerHTML(): string {
return `
<div class="checkbox" id="${this._checkboxId}">
<svg id="${this._checkboxPipId}" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>
</div>
<div class="checkbox-text" id="${this._checkboxTextId}">${this.getValue() ? this._onText : this._offText}</div>
`;
public setCheckedText(label: string) {
this._labelChecked = label;
return this;
}
public registerEvents(): void {
const checkboxElement = document.getElementById(this._checkboxId);
const checkboxPipElement = document.getElementById(this._checkboxPipId);
ASSERT(checkboxElement !== null && checkboxPipElement !== null);
public setUncheckedText(label: string) {
this._labelUnchecked = label;
return this;
}
checkboxElement?.addEventListener('mouseenter', () => {
if (this._isEnabled) {
public override registerEvents(): void {
const checkboxElement = this._getElement();
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
checkboxElement.addEventListener('mouseenter', () => {
if (this.getEnabled()) {
checkboxElement.classList.add('checkbox-hover');
checkboxPipElement.classList.add('checkbox-pip-hover');
}
});
checkboxElement?.addEventListener('mouseleave', () => {
if (this._isEnabled) {
checkboxElement.addEventListener('mouseleave', () => {
if (this.getEnabled()) {
checkboxElement.classList.remove('checkbox-hover');
checkboxPipElement.classList.remove('checkbox-pip-hover');
}
});
checkboxElement.addEventListener('click', () => {
if (this._isEnabled) {
this._value = !this._value;
this._onValueChanged();
if (this.getEnabled()) {
this._setValue(!this.getValue());
}
});
}
private _onValueChanged() {
const checkboxElement = document.getElementById(this._checkboxId);
const checkboxPipElement = document.getElementById(this._checkboxPipId);
ASSERT(checkboxElement !== null && checkboxPipElement !== null);
const checkboxTextElement = document.getElementById(this._checkboxTextId);
ASSERT(checkboxTextElement !== null);
protected override _generateInnerHTML(): string {
return `
<div class="checkbox" id="${this._getId()}">
<svg id="${this._getPipId()}" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>
</div>
<div class="checkbox-text" id="${this._getLabelId()}">${this.getValue() ? this._labelChecked : this._labelUnchecked}</div>
`;
}
checkboxTextElement.innerHTML = this.getValue() ? this._onText : this._offText;
protected override _onValueChanged(): void {
const checkboxElement = this._getElement();
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
const checkboxTextElement = UIUtil.getElementById(this._getLabelId());
checkboxTextElement.innerHTML = this.getValue() ? this._labelChecked : this._labelUnchecked;
checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden';
if (this._isEnabled) {
if (this.getEnabled()) {
checkboxElement.classList.remove('checkbox-disabled');
} else {
checkboxElement.classList.add('checkbox-disabled');
}
this._onValueChangedDelegate?.(this._value!);
}
protected _onEnabledChanged(): void {
protected override _onEnabledChanged(): void {
super._onEnabledChanged();
const checkboxElement = document.getElementById(this._checkboxId);
const checkboxPipElement = document.getElementById(this._checkboxPipId);
ASSERT(checkboxElement !== null && checkboxPipElement !== null);
const checkboxTextElement = document.getElementById(this._checkboxTextId);
ASSERT(checkboxTextElement !== null);
const checkboxElement = this._getElement();
const checkboxPipElement = UIUtil.getElementById(this._getPipId());
const checkboxTextElement = UIUtil.getElementById(this._getLabelId());
checkboxTextElement.innerHTML = this.getValue() ? this._onText : this._offText;
checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden';
if (this._isEnabled) {
if (this.getEnabled()) {
checkboxElement.classList.remove('checkbox-disabled');
checkboxTextElement.classList.remove('checkbox-text-disabled');
checkboxPipElement.classList.remove('checkbox-pip-disabled');
@ -98,13 +89,13 @@ export class CheckboxElement extends LabelledElement<boolean> {
checkboxTextElement.classList.add('checkbox-text-disabled');
checkboxPipElement.classList.add('checkbox-pip-disabled');
}
this._onValueChangedDelegate?.(this._value!);
}
private _onValueChangedDelegate?: (value: boolean) => void;
public onValueChanged(delegate: (value: boolean) => void) {
this._onValueChangedDelegate = delegate;
return this;
private _getPipId() {
return this._getId() + '-pip';
}
private _getLabelId() {
return this._getId() + '-label';
}
}

View File

@ -1,65 +1,91 @@
import { EAppEvent, EventManager } from '../../event';
import { ASSERT } from '../../util/error_util';
import { LabelledElement } from './labelled_element';
import { ConfigUIElement } from './config_element';
export type ComboBoxItem<T> = {
id: T;
payload: T;
displayText: string;
tooltip?: string;
}
export class ComboBoxElement<T> extends LabelledElement<T> {
export class ComboBoxElement<T> extends ConfigUIElement<T, HTMLSelectElement> {
private _items: ComboBoxItem<T>[];
private _small: boolean;
public constructor(id: string, items: ComboBoxItem<T>[]) {
super(id);
this._items = items;
public constructor() {
super();
this._items = [];
this._small = false;
}
public generateInnerHTML() {
public addItems(items: ComboBoxItem<T>[]) {
items.forEach((item) => {
this.addItem(item);
});
return this;
}
public addItem(item: ComboBoxItem<T>) {
this._items.push(item);
this._setValue(this._items[0].payload);
return this;
}
public setSmall() {
this._small = true;
return this;
}
public override registerEvents(): void {
this._getElement().addEventListener('change', (e: Event) => {
const selectedValue = this._items[this._getElement().selectedIndex].payload;
this._setValue(selectedValue);
});
}
/*
public override setDefaultValue(value: T): this {
super.setDefaultValue(value);
const element = this._getElement();
const newSelectedIndex = this._items.findIndex((item) => item.payload === value);
ASSERT(newSelectedIndex !== -1, 'Invalid selected index');
element.selectedIndex = newSelectedIndex;
return this;
}
*/
public override _generateInnerHTML() {
ASSERT(this._items.length > 0);
let itemsHTML = '';
for (const item of this._items) {
itemsHTML += `<option value="${item.id}" title="${item.tooltip || ''}">${item.displayText}</option>`;
itemsHTML += `<option value="${item.payload}" title="${item.tooltip || ''}">${item.displayText}</option>`;
}
return `
<select name="${this._id}" id="${this._id}">
<select class="${this._small ? 'height-small' : 'height-normal'}" name="${this._getId()}" id="${this._getId()}">
${itemsHTML}
</select>
`;
}
public registerEvents(): void {
const element = document.getElementById(this._id) as HTMLSelectElement;
ASSERT(element !== null);
element.addEventListener('change', () => {
if (this._onValueChangedDelegate) {
//EventManager.Get.broadcast(EAppEvent.onComboBoxChanged, element.value);
this._onValueChangedDelegate(element.value);
}
});
}
protected getValue() {
const element = document.getElementById(this._id) as HTMLSelectElement;
ASSERT(element !== null);
return this._items[element.selectedIndex].id;
}
protected _onEnabledChanged() {
protected override _onEnabledChanged() {
super._onEnabledChanged();
const element = document.getElementById(this._id) as HTMLSelectElement;
ASSERT(element !== null);
element.disabled = !this._isEnabled;
this._onValueChangedDelegate?.(element.value);
this._getElement().disabled = !this.getEnabled();
}
private _onValueChangedDelegate?: (value: any) => void;
public onValueChanged(delegate: (value: any) => void) {
this._onValueChangedDelegate = delegate;
return this;
// TODO: Subproperty combo boxes are not updating values when changed!!!
protected override _onValueChanged(): void {
}
public override finalise(): void {
const selectedIndex = this._items.findIndex((item) => item.payload === this.getValue());
const element = this._getElement();
ASSERT(selectedIndex !== -1, 'Invalid selected index');
element.selectedIndex = selectedIndex;
}
}

View File

@ -0,0 +1,119 @@
import { ASSERT } from '../../util/error_util';
import { BaseUIElement } from './base_element';
import { LabelElement } from './label';
/**
* A `ConfigUIElement` is a UI element that has a value the user can change.
* For example, sliders, comboboxes and checkboxes are `ConfigUIElement`.
*/
export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
private _label: string;
private _description?: string;
private _labelElement: LabelElement;
private _hasLabel: boolean;
private _value?: T;
private _cachedValue?: T;
private _onValueChangedListeners: Array<(newValue: T) => void>;
public constructor(defaultValue?: T) {
super();
this._value = defaultValue;
this._label = 'unknown';
this._hasLabel = false;
this._labelElement = new LabelElement(this._label, this._description);
this._onValueChangedListeners = [];
}
public setDefaultValue(value: T) {
this._value = value;
return this;
}
public setLabel(label: string) {
this._hasLabel = true;
this._label = label;
this._labelElement = new LabelElement(this._label, this._description);
return this;
}
public setDescription(text: string) {
this._labelElement = new LabelElement(this._label, text);
return this;
}
/**
* Caches the current value.
*/
public cacheValue() {
this._cachedValue = this._value;
}
/**
* Returns whether the value stored is different from the cached value.
*/
public hasChanged() {
return this._cachedValue !== this._value;
}
/**
* Get the currently set value of this UI element.
*/
public getValue(): T {
ASSERT(this._value !== undefined, 'this._value is undefined');
return this._value;
}
/**
* Add a delegate that will be called when the value changes.
*/
public addValueChangedListener(delegate: (newValue: T) => void) {
this._onValueChangedListeners.push(delegate);
return this;
}
public override finalise(): void {
super.finalise();
this._onValueChanged();
this._onValueChangedListeners.forEach((listener) => {
listener(this._value!);
});
}
public override generateHTML() {
return `
${this._labelElement.generateHTML()}
<div class="prop-value-container">
${this._generateInnerHTML()}
</div>
`;
}
/**
* The UI element that this label is describing.
*/
protected abstract _generateInnerHTML(): string;
protected override _onEnabledChanged() {
if (this._hasLabel) {
this._labelElement.setEnabled(this.getEnabled());
}
}
/**
* Set the value of this UI element.
*/
protected _setValue(value: T) {
this._value = value;
this._onValueChanged();
this._onValueChangedListeners.forEach((listener) => {
listener(this._value!);
});
}
/**
* A delegate that is called when the value of this element changes.
*/
protected abstract _onValueChanged(): void;
}

View File

@ -1,43 +1,49 @@
import { remote } from 'electron';
import * as path from 'path';
import { ASSERT } from '../../util/error_util';
import { LabelledElement } from './labelled_element';
import { ConfigUIElement } from './config_element';
export class FileInputElement extends LabelledElement<string> {
private _fileExtension: string;
export class FileInputElement extends ConfigUIElement<string, HTMLDivElement> {
private _fileExtensions: string[];
private _loadedFilePath: string;
private _hovering: boolean;
public constructor(label: string, fileExtension: string) {
super(label);
this._fileExtension = fileExtension;
public constructor() {
super('');
this._fileExtensions = [];
this._loadedFilePath = '';
this._hovering = false;
}
public generateInnerHTML() {
/**
* Set the allow list of file extensions that can be uploaded.
*/
public setFileExtensions(extensions: string[]) {
this._fileExtensions = extensions;
return this;
}
protected override _generateInnerHTML() {
return `
<div class="input-file" id="${this._id}">
<div class="input-file" id="${this._getId()}">
${this._loadedFilePath}
</div>
`;
}
public registerEvents(): void {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
element.onmouseenter = () => {
public override registerEvents(): void {
this._getElement().addEventListener('mouseenter', () => {
this._hovering = true;
};
this._updateStyle();
});
element.onmouseleave = () => {
this._getElement().addEventListener('mouseleave', () => {
this._hovering = false;
};
this._updateStyle();
});
element.onclick = () => {
if (!this._isEnabled) {
this._getElement().addEventListener('click', () => {
if (!this.getEnabled()) {
return;
}
@ -45,43 +51,48 @@ export class FileInputElement extends LabelledElement<string> {
title: 'Load file',
buttonLabel: 'Load',
filters: [{
name: 'Waveform obj file',
extensions: [`${this._fileExtension}`],
name: 'Model file',
extensions: this._fileExtensions,
}],
});
if (files && files.length === 1) {
if (files && files[0] !== undefined) {
const filePath = files[0];
this._loadedFilePath = filePath;
this._value = filePath;
this._setValue(filePath);
}
const parsedPath = path.parse(this._loadedFilePath);
element.innerHTML = parsedPath.name + parsedPath.ext;
};
});
document.onmousemove = () => {
element.classList.remove('input-file-disabled');
element.classList.remove('input-file-hover');
if (this._isEnabled) {
if (this._hovering) {
element.classList.add('input-file-hover');
}
} else {
element.classList.add('input-file-disabled');
}
};
this._getElement().addEventListener('mousemove', () => {
this._updateStyle();
});
}
protected _onEnabledChanged() {
protected override _onEnabledChanged() {
super._onEnabledChanged();
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
if (this._isEnabled) {
element.classList.remove('input-file-disabled');
if (this.getEnabled()) {
this._getElement().classList.remove('input-file-disabled');
} else {
element.classList.add('input-file-disabled');
this._getElement().classList.add('input-file-disabled');
}
}
protected override _onValueChanged(): void {
const parsedPath = path.parse(this._loadedFilePath);
this._getElement().innerHTML = parsedPath.name + parsedPath.ext;
}
private _updateStyle() {
this._getElement().classList.remove('input-file-disabled');
this._getElement().classList.remove('input-file-hover');
if (this.getEnabled()) {
if (this._hovering) {
this._getElement().classList.add('input-file-hover');
}
} else {
this._getElement().classList.add('input-file-disabled');
}
}
}

View File

@ -0,0 +1,87 @@
import { remote } from 'electron';
import path from 'path';
import { getRandomID } from '../../util';
import { FileUtil } from '../../util/file_util';
import { UIUtil } from '../../util/ui_util';
import { ConfigUIElement } from './config_element';
import { ToolbarItemElement } from './toolbar_item';
export class ImageElement extends ConfigUIElement<string, HTMLDivElement> {
private _switchElement: ToolbarItemElement;
private _openElement: ToolbarItemElement;
private _imageId: string;
public constructor(path: string) {
super(path);
this._switchElement = new ToolbarItemElement({ icon: 'upload' })
.setSmall()
.setLabel('Choose')
.onClick(() => {
const files = remote.dialog.showOpenDialogSync({
title: 'Load',
buttonLabel: 'Load',
filters: [{
name: 'Images',
extensions: ['png', 'jpeg', 'jpg', 'tga'],
}],
});
if (files && files[0]) {
this._setValue(files[0]);
}
});
this._openElement = new ToolbarItemElement({ icon: 'folder' })
.setSmall()
.onClick(() => {
FileUtil.openDir(this.getValue());
});
this._imageId = getRandomID();
}
public override _generateInnerHTML() {
return `
<div class="row-container">
<div class="row-item">
<img id="${this._imageId}" class="texture-preview" src="${this.getValue()}" loading="lazy"></img>
</div>
<div class="row-item">
<div class="col-container">
<div class="col-item">
${this._switchElement.generateHTML()}
</div>
<div class="col-item">
${this._openElement.generateHTML()}
</div>
</div>
</div>
</div>
`;
}
public override registerEvents(): void {
this._switchElement.registerEvents();
this._openElement.registerEvents();
}
protected override _onEnabledChanged(): void {
}
protected override _onValueChanged(): void {
const newPath = this.getValue();
const parsedPath = path.parse(newPath);
this._openElement.setEnabled(parsedPath.base !== 'debug.png');
this._switchElement.setActive(parsedPath.base === 'debug.png');
const imageElement = UIUtil.getElementById(this._imageId) as HTMLImageElement;
imageElement.src = newPath;
}
public override finalise(): void {
super.finalise();
this._onValueChanged();
}
}

View File

@ -1,11 +1,12 @@
import { BaseUIElement } from './base';
import { BaseUIElement } from './base_element';
import { LabelElement } from './label';
export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
private _label: string;
private _labelElement: LabelElement;
public constructor(label: string) {
super(label);
super();
this._label = label;
this._labelElement = new LabelElement(label);
}
@ -22,7 +23,7 @@ export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
protected abstract generateInnerHTML(): string;
protected _onEnabledChanged() {
this._labelElement.setEnabled(this._isEnabled);
this._labelElement.setEnabled(this.getEnabled());
}
public addDescription(text: string) {

View File

@ -1,227 +0,0 @@
import { remote } from 'electron';
import path from 'path';
import { AppContext } from '../../app_context';
import { RGBAUtil } from '../../colour';
import { SolidMaterial, TexturedMaterial } from '../../mesh';
import { getRandomID } from '../../util';
import { ASSERT } from '../../util/error_util';
import { FileUtil } from '../../util/file_util';
export abstract class MaterialUIElement {
protected readonly _materialName: string;
protected readonly _appContext: AppContext;
private _actions: { text: string, onClick: () => void, id: string }[];
private _metadata: string[];
public constructor(materialName: string, appContext: AppContext) {
this._materialName = materialName;
this._appContext = appContext;
this._actions = [];
this._metadata = [];
}
public hasWarning() {
return false;
}
public buildHTML(): string {
let html = `<div class="material-container">`;
html += this.buildChildHTML();
this._metadata.forEach((data) => {
html += `<br>${data}`;
});
this._actions.forEach((action) => {
html += `<br><a id="${action.id}">[${action.text}]</a>`;
});
html += `</div>`;
return html;
}
public registerEvents() {
this._actions.forEach((action) => {
const element = document.getElementById(action.id);
if (element !== null) {
element.addEventListener('click', () => {
action.onClick();
});
}
});
}
public addAction(text: string, onClick: () => void) {
this._actions.push({ text: `<b>${text}</b>`, onClick: onClick, id: getRandomID() });
}
public addMetadata(text: string) {
this._metadata.push(text);
}
public addKeyValueMetadata(key: string, value: string) {
this.addMetadata(`${key}: <div class="metadata-value">${value}</div>`);
}
protected abstract buildChildHTML(): string
}
export class TextureMaterialUIElement extends MaterialUIElement {
private _material: TexturedMaterial;
private _diffuseImageId: string;
private _alphaImageId: string;
public constructor(materialName: string, appContext: AppContext, material: TexturedMaterial) {
super(materialName, appContext);
this._material = material;
this._diffuseImageId = getRandomID();
this._alphaImageId = getRandomID();
const parsedPath = path.parse(material.path);
const isMissingTexture = parsedPath.base === 'debug.png';
const hasAlphaTexture = material.alphaPath !== undefined;
// Actions
super.addAction(isMissingTexture ? 'Find texture' : 'Replace texture', () => {
const files = remote.dialog.showOpenDialogSync({
title: 'Load',
buttonLabel: 'Load',
filters: [{
name: 'Images',
extensions: ['png', 'jpeg', 'jpg', 'tga'],
}],
});
if (files && files[0]) {
this._appContext.onMaterialTextureReplace(materialName, files[0]);
}
});
super.addAction('Switch to colour', () => {
this._appContext.onMaterialTypeSwitched(materialName);
});
super.addAction('Switch interpolation', () => {
this._appContext.onMaterialInterpolationChanged(materialName);
});
super.addAction('Switch extension', () => {
this._appContext.onMaterialExtensionChanged(materialName);
});
// Metadata
super.addKeyValueMetadata('Alpha multiplier', this._material.alphaFactor.toLocaleString(undefined, { minimumFractionDigits: 1 }));
if (this._material.alphaPath !== undefined) {
super.addKeyValueMetadata('Alpha texture', this._material.alphaPath);
}
super.addKeyValueMetadata('Interpolation', this._material.interpolation === 'nearest' ? 'Nearest' : 'Linear');
super.addKeyValueMetadata('Extension', this._material.extension === 'clamp' ? 'Clamp' : 'Repeat');
}
private _isMissingTexture() {
const parsedPath = path.parse(this._material.path);
const isMissingTexture = parsedPath.base === 'debug.png';
return isMissingTexture;
}
public hasWarning(): boolean {
return this._isMissingTexture();
}
protected buildChildHTML(): string {
let html = `<img id="${this._diffuseImageId}" class="texture-preview" src="${this._material.path}" width="75%" loading="lazy"></img>`;
if (this._material.alphaPath !== undefined) {
html += `<br><img id="${this._alphaImageId}" class="texture-preview" src="${this._material.alphaPath}" width="75%" loading="lazy"></img>`;
}
return html;
}
public registerEvents(): void {
super.registerEvents();
{
const element = document.getElementById(this._diffuseImageId) as HTMLLinkElement;
if (element) {
if (!this._isMissingTexture()) {
element.addEventListener('mouseover', () => {
element.classList.add('texture-hover');
});
element.addEventListener('mouseleave', () => {
element.classList.remove('texture-hover');
});
element.addEventListener('click', () => {
FileUtil.openDir(this._material.path);
});
} else {
element.classList.add('texture-preview-missing');
}
}
}
{
const element = document.getElementById(this._alphaImageId) as HTMLLinkElement;
if (element) {
if (!this._isMissingTexture()) {
element.addEventListener('mouseover', () => {
element.classList.add('texture-hover');
});
element.addEventListener('mouseleave', () => {
element.classList.remove('texture-hover');
});
element.addEventListener('click', () => {
ASSERT(this._material.alphaPath !== undefined);
FileUtil.openDir(this._material.alphaPath);
});
} else {
element.classList.add('texture-preview-missing');
}
}
}
}
}
export class SolidMaterialUIElement extends MaterialUIElement {
private _material: SolidMaterial;
private _colourId: string;
public constructor(materialName: string, appContext: AppContext, material: SolidMaterial) {
super(materialName, appContext);
this._material = material;
this._colourId = getRandomID();
if (material.canBeTextured) {
super.addAction('Switch to texture', () => {
this._appContext.onMaterialTypeSwitched(materialName);
});
}
this.addMetadata(`Alpha multiplier: ${this._material.colour.a}`);
}
protected buildChildHTML(): string {
return `<input class="colour-swatch" type="color" id="${this._colourId}" value="${RGBAUtil.toHexString(this._material.colour)}">`;
}
public hasWarning(): boolean {
return !this._material.set;
}
public registerEvents(): void {
super.registerEvents();
const colourElement = document.getElementById(this._colourId) as (HTMLInputElement | null);
if (colourElement !== null) {
colourElement.addEventListener('change', () => {
const newColour = RGBAUtil.fromHexString(colourElement.value);
newColour.a = this._material.colour.a;
this._appContext.onMaterialColourChanged(this._materialName, newColour);
});
colourElement.addEventListener('mouseenter', () => {
colourElement.classList.add('colour-swatch-hover');
});
colourElement.addEventListener('mouseleave', () => {
colourElement.classList.remove('colour-swatch-hover');
});
}
}
}

View File

@ -0,0 +1,52 @@
import { MaterialType } from '../../mesh';
import { ConfigUIElement } from './config_element';
import { ToolbarItemElement } from './toolbar_item';
export class MaterialTypeElement extends ConfigUIElement<MaterialType, HTMLDivElement> {
private _switchElement: ToolbarItemElement;
public constructor(material: MaterialType) {
super(material);
this._switchElement = new ToolbarItemElement({ icon: 'switch' })
.setSmall()
.setLabel('Switch')
.onClick(() => {
this._onClickChangeTypeDelegate?.();
});
}
public override _generateInnerHTML() {
const material = this.getValue();
return `
<div class="row-container">
<div class="row-item">
${material === MaterialType.solid ? 'Solid' : 'Textured'}
</div>
<div class="row-item">
<div class="col-container">
<div class="col-item">
${this._switchElement.generateHTML()}
</div>
</div>
</div>
</div>
`;
}
public override registerEvents(): void {
this._switchElement.registerEvents();
}
protected override _onEnabledChanged(): void {
}
protected override _onValueChanged(): void {
}
private _onClickChangeTypeDelegate?: () => void;
public onClickChangeTypeDelegate(delegate: () => void) {
this._onClickChangeTypeDelegate = delegate;
return this;
}
}

View File

@ -18,20 +18,6 @@ export class OutputElement {
`;
}
public registerEvents() {
const toggler = document.getElementsByClassName('caret');
for (let i = 0; i < toggler.length; i++) {
const temp = toggler[i];
temp.addEventListener('click', function () {
temp.parentElement?.querySelector('.nested')?.classList.toggle('active');
temp.classList.toggle('caret-down');
});
}
this._message.postBuild();
}
private _message: UIMessageBuilder;
public setMessage(message: UIMessageBuilder, style?: OutputStyle) {
@ -74,7 +60,7 @@ export class OutputElement {
ASSERT(element !== null);
element.innerHTML = this._message.toString();
this.registerEvents();
this._message.postBuild();
}
public setStyle(style: OutputStyle) {

View File

@ -1,46 +1,82 @@
import { clamp, mapRange, wayThrough } from '../../math';
import { ASSERT } from '../../util/error_util';
import { LabelledElement } from './labelled_element';
import { UIUtil } from '../../util/ui_util';
import { ConfigUIElement } from './config_element';
export class SliderElement extends LabelledElement<number> {
export type TSliderParams = {
min: number,
max: number,
value: number,
decimals: number,
step: number,
}
export class SliderElement extends ConfigUIElement<number, HTMLDivElement> {
private _min: number;
private _max: number;
private _decimals: number;
private _dragging: boolean;
private _step: number;
private _dragging: boolean;
private _hovering: boolean;
private _internalValue: number;
public constructor(label: string, min: number, max: number, decimals: number, value: number, step: number) {
super(label);
this._min = min;
this._max = max;
this._decimals = decimals;
this._value = value;
this._step = step;
public constructor() {
super();
this._min = 0;
this._max = 1;
this._decimals = 1;
this._step = 0.1;
this._internalValue = 0.5;
this._dragging = false;
this._hovering = false;
}
public generateInnerHTML() {
const norm = (this.getValue() - this._min) / (this._max - this._min);
return `
<input type="number" id="${this._id}-value" min="${this._min}" max="${this._max}" step="${this._step}" value="${this.getValue().toFixed(this._decimals)}">
<div class="new-slider" id="${this._id}" style="flex-grow: 1;">
<div class="new-slider-bar" id="${this._id}-bar"style="width: ${norm * 100}%;">
</div>
</div>
`;
public override setDefaultValue(value: number) {
super.setDefaultValue(value);
this._internalValue = value;
return this;
}
public registerEvents() {
const element = document.getElementById(this._id) as HTMLDivElement;
const elementBar = document.getElementById(this._id + '-bar') as HTMLDivElement;
const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement;
ASSERT(element !== null);
/**
* Set the minimum value the slider can be set to.
*/
public setMin(min: number) {
this._min = min;
return this;
}
/**
* Set the maximum value the slider can be set to.
*/
public setMax(max: number) {
this._max = max;
return this;
}
/**
* Set the number of decimals to display the value to.
*/
public setDecimals(decimals: number) {
this._decimals = decimals;
return this;
}
/**
* Set the step the value is increased/decreased by.
*/
public setStep(step: number) {
this._step = step;
return this;
}
public override registerEvents() {
const element = this._getElement();
const elementBar = UIUtil.getElementById(this._getSliderBarId());
const elementValue = UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement;
element.onmouseenter = () => {
this._hovering = true;
if (this._isEnabled) {
if (this.getEnabled()) {
element.classList.add('new-slider-hover');
elementBar.classList.add('new-slider-bar-hover');
}
@ -76,7 +112,7 @@ export class SliderElement extends LabelledElement<number> {
});
element.addEventListener('wheel', (e: WheelEvent) => {
if (!this._dragging && this._isEnabled) {
if (!this._dragging && this.getEnabled()) {
e.preventDefault();
this._onScrollSlider(e);
}
@ -87,75 +123,26 @@ export class SliderElement extends LabelledElement<number> {
});
}
public setMax(value: number) {
this._max = value;
this._value = clamp(this._value!, this._min, this._max);
this._onValueUpdated();
protected override _generateInnerHTML() {
const norm = (this._internalValue - this._min) / (this._max - this._min);
return `
<input class="slider-number-input" type="number" id="${this._getSliderValueId()}" min="${this._min}" max="${this._max}" step="${this._step}" value="${this.getValue().toFixed(this._decimals)}">
<div class="new-slider" id="${this._getId()}" style="flex-grow: 1;">
<div class="new-slider-bar" id="${this._getSliderBarId()}" style="width: ${norm * 100}%;">
</div>
</div>
`;
}
private _onTypedValue() {
const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement;
const typedNumber = parseFloat(elementValue.value);
if (!isNaN(typedNumber)) {
this._value = clamp(typedNumber, this._min, this._max);
}
this._onValueUpdated();
}
private _onScrollSlider(e: WheelEvent) {
if (!this._isEnabled) {
return;
}
ASSERT(this._value !== undefined);
this._value -= (e.deltaY / 150) * this._step;
this._value = clamp(this._value, this._min, this._max);
this._onValueUpdated();
}
private _onDragSlider(e: MouseEvent) {
if (!this._isEnabled) {
return;
}
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
const box = element.getBoundingClientRect();
const left = box.x;
const right = box.x + box.width;
this._value = mapRange(e.clientX, left, right, this._min, this._max);
this._value = clamp(this._value, this._min, this._max);
this._onValueUpdated();
}
private _onValueUpdated() {
const elementBar = document.getElementById(this._id + '-bar') as HTMLDivElement;
const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement;
ASSERT(elementBar !== null && elementValue !== null);
const norm = wayThrough(this.getValue(), this._min, this._max);
elementBar.style.width = `${norm * 100}%`;
elementValue.value = this.getValue().toFixed(this._decimals);
}
public getDisplayValue() {
return parseFloat(this.getValue().toFixed(this._decimals));
}
protected _onEnabledChanged() {
protected override _onEnabledChanged() {
super._onEnabledChanged();
const element = document.getElementById(this._id) as HTMLDivElement;
const elementBar = document.getElementById(this._id + '-bar') as HTMLDivElement;
const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement;
ASSERT(element !== null && elementBar !== null && elementValue !== null);
const element = this._getElement();
const elementBar = UIUtil.getElementById(this._getSliderBarId());
const elementValue = UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement;
if (this._isEnabled) {
if (this.getEnabled()) {
element.classList.remove('new-slider-disabled');
elementBar.classList.remove('new-slider-bar-disabled');
elementValue.disabled = false;
@ -165,4 +152,67 @@ export class SliderElement extends LabelledElement<number> {
elementValue.disabled = true;
}
}
protected override _onValueChanged(): void {
const percentage = wayThrough(this.getValue(), this._min, this._max);
ASSERT(percentage >= 0.0 && percentage <= 1.0);
UIUtil.getElementById(this._getSliderBarId()).style.width = `${percentage * 100}%`;
(UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement).value = this.getValue().toFixed(this._decimals);
}
private _onScrollSlider(e: WheelEvent) {
if (!this.getEnabled()) {
return;
}
this._internalValue -= (e.deltaY / 150) * this._step;
this._internalValue = clamp(this._internalValue, this._min, this._max);
this._onInternalValueUpdated();
}
private _onDragSlider(e: MouseEvent) {
if (!this.getEnabled()) {
return;
}
const box = this._getElement().getBoundingClientRect();
const left = box.x;
const right = box.x + box.width;
this._internalValue = mapRange(e.clientX, left, right, this._min, this._max);
this._internalValue = clamp(this._internalValue, this._min, this._max);
this._onInternalValueUpdated();
}
private _onTypedValue() {
const elementValue = UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement;
const typedNumber = parseFloat(elementValue.value);
if (!isNaN(typedNumber)) {
this._internalValue = clamp(typedNumber, this._min, this._max);
}
this._onInternalValueUpdated();
}
private _onInternalValueUpdated() {
const displayString = this._internalValue!.toFixed(this._decimals);
this._setValue(parseFloat(displayString));
}
/**
* Gets the ID of the DOM element for the slider's value.
*/
private _getSliderValueId() {
return this._getId() + '-value';
}
/**
* Gets the ID of the DOM element for the slider's bar.
*/
private _getSliderBarId() {
return this._getId() + '-bar';
}
}

View File

@ -0,0 +1,75 @@
import { RGBAUtil } from '../../colour';
import { MaterialType, SolidMaterial } from '../../mesh';
import { getRandomID } from '../../util';
import { UIUtil } from '../../util/ui_util';
import { ConfigUIElement } from './config_element';
import { MaterialTypeElement } from './material_type_element';
export class SolidMaterialElement extends ConfigUIElement<SolidMaterial, HTMLDivElement> {
private _materialName: string;
private _colourId: string;
private _typeElement: MaterialTypeElement;
public constructor(materialName: string, material: SolidMaterial) {
super(material);
this._materialName = materialName;
this._colourId = getRandomID();
this._typeElement = new MaterialTypeElement(MaterialType.solid);
}
public override registerEvents(): void {
this._typeElement.registerEvents();
this._typeElement.onClickChangeTypeDelegate(() => {
this._onChangeTypeDelegate?.();
});
const swatchElement = UIUtil.getElementById(this._colourId) as HTMLInputElement;
swatchElement.addEventListener('change', () => {
const material = this.getValue();
material.colour = RGBAUtil.fromHexString(swatchElement.value);
});
}
protected override _generateInnerHTML(): string {
const material = this.getValue();
const subproperties: string[] = [];
const addSubproperty = (key: string, value: string) => {
subproperties.push(`
<div class="subproperty">
<div class="subprop-key-container">
${key}
</div>
<div class="subprop-value-container">
${value}
</div>
</div>
`);
};
addSubproperty('Type', this._typeElement._generateInnerHTML());
addSubproperty('Colour', `<input class="colour-swatch" type="color" id="${this._colourId}" value="${RGBAUtil.toHexString(material.colour)}">`);
addSubproperty('Alpha', `${material.colour.a.toFixed(4)}`);
return `
<div class="subproperty-container">
${subproperties.join('')}
</div>
`;
}
protected override _onValueChanged(): void {
}
protected override _onEnabledChanged(): void {
super._onEnabledChanged();
}
private _onChangeTypeDelegate?: () => void;
public onChangeTypeDelegate(delegate: () => void) {
this._onChangeTypeDelegate = delegate;
return this;
}
}

View File

@ -0,0 +1,115 @@
import { MaterialType, TexturedMaterial } from '../../mesh';
import { getRandomID } from '../../util';
import { ComboBoxElement } from './combobox';
import { ConfigUIElement } from './config_element';
import { ImageElement } from './image_element';
import { MaterialTypeElement } from './material_type_element';
export class TexturedMaterialElement extends ConfigUIElement<TexturedMaterial, HTMLDivElement> {
private _materialName: string;
private _colourId: string;
private _filteringElement: ComboBoxElement<'nearest' | 'linear'>;
private _wrapElement: ComboBoxElement<'clamp' | 'repeat'>;
private _imageElement: ImageElement;
private _typeElement: MaterialTypeElement;
public constructor(materialName: string, material: TexturedMaterial) {
super(material);
this._materialName = materialName;
this._colourId = getRandomID();
this._filteringElement = new ComboBoxElement<'linear' | 'nearest'>()
.addItem({ payload: 'linear', displayText: 'Linear' })
.addItem({ payload: 'nearest', displayText: 'Nearest' })
.setSmall()
.setDefaultValue(material.interpolation);
this._wrapElement = new ComboBoxElement<'clamp' | 'repeat'>()
.addItem({ payload: 'clamp', displayText: 'Clamp' })
.addItem({ payload: 'repeat', displayText: 'Repeat' })
.setSmall()
.setDefaultValue(material.extension);
this._imageElement = new ImageElement(material.path);
this._typeElement = new MaterialTypeElement(MaterialType.textured);
}
public override registerEvents(): void {
this._imageElement.registerEvents();
this._typeElement.registerEvents();
this._filteringElement.registerEvents();
this._wrapElement.registerEvents();
this._imageElement.addValueChangedListener((newPath) => {
const material = this.getValue();
material.path = newPath;
});
this._filteringElement.addValueChangedListener((newFiltering) => {
const material = this.getValue();
material.interpolation = newFiltering;
});
this._wrapElement.addValueChangedListener((newWrap) => {
const material = this.getValue();
material.extension = newWrap;
});
this._typeElement.onClickChangeTypeDelegate(() => {
this._onChangeTypeDelegate?.();
});
}
protected override _generateInnerHTML(): string {
const material = this.getValue();
const subproperties: string[] = [];
const addSubproperty = (key: string, value: string) => {
subproperties.push(`
<div class="subproperty">
<div class="subprop-key-container">
${key}
</div>
<div class="subprop-value-container">
${value}
</div>
</div>
`);
};
addSubproperty('Type', this._typeElement._generateInnerHTML());
addSubproperty('Alpha', material.alphaFactor.toFixed(4));
addSubproperty('File', this._imageElement._generateInnerHTML());
addSubproperty('Filtering', this._filteringElement._generateInnerHTML());
addSubproperty('Wrap', this._wrapElement._generateInnerHTML());
return `
<div class="subproperty-container">
${subproperties.join('')}
</div>
`;
}
protected override _onValueChanged(): void {
}
protected override _onEnabledChanged(): void {
super._onEnabledChanged();
}
public override finalise(): void {
super.finalise();
this._imageElement.finalise();
this._typeElement.finalise();
this._filteringElement.finalise();
this._wrapElement.finalise();
}
private _onChangeTypeDelegate?: () => void;
public onChangeTypeDelegate(delegate: () => void) {
this._onChangeTypeDelegate = delegate;
return this;
}
}

View File

@ -4,6 +4,7 @@ import { getRandomID } from '../../util';
import { ASSERT } from '../../util/error_util';
import { AppPaths } from '../../util/path_util';
import { PathUtil } from '../../util/path_util';
import { UIUtil } from '../../util/ui_util';
export type TToolbarBooleanProperty = 'enabled' | 'active';
@ -17,6 +18,9 @@ export class ToolbarItemElement {
private _iconPath: string;
private _isEnabled: boolean;
private _isActive: boolean;
private _isHovering: boolean;
private _small: boolean;
private _label: string;
private _onClick?: () => void;
public constructor(params: TToolbarItemParams) {
@ -27,15 +31,35 @@ export class ToolbarItemElement {
this._isEnabled = true;
this._isActive = false;
this._isHovering = false;
this._small = false;
this._label = '';
}
public setSmall() {
this._small = true;
return this;
}
public setLabel(label: string) {
this._label = label;
return this;
}
public tick() {
if (this._isEnabledDelegate !== undefined) {
this.setEnabled(this._isEnabledDelegate());
const newIsEnabled = this._isEnabledDelegate();
//if (newIsEnabled !== this._isEnabled) {
this.setEnabled(newIsEnabled);
//}
}
if (this._isActiveDelegate !== undefined) {
this.setActive(this._isActiveDelegate());
const newIsActive = this._isActiveDelegate();
//if (newIsActive !== this._isActive) {
this.setActive(newIsActive);
//}
}
}
@ -60,8 +84,8 @@ export class ToolbarItemElement {
public generateHTML() {
const svg = fs.readFileSync(this._iconPath, 'utf8');
return `
<div class="toolbar-item" id="${this._id}">
${svg}
<div class="toolbar-item ${this._small ? 'toolbar-item-small' : ''}" id="${this._id}">
${svg} ${this._label}
</div>
`;
}
@ -77,45 +101,61 @@ export class ToolbarItemElement {
});
element.addEventListener('mouseenter', () => {
if (this._isEnabled) {
element.classList.add('toolbar-item-hover');
}
this._isHovering = true;
this._updateElements();
});
element.addEventListener('mouseleave', () => {
if (this._isEnabled) {
element.classList.remove('toolbar-item-hover');
}
this._isHovering = false;
this._updateElements();
});
// Modify the svg's Id so that multiple svgs can be used without Id clashes
const svgElement = document.getElementById(this._iconName + '-svg') as HTMLDivElement;
svgElement.id += `-${this._id}`;
this._updateElements();
}
private _getSVGElement() {
const svgId = `${this._iconName}-svg-${this._id}`;
return UIUtil.getElementById(svgId);
}
private _updateElements() {
const element = document.getElementById(this._id) as HTMLDivElement;
const svgElement = document.getElementById(this._iconName + '-svg') as HTMLDivElement;
const svgElement = this._getSVGElement();
ASSERT(element !== null && svgElement !== null);
element.classList.remove('toolbar-item-active-hover');
element.classList.remove('toolbar-item-disabled');
element.classList.remove('toolbar-item-active');
element.classList.remove('toolbar-item-disabled-active');
svgElement.classList.remove('icon-disabled');
svgElement.classList.remove('icon-active');
svgElement.classList.remove('icon-disabled-active');
element.classList.remove('toolbar-item-hover');
if (this._isEnabled) {
if (this._isActive) {
element.classList.add('toolbar-item-active');
if (this._isHovering) {
element.classList.add('toolbar-item-active-hover');
} else {
element.classList.add('toolbar-item-active');
}
} else {
if (this._isHovering) {
element.classList.add('toolbar-item-hover');
}
}
} else {
element.classList.add('toolbar-item-disabled');
}
svgElement.classList.remove('icon-disabled');
svgElement.classList.remove('icon-active');
if (this._isEnabled) {
if (this._isActive) {
svgElement.classList.add('icon-active');
}
} else {
if (this._isActive) {
element.classList.add('toolbar-item-disabled-active');
svgElement.classList.add('icon-disabled-active');
} else {
element.classList.add('toolbar-item-disabled');
svgElement.classList.add('icon-disabled');
}
svgElement.classList.add('icon-disabled');
}
}

View File

@ -1,59 +1,56 @@
import { ASSERT } from '../../util/error_util';
import { LOG } from '../../util/log_util';
import { TAxis } from '../../util/type_util';
import { UIUtil } from '../../util/ui_util';
import { Vector3 } from '../../vector';
import { LabelledElement } from './labelled_element';
import { ConfigUIElement } from './config_element';
/* eslint-disable */
enum EAxis {
None = 'none',
X = 'x',
Y = 'y',
Z = 'z',
};
/* eslint-enable */
export class VectorSpinboxElement extends LabelledElement<Vector3> {
private _mouseover: EAxis;
private _dragging: EAxis;
export class VectorSpinboxElement extends ConfigUIElement<Vector3, HTMLDivElement> {
private _mouseover: TAxis | null;
private _dragging: TAxis | null;
private _lastClientX: number;
private _showY: boolean;
public constructor(label: string, decimals: number, value: Vector3, showY: boolean) {
super(label);
this._value = value;
this._mouseover = EAxis.None;
this._dragging = EAxis.None;
public constructor() {
super();
this._mouseover = null;
this._dragging = null;
this._lastClientX = 0.0;
this._showY = true;
}
/**
* Set whether or not the Y axis has a UI element
*/
public setShowY(showY: boolean) {
this._showY = showY;
}
public generateInnerHTML() {
ASSERT(this._value !== undefined, 'Value not found');
protected override _generateInnerHTML() {
let html = '';
html += '<div class="spinbox-main-container">';
html += `
<div class="spinbox-element-container">
<div class="spinbox-key" id="${this._id}-kx">X</div>
<div class="spinbox-value" id="${this._id}-vx">
${this._value.x}
<div class="spinbox-key" id="${this._getKeyId('x')}">X</div>
<div class="spinbox-value" id="${this._getValueId('x')}">
${this.getValue().x}
</div>
</div>
`;
if (this._showY) {
html += `
<div class="spinbox-element-container">
<div class="spinbox-key" id="${this._id}-ky">Y</div>
<div class="spinbox-value" id="${this._id}-vy">
${this._value.y}
<div class="spinbox-key" id="${this._getKeyId('y')}">Y</div>
<div class="spinbox-value" id="${this._getValueId('y')}">
${this.getValue().y}
</div>
</div>
`;
}
html += `
<div class="spinbox-element-container">
<div class="spinbox-key" id="${this._id}-kz">Z</div>
<div class="spinbox-value" id="${this._id}-vz">
${this._value.z}
<div class="spinbox-key" id="${this._getKeyId('z')}">Z</div>
<div class="spinbox-value" id="${this._getValueId('z')}">
${this.getValue().z}
</div>
</div>
`;
@ -61,139 +58,121 @@ export class VectorSpinboxElement extends LabelledElement<Vector3> {
return html;
}
private _registerAxis(axis: EAxis) {
ASSERT(axis !== EAxis.None);
private _getKeyId(axis: TAxis) {
return this._getId() + '-k' + axis;
}
const elementXK = document.getElementById(this._id + '-k' + axis) as HTMLDivElement;
const elementXV = document.getElementById(this._id + '-v' + axis) as HTMLDivElement;
ASSERT(elementXK !== null && elementXV !== null);
private _getValueId(axis: TAxis) {
return this._getId() + '-v' + axis;
}
elementXK.onmouseenter = () => {
private _registerAxis(axis: TAxis) {
const elementKey = UIUtil.getElementById(this._getKeyId(axis));
const elementValue = UIUtil.getElementById(this._getValueId(axis));
elementKey.onmouseenter = () => {
this._mouseover = axis;
if (this._isEnabled) {
elementXK.classList.add('spinbox-key-hover');
elementXV.classList.add('spinbox-value-hover');
if (this.getEnabled()) {
elementKey.classList.add('spinbox-key-hover');
elementValue.classList.add('spinbox-value-hover');
}
};
elementXV.onmouseenter = () => {
elementValue.onmouseenter = () => {
this._mouseover = axis;
if (this._isEnabled) {
elementXK.classList.add('spinbox-key-hover');
elementXV.classList.add('spinbox-value-hover');
if (this.getEnabled()) {
elementKey.classList.add('spinbox-key-hover');
elementValue.classList.add('spinbox-value-hover');
}
};
elementXK.onmouseleave = () => {
this._mouseover = EAxis.None;
elementKey.onmouseleave = () => {
this._mouseover = null;
if (this._dragging !== axis) {
elementXK.classList.remove('spinbox-key-hover');
elementXV.classList.remove('spinbox-value-hover');
elementKey.classList.remove('spinbox-key-hover');
elementValue.classList.remove('spinbox-value-hover');
}
};
elementXV.onmouseleave = () => {
this._mouseover = EAxis.None;
elementValue.onmouseleave = () => {
this._mouseover = null;
if (this._dragging !== axis) {
elementXK.classList.remove('spinbox-key-hover');
elementXV.classList.remove('spinbox-value-hover');
elementKey.classList.remove('spinbox-key-hover');
elementValue.classList.remove('spinbox-value-hover');
}
};
}
public registerEvents() {
this._registerAxis(EAxis.X);
this._registerAxis('x');
if (this._showY) {
this._registerAxis(EAxis.Y);
this._registerAxis('y');
}
this._registerAxis(EAxis.Z);
this._registerAxis('z');
document.addEventListener('mousedown', (e: any) => {
if (this._isEnabled && this._mouseover !== EAxis.None) {
if (this.getEnabled() && this._mouseover !== null) {
this._dragging = this._mouseover;
this._lastClientX = e.clientX;
}
});
document.addEventListener('mousemove', (e: any) => {
if (this._isEnabled && this._dragging !== EAxis.None) {
if (this.getEnabled() && this._dragging !== null) {
this._updateValue(e);
}
});
document.addEventListener('mouseup', () => {
const elementXK = document.getElementById(this._id + '-kx') as HTMLDivElement;
const elementYK = document.getElementById(this._id + '-ky') as (HTMLDivElement | undefined);
const elementZK = document.getElementById(this._id + '-kz') as HTMLDivElement;
const elementXV = document.getElementById(this._id + '-vx') as HTMLDivElement;
const elementYV = document.getElementById(this._id + '-vy') as (HTMLDivElement | undefined);
const elementZV = document.getElementById(this._id + '-vz') as HTMLDivElement;
switch (this._dragging) {
case EAxis.X:
elementXK?.classList.remove('spinbox-key-hover');
elementXV.classList.remove('spinbox-value-hover');
break;
case EAxis.Y:
elementYK?.classList.remove('spinbox-key-hover');
elementYV?.classList.remove('spinbox-value-hover');
break;
case EAxis.Z:
elementZK.classList.remove('spinbox-key-hover');
elementZV.classList.remove('spinbox-value-hover');
break;
if (this._dragging !== null) {
const elementKey = UIUtil.getElementById(this._getKeyId(this._dragging));
const elementValue = UIUtil.getElementById(this._getKeyId(this._dragging));
elementKey.classList.remove('spinbox-key-hover');
elementValue.classList.remove('spinbox-value-hover');
}
this._dragging = EAxis.None;
this._dragging = null;
});
}
private _updateValue(e: MouseEvent) {
ASSERT(this._isEnabled, 'Not enabled');
ASSERT(this._dragging !== EAxis.None, 'Dragging nothing');
ASSERT(this._value !== undefined, 'No value to update');
ASSERT(this.getEnabled(), 'Not enabled');
ASSERT(this._dragging !== null, 'Dragging nothing');
const deltaX = e.clientX - this._lastClientX;
this._lastClientX = e.clientX;
switch (this._dragging) {
case EAxis.X:
this._value.x += deltaX;
break;
case EAxis.Y:
this._value.y += deltaX;
break;
case EAxis.Z:
this._value.z += deltaX;
break;
}
const current = this.getValue().copy();
const elementXV = document.getElementById(this._id + '-vx') as HTMLDivElement;
const elementYV = document.getElementById(this._id + '-vy') as (HTMLDivElement | undefined);
const elementZV = document.getElementById(this._id + '-vz') as HTMLDivElement;
elementXV.innerHTML = this._value.x.toString();
if (elementYV) {
elementYV.innerHTML = this._value.y.toString();
switch (this._dragging) {
case 'x':
current.x += deltaX;
break;
case 'y':
current.y += deltaX;
break;
case 'z':
current.z += deltaX;
break;
}
elementZV.innerHTML = this._value.z.toString();
this._setValue(current);
}
protected _onEnabledChanged() {
protected override _onEnabledChanged() {
super._onEnabledChanged();
LOG(this._label, 'is now enabled', this._isEnabled);
const keyElements = [
document.getElementById(this._id + '-kx') as HTMLDivElement,
document.getElementById(this._id + '-ky') as (HTMLDivElement | undefined),
document.getElementById(this._id + '-kz') as HTMLDivElement,
UIUtil.getElementById(this._getKeyId('x')),
UIUtil.getElementById(this._getKeyId('y')),
UIUtil.getElementById(this._getKeyId('z')),
];
const valueElements = [
document.getElementById(this._id + '-vx') as HTMLDivElement,
document.getElementById(this._id + '-vy') as (HTMLDivElement | undefined),
document.getElementById(this._id + '-vz') as HTMLDivElement,
UIUtil.getElementById(this._getValueId('x')),
UIUtil.getElementById(this._getValueId('y')),
UIUtil.getElementById(this._getValueId('z')),
];
if (this._isEnabled) {
if (this.getEnabled()) {
for (const keyElement of keyElements) {
keyElement?.classList.remove('spinbox-key-disabled');
}
@ -209,4 +188,18 @@ export class VectorSpinboxElement extends LabelledElement<Vector3> {
}
}
}
protected override _onValueChanged(): void {
const elementXV = UIUtil.getElementById(this._getValueId('x'));
const elementYV = UIUtil.getElementById(this._getValueId('y'));
const elementZV = UIUtil.getElementById(this._getValueId('z'));
const current = this.getValue().copy();
elementXV.innerHTML = current.x.toString();
if (elementYV) {
elementYV.innerHTML = current.y.toString();
}
elementZV.innerHTML = current.z.toString();
}
}

View File

@ -1,6 +1,7 @@
import fs from 'fs';
import { AppContext } from '../app_context';
import { FallableBehaviour } from '../block_mesh';
import { ArcballCamera } from '../camera';
import { AppConfig } from '../config';
import { EAppEvent, EventManager } from '../event';
@ -15,10 +16,11 @@ import { TAxis, TTexelExtension } from '../util/type_util';
import { TDithering } from '../util/type_util';
import { TVoxelOverlapRule } from '../voxel_mesh';
import { TVoxelisers } from '../voxelisers/voxelisers';
import { BaseUIElement } from './elements/base';
import { BaseUIElement } from './elements/base_element';
import { ButtonElement } from './elements/button';
import { CheckboxElement } from './elements/checkbox';
import { ComboBoxElement, ComboBoxItem } from './elements/combobox';
import { ConfigUIElement } from './elements/config_element';
import { FileInputElement } from './elements/file_input';
import { OutputElement } from './elements/output';
import { SliderElement } from './elements/slider';
@ -26,12 +28,10 @@ import { ToolbarItemElement } from './elements/toolbar_item';
export interface Group {
label: string;
elements: { [key: string]: BaseUIElement<any> };
elements: { [key: string]: ConfigUIElement<any, any> };
elementsOrder: string[];
submitButton: ButtonElement;
output: OutputElement;
postElements?: { [key: string]: BaseUIElement<any> };
postElementsOrder?: string[];
}
export interface ToolbarGroup {
@ -40,153 +40,239 @@ export interface ToolbarGroup {
}
export class UI {
public uiOrder = ['import', 'voxelise', 'assign', 'export'];
public uiOrder = ['import', 'materials', 'voxelise', 'assign', 'export'];
private _ui = {
'import': {
label: 'Import',
elements: {
'input': new FileInputElement('Wavefront .obj file', 'obj'),
'input': new FileInputElement()
.setFileExtensions(['obj'])
.setLabel('Wavefront .obj file'),
},
elementsOrder: ['input'],
submitButton: new ButtonElement('Load mesh', () => {
this._appContext.do(EAction.Import);
}),
submitButton: new ButtonElement()
.setOnClick(() => {
this._appContext.do(EAction.Import);
})
.setLabel('Load mesh'),
output: new OutputElement(),
},
'materials': {
label: 'Materials',
elements: {
},
elementsOrder: [],
submitButton: new ButtonElement()
.setOnClick(() => {
this._appContext.do(EAction.Materials);
})
.setLabel('Update materials'),
output: new OutputElement(),
},
'voxelise': {
label: 'Voxelise',
elements: {
'constraintAxis': new ComboBoxElement<TAxis>('Constraint axis', [
{
id: 'y',
displayText: 'Y (height) (green)',
},
{
id: 'x',
displayText: 'X (width) (red)',
},
{
id: 'z',
displayText: 'Z (depth) (blue)',
},
]).onValueChanged((value: string) => {
if (value === 'x') {
this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.x ?? 400);
} else if (value === 'y') {
this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.y ?? AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT);
} else {
this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.z ?? 400);
}
}),
'size': new SliderElement('Size', 3, AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT, 0, 80, 1),
'voxeliser': new ComboBoxElement<TVoxelisers>('Algorithm', [
{
id: 'bvh-ray',
displayText: 'BVH Ray-based',
},
{
id: 'bvh-ray-plus-thickness',
displayText: 'BVH Ray-based (thicker walls)',
},
{
id: 'ncrb',
displayText: 'NCRB',
},
{
id: 'ray-based',
displayText: 'Ray-based (legacy)',
},
]),
'ambientOcclusion': new CheckboxElement('Ambient occlusion', true, 'On (recommended)', 'Off (faster)'),
'multisampleColouring': new CheckboxElement('Multisampling', true, 'On (recommended)', 'Off (faster)'),
'voxelOverlapRule': new ComboBoxElement<TVoxelOverlapRule>('Voxel overlap', [
{
id: 'average',
'constraintAxis': new ComboBoxElement<TAxis>()
.addItem({ payload: 'y', displayText: 'Y (height) (green)' })
.addItem({ payload: 'x', displayText: 'X (width) (red)' })
.addItem({ payload: 'z', displayText: 'Z (depth) (blue)' })
.setLabel('Constraint axis')
.addValueChangedListener((value: TAxis) => {
/*
switch (value) {
case 'x':
this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.x ?? 400);
break;
case 'y':
this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.y ?? AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT);
break;
case 'z':
this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.z ?? 400);
break;
}
*/
}),
'size': new SliderElement()
.setMin(3)
.setMax(380)
.setDefaultValue(80)
.setDecimals(0)
.setStep(1)
.setLabel('Size'),
'voxeliser': new ComboBoxElement<TVoxelisers>()
.addItem({ payload: 'bvh-ray', displayText: 'BVH Ray-based' })
.addItem({ payload: 'ncrb', displayText: 'NCRB' })
.addItem({ payload: 'ray-based', displayText: 'Ray-based (legacy)' })
.setLabel('Algorithm'),
'ambientOcclusion': new CheckboxElement()
.setCheckedText('On (recommended)')
.setUncheckedText('Off (faster)')
.setDefaultValue(true)
.setLabel('Ambient occlusion'),
'multisampleColouring': new CheckboxElement()
.setCheckedText('On (recommended)')
.setUncheckedText('Off (faster)')
.setDefaultValue(true)
.setLabel('Multisampling'),
'voxelOverlapRule': new ComboBoxElement<TVoxelOverlapRule>()
.addItem({
displayText: 'Average (recommended)',
payload: 'average',
tooltip: 'If multiple voxels are placed in the same location, take the average of their colours',
},
{
id: 'first',
})
.addItem({
displayText: 'First',
payload: 'first',
tooltip: 'If multiple voxels are placed in the same location, use the first voxel\'s colour',
},
]),
})
.setLabel('Voxel overlap'),
},
elementsOrder: ['constraintAxis', 'size', 'voxeliser', 'ambientOcclusion', 'multisampleColouring', 'voxelOverlapRule'],
submitButton: new ButtonElement('Voxelise mesh', () => {
this._appContext.do(EAction.Voxelise);
}),
elementsOrder: [
'constraintAxis',
'size',
'voxeliser',
'ambientOcclusion',
'multisampleColouring',
'voxelOverlapRule',
],
submitButton: new ButtonElement()
.setOnClick(() => {
this._appContext.do(EAction.Voxelise);
})
.setLabel('Voxelise mesh'),
output: new OutputElement(),
},
'assign': {
label: 'Assign',
elements: {
'textureAtlas': new ComboBoxElement('Texture atlas', this._getTextureAtlases()),
'blockPalette': new ComboBoxElement('Block palette', this._getBlockPalettes()),
'dithering': new ComboBoxElement<TDithering>('Dithering', [
{ id: 'ordered', displayText: 'Ordered' },
{ id: 'random', displayText: 'Random' },
{ id: 'off', displayText: 'Off' },
]),
'fallable': new ComboBoxElement('Fallable blocks', [
'textureAtlas': new ComboBoxElement<string>()
.addItems(this._getTextureAtlases())
.setLabel('Texture atlas'),
'blockPalette': new ComboBoxElement<string>()
.addItems(this._getBlockPalettes())
.setLabel('Block palette'),
'dithering': new ComboBoxElement<TDithering>()
.addItems([{
displayText: 'Ordered',
payload: 'ordered',
},
{
id: 'replace-falling',
displayText: 'Random',
payload: 'random',
},
{
displayText: 'Off',
payload: 'off',
}])
.setLabel('Dithering'),
'fallable': new ComboBoxElement<FallableBehaviour>()
.addItems([{
displayText: 'Replace falling with solid',
payload: 'replace-falling',
tooltip: 'Replace all blocks that will fall with solid blocks',
},
{
displayText: 'Replace fallable with solid',
payload: 'replace-fallable',
tooltip: 'Replace all blocks that can fall with solid blocks',
},
{
id: 'replace-fallable',
displayText: 'Replace fallable with solid',
tooltip: 'Replace all blocks that will fall with solid blocks',
},
/*
{
id: 'place-string',
displayText: 'Place string under',
tooltip: 'Place string blocks under all blocks that would fall otherwise',
},
*/
{
id: 'do-nothing',
displayText: 'Do nothing',
payload: 'do-nothing',
tooltip: 'Let the block fall',
},
]),
'colourAccuracy': new SliderElement('Colour accuracy', 1, 8, 1, 5, 0.1),
'contextualAveraging': new CheckboxElement('Smart averaging', true, 'On (recommended)', 'Off (faster)'),
'errorWeight': new SliderElement('Smoothness', 0.0, AppConfig.Get.SMOOTHNESS_MAX, 2, 0.2, 0.01),
'calculateLighting': new CheckboxElement('Calculate lighting', false, 'On', 'Off')
.onValueChanged((value: boolean) => {
}])
.setLabel('Fallable blocks'),
'colourAccuracy': new SliderElement()
.setMin(1)
.setMax(8)
.setDefaultValue(5)
.setDecimals(1)
.setStep(0.1)
.setLabel('Colour accuracy'),
'contextualAveraging': new CheckboxElement()
.setCheckedText('On (recommended)')
.setUncheckedText('Off (faster)')
.setDefaultValue(true)
.setLabel('Smart averaging'),
'errorWeight': new SliderElement()
.setMin(0.0)
.setMax(2.0)
.setDefaultValue(0.2)
.setDecimals(2)
.setStep(0.01)
.setLabel('Smoothness'),
'calculateLighting': new CheckboxElement()
.setCheckedText('On')
.setUncheckedText('Off')
.setDefaultValue(false)
.setLabel('Calculate lighting')
.addValueChangedListener((value: boolean) => {
if (value) {
this._ui.assign.elements.lightThreshold.setEnabled(true, false);
} else {
this._ui.assign.elements.lightThreshold.setEnabled(false, false);
}
}),
'lightThreshold': new SliderElement('Light threshold', 0, 14, 0, 0, 1)
.setObeyGroupEnables(false),
'lightThreshold': new SliderElement()
.setMin(0)
.setMax(14)
.setDefaultValue(1)
.setDecimals(0)
.setStep(1)
.setLabel('Light threshold'),
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'fallable', 'colourAccuracy', 'contextualAveraging', 'errorWeight', 'calculateLighting', 'lightThreshold'],
submitButton: new ButtonElement('Assign blocks', () => {
this._appContext.do(EAction.Assign);
}),
elementsOrder: [
'textureAtlas',
'blockPalette',
'dithering',
'fallable',
'colourAccuracy',
'contextualAveraging',
'errorWeight',
'calculateLighting',
'lightThreshold',
],
submitButton: new ButtonElement()
.setOnClick(() => {
this._appContext.do(EAction.Assign);
})
.setLabel('Assign blocks'),
output: new OutputElement(),
},
'export': {
label: 'Export',
elements: {
'export': new ComboBoxElement<TExporters>('File format', [
{ id: 'litematic', displayText: 'Litematic (.litematic)' },
{ id: 'schematic', displayText: 'Schematic (.schematic)' },
{ id: 'obj', displayText: 'Wavefront OBJ (.obj)' },
{ id: 'schem', displayText: 'Sponge Schematic (.schem)' },
{ id: 'nbt', displayText: 'Structure blocks (.nbt)' },
]),
'export': new ComboBoxElement<TExporters>()
.addItems([
{
displayText: 'Litematic (.litematic)',
payload: 'litematic',
},
{
displayText: 'Schematic (.schematic)',
payload: 'schematic',
},
{
displayText: 'Wavefront OBJ (.obj)',
payload: 'obj',
},
{
displayText: 'Sponge Schematic (.schem)',
payload: 'schem',
},
{
displayText: 'Structure blocks (.nbt)',
payload: 'nbt',
},
])
.setLabel('Exporter'),
},
elementsOrder: ['export'],
submitButton: new ButtonElement('Export structure', () => {
this._appContext.do(EAction.Export);
}),
submitButton: new ButtonElement()
.setLabel('Export structure')
.setOnClick(() => {
this._appContext.do(EAction.Export);
}),
output: new OutputElement(),
},
};
@ -326,9 +412,6 @@ export class UI {
constructor(appContext: AppContext) {
this._appContext = appContext;
this._ui.assign.elements.textureAtlas.addDescription('Textures to use and colour-match with');
this._ui.assign.elements.fallable.addDescription('Read tooltips for more info');
}
public tick(isBusy: boolean) {
@ -372,7 +455,7 @@ export class UI {
</div>
</div>
`;
groupHTML[groupName] += this._buildGroup(group);
groupHTML[groupName] += this._getGroupHTML(group);
}
let itemHTML = '';
@ -422,24 +505,34 @@ export class UI {
}
}
private _buildGroup(group: Group) {
public refreshSubcomponents(group: Group) {
const element = document.getElementById(`subcomponents_${group.label}`);
ASSERT(element !== null);
element.innerHTML = this._getGroupSubcomponentsHTML(group);
for (const elementName in group.elements) {
const element = group.elements[elementName];
element.registerEvents();
element.finalise();
}
}
private _getGroupSubcomponentsHTML(group: Group) {
let groupHTML = '';
for (const elementName of group.elementsOrder) {
const element = group.elements[elementName];
ASSERT(element !== undefined, `No element for: ${elementName}`);
groupHTML += this._buildSubcomponent(element);
}
return groupHTML;
}
let postGroupHTML = '';
if (group.postElements) {
ASSERT(group.postElementsOrder, 'No post elements order');
for (const elementName of group.postElementsOrder) {
const element = group.postElements[elementName];
postGroupHTML += this._buildSubcomponent(element);
}
}
private _getGroupHTML(group: Group) {
return `
${groupHTML}
<div id="subcomponents_${group.label}">
${this._getGroupSubcomponentsHTML(group)}
</div>
<div class="property">
<div class="prop-value-container">
${group.submitButton.generateHTML()}
@ -448,11 +541,10 @@ export class UI {
<div class="property">
${group.output.generateHTML()}
</div>
${postGroupHTML}
`;
}
private _buildSubcomponent(element: BaseUIElement<any>) {
private _buildSubcomponent(element: ConfigUIElement<any, any>) {
return `
<div class="property">
${element.generateHTML()}
@ -476,15 +568,9 @@ export class UI {
for (const elementName in group.elements) {
const element = group.elements[elementName];
element.registerEvents();
element.finalise();
}
group.submitButton.registerEvents();
if (group.postElements) {
ASSERT(group.postElementsOrder);
for (const elementName in group.postElements) {
const element = group.postElements[elementName];
element.registerEvents();
}
}
}
// Register toolbar left
@ -528,14 +614,6 @@ export class UI {
group.elements[compName].setEnabled(true);
}
group.submitButton.setEnabled(true);
// Enable the post elements of the previous group
const prevGroup = this._getEActionGroup(action - 1);
if (prevGroup && prevGroup.postElements) {
ASSERT(prevGroup.postElementsOrder);
for (const postElementName in prevGroup.postElements) {
prevGroup.postElements[postElementName].setEnabled(true);
}
}
}
public disableAll() {
@ -558,22 +636,6 @@ export class UI {
group.output.getMessage().clearAll();
group.output.updateMessage();
}
if (group.postElements) {
LOG(group.label, 'has post-element');
ASSERT(group.postElementsOrder);
for (const postElementName in group.postElements) {
LOG('disabling post-element', postElementName, 'for', group.label);
group.postElements[postElementName].setEnabled(false);
}
}
}
// Disable the post elements of the previous group
const prevGroup = this._getEActionGroup(action - 1);
if (prevGroup && prevGroup.postElements) {
ASSERT(prevGroup.postElementsOrder);
for (const postElementName in prevGroup.postElements) {
prevGroup.postElements[postElementName].setEnabled(false);
}
}
}
@ -590,7 +652,7 @@ export class UI {
const paletteID = file.split('.')[0];
let paletteName = paletteID.replace('-', ' ').toLowerCase();
paletteName = paletteName.charAt(0).toUpperCase() + paletteName.slice(1);
textureAtlases.push({ id: paletteID, displayText: paletteName });
textureAtlases.push({ payload: paletteID, displayText: paletteName });
}
});
@ -603,7 +665,7 @@ export class UI {
const palettes = PaletteManager.getPalettesInfo();
for (const palette of palettes) {
blockPalettes.push({
id: palette.paletteID,
payload: palette.paletteID,
displayText: palette.paletteDisplayName,
});
}

View File

@ -74,6 +74,16 @@ export class UITreeBuilder implements IUIOutputElement {
this._postBuildDelegates.forEach((delegate) => {
delegate();
});
const toggler = document.getElementsByClassName('caret') as HTMLCollectionOf<HTMLElement>;
for (let i = 0; i < toggler.length; i++) {
const temp = toggler[i];
temp.onclick = () => {
temp.parentElement?.querySelector('.nested')?.classList.toggle('active');
temp.classList.toggle('caret-down');
};
}
}
public buildHTML(): string {

View File

@ -4,7 +4,7 @@ export namespace AppUtil {
return text.charAt(0).toUpperCase() + text.slice(1);
}
/**
/**
* Namespaces a block name if it is not already namespaced
* For example `namespaceBlock('stone')` returns `'minecraft:stone'`
*/
@ -22,10 +22,11 @@ export namespace AppUtil {
/* eslint-disable */
export enum EAction {
Import = 0,
Voxelise = 1,
Assign = 2,
Export = 3,
MAX = 4,
Materials = 1,
Voxelise = 2,
Assign = 3,
Export = 4,
MAX = 5,
}
/* eslint-enable */

9
src/util/ui_util.ts Normal file
View File

@ -0,0 +1,9 @@
import { ASSERT } from './error_util';
export namespace UIUtil {
export function getElementById(id: string) {
const element = document.getElementById(id);
ASSERT(element !== null, `Attempting to getElement of nonexistent element: ${id}`);
return element as HTMLElement;
}
}

View File

@ -29,6 +29,8 @@ export abstract class IVoxeliser {
*/
protected _getVoxelColour(mesh: Mesh, triangle: UVTriangle, materialName: string, location: Vector3, multisample: boolean): RGBA {
const material = mesh.getMaterialByName(materialName);
ASSERT(material !== undefined);
if (material.type === MaterialType.solid) {
return RGBAUtil.copy(material.colour);
}
@ -49,7 +51,7 @@ export abstract class IVoxeliser {
private _internalGetVoxelColour(mesh: Mesh, triangle: UVTriangle, materialName: string, location: Vector3) {
const material = mesh.getMaterialByName(materialName);
ASSERT(material.type === MaterialType.textured);
ASSERT(material !== undefined && material.type === MaterialType.textured);
const area01 = new Triangle(triangle.v0, triangle.v1, location).getArea();
const area12 = new Triangle(triangle.v1, triangle.v2, location).getArea();

View File

@ -90,6 +90,7 @@ export class WorkerClient {
return {
materials: this._loadedMesh.getMaterials(),
materialsChanged: Array.from(params.materials.keys()), // TODO: Change to actual materials changed
};
}

View File

@ -27,7 +27,8 @@ export namespace SetMaterialsParams {
}
export type Output = {
materials: MaterialMap
materials: MaterialMap,
materialsChanged: string[],
}
}

View File

@ -3,25 +3,28 @@
:root {
--properties-width: max(400px, 20%);
--pill-radius: 5px;
--prop-bg: #1A1A1A;
--prop-alt: #252525;
--prop-bg: hsl(0, 0%, 10%);
--prop-alt: hsl(0, 0%, 15%);
--prop-accent-border-hovered: #33AAC2;
--prop-accent-hovered: #0095b3;
--prop-accent-standard: #00738a;
--prop-accent-disabled: #00404d;
--prop-border-hovered: #606060;
--prop-hovered: #383838;
--prop-standard: #2F2F2F;
--prop-disabled: #242424;
--prop-sunken: #141414;
--prop-border-hovered: hsl(0, 0%, 38%);
--prop-hovered: hsl(0, 0%, 22%);
--prop-standard: hsl(0, 0%, 18%);
--prop-disabled: hsl(0, 0%, 14%);
--prop-sunken: hsl(0, 0%, 8%);
--text-standard: #A8A8A8;
--text-disabled: #535353;
--text-standard: hsl(0, 0%, 66%);
--text-disabled: hsl(0, 0%, 33%);
--vertical-divider: hsl(0, 0%, 16%);
--border-radius: 5px;
--property-height: 34px;
--subproperty-height: 20px;
}
@ -85,7 +88,7 @@ canvas {
align-self: center;
padding: 0px 10px 0px 0px;
width: 125px;
/*border: 1px solid red;*/
overflow: auto;
}
.prop-key-container-disabled {
@ -97,10 +100,64 @@ canvas {
display: flex;
align-items: center;
/*border: 1px solid yellow;*/
height: var(--property-height);
width: 0px;
/*border-left: 2px solid var(--vertical-divider);*/
padding-left: 5px;
}
.subproperty-container {
display: flex;
flex-direction: column;
width: 100%;
}
.subproperty {
display: flex;
flex-direction: row;
align-items: center;
color: var(--text-standard);
font-size: 85%;
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
border-bottom: 1px solid var(--vertical-divider);
}
.subproperty:last-child {
border-bottom: 0px;
}
.subprop-key-container {
align-self: center;
padding: 0px 10px 0px 0px;
width: 30%;
}
.subprop-value-container {
flex-grow: 1;
display: flex;
align-items: center;
min-height: var(--subproperty-height);
width: 100%;
overflow: auto;
font-weight: 300;
/*border-left: 1px solid var(--vertical-divider);*/
padding-left: 5px;
}
.height-small {
height: var(--subproperty-height);
font-size: 100%;
}
.height-normal {
height: var(--property-height);
}
.item-body-sunken {
background: var(--prop-sunken);
box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 10px 0px inset;
@ -117,9 +174,12 @@ canvas {
overflow-wrap: anywhere;
}
.slider-number-input {
height: calc(var(--property-height) - 4px);
}
input {
user-select: none;
height: calc(100% - 4px);
margin-right: 3px;
border-radius: var(--border-radius);
text-align: center;
@ -147,7 +207,7 @@ input::-webkit-inner-spin-button {
select {
width: 100%;
height: 100%;
height: var(--property-height);
padding-left: 5px;
border-radius: var(--border-radius);
font-family: 'Lexend', sans-serif;
@ -177,7 +237,7 @@ select:disabled {
font-weight: 300;
background: var(--prop-standard);
cursor: ew-resize;
height: calc(100% - 2px);
height: calc(var(--property-height) - 2px);
border: 1px solid var(--prop-bg);
}
.new-slider-hover {
@ -209,7 +269,7 @@ select:disabled {
.button {
width: calc(100% - 2px);
height: calc(100% - 2px);
height: calc(var(--property-height) - 2px);
background: var(--prop-accent-standard);
border-radius: var(--border-radius);
color: white;
@ -252,7 +312,7 @@ select:disabled {
display: flex;
align-items: center;
width: 100%;
height: calc(100% - 2px);
height: calc(var(--property-height) - 2px);
background: var(--prop-standard);
border-radius: var(--border-radius);
font-weight: 300;
@ -286,6 +346,7 @@ select:disabled {
::-webkit-scrollbar {
width: 10px;
height: 5px;
}
::-webkit-scrollbar-track {
@ -405,6 +466,12 @@ select:disabled {
background-color: var(--prop-standard);
border: 1px solid var(--prop-standard);
pointer-events: all;
gap: 2px;
}
.toolbar-item-small {
height: 24px !important;
padding: 0px 4px 0px 4px !important;
}
.toolbar-item-hover {
@ -416,6 +483,8 @@ select:disabled {
.toolbar-item-active-hover {
background-color: var(--prop-accent-hovered) !important;
border: 1px solid var(--prop-accent-border-hovered) !important;
cursor: pointer;
color: white;
}
.toolbar-item-disabled-active {
@ -432,6 +501,7 @@ select:disabled {
.toolbar-item-active {
background-color: var(--prop-accent-standard) !important;
border-color: var(--prop-accent-border-hovered) !important;
color: white;
}
.toolbar-item:first-child {
@ -646,8 +716,7 @@ a:hover {
-moz-appearance: none;
appearance: none;
background: none;
width: 75%;
height: 24px;
width: 100%;
margin: 0px;
padding: 0px;
}
@ -670,6 +739,7 @@ a:hover {
.texture-preview {
border: 1px solid #8C8C8C80;
border-radius: 5px;
width: calc(100% - 2px);
}
.texture-preview-missing {
border-color: orange !important;
@ -683,7 +753,7 @@ a:hover {
}
.checkbox {
height: 75%;
height: calc(var(--property-height) * 0.75);
aspect-ratio: 1/1;
background-color: var(--prop-standard);
border-radius: 5px;
@ -750,4 +820,30 @@ a:hover {
.spinbox-element-container:first-child {
padding-right: 0px;
}
.row-container {
display: flex;
flex-direction: column;
gap: 2px;
}
.row-item {
display: flex;
flex-direction: row;
align-items: center;
}
.col-container {
display: flex;
flex-direction: row;
gap: 2px;
}
.col-item {
display: flex;
flex-direction: column;
align-items: center;
}