Added options to replace falling blocks

This commit is contained in:
Lucas Dower 2022-05-15 15:17:52 +01:00
parent 71ecb62ab0
commit 6672d2eda2
12 changed files with 125 additions and 17 deletions

27
res/fallable_blocks.json Normal file
View File

@ -0,0 +1,27 @@
{
"fallable_blocks": [
"anvil",
"lime_concrete_powder",
"orange_concrete_powder",
"black_concrete_powder",
"brown_concrete_powder",
"cyan_concrete_powder",
"light_gray_concrete_powder",
"purple_concrete_powder",
"magenta_concrete_powder",
"light_blue_concrete_powder",
"yellow_concrete_powder",
"white_concrete_powder",
"blue_concrete_powder",
"red_concrete_powder",
"gray_concrete_powder",
"pink_concrete_powder",
"green_concrete_powder",
"dragon_egg",
"gravel",
"pointed_dripstone",
"red_sand",
"sand",
"scaffolding"
]
}

View File

@ -8,7 +8,7 @@ import { ASSERT, ColourSpace, AppError, LOG, LOG_ERROR, LOG_WARN, TIME_START, TI
import { remote } from 'electron';
import { VoxelMesh, VoxelMeshParams } from './voxel_mesh';
import { BlockMesh, BlockMeshParams } from './block_mesh';
import { BlockMesh, BlockMeshParams, FallableBehaviour } from './block_mesh';
import { TextureFiltering } from './texture';
import { RayVoxeliser } from './voxelisers/ray-voxeliser';
import { IVoxeliser } from './voxelisers/base-voxeliser';
@ -193,6 +193,7 @@ export class AppContext {
blockPalette: uiElements.blockPalette.getCachedValue(),
ditheringEnabled: uiElements.dithering.getCachedValue() === 'on',
colourSpace: uiElements.colourSpace.getCachedValue() === 'rgb' ? ColourSpace.RGB : ColourSpace.LAB,
fallable: uiElements.fallable.getCachedValue() as FallableBehaviour,
};
this._loadedBlockMesh = BlockMesh.createFromVoxelMesh(this._loadedVoxelMesh, blockMeshParams);

View File

@ -3,12 +3,12 @@ import { ASSERT, ColourSpace, RGB } from './util';
import { Vector3 } from './vector';
interface IBlockAssigner {
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace): BlockInfo;
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace, exclude?: string[]): BlockInfo;
}
export class BasicBlockAssigner implements IBlockAssigner {
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace): BlockInfo {
return BlockAtlas.Get.getBlock(voxelColour, colourSpace);
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace, exclude?: string[]): BlockInfo {
return BlockAtlas.Get.getBlock(voxelColour, colourSpace, exclude);
}
}
@ -36,7 +36,7 @@ export class OrderedDitheringBlockAssigner implements IBlockAssigner {
return (OrderedDitheringBlockAssigner._mapMatrix[index] / (size * size * size)) - 0.5;
}
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace): BlockInfo {
assignBlock(voxelColour: RGB, voxelPosition: Vector3, colourSpace: ColourSpace, exclude?: string[]): BlockInfo {
const size = OrderedDitheringBlockAssigner._size;
const map = this._getThresholdValue(
Math.abs(voxelPosition.x % size),
@ -50,6 +50,6 @@ export class OrderedDitheringBlockAssigner implements IBlockAssigner {
((255 * voxelColour.b) + map * OrderedDitheringBlockAssigner._threshold) / 255,
);
return BlockAtlas.Get.getBlock(newVoxelColour, colourSpace);
return BlockAtlas.Get.getBlock(newVoxelColour, colourSpace, exclude);
}
}

View File

@ -1,5 +1,5 @@
import { HashMap } from './hash_map';
import { UV, RGB, ASSERT, fileExists, ColourSpace, ATLASES_DIR, PALETTES_DIR, AppError, LOG_WARN } from './util';
import { UV, RGB, ASSERT, fileExists, ColourSpace, ATLASES_DIR, PALETTES_DIR, AppError, LOG_WARN, LOG } from './util';
import { Vector3 } from './vector';
import fs from 'fs';
@ -124,12 +124,12 @@ export class BlockAtlas {
this._paletteLoaded = true;
}
public getBlock(voxelColour: RGB, colourSpace: ColourSpace): BlockInfo {
public getBlock(voxelColour: RGB, colourSpace: ColourSpace, exclude?: string[]): BlockInfo {
ASSERT(this._atlasLoaded, 'No atlas has been loaded');
ASSERT(this._paletteLoaded, 'No palette has been loaded');
const cachedBlockIndex = this._cachedBlocks.get(voxelColour.toVector3());
if (cachedBlockIndex) {
if (cachedBlockIndex && exclude === undefined) {
return this._atlasBlocks[cachedBlockIndex];
}
@ -137,6 +137,10 @@ export class BlockAtlas {
let blockChoiceIndex!: number;
for (const paletteBlockName of this._palette) {
if (exclude?.includes(paletteBlockName)) {
continue;
}
// TODO: Optimise Use hash map for blockIndex instead of linear search
const blockIndex: (number | undefined) = this._paletteBlockToBlockInfoIndex.get(paletteBlockName);
ASSERT(blockIndex !== undefined);
@ -155,7 +159,9 @@ export class BlockAtlas {
throw new AppError('The chosen palette does not have suitable blocks');
}
this._cachedBlocks.add(voxelColour.toVector3(), blockChoiceIndex);
if (exclude === undefined) {
this._cachedBlocks.add(voxelColour.toVector3(), blockChoiceIndex);
}
return this._atlasBlocks[blockChoiceIndex];
}

View File

@ -1,26 +1,35 @@
import { BasicBlockAssigner, OrderedDitheringBlockAssigner } from './block_assigner';
import { Voxel, VoxelMesh } from './voxel_mesh';
import { BlockAtlas, BlockInfo } from './block_atlas';
import { ColourSpace, AppError, ASSERT } from './util';
import { ColourSpace, AppError, ASSERT, RESOURCES_DIR, LOG } from './util';
import { Renderer } from './renderer';
import { AppConstants } from './constants';
import fs from 'fs';
import path from 'path';
import { StatusHandler } from './status';
import { Vector3 } from './vector';
interface Block {
voxel: Voxel;
blockInfo: BlockInfo;
}
export type FallableBehaviour = 'replace-falling' | 'replace-fallable' | 'place-string' | 'do-nothing';
export interface BlockMeshParams {
textureAtlas: string,
blockPalette: string,
ditheringEnabled: boolean,
colourSpace: ColourSpace,
fallable: FallableBehaviour,
}
export class BlockMesh {
private _blockPalette: string[];
private _blocks: Block[];
private _voxelMesh: VoxelMesh;
private _fallableBlocks: string[];
public static createFromVoxelMesh(voxelMesh: VoxelMesh, blockMeshParams: BlockMeshParams) {
const blockMesh = new BlockMesh(voxelMesh);
@ -32,6 +41,9 @@ export class BlockMesh {
this._blockPalette = [];
this._blocks = [];
this._voxelMesh = voxelMesh;
const fallableBlocksString = fs.readFileSync(path.join(RESOURCES_DIR, 'fallable_blocks.json'), 'utf-8');
this._fallableBlocks = JSON.parse(fallableBlocksString).fallable_blocks;
}
private _assignBlocks(blockMeshParams: BlockMeshParams) {
@ -40,10 +52,27 @@ export class BlockMesh {
const blockAssigner = blockMeshParams.ditheringEnabled ? new OrderedDitheringBlockAssigner() : new BasicBlockAssigner();
let countFalling = 0;
const voxels = this._voxelMesh.getVoxels();
for (let voxelIndex = 0; voxelIndex < voxels.length; ++voxelIndex) {
const voxel = voxels[voxelIndex];
const block = blockAssigner.assignBlock(voxel.colour, voxel.position, blockMeshParams.colourSpace);
let block = blockAssigner.assignBlock(voxel.colour, voxel.position, blockMeshParams.colourSpace);
const isFallable = this._fallableBlocks.includes(block.name);
const isSupported = this._voxelMesh.isVoxelAt(Vector3.add(voxel.position, new Vector3(0, -1, 0)));
if (isFallable && !isSupported) {
++countFalling;
}
let shouldReplace = (blockMeshParams.fallable === 'replace-fallable' && isFallable);
shouldReplace ||= (blockMeshParams.fallable === 'replace-falling' && isFallable && !isSupported);
if (shouldReplace) {
const replacedBlock = blockAssigner.assignBlock(voxel.colour, voxel.position, blockMeshParams.colourSpace, this._fallableBlocks);
// LOG(`Replacing ${block.name} with ${replacedBlock.name}`);
block = replacedBlock;
}
this._blocks.push({
voxel: voxel,
@ -53,6 +82,10 @@ export class BlockMesh {
this._blockPalette.push(block.name);
}
}
if (blockMeshParams.fallable === 'do-nothing' && countFalling > 0) {
StatusHandler.Get.add('warning', `${countFalling.toLocaleString()} blocks will fall under gravity when this structure is placed`);
}
}
public getBlocks(): Block[] {

View File

@ -4,6 +4,7 @@ import { ASSERT } from '../../util';
export interface ComboBoxItem {
id: string;
displayText: string;
tooltip?: string;
}
export class ComboBoxElement extends LabelledElement<string> {
@ -17,7 +18,7 @@ export class ComboBoxElement extends LabelledElement<string> {
public generateInnerHTML() {
let itemsHTML = '';
for (const item of this._items) {
itemsHTML += `<option value="${item.id}">${item.displayText}</option>`;
itemsHTML += `<option value="${item.id}" title="${item.tooltip || ''}">${item.displayText}</option>`;
}
return `

View File

@ -3,16 +3,21 @@ import { ASSERT, getRandomID } from '../../util';
export class LabelElement {
private _id: string;
private _text: string;
private _description?: string;
constructor(text: string) {
constructor(text: string, description?: string) {
this._id = getRandomID();
this._text = text;
this._description = description;
}
public generateHTML(): string {
const description = this._description ? `<br><div style="font-weight: 300; font-size: 85%; color: var(--text-disabled);">
${this._description}
</div>` : '';
return `
<div class="prop-left" id="${this._id}">
${this._text}
${this._text}${description}
</div>
`;
}

View File

@ -6,6 +6,7 @@ export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
public constructor(label: string) {
super(label);
this._label = label;
this._labelElement = new LabelElement(label);
}
@ -24,4 +25,8 @@ export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
protected _onEnabledChanged() {
this._labelElement.setEnabled(this._isEnabled);
}
public addDescription(text: string) {
this._labelElement = new LabelElement(this._label, text);
}
}

View File

@ -94,8 +94,32 @@ export class UI {
{ id: 'rgb', displayText: 'RGB (faster)' },
{ id: 'lab', displayText: 'LAB (recommended, slow)' },
]),
'fallable': new ComboBoxElement('Fallable blocks', [
{
id: 'replace-falling',
displayText: 'Replace falling with solid',
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',
tooltip: 'Let the block fall',
},
]),
},
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'colourSpace'],
elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'colourSpace', 'fallable'],
submitButton: new ButtonElement('Assign blocks', () => {
this._appContext.do(EAction.Assign);
}),
@ -242,6 +266,9 @@ 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 build() {

View File

@ -141,6 +141,7 @@ select {
color: var(--text-standard);
background: var(--prop-standard);
border: 1px solid rgb(255, 255, 255, 0.0);
max-width: 100%;
}
select:hover:enabled {
color: #C6C6C6;

View File

@ -16,6 +16,7 @@ export const headlessConfig = {
blockPalette: 'all-supported', // Must be a palette name that exists in /resources/palettes
ditheringEnabled: true,
colourSpace: 'rgb', // 'rgb' / 'lab';
fallable: 'replace-falling', // 'replace-fallable' / 'place-string';
},
},
export: {

View File

@ -2,7 +2,7 @@ import { Mesh } from '../src/mesh';
import { ObjImporter } from '../src/importers/obj_importer';
import { IVoxeliser } from '../src/voxelisers/base-voxeliser';
import { VoxelMesh, VoxelMeshParams } from '../src/voxel_mesh';
import { BlockMesh, BlockMeshParams } from '../src/block_mesh';
import { BlockMesh, BlockMeshParams, FallableBehaviour } from '../src/block_mesh';
import { IExporter} from '../src/exporters/base_exporter';
import { Schematic } from '../src/exporters/schematic_exporter';
import { Litematic } from '../src/exporters/litematic_exporter';
@ -32,6 +32,7 @@ void async function main() {
blockPalette: headlessConfig.palette.blockMeshParams.blockPalette,
ditheringEnabled: headlessConfig.palette.blockMeshParams.ditheringEnabled,
colourSpace: headlessConfig.palette.blockMeshParams.colourSpace === 'rgb' ? ColourSpace.RGB : ColourSpace.LAB,
fallable: headlessConfig.palette.blockMeshParams.fallable as FallableBehaviour,
},
});
_export(blockMesh, {