Finished moving material editing to the new material step

This commit is contained in:
Lucas Dower 2023-01-19 18:51:05 +00:00
parent ac0e1eb22b
commit eb139e9cad
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
16 changed files with 241 additions and 493 deletions

View File

@ -3,39 +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 { CheckboxElement } from './ui/elements/checkbox';
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');
@ -206,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...');
@ -217,8 +214,9 @@ export class AppContext {
outputElement.setTaskComplete('render', '[Renderer]: Stopped', [message], 'warning');
}
this._updateMaterialsAction(payload.result.materials);
this._updateMaterialsAction();
};
callback.bind(this);
return { id: 'Import', payload: payload, callback: callback };
}
@ -230,7 +228,7 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'SetMaterials',
params: {
materials: this._materialMap,
materials: this._materialManager.materials,
},
};
@ -250,8 +248,10 @@ export class AppContext {
}
payload.result.materialsChanged.forEach((materialName) => {
const material = this._materialMap[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);
@ -260,204 +260,32 @@ export class AppContext {
return { id: 'Import', payload: payload, callback: callback };
}
private _updateMaterialsAction(materials: MaterialMap) {
private _updateMaterialsAction() {
this._ui.layoutDull['materials'].elements = {};
this._ui.layoutDull['materials'].elementsOrder = [];
for (const materialName in materials) {
const material = this._materialMap[materialName];
this._materialManager.materials.forEach((material, materialName) => {
if (material.type === MaterialType.solid) {
this._ui.layoutDull['materials'].elements[`mat_${materialName}`] = new SolidMaterialElement(materialName, material)
.setLabel(materialName);
.setLabel(materialName)
.onChangeTypeDelegate(() => {
this._materialManager.changeMaterialType(materialName, MaterialType.textured);
this._updateMaterialsAction();
});
} else {
this._ui.layoutDull['materials'].elements[`mat_${materialName}`] = new TexturedMaterialElement(materialName, material)
.setLabel(materialName);
.setLabel(materialName)
.onChangeTypeDelegate(() => {
this._materialManager.changeMaterialType(materialName, MaterialType.solid);
this._updateMaterialsAction();
});
}
this._ui.layoutDull['materials'].elementsOrder.push(`mat_${materialName}`);
}
});
this._ui.refreshSubcomponents(this._ui.layoutDull['materials']);
}
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();
}
if (material.type === MaterialType.solid) {
const uiElement = new SolidMaterialUIElement(materialName, this, material);
subTree.addChild({ html: uiElement.buildHTML(), warning: uiElement.hasWarning()}, () => {
uiElement.registerEvents();
});
} else {
const uiElement = new TextureMaterialUIElement(materialName, this, material);
subTree.addChild({ html: uiElement.buildHTML(), warning: uiElement.hasWarning()}, () => {
uiElement.registerEvents();
});
}
tree.addChild(subTree);
}
messageBuilder.setTree('materials', tree);
outputElement.updateMessage();
this._updateMaterialsAction(this._materialMap);
}
private _renderMesh(): TWorkerJob {
const payload: TToWorkerMessage = {
action: 'RenderMesh',

View File

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

View File

@ -21,9 +21,19 @@ export class ObjImporter extends IImporter {
private _uvs: UV[] = [];
private _tris: Tri[] = [];
private _materials: { [key: string]: (SolidMaterial | TexturedMaterial) } = {
'DEFAULT_UNASSIGNED': { type: MaterialType.solid, colour: RGBAColours.WHITE, edited: true, canBeTextured: false, set: true, open: false },
};
private _materials: Map<string, SolidMaterial | TexturedMaterial>;
public constructor() {
super();
this._materials = new Map();
this._materials.set('DEFAULT_UNASSIGNED', {
type: MaterialType.solid,
colour: RGBAColours.WHITE,
canBeTextured: false,
needsAttention: false,
});
}
private _mtlLibs: string[] = [];
private _currentMaterialName: string = 'DEFAULT_UNASSIGNED';
@ -392,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,
@ -413,11 +422,9 @@ export class ObjImporter extends IImporter {
b: this._currentColour.b,
a: this._currentAlpha,
},
edited: false,
canBeTextured: false,
set: true,
open: false,
};
needsAttention: false,
});
}
this._currentAlpha = 1.0;
}

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

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

View File

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

View File

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

View File

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

View File

@ -36,12 +36,26 @@ export class ComboBoxElement<T> extends ConfigUIElement<T, HTMLSelectElement> {
}
public override registerEvents(): void {
this._getElement().addEventListener('onchange', (e: Event) => {
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);
@ -59,10 +73,19 @@ export class ComboBoxElement<T> extends ConfigUIElement<T, HTMLSelectElement> {
protected override _onEnabledChanged() {
super._onEnabledChanged();
this._getElement().disabled = !this.getEnabled();
}
// TODO: Subproperty combo boxes are not updating values when changed!!!
protected override _onValueChanged(): void {
}
public override finalise(): void {
const selectedIndex = this._items.findIndex((item) => item.payload === this.getValue());
const element = this._getElement();
ASSERT(selectedIndex !== -1, 'Invalid selected index');
element.selectedIndex = selectedIndex;
}
}

View File

@ -10,6 +10,7 @@ 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>;
@ -18,6 +19,7 @@ export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
super();
this._value = defaultValue;
this._label = 'unknown';
this._hasLabel = false;
this._labelElement = new LabelElement(this._label, this._description);
this._onValueChangedListeners = [];
}
@ -28,6 +30,7 @@ export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
}
public setLabel(label: string) {
this._hasLabel = true;
this._label = label;
this._labelElement = new LabelElement(this._label, this._description);
return this;
@ -92,7 +95,9 @@ export abstract class ConfigUIElement<T, F> extends BaseUIElement<F> {
protected abstract _generateInnerHTML(): string;
protected override _onEnabledChanged() {
this._labelElement.setEnabled(this.getEnabled());
if (this._hasLabel) {
this._labelElement.setEnabled(this.getEnabled());
}
}
/**

View File

@ -80,6 +80,8 @@ export class ImageElement extends ConfigUIElement<string, HTMLDivElement> {
}
public override finalise(): void {
super.finalise();
this._onValueChanged();
}
}

View File

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

View File

@ -1,4 +1,3 @@
import { ASSERT } from '../../../tools/misc';
import { MaterialType } from '../../mesh';
import { ConfigUIElement } from './config_element';
import { ToolbarItemElement } from './toolbar_item';
@ -12,7 +11,7 @@ export class MaterialTypeElement extends ConfigUIElement<MaterialType, HTMLDivEl
.setSmall()
.setLabel('Switch')
.onClick(() => {
ASSERT(false, 'UNIMPLEMENTED!!!'); // TODO
this._onClickChangeTypeDelegate?.();
});
}
@ -44,4 +43,10 @@ export class MaterialTypeElement extends ConfigUIElement<MaterialType, HTMLDivEl
protected override _onValueChanged(): void {
}
private _onClickChangeTypeDelegate?: () => void;
public onClickChangeTypeDelegate(delegate: () => void) {
this._onClickChangeTypeDelegate = delegate;
return this;
}
}

View File

@ -1,19 +1,35 @@
import { RGBAUtil } from '../../colour';
import { SolidMaterial } from '../../mesh';
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 {
@ -33,7 +49,7 @@ export class SolidMaterialElement extends ConfigUIElement<SolidMaterial, HTMLDiv
`);
};
addSubproperty('Type', `Solid`);
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)}`);
@ -50,4 +66,10 @@ export class SolidMaterialElement extends ConfigUIElement<SolidMaterial, HTMLDiv
protected override _onEnabledChanged(): void {
super._onEnabledChanged();
}
private _onChangeTypeDelegate?: () => void;
public onChangeTypeDelegate(delegate: () => void) {
this._onChangeTypeDelegate = delegate;
return this;
}
}

View File

@ -21,12 +21,14 @@ export class TexturedMaterialElement extends ConfigUIElement<TexturedMaterial, H
this._filteringElement = new ComboBoxElement<'linear' | 'nearest'>()
.addItem({ payload: 'linear', displayText: 'Linear' })
.addItem({ payload: 'nearest', displayText: 'Nearest' })
.setSmall();
.setSmall()
.setDefaultValue(material.interpolation);
this._wrapElement = new ComboBoxElement<'clamp' | 'repeat'>()
.addItem({ payload: 'clamp', displayText: 'Clamp' })
.addItem({ payload: 'repeat', displayText: 'Repeat' })
.setSmall();
.setSmall()
.setDefaultValue(material.extension);
this._imageElement = new ImageElement(material.path);
@ -38,6 +40,25 @@ export class TexturedMaterialElement extends ConfigUIElement<TexturedMaterial, H
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 {
@ -78,6 +99,17 @@ export class TexturedMaterialElement extends ConfigUIElement<TexturedMaterial, H
}
public override finalise(): void {
super.finalise();
this._imageElement.finalise();
this._typeElement.finalise();
this._filteringElement.finalise();
this._wrapElement.finalise();
}
private _onChangeTypeDelegate?: () => void;
public onChangeTypeDelegate(delegate: () => void) {
this._onChangeTypeDelegate = delegate;
return this;
}
}

View File

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

View File

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