Skip to content

Commit

Permalink
Merge pull request #264 from NicDoesCode/feature/2023-10-flip-tile-map
Browse files Browse the repository at this point in the history
Flip/mirror tile maps, reference images from tile maps.
  • Loading branch information
NicDoesCode authored May 8, 2024
2 parents 7c7235b + 4042d37 commit 6986386
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 126 deletions.
2 changes: 1 addition & 1 deletion wwwroot/modules/components/canvasManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ export default class CanvasManager {
const originalSmoothingEnabled = context.imageSmoothingEnabled;
const originalSmoothingQuality = context.imageSmoothingQuality;

context.imageSmoothingEnabled = true;
context.imageSmoothingEnabled = false;
context.imageSmoothingQuality = 'high';

const pxSize = coords.pxSize;
Expand Down
128 changes: 119 additions & 9 deletions wwwroot/modules/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import KeyboardManager, { KeyDownHandler, KeyUpHandler } from "./components/keyb
import ProjectList from "./models/projectList.js";
import SystemUtil from "./util/systemUtil.js";
import { DropPosition } from "./types.js";
import TileMapUtil from "./util/tileMapUtil.js";
import PaintUtil from "./util/paintUtil.js";


/* ****************************************************************************************************
Expand Down Expand Up @@ -1054,7 +1056,7 @@ function handleExportToolbarOnCommand(args) {
break;

case ExportToolbar.Commands.exportImage:
tileEditor.setState({ requestExportImage: true });
exportCurrentTileGridAsImage();
break;

}
Expand Down Expand Up @@ -1383,6 +1385,10 @@ function handleTileManagerOnCommand(args) {
tileMapClone(args.tileMapId);
break;

case TileManager.Commands.tileMapCloneMirror:
tileMapClone(args.tileMapId, { mirror: true });
break;

case TileManager.Commands.tileMapDelete:
tileMapRemove(args.tileMapId);
break;
Expand All @@ -1391,6 +1397,18 @@ function handleTileManagerOnCommand(args) {
tileMapUpdate(args.tileMapId, args);
break;

case TileManager.Commands.tileMapMirror:
tileMapMirrorOrFlip(args.tileMapId, { mirror: true });
break;

case TileManager.Commands.tileMapFlip:
tileMapMirrorOrFlip(args.tileMapId, { flip: true });
break;

case TileManager.Commands.tileMapReference:
referenceImageFromTileMap(args.tileMapId);
break;

case TileManager.Commands.tileSetChange:
tileSetUpdate(args);
break;
Expand Down Expand Up @@ -3313,6 +3331,33 @@ function selectReferenceImage() {
fileInput.click();
}

/**
* Sets a tile map as a reference image.
* @param {string} tileMapId - Unique ID of the tile map.
*/
function referenceImageFromTileMap(tileMapId) {
if (!tileMapId || typeof tileMapId !== 'string') throw new Error('Please pass a tile map ID.');
const tileMap = getTileMapList().getTileMapById(tileMapId);
if (!tileMap) throw new Error('Tile map not found.');

const palettes = tileMap.getPalettes().map((id) => getPaletteList().getPaletteById(id));
const transparentIndicies = tileMap.isSprite ? [0] : [];
const image = PaintUtil.createTileGridImage(tileMap, getTileSet(), palettes, transparentIndicies);

instanceState.referenceImage = new ReferenceImage();
instanceState.referenceImage.setImage(image);
instanceState.referenceImage.setBounds(0, 0, image.width, image.height);

tileContextToolbar.setState({
referenceBounds: instanceState.referenceImage.getBounds(),
referenceTransparency: instanceState.transparencyIndex
});
tileEditor.setState({
referenceImage: instanceState.referenceImage,
transparencyIndicies: getTransparencyIndicies()
});
}

function clearReferenceImage() {

instanceState.referenceImage.clearImage();
Expand Down Expand Up @@ -4216,6 +4261,16 @@ function downloadAssemblyCode(code) {
a.remove();
}

/**
* Exports the currently selected tile set or tile grid to an image.
*/
function exportCurrentTileGridAsImage() {
const tileGrid = getTileGrid();
const palettes = getPaletteListToSuitTileMapOrTileSetSelection();
const image = PaintUtil.createTileGridImage(tileGrid, getTileSet(), palettes);
exportImage(image);
}

/**
* Exports tileset to an image.
* @argument {ImageBitmap} image
Expand Down Expand Up @@ -4554,7 +4609,7 @@ function tileMapReorder(tileMapId, targetTileMapId, position) {

state.saveToLocalStorage();

tileManager.setState({
tileManager.setState({
tileMapList: getTileMapList()
});
}
Expand Down Expand Up @@ -4643,8 +4698,9 @@ function tileSetUpdate(args) {
/**
* Clones a tile map.
* @param {string} tileMapId - Unique ID of the tile map to delete.
* @param {{ mirror: boolean, flip: boolean }} args
*/
function tileMapClone(tileMapId) {
function tileMapClone(tileMapId, args) {
if (!tileMapId) throw new Error('The tile map ID was invalid.');

const sourceTileMap = getTileMapList().getTileMapById(tileMapId);
Expand All @@ -4653,14 +4709,28 @@ function tileMapClone(tileMapId) {

addUndoState();

const clonedTileMap = TileMapFactory.clone(sourceTileMap);
clonedTileMap.title += " (copy)";
try {

const clonedTileMap = TileMapFactory.clone(sourceTileMap);
clonedTileMap.title += " (copy)";

if (args?.mirror === true) {
TileMapUtil.mirrorTileMap(clonedTileMap);
}

if (args?.flip === true) {
TileMapUtil.flipTileMap(clonedTileMap);
}

getTileMapList().addTileMap(clonedTileMap);

getTileMapList().addTileMap(clonedTileMap);
tileMapOrTileSetSelectById(clonedTileMap.tileMapId);

tileMapOrTileSetSelectById(clonedTileMap.tileMapId);
toast.show('Tile map cloned.');

toast.show('Tile map cloned.');
} catch (e) {

}
}

/**
Expand Down Expand Up @@ -4737,7 +4807,7 @@ function tileMapUpdate(tileMapId, args) {
const capabilityType = isTileMap() ? getTileMap().isSprite ? 'sprite' : 'background' : 'background';
const graphicsCapability = SystemUtil.getGraphicsCapability(getProject().systemType, capabilityType);

// Reset UI
// Update UI
tileManager.setState({
tileMapList: getTileMapList(),
selectedTileMapId: getProjectUIState().tileMapId,
Expand All @@ -4755,6 +4825,46 @@ function tileMapUpdate(tileMapId, args) {
});
}

/**
* @param {string} tileMapId
* @param {{ mirror: boolean, flip: boolean }} args
*/
function tileMapMirrorOrFlip(tileMapId, args) {
if (!tileMapId) throw new Error('The tile map ID was invalid.');
const tileMap = getTileMapList().getTileMapById(tileMapId);
if (!tileMap) throw new Error('No tile map matched the given ID.');

addUndoState();

try {

if (args?.mirror === true) {
TileMapUtil.mirrorTileMap(tileMap);
toast.show('Tile map mirrored.');
}

if (args?.flip === true) {
TileMapUtil.flipTileMap(tileMap);
toast.show('Tile map flipped.');
}

} catch (e) {
undoManager.removeLastUndo();
toast.show('Error mirroring or flipping tile map.')
throw e;
}

// Update project
state.saveToLocalStorage();

// Update UI
tileEditor.setState({
selectedTileIndex: -1,
tileGrid: getTileGrid(),
forceRefresh: true
});
}

/**
* @param {string?} field
*/
Expand Down
2 changes: 1 addition & 1 deletion wwwroot/modules/models/referenceImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default class ReferenceImage {
if (image instanceof ImageBitmap) {
this.#image = image;
} else if ((image?.tagName && image.tagName === 'IMG') || (image?.tagName && image.tagName === 'CANVAS') || image instanceof OffscreenCanvas) {
this.#image = ImageUtil.ToImageBitmap(image);
this.#image = ImageUtil.toImageBitmap(image);
} else if (image === null) {
this.#image = null;
} else {
Expand Down
10 changes: 10 additions & 0 deletions wwwroot/modules/models/tileGridProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ export default class TileGridProvider {
}


/**
* Gets information about a tile by the tile index.
* @param {number} tileIndex - Index of the tile.
* @returns {TileProviderTileInfo}
*/
getTileInfoByIndex(tileIndex) {
throw new Error('Not implemented.');
}


/**
* Gets information about a tile by the tile index.
* @param {number} tileIndex - Index of the tile.
Expand Down
6 changes: 3 additions & 3 deletions wwwroot/modules/ui/tileEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -791,8 +791,8 @@ export default class TileEditor extends ComponentBase {
canvasState.canvasHeight = response.canvasSize.height;
canvasState.tileGridRows = response.tileGridDimension.rows;
canvasState.tileGridColumns = response.tileGridDimension.columns;
canvasState.tileGridWidthPixels = response.tileGridImage.width;
canvasState.tileGridHeightPixels = response.tileGridImage.height;
canvasState.tileGridWidthPixels = response.tileGridImageDimensions.width;
canvasState.tileGridHeightPixels = response.tileGridImageDimensions.height;
canvasState.offsetX = response.tileGridOffset.x;
canvasState.offsetY = response.tileGridOffset.y;
canvasState.scale = response.scale;
Expand Down Expand Up @@ -867,7 +867,7 @@ export default class TileEditor extends ComponentBase {
/**
* @typedef {Object} TileEditorEventArgs
* @property {string} event - Event that occurred.
* @property {number} tileGridImage - Image of the tile grid.
* @property {ImageBitmap} tileGridImage - Image of the tile grid.
* @property {number} x - X tile grid pixel thats selected.
* @property {number} y - Y tile grid pixel thats selected.
* @property {number?} [tileGridRowIndex] - Index of the tile grid row that corresponds with the Y mouse coordinate.
Expand Down
21 changes: 18 additions & 3 deletions wwwroot/modules/ui/tileManager.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,31 @@
<div class="sms-tile-map-properties card-body p-2 pt-0 visually-hidden"
data-smsgfx-id="tile-map-properties">
<div class="row mb-2">
<div class="col">
<div class="col d-flex flex-row w-100 m-0">
<button data-smsgfx-id="editTileMapTitle"
class="btn border border-0 p-0 m-0 d-flex flex-row w-100">
class="btn border border-0 p-0 m-0 d-flex flex-row flex-fill">
<label
class="p-2 ps-0 fw-bold text-body-emphasis text-start overflow-hidden text-nowrap flex-shrink w-100">Title</label>
<span class="p-2"><i class="bi bi-pencil-fill"></i></span>
</button>
<input data-command="tileMapChange" data-auto-event="change" data-field="title" type="text"
class="form-control visually-hidden mt-2" aria-label="Enter name"
class="form-control visually-hidden mt-2 flex-fill" aria-label="Enter name"
title="Change the title of the tile map.">
<button data-smsgfx-id="editTileMapMenu" class="btn btn-secondary m-0 p-0"
data-bs-toggle="dropdown" aria-expanded="false">
<span class="p-2"><i class="bi bi-three-dots"></i></span>
</button>
<ul class="dropdown-menu">
<li><button data-command="tileMapReference" data-auto-event="click" class="dropdown-item" type="button">Use as reference</button></li>
<li><hr class="dropdown-divider"></li>
<li><button data-command="tileMapMirror" data-auto-event="click" class="dropdown-item" type="button">Mirror</button></li>
<li><button data-command="tileMapFlip" data-auto-event="click" class="dropdown-item" type="button">Flip</button></li>
<li><hr class="dropdown-divider"></li>
<li><button data-command="tileMapClone" data-auto-event="click" class="dropdown-item" type="button">Clone</button></li>
<li><button data-command="tileMapCloneMirror" data-auto-event="click" class="dropdown-item" type="button">Clone mirrored</button></li>
<li><hr class="dropdown-divider"></li>
<li><button data-command="tileMapDelete" data-auto-event="click" class="dropdown-item" type="button">Delete</button></li>
</ul>
</div>
</div>
<div class="row">
Expand Down
39 changes: 20 additions & 19 deletions wwwroot/modules/ui/tileManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ const commands = {
tileMapSelect: 'tileMapSelect',
tileMapDelete: 'tileMapDelete',
tileMapClone: 'tileMapClone',
tileMapCloneMirror: 'tileMapCloneMirror',
tileMapMirror: 'tileMapMirror',
tileMapFlip: 'tileMapFlip',
tileMapChangePosition: 'tileMapChangePosition',
tileSelect: 'tileSelect',
tileMapChange: 'tileMapChange',
tileMapReference: 'tileMapReference',
tileSetChange: 'tileSetChange',
tileSetToTileMap: 'tileSetToTileMap',
assemblyImport: 'assemblyImport',
Expand Down Expand Up @@ -125,19 +129,6 @@ export default class TileManager extends ComponentBase {
this.#dispatcher = new EventDispatcher();

TemplateUtil.wireUpLabels(this.#element);
TemplateUtil.wireUpCommandAutoEvents(this.#element, (sender, ev, command, event) => {
// Tile set select
if (command === commands.tileSetSelect) {
const args = this.#createArgs(TileManager.Commands.tileSetSelect);
this.#dispatcher.dispatch(EVENT_OnCommand, args);
}
// Sort
else if (command === commands.sort) {
const args = this.#createArgs(TileManager.Commands.sort);
args.field = sender.getAttribute('data-field');
this.#dispatcher.dispatch(EVENT_OnCommand, args);
}
});

this.#reorderHelper = new StackedListReorderHelper(
this.#element.querySelector('[data-smsgfx-id=tile-map-list-container]'),
Expand Down Expand Up @@ -391,13 +382,23 @@ export default class TileManager extends ComponentBase {
* @param {HTMLElement} parentElement
*/
#wireAutoEvents(parentElement) {
parentElement.querySelectorAll('[data-command][data-auto-event]').forEach((element) => {
const event = element.getAttribute('data-auto-event');
element.addEventListener(event, () => {
const command = element.getAttribute('data-command');
const args = this.#createArgs(command, element);
TemplateUtil.wireUpCommandAutoEvents(parentElement, (sender, ev, command, event) => {
if (command === commands.tileSetSelect) {
// Tile set select
const args = this.#createArgs(TileManager.Commands.tileSetSelect);
this.#dispatcher.dispatch(EVENT_OnCommand, args);
});
}
else if (command === commands.sort) {
// Sort
const args = this.#createArgs(TileManager.Commands.sort);
args.field = sender.getAttribute('data-field');
this.#dispatcher.dispatch(EVENT_OnCommand, args);
} else {
// Generic
const args = this.#createArgs(command, sender);
args.tileMapId = this.#selectedTileMapId;
this.#dispatcher.dispatch(EVENT_OnCommand, args);
}
});
}

Expand Down
6 changes: 3 additions & 3 deletions wwwroot/modules/util/imageUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import ProjectFactory from "../factory/projectFactory.js";
import ColourUtil from "./colourUtil.js";
import TileSetFactory from "../factory/tileSetFactory.js";
import PaletteListFactory from "../factory/paletteListFactory.js";
import GeneralUtil from "./generalUtil.js";
import TileMap from "../models/tileMap.js";

const imageMimeTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/svg+xml'];

export default class ImageUtil {


/**
* Converts an image to an image bitmap.
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} image - Image to convert.
* @returns {ImageBitmap}
*/
static ToImageBitmap(image) {
static toImageBitmap(image) {
if (image instanceof ImageBitmap) {
return image;
} else if ((image.tagName && image.tagName === 'CANVAS') || image instanceof OffscreenCanvas) {
Expand Down
Loading

0 comments on commit 6986386

Please sign in to comment.