From 6672d2eda29bdc7d2624a437bf4ff0a261851a66 Mon Sep 17 00:00:00 2001 From: Lucas Dower Date: Sun, 15 May 2022 15:17:52 +0100 Subject: [PATCH] Added options to replace falling blocks --- res/fallable_blocks.json | 27 +++++++++++++++++++++ src/app_context.ts | 3 ++- src/block_assigner.ts | 10 ++++---- src/block_atlas.ts | 14 +++++++---- src/block_mesh.ts | 37 +++++++++++++++++++++++++++-- src/ui/elements/combobox.ts | 3 ++- src/ui/elements/label.ts | 9 +++++-- src/ui/elements/labelled_element.ts | 5 ++++ src/ui/layout.ts | 29 +++++++++++++++++++++- styles.css | 1 + tools/headless-config.ts | 1 + tools/headless.ts | 3 ++- 12 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 res/fallable_blocks.json diff --git a/res/fallable_blocks.json b/res/fallable_blocks.json new file mode 100644 index 0000000..4f10e80 --- /dev/null +++ b/res/fallable_blocks.json @@ -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" + ] +} \ No newline at end of file diff --git a/src/app_context.ts b/src/app_context.ts index 90f93e2..58f4666 100644 --- a/src/app_context.ts +++ b/src/app_context.ts @@ -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); diff --git a/src/block_assigner.ts b/src/block_assigner.ts index eb2b2af..f03df7f 100644 --- a/src/block_assigner.ts +++ b/src/block_assigner.ts @@ -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); } } diff --git a/src/block_atlas.ts b/src/block_atlas.ts index 4b4d8f7..292ec79 100644 --- a/src/block_atlas.ts +++ b/src/block_atlas.ts @@ -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]; } diff --git a/src/block_mesh.ts b/src/block_mesh.ts index ca063d8..a9e24d9 100644 --- a/src/block_mesh.ts +++ b/src/block_mesh.ts @@ -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[] { diff --git a/src/ui/elements/combobox.ts b/src/ui/elements/combobox.ts index ad7e70c..a671c29 100644 --- a/src/ui/elements/combobox.ts +++ b/src/ui/elements/combobox.ts @@ -4,6 +4,7 @@ import { ASSERT } from '../../util'; export interface ComboBoxItem { id: string; displayText: string; + tooltip?: string; } export class ComboBoxElement extends LabelledElement { @@ -17,7 +18,7 @@ export class ComboBoxElement extends LabelledElement { public generateInnerHTML() { let itemsHTML = ''; for (const item of this._items) { - itemsHTML += ``; + itemsHTML += ``; } return ` diff --git a/src/ui/elements/label.ts b/src/ui/elements/label.ts index b10a7f4..d7be38b 100644 --- a/src/ui/elements/label.ts +++ b/src/ui/elements/label.ts @@ -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 ? `
+ ${this._description} +
` : ''; return `
- ${this._text} + ${this._text}${description}
`; } diff --git a/src/ui/elements/labelled_element.ts b/src/ui/elements/labelled_element.ts index 8207b37..6ffe250 100644 --- a/src/ui/elements/labelled_element.ts +++ b/src/ui/elements/labelled_element.ts @@ -6,6 +6,7 @@ export abstract class LabelledElement extends BaseUIElement { public constructor(label: string) { super(label); + this._label = label; this._labelElement = new LabelElement(label); } @@ -24,4 +25,8 @@ export abstract class LabelledElement extends BaseUIElement { protected _onEnabledChanged() { this._labelElement.setEnabled(this._isEnabled); } + + public addDescription(text: string) { + this._labelElement = new LabelElement(this._label, text); + } } diff --git a/src/ui/layout.ts b/src/ui/layout.ts index 46afce5..94c36b5 100644 --- a/src/ui/layout.ts +++ b/src/ui/layout.ts @@ -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() { diff --git a/styles.css b/styles.css index 8b28eee..ac39161 100644 --- a/styles.css +++ b/styles.css @@ -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; diff --git a/tools/headless-config.ts b/tools/headless-config.ts index 88d4631..d28f671 100644 --- a/tools/headless-config.ts +++ b/tools/headless-config.ts @@ -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: { diff --git a/tools/headless.ts b/tools/headless.ts index 6065be1..a6b1fb4 100644 --- a/tools/headless.ts +++ b/tools/headless.ts @@ -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, {