diff --git a/css/ui.css b/css/ui.css index a8fe0a76..29a3a1d5 100644 --- a/css/ui.css +++ b/css/ui.css @@ -495,6 +495,32 @@ div.asset_file_header { border-radius: 8px; padding-left: 1em; } +div.asset_block { + margin-top: 8px; +} +.asset_snip { + font-family: "Andale Mono", "Menlo", "Lucida Console", monospace; + font-weight: bold; + color: #c1c1b0; + background-color: #555; + border-radius: 8px; + padding-left: 1em; +} +.asset_lineno { + color: #99cc99; + display: inline-block; + min-width: 4rem; + text-align: right; + margin-right: 8px; +} +.asset_error_msg { + color: #ff6666; + background-color: #330000; + padding: 4px 8px; + margin: 2px 0; + border-left: 3px solid #ff0000; + font-family: monospace; +} div.asset_grid { line-height:0; margin: 1em; diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 01952193..72cbd9ff 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -192,6 +192,30 @@ export function convertWordsToImages(words: UintArray, fmt: PixelEditorImageForm return images; } +export function validateAssetData(datastr: string, fmt): string | null { + var words = parseHexWords(convertToHexStatements(datastr)); + if (fmt.w > 0 && fmt.h > 0) { + // same variables as convertWordsToImages + var count = fmt.count || 1; + var bpp = fmt.bpp || 1; + var nplanes = fmt.np || 1; + var bitsperword = fmt.bpw || 8; + var wordsperline = fmt.sl || Math.ceil(fmt.w * bpp / bitsperword); + var pofs = fmt.pofs || wordsperline * fmt.h * count; + var skip = fmt.skip || 0; + var wpimg = fmt.wpimg || wordsperline * fmt.h; + var required = wpimg * count + (nplanes > 1 ? (nplanes - 1) * pofs : 0) + skip; + if (words.length != required) { + return `Expected ${required} value(s), found ${words.length}`; + } + } else if (fmt.pal) { + if (words.length < 1) { + return `Palette requires at least 1 value, found ${words.length}`; + } + } + return null; +} + export function convertImagesToWords(images: Uint8Array[], fmt: PixelEditorImageFormat): number[] { if (fmt.destfmt) fmt = fmt.destfmt; var width = fmt.w; diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 4978dfc5..38bf6107 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -6,6 +6,15 @@ import { hex, safeident, rgb2bgr } from "../../common/util"; import * as pixed from "../pixeleditor"; import Mousetrap = require('mousetrap'); +function getLineNumber(data: string, offset: number): number { + let line = 1; + for (let i = 0; i < offset && i < data.length; i++) { + if (data[i] === '\n') line++; + } + return line; +} + + export class AssetEditorView implements ProjectView, pixed.EditorContext { maindiv: JQuery; cureditordiv: JQuery; @@ -146,14 +155,21 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { end = data.indexOf(';', start); // C } //console.log(id, start, end, m[1], data.substring(start,end)); - if (end > start) { + var line = getLineNumber(data, m.index); + var header = m[0]; + if (end < 0) { + var closingDelim = platform_id.includes('verilog') ? '"end"' : m[0].startsWith(';;') ? '";;"' : '";"'; + result.push({ fileid: id, header: header, line: line, error: `No closing ${closingDelim} found after asset header` }); + } else if (end <= start) { + result.push({ fileid: id, header: header, line: line, error: `Empty data block after asset header` }); + } else { try { var jsontxt = m[1].replace(/([A-Za-z]+):/g, '"$1":'); // fix lenient JSON var json = JSON.parse(jsontxt); // TODO: name? - result.push({ fileid: id, fmt: json, start: start, end: end }); + result.push({ fileid: id, header: header, line: line, fmt: json, start: start, end: end }); } catch (e) { - console.log(e); + result.push({ fileid: id, header: header, line: line, error: `Invalid asset format: ${e.message}` }); } } } @@ -304,7 +320,22 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { } else if (typeof data === 'string') { let textfrags = this.scanFileTextForAssets(fileid, data); for (let frag of textfrags) { + const block = $('
').appendTo(this.ensureFileDiv(fileid)); + var snip = $('').appendTo(block); + $('').text(frag.line).appendTo(snip); + snip.append(' ' + frag.header); + if (frag.error) { + $('').text(frag.error).appendTo(block); + continue; + } if (frag.fmt) { + // validate data block size before creating editors + const assetError = pixed.validateAssetData(data.substring(frag.start, frag.end), frag.fmt); + if (assetError) { + $('').text(assetError).appendTo(block); + continue; + } + let label = fileid; // TODO: label let node: pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end); let first = node; @@ -317,19 +348,19 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { node = node.addRight(new pixed.NESNametableConverter(this)); node = node.addRight(new pixed.Palettizer(this, { w: 8, h: 8, bpp: 4 })); const fmt = { w: 8 * (frag.fmt.w || 32), h: 8 * (frag.fmt.h || 30), count: 1 }; // TODO: can't do custom sizes - node = node.addRight(new pixed.MapEditor(this, newDiv(this.ensureFileDiv(fileid)), fmt)); + node = node.addRight(new pixed.MapEditor(this, newDiv(block), fmt)); this.registerAsset("nametable", first, 2); nassets++; } // is this a bitmap? else if (frag.fmt.w > 0 && frag.fmt.h > 0) { - this.addPixelEditor(this.ensureFileDiv(fileid), node, frag.fmt); + this.addPixelEditor(block, node, frag.fmt); this.registerAsset("charmap", first, 1); nassets++; } // is this a palette? else if (frag.fmt.pal) { - this.addPaletteEditor(this.ensureFileDiv(fileid), node, frag.fmt); + this.addPaletteEditor(block, node, frag.fmt); this.registerAsset("palette", first, 0); nassets++; }