From 22c2fb3c2f7b105495077da5a37c995861ba40e0 Mon Sep 17 00:00:00 2001
From: Steven Hugg
Date: Mon, 18 Mar 2019 14:39:02 -0400
Subject: [PATCH] started on asset editor
---
css/ui.css | 23 ++++++
demo.html | 8 ++-
doc/notes.txt | 2 +
index.html | 1 +
src/pixed/pixeleditor.ts | 152 ++++++++++++++++++++++++++++++++++++++-
src/ui.ts | 10 ++-
src/util.ts | 3 +
src/views.ts | 136 ++++++++++++++++++++++++++++++++++-
src/worker/workermain.ts | 4 +-
9 files changed, 327 insertions(+), 12 deletions(-)
diff --git a/css/ui.css b/css/ui.css
index e4231107..f09e2ad8 100644
--- a/css/ui.css
+++ b/css/ui.css
@@ -449,3 +449,26 @@ span.profiler-local {
span.profiler-cident {
color: #99ffff;
}
+div.asset_file {
+ border: 2px solid black;
+ border-radius:8px;
+ color: #ddd;
+ background-color:#666;
+ margin:0.5em;
+ padding:1em;
+}
+div.asset_file_header {
+ font-weight: bold;
+ color: white;
+}
+div.asset_grid {
+ line-height:0;
+ margin: 1em;
+}
+div.asset_grid span {
+ white-space: nowrap;
+}
+div.asset_grid canvas {
+ padding: 0px;
+ border: 1px solid black;
+}
diff --git a/demo.html b/demo.html
index 3546787a..a4a3a3c2 100644
--- a/demo.html
+++ b/demo.html
@@ -150,6 +150,10 @@ We'll delve into these circuits as they morph from Pong into programmable person
At the end of this adventure, you should be well-equipped to begin exploring the world of FPGAs, and maybe even design your own game console. You'll use the 8bitworkshop.com IDE to write Verilog programs that represent digital circuits, and see your code run instantly in the browser.
+
+
+
+
@@ -236,14 +240,16 @@ At the end of this adventure, you should be well-equipped to begin exploring the
You can even write C or 6502 assembler code for Woz's creation, the
Apple ][+ .
Thrill to the unusual frame buffer layout and one-bit speaker output!
+ You can even export to cassette tape!
+
-
+ Become a Patron!
diff --git a/doc/notes.txt b/doc/notes.txt
index 9e93b907..a8e7f96a 100644
--- a/doc/notes.txt
+++ b/doc/notes.txt
@@ -93,6 +93,7 @@ TODO:
- profiler restarts when paused
- it's pretty easy to add a new file named like a library file (bcd.c)
- put globals into view/controller objects
+- open asset editor twice first time?
WEB WORKER FORMAT
@@ -216,4 +217,5 @@ Assets come from:
- chr files
- bitmap data in files
- structured data (palette, sprites, metasprites, levels, etc)
+- think about new comment format, platform-specific types
diff --git a/index.html b/index.html
index da02b346..07e11a22 100644
--- a/index.html
+++ b/index.html
@@ -387,6 +387,7 @@ function require(modname) {
+
diff --git a/src/pixed/pixeleditor.ts b/src/pixed/pixeleditor.ts
index 8aeb393a..e42b2efb 100644
--- a/src/pixed/pixeleditor.ts
+++ b/src/pixed/pixeleditor.ts
@@ -1,6 +1,7 @@
"use strict";
import { hex } from "../util";
+import { current_project } from "../ui";
type PixelEditorImageFormat = {
w:number
@@ -14,7 +15,7 @@ type PixelEditorImageFormat = {
remap?:number[]
brev?:boolean
destfmt?:PixelEditorImageFormat
- xform?
+ xform?:string
};
type PixelEditorPaletteFormat = {
@@ -323,7 +324,7 @@ function remapBits(x:number, arr:number[]) : number {
return y;
}
-function convertWordsToImages(words:number[], fmt:PixelEditorImageFormat) : Uint8Array[] {
+function convertWordsToImages(words:number[] | Uint8Array, fmt:PixelEditorImageFormat) : Uint8Array[] {
var width = fmt.w;
var height = fmt.h;
var count = fmt.count || 1;
@@ -562,7 +563,7 @@ function pixelEditorKeypress(e) {
case 104:
currentPixelEditor.flipx();
break;
- case 118:
+
currentPixelEditor.flipy();
break;
default:
@@ -584,3 +585,148 @@ var PREDEF_PALETTES = {
,0xFFF8D878 ,0xFFD8F878 ,0xFFB8F8B8 ,0xFFB8F8D8 ,0xFF00FCFC ,0xFFF8D8F8 ,0xFF000000 ,0xFF000000
]
};
+
+/////
+
+export abstract class PixelNode {
+ left : PixelNode; // toward text editor
+ right : PixelNode; // toward pixel editor
+ input?
+ output?
+
+ abstract updateLeft(); // update coming from right
+ abstract updateRight(); // update coming from left
+
+ refreshLeft() {
+ var p : PixelNode = this;
+ while (p) {
+ p.updateLeft();
+ p = p.left;
+ }
+ }
+ refreshRight() {
+ var p : PixelNode = this;
+ while (p) {
+ p.updateRight();
+ p = p.right;
+ }
+ }
+ addRight(node : PixelNode) {
+ this.right = node;
+ node.left = this;
+ }
+}
+
+export class PixelFileDataNode extends PixelNode {
+ fileid : string;
+ output : Uint8Array;
+
+ constructor(fileid, data) {
+ super();
+ this.fileid = fileid;
+ this.output = data;
+ }
+ updateLeft() {
+ current_project.updateFile(this.fileid, this.output);
+ }
+ updateRight() {
+ }
+}
+
+export class PixelTextDataNode extends PixelNode {
+ fileid : string;
+ text : string;
+ start : number;
+ end : number;
+ output : Uint8Array;
+
+ constructor(fileid, text, start, end) {
+ super();
+ this.fileid = fileid;
+ this.text = text;
+ this.start = start;
+ this.end = end;
+ }
+ updateLeft() {
+ // TODO: reload editors?
+ current_project.updateFile(this.fileid, this.text);
+ }
+ updateRight() {
+ var datastr = this.text.substring(this.start, this.end);
+ var words = parseHexWords(datastr);
+ this.output = new Uint8Array(words); // TODO: 16/32?
+ }
+}
+
+export class PixelMapper extends PixelNode {
+
+ fmt : PixelEditorImageFormat;
+ input : number[] | Uint8Array;
+ output : Uint8Array[];
+
+ updateLeft() {
+ //TODO
+ this.input = convertImagesToWords(this.output, this.fmt);
+ }
+ updateRight() {
+ // convert each word array to images
+ this.input = this.left.output;
+ this.output = convertWordsToImages(this.input, this.fmt);
+ }
+
+}
+
+export class PixelPalettizer extends PixelNode {
+
+ input : Uint8Array[];
+ output : Uint32Array[];
+ palette : Uint32Array;
+
+ updateLeft() {
+ //TODO
+ }
+ updateRight() {
+ var mask = this.palette.length - 1; // must be power of 2
+ this.input = this.left.output;
+ // for each image, map bytes to RGB colors
+ this.output = this.input.map( (im:Uint8Array) => {
+ var out = new Uint32Array(im.length);
+ for (var i=0; i {
var zip = new JSZip();
- current_project.iterateFiles(function(id, text) {
- if (text)
- zip.file(getFilenameForPath(id), text);
+ current_project.iterateFiles( (id, data) => {
+ if (data) {
+ zip.file(getFilenameForPath(id), data);
+ }
});
zip.generateAsync({type:"blob"}).then( (content) => {
saveAs(content, getCurrentMainFilename() + ".zip");
diff --git a/src/util.ts b/src/util.ts
index cc7dd1b7..972a4c86 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -420,3 +420,6 @@ export function clamp(minv:number, maxv:number, v:number) {
return (v < minv) ? minv : (v > maxv) ? maxv : v;
}
+export function safeident(s : string) : string {
+ return s.replace(/\W+/g, "_");
+}
diff --git a/src/views.ts b/src/views.ts
index f27b7fe4..8afcf3b8 100644
--- a/src/views.ts
+++ b/src/views.ts
@@ -3,11 +3,12 @@
import $ = require("jquery");
//import CodeMirror = require("codemirror");
import { CodeProject } from "./project";
-import { SourceFile, WorkerError, Segment } from "./workertypes";
+import { SourceFile, WorkerError, Segment, FileData } from "./workertypes";
import { Platform, EmuState, ProfilerOutput, lookupSymbol } from "./baseplatform";
-import { hex, lpad, rpad } from "./util";
+import { hex, lpad, rpad, safeident } from "./util";
import { CodeAnalyzer } from "./analysis";
import { platform, platform_id, compparams, current_project, lastDebugState, projectWindows } from "./ui";
+import { PixelViewer, PixelMapper, PixelPalettizer, PixelFileDataNode, PixelTextDataNode, parseHexWords } from "./pixed/pixeleditor";
export interface ProjectView {
createDiv(parent:HTMLElement, text:string) : HTMLElement;
@@ -819,7 +820,6 @@ export class MemoryMapView implements ProjectView {
createDiv(parent : HTMLElement) {
this.maindiv = $('
');
$(parent).append(this.maindiv);
- this.refresh();
return this.maindiv[0];
}
@@ -949,3 +949,133 @@ export class ProfileView implements ProjectView {
platform.stopProfiling();
}
}
+
+///
+
+export class AssetEditorView implements ProjectView {
+ maindiv : JQuery;
+
+ createDiv(parent : HTMLElement) {
+ this.maindiv = $('
');
+ $(parent).append(this.maindiv);
+ return this.maindiv[0];
+ }
+
+ scanFileTextForAssets(id : string, data : string) {
+ // scan file for assets
+ // /*{json}*/ or ;;{json};;
+ 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 (m[0].startsWith(';;')) {
+ end = data.indexOf(';;', start); // asm
+ } else {
+ end = data.indexOf(';', start); // C
+ }
+ console.log(id, start, end, m[1]);
+ 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);
+ }
+ }
+ }
+ return result;
+ }
+
+ addPixelEditor(filediv:JQuery, firstnode:PixelFileDataNode|PixelTextDataNode, fmt?) {
+ var adual = $('
');
+ // create tile set
+ var aedit = $('
');
+ adual.append(aedit);
+ filediv.append(adual);
+ // data -> pixels
+ var mapper = new PixelMapper();
+ fmt = fmt || {w:8,h:8,bpp:1,count:256/*(data.length>>4)*/,brev:true,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}; // TODO
+ fmt.xform = 'scale(2)';
+ mapper.fmt = fmt;
+ var imgsperline = fmt.w == 8 ? 16 : 8; // TODO
+ // TODO: rotate node?
+ firstnode.addRight(mapper);
+ // pixels -> RGBA
+ var palizer = new PixelPalettizer();
+ if (fmt.bpp*(fmt.np|1) == 1)
+ palizer.palette = new Uint32Array([0xff000000, 0xffffffff]);
+ else
+ palizer.palette = new Uint32Array([0xff000000, 0xffff00ff, 0xffffff00, 0xffffffff]); // TODO
+ mapper.addRight(palizer);
+ // refresh
+ firstnode.refreshRight();
+ // add view objects (TODO)
+ // TODO: they need to update when refreshed from right
+ var i = 0;
+ var span = null;
+ for (var imdata of palizer.output) {
+ var viewer = new PixelViewer();
+ viewer.width = mapper.fmt.w | 0;
+ viewer.height = mapper.fmt.h | 0;
+ viewer.recreate();
+ viewer.canvas.style.width = (viewer.width*2)+'px'; // TODO
+ viewer.updateImage(imdata); // TODO
+ $(viewer.canvas).click((e) => {
+ console.log(e); // TODO
+ });
+ if (!span) {
+ span = $(' ');
+ aedit.append(span);
+ }
+ span.append(viewer.canvas);
+ if (++i == imgsperline) {
+ aedit.append($(" "));
+ span = null;
+ i = 0;
+ }
+ }
+ }
+
+ refreshAssetsInFile(fileid : string, data : FileData) {
+ let filediv = $('#'+this.getFileDivId(fileid)).empty();
+ // TODO
+ // TODO: check if open
+ if (fileid.endsWith('.chr') && data instanceof Uint8Array) {
+ let node = new PixelFileDataNode(fileid, data);
+ this.addPixelEditor(filediv, node);
+ } else if (typeof data === 'string') {
+ let textfrags = this.scanFileTextForAssets(fileid, data);
+ for (let frag of textfrags) {
+ // is this a bitmap?
+ if (frag.fmt && frag.fmt.w > 0 && frag.fmt.h > 0) {
+ let node = new PixelTextDataNode(fileid, data, frag.start, frag.end);
+ this.addPixelEditor(filediv, node, frag.fmt);
+ } else {
+ // TODO: other kinds of resources?
+ }
+ }
+ }
+ }
+
+ getFileDivId(id : string) {
+ return '__asset__' + safeident(id);
+ }
+
+ refresh() {
+ this.maindiv.empty();
+ current_project.iterateFiles((id, data) => {
+ var divid = this.getFileDivId(id);
+ var header = $('').text(id);
+ var body = $('
').attr('id',divid);
+ var filediv = $('
').append(header, body).appendTo(this.maindiv);
+ this.refreshAssetsInFile(id, data);
+ });
+ }
+
+}
+
diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts
index 59a1275d..a26bd02d 100644
--- a/src/worker/workermain.ts
+++ b/src/worker/workermain.ts
@@ -177,8 +177,8 @@ var PLATFORM_PARAMS = {
extra_segments:[
//{name:'Work RAM',start:0x0,size:0x800,type:'ram'},
{name:'OAM Buffer',start:0x200,size:0x100,type:'ram'},
- {name:'PPU Registers',start:0x2000,size:0x8,type:'io'},
- {name:'APU Registers',start:0x4000,size:0x20,type:'io'},
+ {name:'PPU Registers',start:0x2000,last:0x2008,size:0x2000,type:'io'},
+ {name:'APU Registers',start:0x4000,last:0x4020,size:0x2000,type:'io'},
{name:'Cartridge RAM',start:0x6000,size:0x2000,type:'ram'},
],
},