From bf192f41cb68f85e0bbfb9b0e94bb1b9501a2ba6 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 11:55:33 -0800 Subject: [PATCH 01/22] reformat src/ide/pixeleditor.ts --- src/ide/pixeleditor.ts | 604 ++++++++++++++++++++--------------------- 1 file changed, 302 insertions(+), 302 deletions(-) diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 9846d230..ce001e65 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -8,57 +8,57 @@ export type UintArray = number[] | Uint8Array | Uint16Array | Uint32Array; //{[i // TODO: separate view/controller export interface EditorContext { - setCurrentEditor(div:JQuery, editing:JQuery, node:PixNode) : void; - getPalettes(matchlen : number) : SelectablePalette[]; - getTilemaps(matchlen : number) : SelectableTilemap[]; + setCurrentEditor(div: JQuery, editing: JQuery, node: PixNode): void; + getPalettes(matchlen: number): SelectablePalette[]; + getTilemaps(matchlen: number): SelectableTilemap[]; } export type SelectablePalette = { - node:PixNode - name:string - palette:Uint32Array + node: PixNode + name: string + palette: Uint32Array } export type SelectableTilemap = { - node:PixNode - name:string - images:Uint8Array[] - rgbimgs:Uint32Array[] // TODO: different palettes? + node: PixNode + name: string + images: Uint8Array[] + rgbimgs: Uint32Array[] // TODO: different palettes? } export type PixelEditorImageFormat = { - w:number // width - h:number // height - count?:number // # of images - bpp?:number // bits per pixel - np?:number // number of planes - bpw?:number // bits per word - sl?:number // words per line - pofs?:number // plane offset - remap?:number[] // remap array - reindex?:number[] // reindex array - brev?:boolean // bit reverse (msb is leftmost) - flip?:boolean // flip vertically - skip?:number // skip bytes - wpimg?:number // words per image - aspect?:number // aspect ratio - xform?:string // CSS transform - destfmt?:PixelEditorImageFormat + w: number // width + h: number // height + count?: number // # of images + bpp?: number // bits per pixel + np?: number // number of planes + bpw?: number // bits per word + sl?: number // words per line + pofs?: number // plane offset + remap?: number[] // remap array + reindex?: number[] // reindex array + brev?: boolean // bit reverse (msb is leftmost) + flip?: boolean // flip vertically + skip?: number // skip bytes + wpimg?: number // words per image + aspect?: number // aspect ratio + xform?: string // CSS transform + destfmt?: PixelEditorImageFormat }; export type PixelEditorPaletteFormat = { - pal?:number|string - n?:number - layout?:string + pal?: number | string + n?: number + layout?: string }; export type PixelEditorPaletteLayout = [string, number, number][]; type PixelEditorMessage = { - fmt : PixelEditorImageFormat - palfmt : PixelEditorPaletteFormat - bytestr : string - palstr : string + fmt: PixelEditorImageFormat + palfmt: PixelEditorPaletteFormat + bytestr: string + palstr: string }; ///////////////// @@ -67,28 +67,28 @@ type PixelEditorMessage = { // 0xabcd, #$abcd, 5'010101, 0b010101, etc var pixel_re = /([0#]?)([x$%]|\d'h)([0-9a-f]+)(?:[;].*)?|(\d'b|0b)([01]+)/gim; -function convertToHexStatements(s:string) : string { +function convertToHexStatements(s: string): string { // convert 'hex ....' asm format - return s.replace(/(\shex\s+)([0-9a-f]+)/ig, function(m,hexprefix,hexstr) { + return s.replace(/(\shex\s+)([0-9a-f]+)/ig, function (m, hexprefix, hexstr) { var rtn = hexprefix; - for (var i=0; i { + result = result.replace(/(\shex\s+)([,x0-9a-f]+)/ig, (m, hexprefix, hexstr) => { var rtn = hexprefix + hexstr; - rtn = rtn.replace(/0x/ig,'').replace(/,/ig,'') + rtn = rtn.replace(/0x/ig, '').replace(/,/ig, '') return rtn; }); return result; } -function remapBits(x:number, arr:number[]) : number { +function remapBits(x: number, arr: number[]): number { if (!arr) return x; var y = 0; - for (var i=0; i> 3, i & 7]; } -export function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat) : Uint8Array[] { +export function convertWordsToImages(words: UintArray, fmt: PixelEditorImageFormat): Uint8Array[] { var width = fmt.w; var height = fmt.h; var count = fmt.count || 1; @@ -160,24 +160,24 @@ export function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat var nplanes = fmt.np || 1; var bitsperword = fmt.bpw || 8; var wordsperline = fmt.sl || Math.ceil(width * bpp / bitsperword); - var mask = (1 << bpp)-1; - var pofs = fmt.pofs || wordsperline*height*count; + var mask = (1 << bpp) - 1; + var pofs = fmt.pofs || wordsperline * height * count; var skip = fmt.skip || 0; - var wpimg = fmt.wpimg || wordsperline*height; + var wpimg = fmt.wpimg || wordsperline * height; var images = []; - for (var n=0; n>(bitsperword-shift-bpp) : byte>>shift) & mask) << (p*bpp); + for (var p = 0; p < nplanes; p++) { + var byte = words[ofs + p * pofs + skip]; + color |= ((fmt.brev ? byte >> (bitsperword - shift - bpp) : byte >> shift) & mask) << (p * bpp); } imgdata.push(color); shift += bpp; @@ -192,7 +192,7 @@ export function convertWordsToImages(words:UintArray, fmt:PixelEditorImageFormat return images; } -export function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFormat) : number[] { +export function convertImagesToWords(images: Uint8Array[], fmt: PixelEditorImageFormat): number[] { if (fmt.destfmt) fmt = fmt.destfmt; var width = fmt.w; var height = fmt.h; @@ -201,33 +201,33 @@ export function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFo var nplanes = fmt.np || 1; var bitsperword = fmt.bpw || 8; var wordsperline = fmt.sl || Math.ceil(fmt.w * bpp / bitsperword); - var mask = (1 << bpp)-1; - var pofs = fmt.pofs || wordsperline*height*count; + var mask = (1 << bpp) - 1; + var pofs = fmt.pofs || wordsperline * height * count; var skip = fmt.skip || 0; - var wpimg = fmt.wpimg || wordsperline*height; - + var wpimg = fmt.wpimg || wordsperline * height; + var words; if (nplanes > 0 && fmt.sl) // TODO? - words = new Uint8Array(wpimg*count); + words = new Uint8Array(wpimg * count); else if (bitsperword <= 8) - words = new Uint8Array(wpimg*count*nplanes); + words = new Uint8Array(wpimg * count * nplanes); else - words = new Uint32Array(wpimg*count*nplanes); + words = new Uint32Array(wpimg * count * nplanes); - for (var n=0; n> (p*bpp)) & mask; - words[ofs + p*pofs + skip] |= (fmt.brev ? (c << (bitsperword-shift-bpp)) : (c << shift)); + for (var p = 0; p < nplanes; p++) { + var c = (color >> (p * bpp)) & mask; + words[ofs + p * pofs + skip] |= (fmt.brev ? (c << (bitsperword - shift - bpp)) : (c << shift)); } shift += bpp; if (shift >= bitsperword && !fmt.reindex) { @@ -241,26 +241,26 @@ export function convertImagesToWords(images:Uint8Array[], fmt:PixelEditorImageFo } // TODO -export function convertPaletteBytes(arr:UintArray,r0,r1,g0,g1,b0,b1) : number[] { +export function convertPaletteBytes(arr: UintArray, r0, r1, g0, g1, b0, b1): number[] { var result = []; - for (var i=0; i> r0) & ((1<> g0) & ((1<> b0) & ((1<> r0) & ((1 << r1) - 1)) << (0 + 8 - r1); + rgb |= ((d >> g0) & ((1 << g1) - 1)) << (8 + 8 - g1); + rgb |= ((d >> b0) & ((1 << b1) - 1)) << (16 + 8 - b1); result.push(rgb); } return result; } -export function getPaletteLength(palfmt: PixelEditorPaletteFormat) : number { +export function getPaletteLength(palfmt: PixelEditorPaletteFormat): number { var pal = palfmt.pal; if (typeof pal === 'number') { - var rr = Math.floor(Math.abs(pal/100) % 10); - var gg = Math.floor(Math.abs(pal/10) % 10); + var rr = Math.floor(Math.abs(pal / 100) % 10); + var gg = Math.floor(Math.abs(pal / 10) % 10); var bb = Math.floor(Math.abs(pal) % 10); - return 1<<(rr+gg+bb); + return 1 << (rr + gg + bb); } else { var paltable = PREDEF_PALETTES[pal]; if (paltable) { @@ -271,22 +271,22 @@ export function getPaletteLength(palfmt: PixelEditorPaletteFormat) : number { } } -export function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPaletteFormat) : number[] { +export function convertPaletteFormat(palbytes: UintArray, palfmt: PixelEditorPaletteFormat): number[] { var pal = palfmt.pal; var newpalette; if (typeof pal === 'number') { - var rr = Math.floor(Math.abs(pal/100) % 10); - var gg = Math.floor(Math.abs(pal/10) % 10); + var rr = Math.floor(Math.abs(pal / 100) % 10); + var gg = Math.floor(Math.abs(pal / 10) % 10); var bb = Math.floor(Math.abs(pal) % 10); // TODO: n if (pal >= 0) - newpalette = convertPaletteBytes(palbytes, 0, rr, rr, gg, rr+gg, bb); + newpalette = convertPaletteBytes(palbytes, 0, rr, rr, gg, rr + gg, bb); else - newpalette = convertPaletteBytes(palbytes, rr+gg, bb, rr, gg, 0, rr); + newpalette = convertPaletteBytes(palbytes, rr + gg, bb, rr, gg, 0, rr); } else { var paltable = PREDEF_PALETTES[pal]; if (paltable) { - newpalette = new Uint32Array(palbytes).map((i) => { return paltable[i & (paltable.length-1)] | 0xff000000; }); + newpalette = new Uint32Array(palbytes).map((i) => { return paltable[i & (paltable.length - 1)] | 0xff000000; }); } else { throw new Error("No palette named " + pal); } @@ -296,79 +296,79 @@ export function convertPaletteFormat(palbytes:UintArray, palfmt: PixelEditorPale // TODO: illegal colors? const PREDEF_PALETTES = { - 'nes':[ - 0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, - 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, - 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, - 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000 - ], - 'ap2lores':[ - (0x000000), (0xff00ff), (0x00007f), (0x7f007f), (0x007f00), (0x7f7f7f), (0x0000bf), (0x0000ff), - (0xbf7f00), (0xffbf00), (0xbfbfbf), (0xff7f7f), (0x00ff00), (0xffff00), (0x00bf7f), (0xffffff), - ], - 'vcs':[ - 0x000000,0x000000, 0x404040,0x404040, 0x6c6c6c,0x6c6c6c, 0x909090,0x909090, 0xb0b0b0,0xb0b0b0, 0xc8c8c8,0xc8c8c8, 0xdcdcdc,0xdcdcdc, 0xf4f4f4,0xf4f4f4, - 0x004444,0x004444, 0x106464,0x106464, 0x248484,0x248484, 0x34a0a0,0x34a0a0, 0x40b8b8,0x40b8b8, 0x50d0d0,0x50d0d0, 0x5ce8e8,0x5ce8e8, 0x68fcfc,0x68fcfc, - 0x002870,0x002870, 0x144484,0x144484, 0x285c98,0x285c98, 0x3c78ac,0x3c78ac, 0x4c8cbc,0x4c8cbc, 0x5ca0cc,0x5ca0cc, 0x68b4dc,0x68b4dc, 0x78c8ec,0x78c8ec, - 0x001884,0x001884, 0x183498,0x183498, 0x3050ac,0x3050ac, 0x4868c0,0x4868c0, 0x5c80d0,0x5c80d0, 0x7094e0,0x7094e0, 0x80a8ec,0x80a8ec, 0x94bcfc,0x94bcfc, - 0x000088,0x000088, 0x20209c,0x20209c, 0x3c3cb0,0x3c3cb0, 0x5858c0,0x5858c0, 0x7070d0,0x7070d0, 0x8888e0,0x8888e0, 0xa0a0ec,0xa0a0ec, 0xb4b4fc,0xb4b4fc, - 0x5c0078,0x5c0078, 0x74208c,0x74208c, 0x883ca0,0x883ca0, 0x9c58b0,0x9c58b0, 0xb070c0,0xb070c0, 0xc084d0,0xc084d0, 0xd09cdc,0xd09cdc, 0xe0b0ec,0xe0b0ec, - 0x780048,0x780048, 0x902060,0x902060, 0xa43c78,0xa43c78, 0xb8588c,0xb8588c, 0xcc70a0,0xcc70a0, 0xdc84b4,0xdc84b4, 0xec9cc4,0xec9cc4, 0xfcb0d4,0xfcb0d4, - 0x840014,0x840014, 0x982030,0x982030, 0xac3c4c,0xac3c4c, 0xc05868,0xc05868, 0xd0707c,0xd0707c, 0xe08894,0xe08894, 0xeca0a8,0xeca0a8, 0xfcb4bc,0xfcb4bc, - 0x880000,0x880000, 0x9c201c,0x9c201c, 0xb04038,0xb04038, 0xc05c50,0xc05c50, 0xd07468,0xd07468, 0xe08c7c,0xe08c7c, 0xeca490,0xeca490, 0xfcb8a4,0xfcb8a4, - 0x7c1800,0x7c1800, 0x90381c,0x90381c, 0xa85438,0xa85438, 0xbc7050,0xbc7050, 0xcc8868,0xcc8868, 0xdc9c7c,0xdc9c7c, 0xecb490,0xecb490, 0xfcc8a4,0xfcc8a4, - 0x5c2c00,0x5c2c00, 0x784c1c,0x784c1c, 0x906838,0x906838, 0xac8450,0xac8450, 0xc09c68,0xc09c68, 0xd4b47c,0xd4b47c, 0xe8cc90,0xe8cc90, 0xfce0a4,0xfce0a4, - 0x2c3c00,0x2c3c00, 0x485c1c,0x485c1c, 0x647c38,0x647c38, 0x809c50,0x809c50, 0x94b468,0x94b468, 0xacd07c,0xacd07c, 0xc0e490,0xc0e490, 0xd4fca4,0xd4fca4, - 0x003c00,0x003c00, 0x205c20,0x205c20, 0x407c40,0x407c40, 0x5c9c5c,0x5c9c5c, 0x74b474,0x74b474, 0x8cd08c,0x8cd08c, 0xa4e4a4,0xa4e4a4, 0xb8fcb8,0xb8fcb8, - 0x003814,0x003814, 0x1c5c34,0x1c5c34, 0x387c50,0x387c50, 0x50986c,0x50986c, 0x68b484,0x68b484, 0x7ccc9c,0x7ccc9c, 0x90e4b4,0x90e4b4, 0xa4fcc8,0xa4fcc8, - 0x00302c,0x00302c, 0x1c504c,0x1c504c, 0x347068,0x347068, 0x4c8c84,0x4c8c84, 0x64a89c,0x64a89c, 0x78c0b4,0x78c0b4, 0x88d4cc,0x88d4cc, 0x9cece0,0x9cece0, - 0x002844,0x002844, 0x184864,0x184864, 0x306884,0x306884, 0x4484a0,0x4484a0, 0x589cb8,0x589cb8, 0x6cb4d0,0x6cb4d0, 0x7ccce8,0x7ccce8, 0x8ce0fc,0x8ce0fc - ], - 'astrocade':[0,2368548,4737096,7171437,9539985,11974326,14342874,16777215,12255269,14680137,16716142,16725394,16734903,16744155,16753663,16762879,11534409,13959277,16318866,16721334,16730842,16740095,16749311,16758783,10420330,12779662,15138995,16718039,16727291,16736767,16745983,16755199,8847495,11206827,13631696,15994612,16724735,16733951,16743423,16752639,6946975,9306307,11731175,14092287,16461055,16732415,16741631,16751103,4784304,7143637,9568505,11929087,14297599,16731647,16741119,16750335,2425019,4784352,7209215,9570047,12004095,14372863,16741375,16750847,191,2359523,4718847,7146495,9515263,11949311,14318079,16752127,187,224,2294015,4658431,7092735,9461247,11895551,14264063,176,213,249,2367999,4736511,7105279,9539327,11908095,159,195,3303,209151,2577919,4946431,7380735,9749247,135,171,7888,17140,681983,3050495,5484543,7853311,106,3470,12723,22231,31483,1548031,3916799,6285311,73,8557,17810,27318,36570,373759,2742271,5176575,4389,13641,23150,32402,41911,51163,2026495,4456447,9472,18724,27976,37485,46737,56246,1834970,4194303,14080,23296,32803,42055,51564,60816,2031541,4456409,18176,27648,36864,46116,55624,392556,2752401,5177269,21760,30976,40192,49667,58919,1572683,3932016,6291348,24320,33536,43008,52224,716810,3079982,5504851,7864183,25856,35328,44544,250368,2619136,4980503,7405371,9764703,26624,35840,45312,2413824,4782336,7143173,9568041,11927374,26112,35584,2338560,4707328,7141376,9502464,11927326,14286659,24832,2393344,4762112,7196160,9564928,11992832,14352155,16711487,2447360,4815872,7250176,9618688,12052992,14417664,16776990,16777027,4803328,7172096,9606144,11974912,14343424,16776965,16777001,16777038,6962176,9330688,11764992,14133504,16502272,16773655,16777019,16777055,8858112,11226880,13660928,16029440,16759818,16769070,16777043,16777079,10426112,12794624,15163392,16745475,16754727,16764235,16773488,16777108,11534848,13969152,16337664,16740388,16749640,16759148,16768401,16777141,12255232,14684928,16725795,16735047,16744556,16753808,16763317,16772569], - 'c64':[0x000000,0xffffff,0x2b3768,0xb2a470,0x863d6f,0x438d58,0x792835,0x6fc7b8,0x254f6f,0x003943,0x59679a,0x444444,0x6c6c6c,0x84d29a,0xb55e6c,0x959595], + 'nes': [ + 0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, + 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, + 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, + 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000 + ], + 'ap2lores': [ + (0x000000), (0xff00ff), (0x00007f), (0x7f007f), (0x007f00), (0x7f7f7f), (0x0000bf), (0x0000ff), + (0xbf7f00), (0xffbf00), (0xbfbfbf), (0xff7f7f), (0x00ff00), (0xffff00), (0x00bf7f), (0xffffff), + ], + 'vcs': [ + 0x000000, 0x000000, 0x404040, 0x404040, 0x6c6c6c, 0x6c6c6c, 0x909090, 0x909090, 0xb0b0b0, 0xb0b0b0, 0xc8c8c8, 0xc8c8c8, 0xdcdcdc, 0xdcdcdc, 0xf4f4f4, 0xf4f4f4, + 0x004444, 0x004444, 0x106464, 0x106464, 0x248484, 0x248484, 0x34a0a0, 0x34a0a0, 0x40b8b8, 0x40b8b8, 0x50d0d0, 0x50d0d0, 0x5ce8e8, 0x5ce8e8, 0x68fcfc, 0x68fcfc, + 0x002870, 0x002870, 0x144484, 0x144484, 0x285c98, 0x285c98, 0x3c78ac, 0x3c78ac, 0x4c8cbc, 0x4c8cbc, 0x5ca0cc, 0x5ca0cc, 0x68b4dc, 0x68b4dc, 0x78c8ec, 0x78c8ec, + 0x001884, 0x001884, 0x183498, 0x183498, 0x3050ac, 0x3050ac, 0x4868c0, 0x4868c0, 0x5c80d0, 0x5c80d0, 0x7094e0, 0x7094e0, 0x80a8ec, 0x80a8ec, 0x94bcfc, 0x94bcfc, + 0x000088, 0x000088, 0x20209c, 0x20209c, 0x3c3cb0, 0x3c3cb0, 0x5858c0, 0x5858c0, 0x7070d0, 0x7070d0, 0x8888e0, 0x8888e0, 0xa0a0ec, 0xa0a0ec, 0xb4b4fc, 0xb4b4fc, + 0x5c0078, 0x5c0078, 0x74208c, 0x74208c, 0x883ca0, 0x883ca0, 0x9c58b0, 0x9c58b0, 0xb070c0, 0xb070c0, 0xc084d0, 0xc084d0, 0xd09cdc, 0xd09cdc, 0xe0b0ec, 0xe0b0ec, + 0x780048, 0x780048, 0x902060, 0x902060, 0xa43c78, 0xa43c78, 0xb8588c, 0xb8588c, 0xcc70a0, 0xcc70a0, 0xdc84b4, 0xdc84b4, 0xec9cc4, 0xec9cc4, 0xfcb0d4, 0xfcb0d4, + 0x840014, 0x840014, 0x982030, 0x982030, 0xac3c4c, 0xac3c4c, 0xc05868, 0xc05868, 0xd0707c, 0xd0707c, 0xe08894, 0xe08894, 0xeca0a8, 0xeca0a8, 0xfcb4bc, 0xfcb4bc, + 0x880000, 0x880000, 0x9c201c, 0x9c201c, 0xb04038, 0xb04038, 0xc05c50, 0xc05c50, 0xd07468, 0xd07468, 0xe08c7c, 0xe08c7c, 0xeca490, 0xeca490, 0xfcb8a4, 0xfcb8a4, + 0x7c1800, 0x7c1800, 0x90381c, 0x90381c, 0xa85438, 0xa85438, 0xbc7050, 0xbc7050, 0xcc8868, 0xcc8868, 0xdc9c7c, 0xdc9c7c, 0xecb490, 0xecb490, 0xfcc8a4, 0xfcc8a4, + 0x5c2c00, 0x5c2c00, 0x784c1c, 0x784c1c, 0x906838, 0x906838, 0xac8450, 0xac8450, 0xc09c68, 0xc09c68, 0xd4b47c, 0xd4b47c, 0xe8cc90, 0xe8cc90, 0xfce0a4, 0xfce0a4, + 0x2c3c00, 0x2c3c00, 0x485c1c, 0x485c1c, 0x647c38, 0x647c38, 0x809c50, 0x809c50, 0x94b468, 0x94b468, 0xacd07c, 0xacd07c, 0xc0e490, 0xc0e490, 0xd4fca4, 0xd4fca4, + 0x003c00, 0x003c00, 0x205c20, 0x205c20, 0x407c40, 0x407c40, 0x5c9c5c, 0x5c9c5c, 0x74b474, 0x74b474, 0x8cd08c, 0x8cd08c, 0xa4e4a4, 0xa4e4a4, 0xb8fcb8, 0xb8fcb8, + 0x003814, 0x003814, 0x1c5c34, 0x1c5c34, 0x387c50, 0x387c50, 0x50986c, 0x50986c, 0x68b484, 0x68b484, 0x7ccc9c, 0x7ccc9c, 0x90e4b4, 0x90e4b4, 0xa4fcc8, 0xa4fcc8, + 0x00302c, 0x00302c, 0x1c504c, 0x1c504c, 0x347068, 0x347068, 0x4c8c84, 0x4c8c84, 0x64a89c, 0x64a89c, 0x78c0b4, 0x78c0b4, 0x88d4cc, 0x88d4cc, 0x9cece0, 0x9cece0, + 0x002844, 0x002844, 0x184864, 0x184864, 0x306884, 0x306884, 0x4484a0, 0x4484a0, 0x589cb8, 0x589cb8, 0x6cb4d0, 0x6cb4d0, 0x7ccce8, 0x7ccce8, 0x8ce0fc, 0x8ce0fc + ], + 'astrocade': [0, 2368548, 4737096, 7171437, 9539985, 11974326, 14342874, 16777215, 12255269, 14680137, 16716142, 16725394, 16734903, 16744155, 16753663, 16762879, 11534409, 13959277, 16318866, 16721334, 16730842, 16740095, 16749311, 16758783, 10420330, 12779662, 15138995, 16718039, 16727291, 16736767, 16745983, 16755199, 8847495, 11206827, 13631696, 15994612, 16724735, 16733951, 16743423, 16752639, 6946975, 9306307, 11731175, 14092287, 16461055, 16732415, 16741631, 16751103, 4784304, 7143637, 9568505, 11929087, 14297599, 16731647, 16741119, 16750335, 2425019, 4784352, 7209215, 9570047, 12004095, 14372863, 16741375, 16750847, 191, 2359523, 4718847, 7146495, 9515263, 11949311, 14318079, 16752127, 187, 224, 2294015, 4658431, 7092735, 9461247, 11895551, 14264063, 176, 213, 249, 2367999, 4736511, 7105279, 9539327, 11908095, 159, 195, 3303, 209151, 2577919, 4946431, 7380735, 9749247, 135, 171, 7888, 17140, 681983, 3050495, 5484543, 7853311, 106, 3470, 12723, 22231, 31483, 1548031, 3916799, 6285311, 73, 8557, 17810, 27318, 36570, 373759, 2742271, 5176575, 4389, 13641, 23150, 32402, 41911, 51163, 2026495, 4456447, 9472, 18724, 27976, 37485, 46737, 56246, 1834970, 4194303, 14080, 23296, 32803, 42055, 51564, 60816, 2031541, 4456409, 18176, 27648, 36864, 46116, 55624, 392556, 2752401, 5177269, 21760, 30976, 40192, 49667, 58919, 1572683, 3932016, 6291348, 24320, 33536, 43008, 52224, 716810, 3079982, 5504851, 7864183, 25856, 35328, 44544, 250368, 2619136, 4980503, 7405371, 9764703, 26624, 35840, 45312, 2413824, 4782336, 7143173, 9568041, 11927374, 26112, 35584, 2338560, 4707328, 7141376, 9502464, 11927326, 14286659, 24832, 2393344, 4762112, 7196160, 9564928, 11992832, 14352155, 16711487, 2447360, 4815872, 7250176, 9618688, 12052992, 14417664, 16776990, 16777027, 4803328, 7172096, 9606144, 11974912, 14343424, 16776965, 16777001, 16777038, 6962176, 9330688, 11764992, 14133504, 16502272, 16773655, 16777019, 16777055, 8858112, 11226880, 13660928, 16029440, 16759818, 16769070, 16777043, 16777079, 10426112, 12794624, 15163392, 16745475, 16754727, 16764235, 16773488, 16777108, 11534848, 13969152, 16337664, 16740388, 16749640, 16759148, 16768401, 16777141, 12255232, 14684928, 16725795, 16735047, 16744556, 16753808, 16763317, 16772569], + 'c64': [0x000000, 0xffffff, 0x2b3768, 0xb2a470, 0x863d6f, 0x438d58, 0x792835, 0x6fc7b8, 0x254f6f, 0x003943, 0x59679a, 0x444444, 0x6c6c6c, 0x84d29a, 0xb55e6c, 0x959595], }; -var PREDEF_LAYOUTS : {[id:string]:PixelEditorPaletteLayout} = { - 'nes':[ - ['Screen Color', 0x00, 1], - ['Background 0', 0x01, 3], - ['Background 1', 0x05, 3], - ['Background 2', 0x09, 3], - ['Background 3', 0x0d, 3], - ['Sprite 0', 0x11, 3], - ['Sprite 1', 0x15, 3], - ['Sprite 2', 0x19, 3], - ['Sprite 3', 0x1d, 3] +var PREDEF_LAYOUTS: { [id: string]: PixelEditorPaletteLayout } = { + 'nes': [ + ['Screen Color', 0x00, 1], + ['Background 0', 0x01, 3], + ['Background 1', 0x05, 3], + ['Background 2', 0x09, 3], + ['Background 3', 0x0d, 3], + ['Sprite 0', 0x11, 3], + ['Sprite 1', 0x15, 3], + ['Sprite 2', 0x19, 3], + ['Sprite 3', 0x1d, 3] ], - 'astrocade':[ - ['Left', 0x00, -4], - ['Right', 0x04, -4] + 'astrocade': [ + ['Left', 0x00, -4], + ['Right', 0x04, -4] ], }; ///// -function equalArrays(a:UintArray, b:UintArray) : boolean { +function equalArrays(a: UintArray, b: UintArray): boolean { if (a == null || b == null) return false; if (a.length !== b.length) return false; if (a === b) return true; - for (var i=0; i { + var newimages = this.rgbimgs.map((im: Uint32Array) => { var out = new Uint8Array(im.length); - for (var i=0; i { + this.rgbimgs = this.images.map((im: Uint8Array) => { var out = new Uint32Array(im.length); - for (var i=0; i { + this.palette.forEach((rgba: number) => { this.rgbimgs.push(new Uint32Array([rgba])); }); return true; } getAllColors() { var arr = []; - for (var i=0; i> 4) & 0x38) | ((a >> 2) & 0x07); - var attr = this.words[attraddr]; - var tag = name ^ (attr<<9) ^ 0x80000000; - var i = row*this.cols*8*8 + col*8; - var j = 0; - var attrshift = (col&2) + ((a&0x40)>>4); - var coloradd = ((attr >> attrshift) & 3) << 2; - for (var y=0; y<8; y++) { - for (var x=0; x<8; x++) { - var color = t[j++]; - if (color) color += coloradd; - idata[i++] = color; - } - i += this.cols*8-8; - } - a++; + for (var row = 0; row < this.rows; row++) { + for (var col = 0; col < this.cols; col++) { + var name = this.words[this.baseofs + a]; + if (typeof name === 'undefined') throw Error("No name for address " + this.baseofs + " + " + a); + var t = this.tilemap[name]; + if (!t) throw Error("No tilemap found for tile index " + name); + attraddr = (a & 0x2c00) | 0x3c0 | (a & 0x0C00) | ((a >> 4) & 0x38) | ((a >> 2) & 0x07); + var attr = this.words[attraddr]; + var tag = name ^ (attr << 9) ^ 0x80000000; + var i = row * this.cols * 8 * 8 + col * 8; + var j = 0; + var attrshift = (col & 2) + ((a & 0x40) >> 4); + var coloradd = ((attr >> attrshift) & 3) << 2; + for (var y = 0; y < 8; y++) { + for (var x = 0; x < 8; x++) { + var color = t[j++]; + if (color) color += coloradd; + idata[i++] = color; + } + i += this.cols * 8 - 8; + } + a++; } } // TODO @@ -764,14 +764,14 @@ export class NESNametableConverter extends Compositor { export class ImageChooser { - rgbimgs : Uint32Array[]; - width : number; - height : number; + rgbimgs: Uint32Array[]; + width: number; + height: number; - recreate(parentdiv:JQuery, onclick) { + recreate(parentdiv: JQuery, onclick) { var agrid = $('
'); // grid (or 1) of preview images parentdiv.empty().append(agrid); - var cscale = Math.max(2, Math.ceil(16/this.width)); // TODO + var cscale = Math.max(2, Math.ceil(16 / this.width)); // TODO var imgsperline = this.width <= 8 ? 16 : 8; // TODO var span = null; this.rgbimgs.forEach((imdata, i) => { @@ -779,8 +779,8 @@ export class ImageChooser { viewer.width = this.width; viewer.height = this.height; viewer.recreate(); - viewer.canvas.style.width = (viewer.width*cscale)+'px'; // TODO - viewer.canvas.title = '$'+hex(i); + viewer.canvas.style.width = (viewer.width * cscale) + 'px'; // TODO + viewer.canvas.title = '$' + hex(i); viewer.updateImage(imdata); $(viewer.canvas).addClass('asset_cell'); $(viewer.canvas).click((e) => { @@ -791,7 +791,7 @@ export class ImageChooser { agrid.append(span); } span.append(viewer.canvas); - var brk = (i % imgsperline) == imgsperline-1; + var brk = (i % imgsperline) == imgsperline - 1; if (brk) { agrid.append($("
")); span = null; @@ -800,7 +800,7 @@ export class ImageChooser { } } -function newDiv(parent?, cls? : string) { +function newDiv(parent?, cls?: string) { var div = $(document.createElement("div")); if (parent) div.appendTo(parent) if (cls) div.addClass(cls); @@ -814,7 +814,7 @@ export class CharmapEditor extends PixNode { fmt; chooser; - constructor(context:EditorContext, parentdiv:JQuery, fmt:PixelEditorImageFormat) { + constructor(context: EditorContext, parentdiv: JQuery, fmt: PixelEditorImageFormat) { super(); this.context = context; this.parentdiv = parentdiv; @@ -852,7 +852,7 @@ export class CharmapEditor extends PixNode { // TODO: full identifier var sel = $(document.createElement('option')).text(palopt.name).val(i).appendTo(palselect); if (i == (palizer as Palettizer).palindex) - sel.attr('selected','selected'); + sel.attr('selected', 'selected'); }); palselect.appendTo(agrid).change((e) => { var index = ($(e.target).val() as any) as number; @@ -863,7 +863,7 @@ export class CharmapEditor extends PixNode { return true; } - createEditor(aeditor: JQuery, viewer: Viewer, xscale: number, yscale: number) : PixEditor { + createEditor(aeditor: JQuery, viewer: Viewer, xscale: number, yscale: number): PixEditor { var im = new PixEditor(); im.createWith(viewer); im.updateImage(); @@ -872,8 +872,8 @@ export class CharmapEditor extends PixNode { while (w > 500 || h > 500) { w /= 2; h /= 2; } - im.canvas.style.width = w+'px'; // TODO - im.canvas.style.height = h+'px'; // TODO + im.canvas.style.width = w + 'px'; // TODO + im.canvas.style.height = h + 'px'; // TODO im.makeEditable(this, aeditor, this.left.palette); return im; } @@ -885,7 +885,7 @@ export class MapEditor extends PixNode { parentdiv; fmt; - constructor(context:EditorContext, parentdiv:JQuery, fmt:PixelEditorImageFormat) { + constructor(context: EditorContext, parentdiv: JQuery, fmt: PixelEditorImageFormat) { super(); this.context = context; this.parentdiv = parentdiv; @@ -915,13 +915,13 @@ export class MapEditor extends PixNode { export class Viewer { - width : number; - height : number; - canvas : HTMLCanvasElement; - ctx : CanvasRenderingContext2D; - imagedata : ImageData; - rgbdata : Uint32Array; - peerviewers : Viewer[]; + width: number; + height: number; + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + imagedata: ImageData; + rgbdata: Uint32Array; + peerviewers: Viewer[]; recreate() { this.canvas = this.newCanvas(); @@ -930,7 +930,7 @@ export class Viewer { this.peerviewers = [this]; } - createWith(pv : Viewer) { + createWith(pv: Viewer) { this.width = pv.width; this.height = pv.height; this.imagedata = pv.imagedata; @@ -939,7 +939,7 @@ export class Viewer { this.peerviewers = [this, pv]; } - newCanvas() : HTMLCanvasElement { + newCanvas(): HTMLCanvasElement { var c = document.createElement('canvas'); c.width = this.width; c.height = this.height; @@ -950,7 +950,7 @@ export class Viewer { return c; } - updateImage(imdata? : Uint32Array) { + updateImage(imdata?: Uint32Array) { if (imdata) { this.rgbdata.set(imdata); } @@ -962,31 +962,31 @@ export class Viewer { class PixEditor extends Viewer { - left : PixNode; - palette : Uint32Array; - curpalcol : number = -1; - currgba : number; - palbtns : JQuery[]; - offscreen : Map = new Map(); + left: PixNode; + palette: Uint32Array; + curpalcol: number = -1; + currgba: number; + palbtns: JQuery[]; + offscreen: Map = new Map(); getPositionFromEvent(e) { var x = Math.floor(e.offsetX * this.width / $(this.canvas).width()); var y = Math.floor(e.offsetY * this.height / $(this.canvas).height()); - return {x:x, y:y}; + return { x: x, y: y }; } setPaletteColor(col: number) { - col &= this.palette.length-1; + col &= this.palette.length - 1; if (this.curpalcol != col) { if (this.curpalcol >= 0) this.palbtns[this.curpalcol].removeClass('selected'); this.curpalcol = col; - this.currgba = this.palette[col & this.palette.length-1]; + this.currgba = this.palette[col & this.palette.length - 1]; this.palbtns[col].addClass('selected'); } } - makeEditable(leftnode:PixNode, aeditor:JQuery, palette:Uint32Array) { + makeEditable(leftnode: PixNode, aeditor: JQuery, palette: Uint32Array) { this.left = leftnode; this.palette = palette; @@ -994,12 +994,12 @@ class PixEditor extends Viewer { var dragging = false; var pxls = $(this.canvas); - pxls.mousedown( (e) => { + pxls.mousedown((e) => { var pos = this.getPositionFromEvent(e); dragcol = this.getPixel(pos.x, pos.y) == this.currgba ? this.palette[0] : this.currgba; this.setPixel(pos.x, pos.y, this.currgba); dragging = true; - $(document).mouseup( (e) => { + $(document).mouseup((e) => { $(document).off('mouseup'); var pos = this.getPositionFromEvent(e); this.setPixel(pos.x, pos.y, dragcol); @@ -1007,12 +1007,12 @@ class PixEditor extends Viewer { this.commit(); }); }) - .mousemove( (e) => { - var pos = this.getPositionFromEvent(e); - if (dragging) { - this.setPixel(pos.x, pos.y, dragcol); - } - }); + .mousemove((e) => { + var pos = this.getPositionFromEvent(e); + if (dragging) { + this.setPixel(pos.x, pos.y, dragcol); + } + }); aeditor.empty(); this.createToolbarButtons(aeditor[0]); @@ -1021,24 +1021,24 @@ class PixEditor extends Viewer { this.setPaletteColor(1); } - getPixel(x:number, y:number) : number { + getPixel(x: number, y: number): number { x = Math.round(x); y = Math.round(y); if (x < 0 || x >= this.width || y < 0 || y >= this.height) { - return this.offscreen[x+','+y] | this.palette[0]; + return this.offscreen[x + ',' + y] | this.palette[0]; } else { - var ofs = x+y*this.width; + var ofs = x + y * this.width; return this.rgbdata[ofs]; } } - setPixel(x:number, y:number, rgba:number) : void { + setPixel(x: number, y: number, rgba: number): void { x = Math.round(x); y = Math.round(y); if (x < 0 || x >= this.width || y < 0 || y >= this.height) { - this.offscreen[x+','+y] = rgba; + this.offscreen[x + ',' + y] = rgba; } else { - var ofs = x+y*this.width; + var ofs = x + y * this.width; var oldrgba = this.rgbdata[ofs]; if (oldrgba != rgba) { this.rgbdata[ofs] = rgba; @@ -1050,7 +1050,7 @@ class PixEditor extends Viewer { createPaletteButtons() { this.palbtns = []; var span = newDiv(null, "asset_toolbar"); - for (var i=0; i number) { + remapPixels(mapfn: (x: number, y: number) => number) { var i = 0; var pixels = new Uint32Array(this.rgbdata.length); - for (var y=0; y { - var xx = x + 0.5 - this.width/2.0; - var yy = y + 0.5 - this.height/2.0; - var xx2 = xx*c1 - yy*s1 + this.width/2.0 - 0.5; - var yy2 = yy*c1 + xx*s1 + this.height/2.0 - 0.5; + this.remapPixels((x, y) => { + var xx = x + 0.5 - this.width / 2.0; + var yy = y + 0.5 - this.height / 2.0; + var xx2 = xx * c1 - yy * s1 + this.width / 2.0 - 0.5; + var yy2 = yy * c1 + xx * s1 + this.height / 2.0 - 0.5; return this.getPixel(xx2, yy2); }); } @@ -1113,18 +1113,18 @@ class PixEditor extends Viewer { this.rotate(90); } flipX() { - this.remapPixels((x,y) => { - return this.getPixel(this.width-1-x, y); + this.remapPixels((x, y) => { + return this.getPixel(this.width - 1 - x, y); }); } flipY() { - this.remapPixels((x,y) => { - return this.getPixel(x, this.height-1-y); + this.remapPixels((x, y) => { + return this.getPixel(x, this.height - 1 - y); }); } - translate(dx:number, dy:number) { - this.remapPixels((x,y) => { - return this.getPixel(x+dx, y+dy); + translate(dx: number, dy: number) { + this.remapPixels((x, y) => { + return this.getPixel(x + dx, y + dy); }); } @@ -1134,11 +1134,11 @@ class PixEditor extends Viewer { abstract class TwoWayPixelConverter { - w : number; - h : number; - words : Uint8Array; - bitoffsets : Uint32Array; - coloffsets : Uint32Array; + w: number; + h: number; + words: Uint8Array; + bitoffsets: Uint32Array; + coloffsets: Uint32Array; constructor(width: number, height: number, bpp: number, colpp: number) { this.w = width; @@ -1148,16 +1148,16 @@ abstract class TwoWayPixelConverter { this.coloffsets = new Uint32Array(width * height); } - setPixel(x:number, y:number, col:number, bitofs:number, colofs:number) { + setPixel(x: number, y: number, col: number, bitofs: number, colofs: number) { var ofs = x + y * this.w; this.words[ofs] = col; this.bitoffsets[ofs] = bitofs; this.coloffsets[ofs] = colofs; } - abstract wordsToImage(words: UintArray) : Uint8Array[]; + abstract wordsToImage(words: UintArray): Uint8Array[]; - abstract imageToWords(image: Uint8Array) : UintArray; + abstract imageToWords(image: Uint8Array): UintArray; } export class TwoWayMapper extends PixNode { From abf9ebd6c9dd36aa5809ad0ff992c5c2a83f3b3a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 11:55:46 -0800 Subject: [PATCH 02/22] reformat src/ide/views/asseteditor.ts --- src/ide/views/asseteditor.ts | 706 +++++++++++++++++------------------ 1 file changed, 353 insertions(+), 353 deletions(-) diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index a8745edc..dd826363 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -7,389 +7,389 @@ import * as pixed from "../pixeleditor"; import Mousetrap = require('mousetrap'); export class AssetEditorView implements ProjectView, pixed.EditorContext { - maindiv : JQuery; - cureditordiv : JQuery; - cureditelem : JQuery; - cureditnode : pixed.PixNode; - rootnodes : pixed.PixNode[]; - deferrednodes : pixed.PixNode[]; - - createDiv(parent : HTMLElement) { - this.maindiv = newDiv(parent, "vertical-scroll"); - return this.maindiv[0]; - } - - clearAssets() { - this.rootnodes = []; - this.deferrednodes = []; - } - - registerAsset(type:string, node:pixed.PixNode, deferred:number) { - this.rootnodes.push(node); - if (deferred) { - if (deferred > 1) - this.deferrednodes.push(node); - else - this.deferrednodes.unshift(node); - } else { - node.refreshRight(); - } + maindiv: JQuery; + cureditordiv: JQuery; + cureditelem: JQuery; + cureditnode: pixed.PixNode; + rootnodes: pixed.PixNode[]; + deferrednodes: pixed.PixNode[]; + + createDiv(parent: HTMLElement) { + this.maindiv = newDiv(parent, "vertical-scroll"); + return this.maindiv[0]; + } + + clearAssets() { + this.rootnodes = []; + this.deferrednodes = []; + } + + registerAsset(type: string, node: pixed.PixNode, deferred: number) { + this.rootnodes.push(node); + if (deferred) { + if (deferred > 1) + this.deferrednodes.push(node); + else + this.deferrednodes.unshift(node); + } else { + node.refreshRight(); } - - getPalettes(matchlen : number) : pixed.SelectablePalette[] { - var result = []; - this.rootnodes.forEach((node) => { - while (node != null) { - if (node instanceof pixed.PaletteFormatToRGB) { - // TODO: move to node class? - var palette = node.palette; - // match full palette length? - if (matchlen == palette.length) { - result.push({node:node, name:"Palette", palette:palette}); - } - // look at palette slices - if (node.layout) { - node.layout.forEach(([name, start, len]) => { - if (start < palette.length) { - if (len == matchlen) { - var rgbs = palette.slice(start, start+len); - result.push({node:node, name:name, palette:rgbs}); - } else if (-len == matchlen) { // reverse order - var rgbs = palette.slice(start, start-len); - rgbs.reverse(); - result.push({node:node, name:name, palette:rgbs}); - } else if (len+1 == matchlen) { - var rgbs = new Uint32Array(matchlen); - rgbs[0] = palette[0]; - rgbs.set(palette.slice(start, start+len), 1); - result.push({node:node, name:name, palette:rgbs}); - } + } + + getPalettes(matchlen: number): pixed.SelectablePalette[] { + var result = []; + this.rootnodes.forEach((node) => { + while (node != null) { + if (node instanceof pixed.PaletteFormatToRGB) { + // TODO: move to node class? + var palette = node.palette; + // match full palette length? + if (matchlen == palette.length) { + result.push({ node: node, name: "Palette", palette: palette }); + } + // look at palette slices + if (node.layout) { + node.layout.forEach(([name, start, len]) => { + if (start < palette.length) { + if (len == matchlen) { + var rgbs = palette.slice(start, start + len); + result.push({ node: node, name: name, palette: rgbs }); + } else if (-len == matchlen) { // reverse order + var rgbs = palette.slice(start, start - len); + rgbs.reverse(); + result.push({ node: node, name: name, palette: rgbs }); + } else if (len + 1 == matchlen) { + var rgbs = new Uint32Array(matchlen); + rgbs[0] = palette[0]; + rgbs.set(palette.slice(start, start + len), 1); + result.push({ node: node, name: name, palette: rgbs }); } - }); - } - break; + } + }); } - node = node.right; + break; } - }); - return result; - } - - getTilemaps(matchlen : number) : pixed.SelectableTilemap[] { - var result = []; - this.rootnodes.forEach((node) => { - while (node != null) { - if (node instanceof pixed.Palettizer) { - var rgbimgs = node.rgbimgs; - if (rgbimgs && rgbimgs.length >= matchlen) { - result.push({node:node, name:"Tilemap", images:node.images, rgbimgs:rgbimgs}); // TODO - } + node = node.right; + } + }); + return result; + } + + getTilemaps(matchlen: number): pixed.SelectableTilemap[] { + var result = []; + this.rootnodes.forEach((node) => { + while (node != null) { + if (node instanceof pixed.Palettizer) { + var rgbimgs = node.rgbimgs; + if (rgbimgs && rgbimgs.length >= matchlen) { + result.push({ node: node, name: "Tilemap", images: node.images, rgbimgs: rgbimgs }); // TODO } - node = node.right; } - }); - return result; - } - - isEditing() { - return this.cureditordiv != null; - } - - getCurrentEditNode() { - return this.cureditnode; - } - - setCurrentEditor(div:JQuery, editing:JQuery, node:pixed.PixNode) { - const timeout = 250; - if (this.cureditordiv != div) { - if (this.cureditordiv) { - this.cureditordiv.hide(timeout); - this.cureditordiv = null; - } - if (div) { - this.cureditordiv = div; - this.cureditordiv.show(); - this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}); - //setTimeout(() => { this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}) }, timeout); - } - } - if (this.cureditelem) { - this.cureditelem.removeClass('selected'); - this.cureditelem = null; + node = node.right; } - if (editing) { - this.cureditelem = editing; - this.cureditelem.addClass('selected'); + }); + return result; + } + + isEditing() { + return this.cureditordiv != null; + } + + getCurrentEditNode() { + return this.cureditnode; + } + + setCurrentEditor(div: JQuery, editing: JQuery, node: pixed.PixNode) { + const timeout = 250; + if (this.cureditordiv != div) { + if (this.cureditordiv) { + this.cureditordiv.hide(timeout); + this.cureditordiv = null; } - while (node.left) { - node = node.left; + if (div) { + this.cureditordiv = div; + this.cureditordiv.show(); + this.cureditordiv[0].scrollIntoView({ behavior: "smooth", block: "center" }); + //setTimeout(() => { this.cureditordiv[0].scrollIntoView({behavior: "smooth", block: "center"}) }, timeout); } - this.cureditnode = node; } - - scanFileTextForAssets(id : string, data : string) { - // scan file for assets - // /*{json}*/ or ;;{json};; - // TODO: put before ident, look for = { - var result = []; - var re1 = /[/;][*;]([{].+[}])[*;][/;]/g; - var m; - while (m = re1.exec(data)) { - var start = m.index + m[0].length; - var end; - // TODO: verilog end - if (platform_id.includes('verilog')) { - end = data.indexOf("end", start); // asm - } else if (m[0].startsWith(';;')) { - end = data.indexOf(';;', start); // asm - } else { - end = data.indexOf(';', start); // C - } - //console.log(id, start, end, m[1], data.substring(start,end)); - if (end > start) { - 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}); - } catch (e) { - console.log(e); - } - } + if (this.cureditelem) { + this.cureditelem.removeClass('selected'); + this.cureditelem = null; + } + if (editing) { + this.cureditelem = editing; + this.cureditelem.addClass('selected'); + } + while (node.left) { + node = node.left; + } + this.cureditnode = node; + } + + scanFileTextForAssets(id: string, data: string) { + // scan file for assets + // /*{json}*/ or ;;{json};; + // TODO: put before ident, look for = { + var result = []; + var re1 = /[/;][*;]([{].+[}])[*;][/;]/g; + var m; + while (m = re1.exec(data)) { + var start = m.index + m[0].length; + var end; + // TODO: verilog end + if (platform_id.includes('verilog')) { + end = data.indexOf("end", start); // asm + } else if (m[0].startsWith(';;')) { + end = data.indexOf(';;', start); // asm + } else { + end = data.indexOf(';', start); // C } - // look for DEF_METASPRITE_2x2(playerRStand, 0xd8, 0) - // TODO: could also look in ROM - var re2 = /DEF_METASPRITE_(\d+)x(\d+)[(](\w+),\s*(\w+),\s*(\w+)/gi; - while (m = re2.exec(data)) { - var width = parseInt(m[1]); - var height = parseInt(m[2]); - var ident = m[3]; - var tile = parseInt(m[4]); - var attr = parseInt(m[5]); - var metadefs = []; - for (var x=0; x start) { + 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 }); + } catch (e) { + console.log(e); } - var meta = {defs:metadefs,width:width*8,height:height*8}; - result.push({fileid:id,label:ident,meta:meta}); - } - // TODO: look for decode --- ... --- - /* - var re3 = /\bdecode\s+(\w+)\s*---(.+?)---/gims; - while (m = re3.exec(data)) { } - */ - return result; } - - // TODO: move to pixeleditor.ts? - addPaletteEditorViews(parentdiv:JQuery, pal2rgb:pixed.PaletteFormatToRGB, callback) { - var adual = $('
').appendTo(parentdiv); - var aeditor = $('
').hide(); // contains editor, when selected - // TODO: they need to update when refreshed from right - var allrgbimgs = []; - pal2rgb.getAllColors().forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker) - var atable = $('').appendTo(adual); - aeditor.appendTo(adual); - // make default layout if not exists - var layout = pal2rgb.layout; - if (!layout) { - var len = pal2rgb.palette.length; - var imgsperline = len > 32 ? 8 : 4; // TODO: use 'n'? - layout = []; - for (var i=0; i --- ... --- + /* + var re3 = /\bdecode\s+(\w+)\s*---(.+?)---/gims; + while (m = re3.exec(data)) { + } + */ + return result; + } + + // TODO: move to pixeleditor.ts? + addPaletteEditorViews(parentdiv: JQuery, pal2rgb: pixed.PaletteFormatToRGB, callback) { + var adual = $('
').appendTo(parentdiv); + var aeditor = $('
').hide(); // contains editor, when selected + // TODO: they need to update when refreshed from right + var allrgbimgs = []; + pal2rgb.getAllColors().forEach((rgba) => { allrgbimgs.push(new Uint32Array([rgba])); }); // array of array of 1 rgb color (for picker) + var atable = $('
').appendTo(adual); + aeditor.appendTo(adual); + // make default layout if not exists + var layout = pal2rgb.layout; + if (!layout) { + var len = pal2rgb.palette.length; + var imgsperline = len > 32 ? 8 : 4; // TODO: use 'n'? + layout = []; + for (var i = 0; i < len; i += imgsperline) { + layout.push(["", i, Math.min(len - i, imgsperline)]); } - // iterate over each row of the layout - layout.forEach( ([name, start, len]) => { - if (start < pal2rgb.palette.length) { // skip row if out of range - var arow = $('').appendTo(atable); - $('').appendTo(atable); + $('
').text(name).appendTo(arow); - var inds = []; - for (var k=start; k { - var cell = $('').addClass('asset_cell asset_editable').appendTo(arow); - updateCell(cell, i); - cell.click((e) => { - var chooser = new pixed.ImageChooser(); - chooser.rgbimgs = allrgbimgs; - chooser.width = 1; - chooser.height = 1; - chooser.recreate(aeditor, (index, newvalue) => { - callback(i, index); - updateCell(cell, i); - }); - this.setCurrentEditor(aeditor, cell, pal2rgb); - }); - }); - } - }); } - - addPixelEditor(parentdiv:JQuery, firstnode:pixed.PixNode, fmt:pixed.PixelEditorImageFormat) { - // data -> pixels - fmt.xform = 'scale(2)'; - var mapper = new pixed.Mapper(fmt); - // TODO: rotate node? - firstnode.addRight(mapper); - // pixels -> RGBA - var palizer = new pixed.Palettizer(this, fmt); - mapper.addRight(palizer); - // add view objects - palizer.addRight(new pixed.CharmapEditor(this, newDiv(parentdiv), fmt)); + function updateCell(cell, j) { + var val = pal2rgb.words[j]; + var rgb = pal2rgb.palette[j]; + var hexcol = '#' + hex(rgb2bgr(rgb), 6); + var textcol = (rgb & 0x008000) ? 'black' : 'white'; + cell.text(hex(val, 2)).css('background-color', hexcol).css('color', textcol); } - - addPaletteEditor(parentdiv:JQuery, firstnode:pixed.PixNode, palfmt?) { - // palette -> RGBA - var pal2rgb = new pixed.PaletteFormatToRGB(palfmt); - firstnode.addRight(pal2rgb); - // TODO: refresh twice? - firstnode.refreshRight(); - // TODO: add view objects - // TODO: show which one is selected? - this.addPaletteEditorViews(parentdiv, pal2rgb, - (index, newvalue) => { - console.log('set entry', index, '=', newvalue); - // TODO: this forces update of palette rgb colors and file data - firstnode.words[index] = newvalue; - pal2rgb.words = null; - pal2rgb.updateRight(); - pal2rgb.refreshLeft(); + // iterate over each row of the layout + layout.forEach(([name, start, len]) => { + if (start < pal2rgb.palette.length) { // skip row if out of range + var arow = $('
').text(name).appendTo(arow); + var inds = []; + for (var k = start; k < start + Math.abs(len); k++) + inds.push(k); + if (len < 0) + inds.reverse(); + inds.forEach((i) => { + var cell = $('').addClass('asset_cell asset_editable').appendTo(arow); + updateCell(cell, i); + cell.click((e) => { + var chooser = new pixed.ImageChooser(); + chooser.rgbimgs = allrgbimgs; + chooser.width = 1; + chooser.height = 1; + chooser.recreate(aeditor, (index, newvalue) => { + callback(i, index); + updateCell(cell, i); + }); + this.setCurrentEditor(aeditor, cell, pal2rgb); + }); }); - } - - ensureFileDiv(fileid : string) : JQuery { - var divid = this.getFileDivId(fileid); - var body = $(document.getElementById(divid)); - if (body.length === 0) { - var filediv = newDiv(this.maindiv, 'asset_file'); - var header = newDiv(filediv, 'asset_file_header').text(fileid); - body = newDiv(filediv).attr('id',divid).addClass('disable-select'); } - return body; + }); + } + + addPixelEditor(parentdiv: JQuery, firstnode: pixed.PixNode, fmt: pixed.PixelEditorImageFormat) { + // data -> pixels + fmt.xform = 'scale(2)'; + var mapper = new pixed.Mapper(fmt); + // TODO: rotate node? + firstnode.addRight(mapper); + // pixels -> RGBA + var palizer = new pixed.Palettizer(this, fmt); + mapper.addRight(palizer); + // add view objects + palizer.addRight(new pixed.CharmapEditor(this, newDiv(parentdiv), fmt)); + } + + addPaletteEditor(parentdiv: JQuery, firstnode: pixed.PixNode, palfmt?) { + // palette -> RGBA + var pal2rgb = new pixed.PaletteFormatToRGB(palfmt); + firstnode.addRight(pal2rgb); + // TODO: refresh twice? + firstnode.refreshRight(); + // TODO: add view objects + // TODO: show which one is selected? + this.addPaletteEditorViews(parentdiv, pal2rgb, + (index, newvalue) => { + console.log('set entry', index, '=', newvalue); + // TODO: this forces update of palette rgb colors and file data + firstnode.words[index] = newvalue; + pal2rgb.words = null; + pal2rgb.updateRight(); + pal2rgb.refreshLeft(); + }); + } + + ensureFileDiv(fileid: string): JQuery { + var divid = this.getFileDivId(fileid); + var body = $(document.getElementById(divid)); + if (body.length === 0) { + var filediv = newDiv(this.maindiv, 'asset_file'); + var header = newDiv(filediv, 'asset_file_header').text(fileid); + body = newDiv(filediv).attr('id', divid).addClass('disable-select'); } - - refreshAssetsInFile(fileid : string, data : FileData) : number { - let nassets = 0; - // TODO: check fmt w/h/etc limits - // TODO: defer editor creation - // TODO: only refresh when needed - if (platform_id.startsWith('nes') && fileid.endsWith('.chr') && data instanceof Uint8Array) { - // is this a NES CHR? - let node = new pixed.FileDataNode(projectWindows, fileid); - const neschrfmt = {w:8,h:8,bpp:1,count:(data.length>>4),brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO - this.addPixelEditor(this.ensureFileDiv(fileid), node, neschrfmt); - this.registerAsset("charmap", node, 1); - nassets++; - } else if (platform_id.startsWith('nes') && fileid.endsWith('.pal') && data instanceof Uint8Array) { - // is this a NES PAL? - let node = new pixed.FileDataNode(projectWindows, fileid); - const nespalfmt = {pal:"nes",layout:"nes"}; - this.addPaletteEditor(this.ensureFileDiv(fileid), node, nespalfmt); - this.registerAsset("palette", node, 0); - nassets++; - } else if (typeof data === 'string') { - let textfrags = this.scanFileTextForAssets(fileid, data); - for (let frag of textfrags) { - if (frag.fmt) { - let label = fileid; // TODO: label - let node : pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end); - let first = node; - // rle-compressed? TODO: how to edit? - if (frag.fmt.comp == 'rletag') { - node = node.addRight(new pixed.Compressor()); - } - // is this a nes nametable? - if (frag.fmt.map == 'nesnt') { - 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)); - 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.registerAsset("charmap", first, 1); - nassets++; - } - // is this a palette? - else if (frag.fmt.pal) { - this.addPaletteEditor(this.ensureFileDiv(fileid), node, frag.fmt); - this.registerAsset("palette", first, 0); - nassets++; - } - else { - // TODO: other kinds of resources? - } + return body; + } + + refreshAssetsInFile(fileid: string, data: FileData): number { + let nassets = 0; + // TODO: check fmt w/h/etc limits + // TODO: defer editor creation + // TODO: only refresh when needed + if (platform_id.startsWith('nes') && fileid.endsWith('.chr') && data instanceof Uint8Array) { + // is this a NES CHR? + let node = new pixed.FileDataNode(projectWindows, fileid); + const neschrfmt = { w: 8, h: 8, bpp: 1, count: (data.length >> 4), brev: true, np: 2, pofs: 8, remap: [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12] }; // TODO + this.addPixelEditor(this.ensureFileDiv(fileid), node, neschrfmt); + this.registerAsset("charmap", node, 1); + nassets++; + } else if (platform_id.startsWith('nes') && fileid.endsWith('.pal') && data instanceof Uint8Array) { + // is this a NES PAL? + let node = new pixed.FileDataNode(projectWindows, fileid); + const nespalfmt = { pal: "nes", layout: "nes" }; + this.addPaletteEditor(this.ensureFileDiv(fileid), node, nespalfmt); + this.registerAsset("palette", node, 0); + nassets++; + } else if (typeof data === 'string') { + let textfrags = this.scanFileTextForAssets(fileid, data); + for (let frag of textfrags) { + if (frag.fmt) { + let label = fileid; // TODO: label + let node: pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end); + let first = node; + // rle-compressed? TODO: how to edit? + if (frag.fmt.comp == 'rletag') { + node = node.addRight(new pixed.Compressor()); + } + // is this a nes nametable? + if (frag.fmt.map == 'nesnt') { + 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)); + 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.registerAsset("charmap", first, 1); + nassets++; + } + // is this a palette? + else if (frag.fmt.pal) { + this.addPaletteEditor(this.ensureFileDiv(fileid), node, frag.fmt); + this.registerAsset("palette", first, 0); + nassets++; + } + else { + // TODO: other kinds of resources? } } } - return nassets; } - - getFileDivId(id : string) { - return '__asset__' + safeident(id); - } - + return nassets; + } + + getFileDivId(id: string) { + return '__asset__' + safeident(id); + } + // TODO: recreate editors when refreshing // TODO: look for changes, not moveCursor - refresh(moveCursor : boolean) { - // clear and refresh all files/nodes? - if (moveCursor) { - this.maindiv.empty(); - this.clearAssets(); - current_project.iterateFiles((fileid, data) => { - try { - var nassets = this.refreshAssetsInFile(fileid, data); - } catch (e) { - console.log(e); - this.ensureFileDiv(fileid).text(e+""); // TODO: error msg? - } - }); - console.log("Found " + this.rootnodes.length + " assets"); - this.deferrednodes.forEach((node) => { - try { - node.refreshRight(); - } catch (e) { - console.log(e); - alert(e+""); - } - }); - this.deferrednodes = []; - } else { - // only refresh nodes if not actively editing - // since we could be in the middle of an operation that hasn't been committed - for (var node of this.rootnodes) { - if (node !== this.getCurrentEditNode()) { - node.refreshRight(); - } + refresh(moveCursor: boolean) { + // clear and refresh all files/nodes? + if (moveCursor) { + this.maindiv.empty(); + this.clearAssets(); + current_project.iterateFiles((fileid, data) => { + try { + var nassets = this.refreshAssetsInFile(fileid, data); + } catch (e) { + console.log(e); + this.ensureFileDiv(fileid).text(e + ""); // TODO: error msg? + } + }); + console.log("Found " + this.rootnodes.length + " assets"); + this.deferrednodes.forEach((node) => { + try { + node.refreshRight(); + } catch (e) { + console.log(e); + alert(e + ""); + } + }); + this.deferrednodes = []; + } else { + // only refresh nodes if not actively editing + // since we could be in the middle of an operation that hasn't been committed + for (var node of this.rootnodes) { + if (node !== this.getCurrentEditNode()) { + node.refreshRight(); } } } - - setVisible?(showing : boolean) : void { - // TODO: make into toolbar? - if (showing) { - if (Mousetrap.bind) Mousetrap.bind('mod+z', projectWindows.undoStep.bind(projectWindows)); - } else { - if (Mousetrap.unbind) Mousetrap.unbind('mod+z'); - } + } + + setVisible?(showing: boolean): void { + // TODO: make into toolbar? + if (showing) { + if (Mousetrap.bind) Mousetrap.bind('mod+z', projectWindows.undoStep.bind(projectWindows)); + } else { + if (Mousetrap.unbind) Mousetrap.unbind('mod+z'); } - } - + +} + From 955d872893b596c27023d69c72f0977d58742ab4 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 11:55:51 -0800 Subject: [PATCH 03/22] reformat src/ide/windows.ts --- src/ide/windows.ts | 70 +++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/ide/windows.ts b/src/ide/windows.ts index f19664d6..1b4003a9 100644 --- a/src/ide/windows.ts +++ b/src/ide/windows.ts @@ -5,45 +5,45 @@ import { WorkerError, FileData } from "../common/workertypes"; import { getFilenamePrefix, getFilenameForPath } from "../common/util"; import { ProjectView } from "./views/baseviews"; -type WindowCreateFunction = (id:string) => ProjectView; -type WindowShowFunction = (id:string, view:ProjectView) => void; +type WindowCreateFunction = (id: string) => ProjectView; +type WindowShowFunction = (id: string, view: ProjectView) => void; export class ProjectWindows { - containerdiv : HTMLElement; - project : CodeProject; - id2window : {[id:string]:ProjectView} = {}; - id2createfn : {[id:string]:WindowCreateFunction} = {}; - id2showfn : {[id:string]:WindowShowFunction} = {}; - id2div : {[id:string]:HTMLElement} = {}; - activeid : string; - activewnd : ProjectView; - activediv : HTMLElement; - lasterrors : WorkerError[]; - undofiles : string[]; - - constructor(containerdiv:HTMLElement, project:CodeProject) { + containerdiv: HTMLElement; + project: CodeProject; + id2window: { [id: string]: ProjectView } = {}; + id2createfn: { [id: string]: WindowCreateFunction } = {}; + id2showfn: { [id: string]: WindowShowFunction } = {}; + id2div: { [id: string]: HTMLElement } = {}; + activeid: string; + activewnd: ProjectView; + activediv: HTMLElement; + lasterrors: WorkerError[]; + undofiles: string[]; + + constructor(containerdiv: HTMLElement, project: CodeProject) { this.containerdiv = containerdiv; this.project = project; this.undofiles = []; } // TODO: delete windows ever? - isWindow(id:string) : boolean { + isWindow(id: string): boolean { return this.id2createfn[id] != null; } - setCreateFunc(id:string, createfn:WindowCreateFunction) : void { + setCreateFunc(id: string, createfn: WindowCreateFunction): void { this.id2createfn[id] = createfn; } - - setShowFunc(id:string, showfn:WindowShowFunction) : void { + + setShowFunc(id: string, showfn: WindowShowFunction): void { this.id2showfn[id] = showfn; } - create(id:string) : ProjectView { + create(id: string): ProjectView { var wnd = this.id2window[id]; if (!wnd) { - console.log("creating window",id); + console.log("creating window", id); wnd = this.id2window[id] = this.id2createfn[id](id); } var div = this.id2div[id]; @@ -54,7 +54,7 @@ export class ProjectWindows { return wnd; } - createOrShow(id: string, moveCursor?: boolean) : ProjectView { + createOrShow(id: string, moveCursor?: boolean): ProjectView { var wnd = this.create(id); var div = this.id2div[id]; if (this.activewnd != wnd) { @@ -74,27 +74,27 @@ export class ProjectWindows { return wnd; } - put(id:string, window:ProjectView) : void { + put(id: string, window: ProjectView): void { this.id2window[id] = window; } - refresh(moveCursor:boolean) : void { + refresh(moveCursor: boolean): void { // refresh current window if (this.activewnd && this.activewnd.refresh) this.activewnd.refresh(moveCursor); } - tick() : void { + tick(): void { if (this.activewnd && this.activewnd.tick) this.activewnd.tick(); } - setErrors(errors:WorkerError[]) : void { + setErrors(errors: WorkerError[]): void { this.lasterrors = errors; this.refreshErrors(); } - refreshErrors() : void { + refreshErrors(): void { if (this.activewnd && this.activewnd.markErrors) { if (this.lasterrors && this.lasterrors.length) this.activewnd.markErrors(this.lasterrors); @@ -103,18 +103,18 @@ export class ProjectWindows { } } - getActive() : ProjectView { return this.activewnd; } + getActive(): ProjectView { return this.activewnd; } - getActiveID() : string { return this.activeid; } + getActiveID(): string { return this.activeid; } - getCurrentText() : string { + getCurrentText(): string { if (this.activewnd && this.activewnd.getValue) return this.activewnd.getValue(); else bootbox.alert("Please switch to an editor window."); } - resize() : void { + resize(): void { if (this.activeid && this.activewnd && this.activewnd.recreateOnResize) { this.activewnd = null; this.id2window[this.activeid] = null; @@ -123,7 +123,7 @@ export class ProjectWindows { } } - updateFile(fileid:string, data:FileData) { + updateFile(fileid: string, data: FileData) { // is there an editor? if so, use it var wnd = this.id2window[fileid]; if (wnd && wnd.setText && typeof data === 'string') { @@ -133,7 +133,7 @@ export class ProjectWindows { this.project.updateFile(fileid, data); } } - + undoStep() { var fileid = this.undofiles.pop(); var wnd = this.id2window[fileid]; @@ -155,11 +155,11 @@ export class ProjectWindows { } } - findWindowWithFilePrefix(filename : string) : string { + findWindowWithFilePrefix(filename: string): string { filename = getFilenameForPath(getFilenamePrefix(filename)); for (var fileid in this.id2createfn) { // ignore include files (TODO) - if (fileid.toLowerCase().endsWith('.h') || fileid.toLowerCase().endsWith('.inc') || fileid.toLowerCase().endsWith('.bas')) + if (fileid.toLowerCase().endsWith('.h') || fileid.toLowerCase().endsWith('.inc') || fileid.toLowerCase().endsWith('.bas')) continue; if (getFilenameForPath(getFilenamePrefix(fileid)) == filename) return fileid; } From b5d351519f7b6b4df688c036c40020ead5721d7b Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 12:04:40 -0800 Subject: [PATCH 04/22] fix asset undo #213 --- src/ide/pixeleditor.ts | 22 +++++++++++++++++++++- src/ide/views/asseteditor.ts | 6 +----- src/ide/windows.ts | 3 +++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index ce001e65..01952193 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -767,10 +767,12 @@ export class ImageChooser { rgbimgs: Uint32Array[]; width: number; height: number; + viewers: Viewer[]; recreate(parentdiv: JQuery, onclick) { var agrid = $('
'); // grid (or 1) of preview images parentdiv.empty().append(agrid); + this.viewers = []; var cscale = Math.max(2, Math.ceil(16 / this.width)); // TODO var imgsperline = this.width <= 8 ? 16 : 8; // TODO var span = null; @@ -786,6 +788,7 @@ export class ImageChooser { $(viewer.canvas).click((e) => { onclick(i, viewer); }); + this.viewers.push(viewer); if (!span) { span = $(''); agrid.append(span); @@ -798,6 +801,13 @@ export class ImageChooser { } }); } + + updateImages(rgbimgs: Uint32Array[]) { + this.rgbimgs = rgbimgs; + for (var i = 0; i < this.viewers.length; i++) { + this.viewers[i].updateImage(rgbimgs[i]); + } + } } function newDiv(parent?, cls?: string) { @@ -828,6 +838,15 @@ export class CharmapEditor extends PixNode { updateRight() { if (equalNestedArrays(this.rgbimgs, this.left.rgbimgs)) return false; this.rgbimgs = this.left.rgbimgs; + // if chooser already exists with same number of images, update in place + if (this.chooser && this.chooser.viewers && this.chooser.viewers.length == this.rgbimgs.length) { + this.chooser.updateImages(this.rgbimgs); + // keep rgbimgs pointing to viewer buffers so edits propagate back via refreshLeft + for (var i = 0; i < this.chooser.viewers.length; i++) { + this.rgbimgs[i] = this.chooser.viewers[i].rgbdata; + } + return true; + } var adual = newDiv(this.parentdiv.empty(), "asset_dual"); // contains grid and editor var agrid = newDiv(adual); var aeditor = newDiv(adual, "asset_editor").hide(); // contains editor, when selected @@ -936,7 +955,8 @@ export class Viewer { this.imagedata = pv.imagedata; this.rgbdata = pv.rgbdata; this.canvas = this.newCanvas(); - this.peerviewers = [this, pv]; + pv.peerviewers.push(this); + this.peerviewers = pv.peerviewers; } newCanvas(): HTMLCanvasElement { diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index dd826363..d3a8b0b0 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -372,12 +372,8 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { }); this.deferrednodes = []; } else { - // only refresh nodes if not actively editing - // since we could be in the middle of an operation that hasn't been committed for (var node of this.rootnodes) { - if (node !== this.getCurrentEditNode()) { - node.refreshRight(); - } + node.refreshRight(); } } } diff --git a/src/ide/windows.ts b/src/ide/windows.ts index 1b4003a9..5d95b6fb 100644 --- a/src/ide/windows.ts +++ b/src/ide/windows.ts @@ -138,7 +138,10 @@ export class ProjectWindows { var fileid = this.undofiles.pop(); var wnd = this.id2window[fileid]; if (wnd && wnd.undoStep) { + // undo source wnd.undoStep(); + // refresh active window from updated source + this.refresh(false); } else { bootbox.alert("No more steps to undo."); } From d1076105d02894851cee6c77dc5cf68f0a72099c Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 12:29:47 -0800 Subject: [PATCH 05/22] distinct undo for each asset editor edit --- src/ide/views/editors.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 38f06a9a..6a952585 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -1,5 +1,5 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; -import { defaultKeymap, history, historyKeymap, undo } from "@codemirror/commands"; +import { defaultKeymap, history, historyKeymap, indentWithTab, isolateHistory, undo } from "@codemirror/commands"; import { cpp } from "@codemirror/lang-cpp"; import { markdown } from "@codemirror/lang-markdown"; import { bracketMatching, foldGutter, indentOnInput, indentUnit } from "@codemirror/language"; @@ -296,7 +296,8 @@ export class SourceEditor implements ProjectView { var oldtext = this.editor.state.doc.toString(); if (oldtext != text) { this.editor.dispatch({ - changes: { from: 0, to: this.editor.state.doc.length, insert: text } + changes: { from: 0, to: this.editor.state.doc.length, insert: text }, + annotations: isolateHistory.of("full") }); } } From fdd6af49187f4c73df954dd218f943993b3f93f7 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 12:30:00 -0800 Subject: [PATCH 06/22] limit asset editor undo to current session --- src/ide/views/asseteditor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index d3a8b0b0..1fa1600b 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -381,6 +381,8 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { setVisible?(showing: boolean): void { // TODO: make into toolbar? if (showing) { + // limit undo to since opening this editor + projectWindows.undofiles = []; if (Mousetrap.bind) Mousetrap.bind('mod+z', projectWindows.undoStep.bind(projectWindows)); } else { if (Mousetrap.unbind) Mousetrap.unbind('mod+z'); From 14401f5dbdc84c6615d19225a7f563b41de4855a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 12:43:17 -0800 Subject: [PATCH 07/22] asset editor redo --- src/ide/views/asseteditor.ts | 13 ++++++++++--- src/ide/views/baseviews.ts | 1 + src/ide/views/editors.ts | 6 +++++- src/ide/windows.ts | 18 ++++++++++++++++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 1fa1600b..4978dfc5 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -381,11 +381,18 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { setVisible?(showing: boolean): void { // TODO: make into toolbar? if (showing) { - // limit undo to since opening this editor + // limit undo/redo to since opening this editor projectWindows.undofiles = []; - if (Mousetrap.bind) Mousetrap.bind('mod+z', projectWindows.undoStep.bind(projectWindows)); + projectWindows.redofiles = []; + if (Mousetrap.bind) { + Mousetrap.bind('mod+z', projectWindows.undoStep.bind(projectWindows)); + Mousetrap.bind('mod+shift+z', projectWindows.redoStep.bind(projectWindows)); + } } else { - if (Mousetrap.unbind) Mousetrap.unbind('mod+z'); + if (Mousetrap.unbind) { + Mousetrap.unbind('mod+z'); + Mousetrap.unbind('mod+shift+z'); + } } } diff --git a/src/ide/views/baseviews.ts b/src/ide/views/baseviews.ts index 4b87f647..0778b797 100644 --- a/src/ide/views/baseviews.ts +++ b/src/ide/views/baseviews.ts @@ -19,6 +19,7 @@ export interface ProjectView { setTimingResult?(result: CodeAnalyzer): void; recreateOnResize?: boolean; undoStep?(): void; + redoStep?(): void; }; // detect mobile (https://stackoverflow.com/questions/3514784/what-is-the-best-way-to-detect-a-mobile-device) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 6a952585..872bb212 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -1,5 +1,5 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; -import { defaultKeymap, history, historyKeymap, indentWithTab, isolateHistory, undo } from "@codemirror/commands"; +import { defaultKeymap, history, historyKeymap, indentWithTab, isolateHistory, redo, undo } from "@codemirror/commands"; import { cpp } from "@codemirror/lang-cpp"; import { markdown } from "@codemirror/lang-markdown"; import { bracketMatching, foldGutter, indentOnInput, indentUnit } from "@codemirror/language"; @@ -545,6 +545,10 @@ export class SourceEditor implements ProjectView { undo(this.editor); } + redoStep() { + redo(this.editor); + } + getBreakpointPCs(): number[] { if (this.sourcefile == null) return []; const pcs: number[] = []; diff --git a/src/ide/windows.ts b/src/ide/windows.ts index 5d95b6fb..7d9982d3 100644 --- a/src/ide/windows.ts +++ b/src/ide/windows.ts @@ -20,11 +20,13 @@ export class ProjectWindows { activediv: HTMLElement; lasterrors: WorkerError[]; undofiles: string[]; + redofiles: string[]; constructor(containerdiv: HTMLElement, project: CodeProject) { this.containerdiv = containerdiv; this.project = project; this.undofiles = []; + this.redofiles = []; } // TODO: delete windows ever? @@ -129,6 +131,7 @@ export class ProjectWindows { if (wnd && wnd.setText && typeof data === 'string') { wnd.setText(data); this.undofiles.push(fileid); + this.redofiles = []; } else { this.project.updateFile(fileid, data); } @@ -138,15 +141,26 @@ export class ProjectWindows { var fileid = this.undofiles.pop(); var wnd = this.id2window[fileid]; if (wnd && wnd.undoStep) { - // undo source wnd.undoStep(); - // refresh active window from updated source + this.redofiles.push(fileid); this.refresh(false); } else { bootbox.alert("No more steps to undo."); } } + redoStep() { + var fileid = this.redofiles.pop(); + var wnd = this.id2window[fileid]; + if (wnd && wnd.redoStep) { + wnd.redoStep(); + this.undofiles.push(fileid); + this.refresh(false); + } else { + bootbox.alert("No more steps to redo."); + } + } + updateAllOpenWindows(store) { for (var fileid in this.id2window) { var wnd = this.id2window[fileid]; From d37b305cbb3a51147c5ba5093134ef614547d626 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 12:48:34 -0800 Subject: [PATCH 08/22] prevent stacking alerts: no more undo/redo --- src/ide/windows.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ide/windows.ts b/src/ide/windows.ts index 7d9982d3..fd6c7f07 100644 --- a/src/ide/windows.ts +++ b/src/ide/windows.ts @@ -21,6 +21,7 @@ export class ProjectWindows { lasterrors: WorkerError[]; undofiles: string[]; redofiles: string[]; + alerting: boolean; constructor(containerdiv: HTMLElement, project: CodeProject) { this.containerdiv = containerdiv; @@ -145,7 +146,7 @@ export class ProjectWindows { this.redofiles.push(fileid); this.refresh(false); } else { - bootbox.alert("No more steps to undo."); + this.showAlert("No more steps to undo."); } } @@ -157,10 +158,16 @@ export class ProjectWindows { this.undofiles.push(fileid); this.refresh(false); } else { - bootbox.alert("No more steps to redo."); + this.showAlert("No more steps to redo."); } } + showAlert(msg: string) { + if (this.alerting) return; + this.alerting = true; + bootbox.alert(msg, () => { this.alerting = false; }); + } + updateAllOpenWindows(store) { for (var fileid in this.id2window) { var wnd = this.id2window[fileid]; From 9468a32cf311e06c00dd9878874cb8d9d4f09623 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 16:35:33 -0800 Subject: [PATCH 09/22] Add missing `;;` asset closing blocks --- presets/vcs/ecs/score.ecs | 1 + presets/vcs/examples/collisions.a | 2 ++ presets/vcs/examples/complexscene.a | 1 + presets/vcs/examples/complexscene2.a | 1 + presets/vcs/examples/multisprite3.a | 4 ++++ presets/vcs/examples/procgen1.a | 2 ++ presets/vcs/examples/retrigger.a | 2 ++ test/ecs/score.ecs | 1 + 8 files changed, 14 insertions(+) diff --git a/presets/vcs/ecs/score.ecs b/presets/vcs/ecs/score.ecs index 6797d3e5..8c11ce26 100644 --- a/presets/vcs/ecs/score.ecs +++ b/presets/vcs/ecs/score.ecs @@ -146,6 +146,7 @@ resource FontTable --- .byte $00,$06,$06,$7f,$66,$1e,$0e,$06,$00,$3c,$66,$06,$06,$7c,$60,$7e .byte $00,$3c,$66,$66,$7c,$60,$66,$3c,$00,$18,$18,$18,$18,$0c,$66,$7e .byte $00,$3c,$66,$66,$3c,$66,$66,$3c,$00,$3c,$66,$06,$3e,$66,$66,$3c +;; --- system Kernel2Digit diff --git a/presets/vcs/examples/collisions.a b/presets/vcs/examples/collisions.a index ca5bc39b..b08aa2c8 100644 --- a/presets/vcs/examples/collisions.a +++ b/presets/vcs/examples/collisions.a @@ -406,6 +406,7 @@ Frame0 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Bitmap data "throwing" position ;;{w:8,h:16,brev:1,flip:1};; Frame1 @@ -426,6 +427,7 @@ Frame1 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Color data for each line of sprite ;;{pal:"vcs"};; ColorFrame0 diff --git a/presets/vcs/examples/complexscene.a b/presets/vcs/examples/complexscene.a index ffb2fb7d..20cde1da 100644 --- a/presets/vcs/examples/complexscene.a +++ b/presets/vcs/examples/complexscene.a @@ -256,6 +256,7 @@ Frame0 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Color data for each line of sprite ;;{pal:"vcs"};; ColorFrame0 diff --git a/presets/vcs/examples/complexscene2.a b/presets/vcs/examples/complexscene2.a index a9630133..5d9b8ed6 100644 --- a/presets/vcs/examples/complexscene2.a +++ b/presets/vcs/examples/complexscene2.a @@ -271,6 +271,7 @@ Frame0 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Color data for each line of sprite ;;{pal:"vcs"};; ColorFrame0 diff --git a/presets/vcs/examples/multisprite3.a b/presets/vcs/examples/multisprite3.a index 1e084d03..d98b1f04 100644 --- a/presets/vcs/examples/multisprite3.a +++ b/presets/vcs/examples/multisprite3.a @@ -483,6 +483,7 @@ Frame0 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Bitmap data "throwing" position ;;{w:8,h:16,brev:1,flip:1};; Frame1 @@ -503,6 +504,7 @@ Frame1 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Color data for each line of sprite ;;{pal:"vcs"};; ColorFrame0 @@ -523,6 +525,7 @@ ColorFrame0 .byte #$18; .byte #$F2; .byte #$F4; +;; ;; Enemy cat-head graphics data ;;{w:8,h:8,brev:1,flip:1};; EnemyFrame0 @@ -535,6 +538,7 @@ EnemyFrame0 .byte #%01111110;$8E .byte #%11000011;$98 .byte #%10000001;$98 +;; ;; Enemy cat-head color data ;;{pal:"vcs"};; EnemyColorFrame0 diff --git a/presets/vcs/examples/procgen1.a b/presets/vcs/examples/procgen1.a index 64b6f949..da4834b0 100644 --- a/presets/vcs/examples/procgen1.a +++ b/presets/vcs/examples/procgen1.a @@ -480,6 +480,7 @@ Frame0 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 +;; ;; Color data for each line of sprite ;;{pal:"vcs"};; ColorFrame0 @@ -500,6 +501,7 @@ ColorFrame0 .byte #$18; .byte #$F2; .byte #$F4; +;; ;; Color data for each line of sprite ;;{pal:"vcs"};; ColorFrame1 diff --git a/presets/vcs/examples/retrigger.a b/presets/vcs/examples/retrigger.a index 5886767c..03ab229b 100644 --- a/presets/vcs/examples/retrigger.a +++ b/presets/vcs/examples/retrigger.a @@ -365,6 +365,7 @@ EnemyFrame0 .byte #%01111110;$8E .byte #%11000011;$98 .byte #%10000001;$98 +;; ;;{pal:"vcs"};; EnemyColorFrame0 .byte #8 ; height @@ -376,6 +377,7 @@ EnemyColorFrame0 .byte #$8E; .byte #$98; .byte #$94; +;; ;; Player graphics data, such bitmap ;;{w:8,h:10,brev:1,flip:1};; Frame0 diff --git a/test/ecs/score.ecs b/test/ecs/score.ecs index 6797d3e5..8c11ce26 100644 --- a/test/ecs/score.ecs +++ b/test/ecs/score.ecs @@ -146,6 +146,7 @@ resource FontTable --- .byte $00,$06,$06,$7f,$66,$1e,$0e,$06,$00,$3c,$66,$06,$06,$7c,$60,$7e .byte $00,$3c,$66,$66,$7c,$60,$66,$3c,$00,$18,$18,$18,$18,$0c,$66,$7e .byte $00,$3c,$66,$66,$3c,$66,$66,$3c,$00,$3c,$66,$06,$3e,$66,$66,$3c +;; --- system Kernel2Digit From 3a997d49c8bf5dbb463fff5e45deec3a79298d8f Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 18:42:56 -0800 Subject: [PATCH 10/22] display asset line numbers, headers, and errors - For each detected asset, display its line number and header snippet for reference. - When asset data cannot be found or the number of values detected is incorrect, and inline error message is displayed instead of a (broken) asset editor. --- css/ui.css | 26 ++++++++++++++++++++++ src/ide/pixeleditor.ts | 24 ++++++++++++++++++++ src/ide/views/asseteditor.ts | 43 +++++++++++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/css/ui.css b/css/ui.css index 1a2ad748..bcf2a6b3 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++; } From 8ca6cddcd58cffc06399d0b740701422e9152036 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 19:21:42 -0800 Subject: [PATCH 11/22] fix asset editor replace with different length text --- src/ide/pixeleditor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 72cbd9ff..01d74f0a 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -490,6 +490,7 @@ export class TextDataNode extends CodeProjectDataNode { var datastr = this.text.substring(this.start, this.end); datastr = replaceHexWords(datastr, this.words); this.text = this.text.substring(0, this.start) + datastr + this.text.substring(this.end); + this.end = this.start + datastr.length; if (this.project) { this.project.updateFile(this.fileid, this.text); //this.project.replaceTextRange(this.fileid, this.start, this.end, datastr); From 323c83e45ec5f623deb94cf5760ee2f32cc172f9 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 19:28:46 -0800 Subject: [PATCH 12/22] stable asset thumbnail position align-items: flex-start --- css/ui.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/ui.css b/css/ui.css index bcf2a6b3..48fa764c 100644 --- a/css/ui.css +++ b/css/ui.css @@ -545,7 +545,7 @@ td.asset_editable { } div.asset_dual { display: flex; - align-items: center; + align-items: flex-start; } div.asset_dual table { border-spacing: 10px; From 2c77a4225e9da65d227961b168921d215a859fec Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 20:34:07 -0800 Subject: [PATCH 13/22] asset editor pads rewritten bytes Use bpw or verilog spec to pad correct number of chars. --- src/ide/pixeleditor.ts | 29 +++++++++++++++-------------- src/ide/views/asseteditor.ts | 2 +- test/cli/testpixelconvert.js | 6 +++--- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 01d74f0a..e5097a02 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -1,5 +1,5 @@ -import { hex, rgb2bgr, rle_unpack } from "../common/util"; +import { hex, tobin, rgb2bgr, rle_unpack } from "../common/util"; import { ProjectWindows } from "./windows"; import { Toolbar } from "./toolbar"; import Mousetrap = require('mousetrap'); @@ -83,9 +83,9 @@ export function parseHexWords(s: string): number[] { var m; while (m = pixel_re.exec(s)) { var n; - if (typeof m[4] !== 'undefined') + if (m[4]) n = parseInt(m[5], 2); - else if (m[2].startsWith('%') || m[2].endsWith("b")) + else if (m[2].startsWith('%')) n = parseInt(m[3], 2); else if (m[2].startsWith('x') || m[2].startsWith('$') || m[2].endsWith('h')) n = parseInt(m[3], 16); @@ -96,26 +96,25 @@ export function parseHexWords(s: string): number[] { return arr; } -export function replaceHexWords(s: string, words: UintArray): string { +export function replaceHexWords(s: string, words: UintArray, bpw: number): string { var result = ""; var m; var li = 0; var i = 0; + var nibbles = Math.ceil(bpw / 4); while (m = pixel_re.exec(s)) { result += s.slice(li, pixel_re.lastIndex - m[0].length); li = pixel_re.lastIndex; - if (typeof m[4] !== 'undefined') - result += m[4] + words[i++].toString(2); + if (m[4]) + result += m[4] + tobin(words[i++], parseInt(m[4])); else if (m[2].startsWith('%')) - result += m[1] + "%" + words[i++].toString(2); - else if (m[2].endsWith('b')) - result += m[1] + m[2] + words[i++].toString(2); // TODO + result += m[1] + m[2] + tobin(words[i++], bpw); else if (m[2].endsWith('h')) - result += m[1] + m[2] + words[i++].toString(16); // TODO + result += m[1] + m[2] + hex(words[i++], parseInt(m[2])); else if (m[2].startsWith('x')) - result += m[1] + "x" + hex(words[i++]); + result += m[1] + "x" + hex(words[i++], nibbles); else if (m[2].startsWith('$')) - result += m[1] + "$" + hex(words[i++]); + result += m[1] + "$" + hex(words[i++], nibbles); else result += m[1] + words[i++].toString(); } @@ -472,15 +471,17 @@ export class TextDataNode extends CodeProjectDataNode { text: string; start: number; end: number; + bpw: number; // TODO: what if file size/layout changes? - constructor(project: ProjectWindows, fileid: string, label: string, start: number, end: number) { + constructor(project: ProjectWindows, fileid: string, label: string, start: number, end: number, bpw?: number) { super(); this.project = project; this.fileid = fileid; this.label = label; this.start = start; this.end = end; + this.bpw = bpw || 8; } updateLeft() { if (this.right.words.length != this.words.length) @@ -488,7 +489,7 @@ export class TextDataNode extends CodeProjectDataNode { this.words = this.right.words; // TODO: reload editors? var datastr = this.text.substring(this.start, this.end); - datastr = replaceHexWords(datastr, this.words); + datastr = replaceHexWords(datastr, this.words, this.bpw); this.text = this.text.substring(0, this.start) + datastr + this.text.substring(this.end); this.end = this.start + datastr.length; if (this.project) { diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 38bf6107..e951d825 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -337,7 +337,7 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { } let label = fileid; // TODO: label - let node: pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end); + let node: pixed.PixNode = new pixed.TextDataNode(projectWindows, fileid, label, frag.start, frag.end, frag.fmt.bpw); let first = node; // rle-compressed? TODO: how to edit? if (frag.fmt.comp == 'rletag') { diff --git a/test/cli/testpixelconvert.js b/test/cli/testpixelconvert.js index f104c5d4..c7460bd5 100644 --- a/test/cli/testpixelconvert.js +++ b/test/cli/testpixelconvert.js @@ -62,7 +62,7 @@ describe('Pixel editor', function() { dumbEqual(node2.images[0], [0,0,0,0,14,15,14,15,14,0,0,0,0,0,0,0,14,14,14,14,15,14,14,14,14,0,0,0,0,14,14,13,14,15,14,15,14,13,14,14,0,0,0,14,14,14,13,13,13,13,13,14,14,14,0,0,0,14,14,14,14,13,13,14,14,14,14,14,0,0,0,0,14,14,14,14,13,14,14,14,14,0,0,0,0,0,14,14,14,14,13,14,14,14,14,0,0,0,0,0,0,0,14,13,13,13,14,0,0,0,0,13,13,13,13,13,14,14,14,14,14,13,13,13,13,0,0,13,14,14,14,14,14,14,14,14,14,14,0,0,0,14,14,0,14,14,14,14,14,0,14,14,0,0,0,14,14,0,14,14,14,14,14,0,14,14,0,0,0,14,14,0,13,13,13,13,13,0,13,14,0,0,0,13,0,0,14,14,0,14,14,0,0,13,0,0,0,0,0,0,14,13,0,14,14,0,0,0,0,0,0,0,0,13,13,13,0,13,13,13,0,0,1,8]); assert.equal(" 0x00, 0x03, 0x19, 0x50, 0x52, 0x07, 0x1F, 0x37, 0xE0, 0xA4, 0xFD, 0xFF, 0x38, 0x70, 0x7F, 0x7F, ", - pixed.replaceHexWords(paldatastr, pixed.parseHexWords(paldatastr))); + pixed.replaceHexWords(paldatastr, pixed.parseHexWords(paldatastr), 8)); node3.refreshLeft(); dumbEqual(node2.images[0], [0,0,0,0,14,15,14,15,14,0,0,0,0,0,0,0,14,14,14,14,15,14,14,14,14,0,0,0,0,14,14,13,14,15,14,15,14,13,14,14,0,0,0,14,14,14,13,13,13,13,13,14,14,14,0,0,0,14,14,14,14,13,13,14,14,14,14,14,0,0,0,0,14,14,14,14,13,14,14,14,14,0,0,0,0,0,14,14,14,14,13,14,14,14,14,0,0,0,0,0,0,0,14,13,13,13,14,0,0,0,0,13,13,13,13,13,14,14,14,14,14,13,13,13,13,0,0,13,14,14,14,14,14,14,14,14,14,14,0,0,0,14,14,0,14,14,14,14,14,0,14,14,0,0,0,14,14,0,14,14,14,14,14,0,14,14,0,0,0,14,14,0,13,13,13,13,13,0,13,14,0,0,0,13,0,0,14,14,0,14,14,0,0,13,0,0,0,0,0,0,14,13,0,14,14,0,0,0,0,0,0,0,0,13,13,13,0,13,13,13,0,0,1,8]); @@ -74,13 +74,13 @@ describe('Pixel editor', function() { var datastr3 = " 7'o00: bits = 5'b11111; "; var words3 = pixed.parseHexWords(datastr3); dumbEqual(words3, [31]); - assert.equal(datastr3, pixed.replaceHexWords(datastr3, pixed.parseHexWords(datastr3))); + assert.equal(datastr3, pixed.replaceHexWords(datastr3, pixed.parseHexWords(datastr3), 8)); // TODO: test (nplanes > 0 && fmt.sl) // test comments var datastr4 = " .byte #%01111110;$7E \n .byte #%01111111;$7F"; var words4 = pixed.parseHexWords(datastr4); dumbEqual(words4, [0x7E,0x7F]); - assert.notEqual(datastr4, pixed.replaceHexWords(datastr4, pixed.parseHexWords(datastr4))); // removed comment + assert.notEqual(datastr4, pixed.replaceHexWords(datastr4, pixed.parseHexWords(datastr4), 8)); // removed comment }); }); From e7ef6ea8fabc2c9795e3c49ef5e50b81380fdd3b Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 20:49:58 -0800 Subject: [PATCH 14/22] end of asset `;;` must not be `;;{` Do not treat a new asset block as the end of a previous block --- src/ide/views/asseteditor.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index e951d825..ebf60075 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -140,8 +140,8 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { // scan file for assets // /*{json}*/ or ;;{json};; // TODO: put before ident, look for = { - var result = []; var re1 = /[/;][*;]([{].+[}])[*;][/;]/g; + var result = []; var m; while (m = re1.exec(data)) { var start = m.index + m[0].length; @@ -151,6 +151,10 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { end = data.indexOf("end", start); // asm } else if (m[0].startsWith(';;')) { end = data.indexOf(';;', start); // asm + if (end == data.indexOf(';;{', start)) { + // ignore start of next asset + end = -1; + } } else { end = data.indexOf(';', start); // C } From bd83d7407099627a8bf52aa394da0a3cdbb38b2e Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 21:03:04 -0800 Subject: [PATCH 15/22] assets show start/end line numbers --- css/ui.css | 7 ++++--- src/ide/views/asseteditor.ts | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/css/ui.css b/css/ui.css index 48fa764c..22a4e517 100644 --- a/css/ui.css +++ b/css/ui.css @@ -506,12 +506,13 @@ div.asset_block { border-radius: 8px; padding-left: 1em; } +.asset_linenos { + display: inline-block; + margin-right: 8px; +} .asset_lineno { color: #99cc99; - display: inline-block; - min-width: 4rem; text-align: right; - margin-right: 8px; } .asset_error_msg { color: #ff6666; diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index ebf60075..5a4fc7a4 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -161,19 +161,20 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { //console.log(id, start, end, m[1], data.substring(start,end)); var line = getLineNumber(data, m.index); var header = m[0]; + var endline = end >= 0 ? getLineNumber(data, end) : '???'; 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` }); + result.push({ fileid: id, header: header, line: line, endline: endline, 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` }); + result.push({ fileid: id, header: header, line: line, endline: endline, 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, header: header, line: line, fmt: json, start: start, end: end }); + result.push({ fileid: id, header: header, line: line, endline: endline, fmt: json, start: start, end: end }); } catch (e) { - result.push({ fileid: id, header: header, line: line, error: `Invalid asset format: ${e.message}` }); + result.push({ fileid: id, header: header, line: line, endline: endline, error: `Invalid asset format: ${e.message}` }); } } } @@ -326,7 +327,10 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { for (let frag of textfrags) { const block = $('
').appendTo(this.ensureFileDiv(fileid)); var snip = $('
').appendTo(block); - $('').text(frag.line).appendTo(snip); + var linenos = $('').appendTo(snip); + $('').text(frag.line).appendTo(linenos); + linenos.append('-'); + $('').text(frag.endline).appendTo(linenos); snip.append(' ' + frag.header); if (frag.error) { $('
').text(frag.error).appendTo(block); From 782a3c19afb4241a023ed29c5ca5c665ac90ce4b Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 21:03:17 -0800 Subject: [PATCH 16/22] rename line to startline --- src/ide/views/asseteditor.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 5a4fc7a4..1b98db47 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -159,22 +159,22 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { end = data.indexOf(';', start); // C } //console.log(id, start, end, m[1], data.substring(start,end)); - var line = getLineNumber(data, m.index); + var startline = getLineNumber(data, m.index); var header = m[0]; var endline = end >= 0 ? getLineNumber(data, end) : '???'; if (end < 0) { var closingDelim = platform_id.includes('verilog') ? '"end"' : m[0].startsWith(';;') ? '";;"' : '";"'; - result.push({ fileid: id, header: header, line: line, endline: endline, error: `No closing ${closingDelim} found after asset header` }); + result.push({ fileid: id, header: header, startline: startline, endline: endline, error: `No closing ${closingDelim} found after asset header` }); } else if (end <= start) { - result.push({ fileid: id, header: header, line: line, endline: endline, error: `Empty data block after asset header` }); + result.push({ fileid: id, header: header, startline: startline, endline: endline, 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, header: header, line: line, endline: endline, fmt: json, start: start, end: end }); + result.push({ fileid: id, header: header, startline: startline, endline: endline, fmt: json, start: start, end: end }); } catch (e) { - result.push({ fileid: id, header: header, line: line, endline: endline, error: `Invalid asset format: ${e.message}` }); + result.push({ fileid: id, header: header, startline: startline, endline: endline, error: `Invalid asset format: ${e.message}` }); } } } @@ -328,7 +328,7 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { const block = $('
').appendTo(this.ensureFileDiv(fileid)); var snip = $('
').appendTo(block); var linenos = $('').appendTo(snip); - $('').text(frag.line).appendTo(linenos); + $('').text(frag.startline).appendTo(linenos); linenos.append('-'); $('').text(frag.endline).appendTo(linenos); snip.append(' ' + frag.header); From 3ff783852ace60b3b5a31d53e55a92d5dfb9e480 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 21:06:46 -0800 Subject: [PATCH 17/22] clicking asset line range highlights source lines --- css/ui.css | 1 + src/ide/views/asseteditor.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/css/ui.css b/css/ui.css index 22a4e517..4dffc52f 100644 --- a/css/ui.css +++ b/css/ui.css @@ -509,6 +509,7 @@ div.asset_block { .asset_linenos { display: inline-block; margin-right: 8px; + cursor: pointer; } .asset_lineno { color: #99cc99; diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 1b98db47..238a67fc 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -331,6 +331,12 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { $('').text(frag.startline).appendTo(linenos); linenos.append('-'); $('').text(frag.endline).appendTo(linenos); + linenos.click(() => { + var editor = projectWindows.createOrShow(frag.fileid, true); + if (editor && (editor as any).highlightLines) { + (editor as any).highlightLines(frag.startline - 1, frag.endline - 1); + } + }); snip.append(' ' + frag.header); if (frag.error) { $('
').text(frag.error).appendTo(block); From d1e21186e1533c6a804c4e4aec03b5b21b724a10 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 21:10:57 -0800 Subject: [PATCH 18/22] dismiss highlighted lines on user interaction --- src/ide/views/visuals.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ide/views/visuals.ts b/src/ide/views/visuals.ts index f43d168d..58bc8c4a 100644 --- a/src/ide/views/visuals.ts +++ b/src/ide/views/visuals.ts @@ -228,6 +228,10 @@ const highlightLinesField = StateField.define({ return Decoration.set(decorationRanges); } } + // dismiss highlights on user interaction + if (decorations !== Decoration.none && (tr.docChanged || tr.selection)) { + return Decoration.none; + } return decorations; }, provide: f => EditorView.decorations.from(f), From d84e943b9d1578ec864372690390093631345481 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 21:11:26 -0800 Subject: [PATCH 19/22] highlight invalid asset's header line only --- src/ide/views/asseteditor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 238a67fc..768c7f06 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -334,7 +334,7 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { linenos.click(() => { var editor = projectWindows.createOrShow(frag.fileid, true); if (editor && (editor as any).highlightLines) { - (editor as any).highlightLines(frag.startline - 1, frag.endline - 1); + (editor as any).highlightLines(frag.startline - 1, (frag.endline > 0 ? frag.endline : frag.startline) - 1); } }); snip.append(' ' + frag.header); From 75b80f68c1287ff40f5d9c03c2b1fac45012a05d Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 1 Mar 2026 21:43:07 -0800 Subject: [PATCH 20/22] Improve asset editor source undo Asset editor replaces text ranges instead of the entire source file. Undo inside the source code now scrolls asset editor edits into view, and selects the text being edited, so that what is being (un)edited is much clearer. Also, undo history is no longer burdened by full file edits. --- src/ide/pixeleditor.ts | 7 +++---- src/ide/views/baseviews.ts | 1 + src/ide/views/editors.ts | 13 +++++++++++++ src/ide/windows.ts | 7 +++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index e5097a02..fa102d0a 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -490,12 +490,11 @@ export class TextDataNode extends CodeProjectDataNode { // TODO: reload editors? var datastr = this.text.substring(this.start, this.end); datastr = replaceHexWords(datastr, this.words, this.bpw); - this.text = this.text.substring(0, this.start) + datastr + this.text.substring(this.end); - this.end = this.start + datastr.length; if (this.project) { - this.project.updateFile(this.fileid, this.text); - //this.project.replaceTextRange(this.fileid, this.start, this.end, datastr); + this.project.replaceTextRange(this.fileid, this.start, this.end, datastr); } + this.text = this.text.substring(0, this.start) + datastr + this.text.substring(this.end); + this.end = this.start + datastr.length; return true; } updateRight() { diff --git a/src/ide/views/baseviews.ts b/src/ide/views/baseviews.ts index 0778b797..76e785f1 100644 --- a/src/ide/views/baseviews.ts +++ b/src/ide/views/baseviews.ts @@ -20,6 +20,7 @@ export interface ProjectView { recreateOnResize?: boolean; undoStep?(): void; redoStep?(): void; + replaceTextRange?(from: number, to: number, text: string): void; }; // detect mobile (https://stackoverflow.com/questions/3514784/what-is-the-best-way-to-detect-a-mobile-device) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 872bb212..0722fa6a 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -302,6 +302,19 @@ export class SourceEditor implements ProjectView { } } + replaceTextRange(from: number, to: number, text: string) { + const fromline = this.editor.state.doc.lineAt(from).number; + const toline = this.editor.state.doc.lineAt(to).number; + this.editor.dispatch({ + changes: { from, to, insert: text }, + annotations: isolateHistory.of("full"), + selection: { anchor: from, head: to }, + effects: [ + EditorView.scrollIntoView(this.editor.state.doc.line(fromline).from, { y: "start", yMargin: 100/*pixels*/ }), + ] + }); + } + insertText(text: string) { const main = this.editor.state.selection.main; this.editor.dispatch({ diff --git a/src/ide/windows.ts b/src/ide/windows.ts index fd6c7f07..00629355 100644 --- a/src/ide/windows.ts +++ b/src/ide/windows.ts @@ -138,6 +138,13 @@ export class ProjectWindows { } } + replaceTextRange(fileid: string, from: number, to: number, text: string) { + var wnd = this.id2window[fileid]; + wnd.replaceTextRange(from, to, text); + this.undofiles.push(fileid); + this.redofiles = []; + } + undoStep() { var fileid = this.undofiles.pop(); var wnd = this.id2window[fileid]; From aef263e10e643ccb839efb9166834d1c9e476411 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Mon, 2 Mar 2026 10:31:58 -0800 Subject: [PATCH 21/22] Add affordance for asset line numbers --- css/ui.css | 6 ++++++ src/ide/views/asseteditor.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/css/ui.css b/css/ui.css index 4dffc52f..b892ba77 100644 --- a/css/ui.css +++ b/css/ui.css @@ -510,6 +510,12 @@ div.asset_block { display: inline-block; margin-right: 8px; cursor: pointer; + padding: 1px 4px; + border-radius: 4px; +} +.asset_linenos:hover { + text-decoration: underline; + background: rgba(255,255,255,0.1); } .asset_lineno { color: #99cc99; diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 768c7f06..60bcf7f2 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -331,6 +331,7 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { $('').text(frag.startline).appendTo(linenos); linenos.append('-'); $('').text(frag.endline).appendTo(linenos); + linenos.attr('title', 'Jump to source'); linenos.click(() => { var editor = projectWindows.createOrShow(frag.fileid, true); if (editor && (editor as any).highlightLines) { From 7635b5dee830102f6e0d17e4e439d7447d63e8e3 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Mon, 2 Mar 2026 15:31:35 -0800 Subject: [PATCH 22/22] clickable asset editor decorations --- css/ui.css | 15 ++++++++ src/ide/views/assetdecorations.ts | 64 +++++++++++++++++++++++++++++++ src/ide/views/editors.ts | 7 +++- 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/ide/views/assetdecorations.ts diff --git a/css/ui.css b/css/ui.css index b892ba77..2e64721b 100644 --- a/css/ui.css +++ b/css/ui.css @@ -521,6 +521,21 @@ div.asset_block { color: #99cc99; text-align: right; } +.asset-header-badge { + display: inline-block; + padding: 1px 6px; + border-radius: 4px; + background: rgba(153, 204, 153, 0.15); + color: #99cc99; + font-size: 0.85em; + cursor: pointer; + opacity: 0.6; +} +.asset-header-badge:hover { + opacity: 1; + background: rgba(153, 204, 153, 0.3); + text-decoration: underline; +} .asset_error_msg { color: #ff6666; background-color: #330000; diff --git a/src/ide/views/assetdecorations.ts b/src/ide/views/assetdecorations.ts new file mode 100644 index 00000000..52b09111 --- /dev/null +++ b/src/ide/views/assetdecorations.ts @@ -0,0 +1,64 @@ +import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view"; + +// Asset header detection — shows a clickable badge on lines with ;;{json};; or /*{json}*/ +class AssetHeaderWidget extends WidgetType { + constructor(readonly header: string, readonly onClick: () => void) { super() } + + toDOM() { + const span = document.createElement("span"); + span.className = "asset-header-badge"; + span.textContent = "↗ Asset Editor"; + span.title = "Open in Asset Editor"; + span.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.onClick(); + }); + return span; + } + + eq(other: AssetHeaderWidget) { return this.header === other.header; } + ignoreEvent() { return false; } +} + +const assetHeaderRegex = /[/;][*;](\{.+?\})[*;][/;]/g; + +function buildAssetHeaderDecorations(view: EditorView, onClick: () => void): DecorationSet { + const widgets: any[] = []; + for (let { from, to } of view.visibleRanges) { + const text = view.state.sliceDoc(from, to); + let lineStart = from; + const lines = text.split('\n'); + for (const line of lines) { + assetHeaderRegex.lastIndex = 0; + const m = assetHeaderRegex.exec(line); + if (m) { + const lineEnd = lineStart + line.length; + widgets.push( + Decoration.widget({ + widget: new AssetHeaderWidget(m[0], onClick), + side: 1, + }).range(lineEnd) + ); + } + lineStart += line.length + 1; + } + } + return Decoration.set(widgets, true); +} + +export function createAssetHeaderPlugin(onClick: () => void) { + return ViewPlugin.fromClass(class { + decorations: DecorationSet; + constructor(view: EditorView) { + this.decorations = buildAssetHeaderDecorations(view, onClick); + } + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildAssetHeaderDecorations(update.view, onClick); + } + } + }, { + decorations: v => v.decorations, + }); +} diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 0722fa6a..15e8d021 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -21,11 +21,12 @@ import { cobalt } from "../../themes/cobalt"; import { disassemblyTheme } from "../../themes/disassemblyTheme"; import { editorTheme } from "../../themes/editorTheme"; import { mbo } from "../../themes/mbo"; -import { clearBreakpoint, current_project, lastDebugState, platform, qs, runToPC } from "../ui"; +import { clearBreakpoint, current_project, lastDebugState, platform, projectWindows, qs, runToPC } from "../ui"; import { isMobileDevice, ProjectView } from "./baseviews"; import { debugHighlightTagsTooltip } from "./debug"; import { createTextTransformFilterEffect, textTransformFilterCompartment } from "./filters"; import { breakpointMarkers, bytes, clock, currentPcMarker, errorMarkers, offset, statusMarkers } from "./gutter"; +import { createAssetHeaderPlugin } from "./assetdecorations"; import { tabKeymap } from "./tabs"; import { currentPc, errorMessages, errorSpans, highlightLines, showValue } from "./visuals"; @@ -244,6 +245,10 @@ export class SourceEditor implements ProjectView { highlightLines.field, + createAssetHeaderPlugin(() => { + projectWindows.createOrShow('#asseteditor'); + }), + textTransformFilterCompartment.of([]), // update file in project (and recompile) when edits made