forked from mirror/ObjToSchematic
Merge pull request #96 from LucasDower/0.7-material-action
0.7 material action
This commit is contained in:
commit
8379ff51b0
4
res/static/folder.svg
Normal file
4
res/static/folder.svg
Normal 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
7
res/static/switch.svg
Normal 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
6
res/static/upload.svg
Normal 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 |
@ -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({
|
||||
|
@ -243,6 +243,8 @@ export class BufferGenerator {
|
||||
}
|
||||
|
||||
const material = mesh.getMaterialByName(materialName);
|
||||
ASSERT(material !== undefined);
|
||||
|
||||
materialBuffers.push({
|
||||
buffer: materialBuffer,
|
||||
material: material,
|
||||
|
@ -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
48
src/material-map.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
61
src/mesh.ts
61
src/mesh.ts
@ -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');
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
85
src/ui/elements/base_element.ts
Normal file
85
src/ui/elements/base_element.ts
Normal 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;
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
119
src/ui/elements/config_element.ts
Normal file
119
src/ui/elements/config_element.ts
Normal 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;
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
src/ui/elements/image_element.ts
Normal file
87
src/ui/elements/image_element.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
52
src/ui/elements/material_type_element.ts
Normal file
52
src/ui/elements/material_type_element.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
75
src/ui/elements/solid_material_element.ts
Normal file
75
src/ui/elements/solid_material_element.ts
Normal 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;
|
||||
}
|
||||
}
|
115
src/ui/elements/textured_material_element.ts
Normal file
115
src/ui/elements/textured_material_element.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
380
src/ui/layout.ts
380
src/ui/layout.ts
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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 {
|
||||
|
11
src/util.ts
11
src/util.ts
@ -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
9
src/util/ui_util.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -90,6 +90,7 @@ export class WorkerClient {
|
||||
|
||||
return {
|
||||
materials: this._loadedMesh.getMaterials(),
|
||||
materialsChanged: Array.from(params.materials.keys()), // TODO: Change to actual materials changed
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,8 @@ export namespace SetMaterialsParams {
|
||||
}
|
||||
|
||||
export type Output = {
|
||||
materials: MaterialMap
|
||||
materials: MaterialMap,
|
||||
materialsChanged: string[],
|
||||
}
|
||||
}
|
||||
|
||||
|
134
styles.css
134
styles.css
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user