diff --git a/h2d/Font.hx b/h2d/Font.hx index 1949b2d53b..dc76b19ddc 100644 --- a/h2d/Font.hx +++ b/h2d/Font.hx @@ -185,6 +185,18 @@ class Font { Defaults to `hxd.Charset.getDefault()`. **/ public var charset : hxd.Charset; + /** + Optional fallback font used when a character glyph is not found in this font. + **/ + public var fallback : Null; + /** + List of fallback fonts used when a character is not found in this font or fallback. + **/ + public var fallbacks : Array; + /** + List of tiles if font is split across multiple texture pages. + **/ + public var tiles : Array; var glyphs : Map; var nullChar : FontChar; var defaultChar : FontChar; @@ -216,13 +228,31 @@ class Font { /** Returns a `FontChar` instance corresponding to the `code`. If font char is not present in glyph list, `charset.resolveChar` is called. + If still not found, queries fallback and fallbacks fonts. Returns `null` if glyph under specified charcode does not exist. @param code The charcode to search for. **/ - public inline function getChar( code : Int ) { + public function getChar( code : Int ) { var c = glyphs.get(code); if( c == null ) { c = charset.resolveChar(code, glyphs); + if( c == null ) { + if( fallback != null ) { + var fc = fallback.getChar(code); + if( fc != null && fc != fallback.defaultChar && fc != fallback.nullChar ) + c = fc; + } + if( c == null && fallbacks != null ) { + for( f in fallbacks ) { + if( f == null ) continue; + var fc = f.getChar(code); + if( fc != null && fc != f.defaultChar && fc != f.nullChar ) { + c = fc; + break; + } + } + } + } if( c == null ) c = code == "\r".code || code == "\n".code ? nullChar : defaultChar; } @@ -260,6 +290,9 @@ class Font { f.type = type; f.offsetX = offsetX; f.offsetY = offsetY; + f.fallback = fallback; + if( fallbacks != null ) f.fallbacks = fallbacks.copy(); + if( tiles != null ) f.tiles = [for( t in tiles ) t.clone()]; for( g in glyphs.keys() ) { var c = glyphs.get(g); var c2 = c.clone(); @@ -297,19 +330,29 @@ class Font { } /** - Checks if character is present in glyph list. + Checks if character is present in glyph list or any fallback font. Compared to `getChar` does not check if it exists through `Font.charset`. @param code The charcode to look up. **/ public function hasChar( code : Int ) : Bool { - return glyphs.get(code) != null; + if( glyphs.get(code) != null ) return true; + if( fallback != null && fallback.hasChar(code) ) return true; + if( fallbacks != null ) { + for( f in fallbacks ) + if( f != null && f.hasChar(code) ) return true; + } + return false; } /** - Disposes of the Font instance. Equivalent to `Tile.dispose`. + Disposes of the Font instance and its tiles. **/ public function dispose() { - tile.dispose(); + if( tile != null ) tile.dispose(); + if( tiles != null ) { + for( t in tiles ) + if( t != null && t != tile ) t.dispose(); + } } /** diff --git a/h2d/Text.hx b/h2d/Text.hx index 527d913c6b..664617d7f4 100644 --- a/h2d/Text.hx +++ b/h2d/Text.hx @@ -333,7 +333,8 @@ class Text extends Drawable { var newline = cc == '\n'.code; var esize = e.width + e.getKerningOffset(prevChar); var isComplement = (i < text.length - 1 && font.charset.isComplementChar(StringTools.fastCodeAt(text, i + 1))); - if( font.charset.isBreakChar(cc) && !isComplement ) { + var isLeading = font.charset.isLeadingChar(cc); + if( font.charset.isBreakChar(cc) && !isComplement && !isLeading ) { if( lines.length == 0 && leftMargin > 0 && x > maxWidth ) { lines.push(""); if ( sizes != null ) sizes.push(leftMargin); @@ -356,7 +357,7 @@ class Text extends Drawable { if ( k >= text.length ) break; var nc = StringTools.fastCodeAt(text, k); - if ( !font.charset.isComplementChar(nc) ) break; + if ( !font.charset.isComplementChar(nc) && !font.charset.isLeadingChar(cc) ) break; } } if( lineBreak && (size > maxWidth || (!breakFound && size + afterData > maxWidth)) ) { diff --git a/h3d/prim/ModelDatabase.hx b/h3d/prim/ModelDatabase.hx index c84d688159..d48b720cd1 100644 --- a/h3d/prim/ModelDatabase.hx +++ b/h3d/prim/ModelDatabase.hx @@ -92,6 +92,7 @@ class ModelDatabase { } public function getDefaultLodConfig( dir : String ) : Array { + #if (sys || nodejs) var fs = Std.downcast(hxd.res.Loader.currentInstance.fs, hxd.fs.LocalFileSystem); if (fs == null) return baseLodConfig; @@ -103,6 +104,9 @@ class ModelDatabase { }); return c; + #else + return baseLodConfig; + #end } public static var current = new ModelDatabase(); diff --git a/hxd/Charset.hx b/hxd/Charset.hx index fb8dfb42d5..4f1fdb0f32 100644 --- a/hxd/Charset.hx +++ b/hxd/Charset.hx @@ -140,7 +140,12 @@ class Charset { } static var complementChars : Map = { - var str = "ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻。,"; + var str = "ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々㻉。,、!?:;”’)》】〉』〗〕…—~·!?:;)]}.,/\"; + [for( i in 0...str.length ) str.charCodeAt(i) => true]; + } + + static var leadingChars : Map = { + var str = "“‘(《【〈『〖〔([{"; [for( i in 0...str.length ) str.charCodeAt(i) => true]; } @@ -148,6 +153,10 @@ class Charset { return complementChars.exists(code); } + public function isLeadingChar(code) { + return leadingChars.exists(code); + } + static var inst : Charset; public static function getDefault() { if( inst == null ) inst = new Charset(); diff --git a/hxd/fmt/bfnt/FontParser.hx b/hxd/fmt/bfnt/FontParser.hx index 885d5dd572..b136eff95a 100644 --- a/hxd/fmt/bfnt/FontParser.hx +++ b/hxd/fmt/bfnt/FontParser.hx @@ -10,22 +10,33 @@ class FontParser { // TODO: Support multiple textures per font. var tile : h2d.Tile = null; + var pages : Array = []; var font : h2d.Font = new h2d.Font(null, 0); var glyphs = font.glyphs; - inline function resolveTileSameName() { + inline function resolveTileSameName() : h2d.Tile { font.tilePath = new haxe.io.Path(path).file + ".png"; tile = resolveTile(haxe.io.Path.withExtension(path, "png")); + return tile; } - inline function resolveTileWithFallback( tilePath : String ) { + inline function resolveTileWithFallback( tilePath : String ) : h2d.Tile { + var t : h2d.Tile = null; try { font.tilePath = tilePath; - tile = resolveTile(haxe.io.Path.join([haxe.io.Path.directory(path), tilePath])); + t = resolveTile(haxe.io.Path.join([haxe.io.Path.directory(path), tilePath])); } catch ( e : Dynamic ) { trace('Warning: Could not find referenced font texture at "${tilePath}", trying to resolve same name as fnt!'); - resolveTileSameName(); + t = resolveTileSameName(); } + if ( tile == null ) tile = t; + return t; + } + + inline function getPageTile( pageId : Int ) : h2d.Tile { + if ( pages != null && pageId >= 0 && pageId < pages.length && pages[pageId] != null ) + return pages[pageId]; + return tile; } // Supported formats: @@ -42,7 +53,7 @@ class FontParser { switch( bytes.getInt32(0) ) { case 0x544E4642: // Internal BFNT - return hxd.fmt.bfnt.Reader.parse(bytes, function( tp : String ) { resolveTileWithFallback(tp); return tile; }); + return hxd.fmt.bfnt.Reader.parse(bytes, function( tp : String ) { return resolveTileWithFallback(tp); }); case 0x6D783F3C, // @@ -56,24 +67,29 @@ class FontParser { font.baseLine = Std.parseInt(xml.node.common.att.base); for ( p in xml.node.pages.elements ) { - if ( p.att.id == "0" ) { - resolveTileWithFallback(p.att.file); - } else { - trace("Warning: BMF format only supports one page at the moment."); - } + var pid = Std.parseInt(p.att.id); + var pt = resolveTileWithFallback(p.att.file); + pages[pid] = pt; + if ( pid == 0 || tile == null ) tile = pt; } var chars = xml.node.chars.elements; for( c in chars) { - var t = tile.sub(Std.parseInt(c.att.x), Std.parseInt(c.att.y), Std.parseInt(c.att.width), Std.parseInt(c.att.height), Std.parseInt(c.att.xoffset), Std.parseInt(c.att.yoffset)); + var pageId = c.has.page ? Std.parseInt(c.att.page) : 0; + var curTile = getPageTile(pageId); + var t = curTile.sub(Std.parseInt(c.att.x), Std.parseInt(c.att.y), Std.parseInt(c.att.width), Std.parseInt(c.att.height), Std.parseInt(c.att.xoffset), Std.parseInt(c.att.yoffset)); var fc = new h2d.Font.FontChar(t, Std.parseInt(c.att.xadvance)); - var kerns = xml.node.kernings.elements; - for (k in kerns) - if (k.att.second == c.att.id) - fc.addKerning(Std.parseInt(k.att.first), Std.parseInt(k.att.amount)); - glyphs.set(Std.parseInt(c.att.id), fc); } + + if ( xml.hasNode.kernings ) { + for (k in xml.node.kernings.elements) { + var second = Std.parseInt(k.att.second); + var fc = glyphs.get(second); + if (fc != null) + fc.addKerning(Std.parseInt(k.att.first), Std.parseInt(k.att.amount)); + } + } } else { // support for the FontBuilder/Divo format resolveTileSameName(); @@ -150,19 +166,23 @@ class FontParser { case "base": font.baseLine = extractInt(); case "pages": pageCount = extractInt(); - if (pageCount != 1) trace("Warning: BMF format only supports one page at the moment."); } next(); } case "page": + var pageId = 0; while (idx < line.length && reg.matchSub(line, idx)) { switch (reg.matched(1)) { - case "file": resolveTileWithFallback(processValue()); + case "id": pageId = extractInt(); + case "file": + var pt = resolveTileWithFallback(processValue()); + pages[pageId] = pt; + if ( pageId == 0 || tile == null ) tile = pt; } next(); } case "char": - var id = 0, x = 0, y = 0, width = 0, height = 0, xoffset = 0, yoffset = 0, xadvance = 0; + var id = 0, x = 0, y = 0, width = 0, height = 0, xoffset = 0, yoffset = 0, xadvance = 0, page = 0; while (idx < line.length && reg.matchSub(line, idx)) { switch (reg.matched(1)) { case "id": id = extractInt(); @@ -173,10 +193,12 @@ class FontParser { case "xoffset": xoffset = extractInt(); case "yoffset": yoffset = extractInt(); case "xadvance": xadvance = extractInt(); + case "page": page = extractInt(); } next(); } - var t = tile.sub(x, y, width, height, xoffset, yoffset); + var curTile = getPageTile(page); + var t = curTile.sub(x, y, width, height, xoffset, yoffset); var fc = new h2d.Font.FontChar(t, xadvance); glyphs.set(id, fc); case "kerning": @@ -217,19 +239,33 @@ class FontParser { // skip scaleW (2), scaleH (2) bytes.position += 4; pageCount = bytes.readUInt16(); - if (pageCount != 1) trace("Warning: BMF format only supports one page at the moment."); // skip bitField (1), channels (4) case 3: // pages - var name : String = bytes.readUntil(0); - resolveTileWithFallback(name); + while ( bytes.position < pos + length ) { + var name : String = bytes.readUntil(0); + if ( name.length > 0 ) { + var pt = resolveTileWithFallback(name); + pages.push(pt); + if ( tile == null ) tile = pt; + } + } case 4: // chars var count : Int = Std.int(length / 20); while ( count > 0 ) { var cid = bytes.readInt32(); - var t = tile.sub(bytes.readUInt16(), bytes.readUInt16(), bytes.readUInt16(), bytes.readUInt16(), bytes.readInt16(), bytes.readInt16()); - var fc = new h2d.Font.FontChar(t, bytes.readInt16()); + var cx = bytes.readUInt16(); + var cy = bytes.readUInt16(); + var cw = bytes.readUInt16(); + var ch = bytes.readUInt16(); + var cdx = bytes.readInt16(); + var cdy = bytes.readInt16(); + var cadv = bytes.readInt16(); + var pageId = bytes.readByte(); + var chnl = bytes.readByte(); + var curTile = getPageTile(pageId); + var t = curTile.sub(cx, cy, cw, ch, cdx, cdy); + var fc = new h2d.Font.FontChar(t, cadv); glyphs.set(cid, fc); - bytes.position += 2; // skip page and channel count--; } case 5: // kerning @@ -251,8 +287,12 @@ class FontParser { throw "Unknown font signature " + StringTools.hex(sign, 8); } if( glyphs.get(" ".code) == null ) - glyphs.set(" ".code, new h2d.Font.FontChar(tile.sub(0, 0, 0, 0), font.size>>1)); + glyphs.set(" ".code, new h2d.Font.FontChar((tile != null ? tile : h2d.Tile.fromColor(0, 0, 0, 0)).sub(0, 0, 0, 0), font.size>>1)); + if ( pages.length > 0 ) { + font.tiles = pages; + if ( tile == null ) tile = pages[0]; + } font.tile = tile; if( font.baseLine == 0 ) diff --git a/hxd/res/BDFFont.hx b/hxd/res/BDFFont.hx index 0a37d569f3..817efffae6 100644 --- a/hxd/res/BDFFont.hx +++ b/hxd/res/BDFFont.hx @@ -16,6 +16,7 @@ using StringTools; public var yoffset : Int; public var stride : Int; public var bits : Array; + public var page : Int = 0; public function new( code, width, height, xoffset, yoffset, stride ) { this.code = code; @@ -232,20 +233,19 @@ class BDFFont extends Resource { */ @:access(h2d.Font) function generateGlyphs() { - // Firstly, sort glyphData by height + // Sort glyphData by height descending glyphData.sort( BDFFontChar.sortOnHeight ); - // Calculate total volume, and from that an approx width and height if packing with 80% - // efficiency (i.e. add a 10% buffer). This is from trial and error :) - var volume : Int = 0; - for ( d in glyphData ) volume += ( d.width * d.height ); - var bitmapWidth : Int = Math.ceil( Math.sqrt( volume * (1 + BitmapPad) ) ); - if ( bitmapWidth > BitmapMaxWidth ) throw 'The font bitmap is too big: ${bitmapWidth}x${bitmapWidth} (max ${BitmapMaxWidth}x${BitmapMaxWidth})'; + // Calculate total volume to choose an optimal page size + var pad = 1; + var totalVolume : Float = 0; + for ( d in glyphData ) totalVolume += ( d.width + pad ) * ( d.height + pad ); - // Create the bitmap - var bitmapData : hxd.BitmapData = new hxd.BitmapData( bitmapWidth, bitmapWidth ); - bitmapData.lock(); - bitmapData.clear( ClearColor ); // Blue, but transparent + var maxPageSize = 2048; + var pageSize = 128; + while ( pageSize * pageSize < totalVolume * 1.2 && pageSize < maxPageSize ) { + pageSize <<= 1; + } // Calculate values for extracting pixel data var bppMask : Int = 0x80; @@ -256,87 +256,79 @@ class BDFFont extends Resource { } var pixPerByte : Int = Math.floor( 8 / bitsPerPixel ); var bppScale : Float = 255 / ((1 << bitsPerPixel) - 1); - var pixLeftInByte : Int = 0; - var pixBits : Int = 0; - var pixAlpha : Int = 0; - - // Draw glyphs to bitmap in height order and save position on bitmap - var x : Int = 0; - var y : Int = 0; - var found : Bool = false; + + var pagesData : Array = []; + var curBitmap : hxd.BitmapData = new hxd.BitmapData( pageSize, pageSize ); + curBitmap.lock(); + curBitmap.clear( ClearColor ); + pagesData.push( curBitmap ); + + var curX = pad; + var curY = pad; + var shelfHeight = 0; + var curPage = 0; + for ( d in glyphData ) { - found = false; - - // Wrap x if glyph will not fit in width - if ( ( x + d.width ) > bitmapWidth ) x = 0; - - // Find nearest space big enough for glyph, left to right, top to bottom - while ( x <= (bitmapWidth - d.width) ) { - y = 0; - while ( y <= (bitmapWidth - d.height) ) { - // If top-left pixel is clear... - if ( bitmapData.getPixel( x, y ) == ClearColor ) { - found = true; - // Check first row and first column are clear to ensure space is clear - for ( xx in x...(x + d.width) ) { - if ( bitmapData.getPixel( xx, y ) != ClearColor ) { - found = false; - break; - } - } - if ( found ) { - for ( yy in y...(y + d.height) ) { - if ( bitmapData.getPixel( x, yy ) != ClearColor ) { - found = false; - break; - } - } - } - if ( found ) break; - } - y++; - } - if ( found ) break; - x++; + if ( ( curX + d.width + pad ) > pageSize ) { + curX = pad; + curY += shelfHeight + pad; + shelfHeight = 0; + } + if ( ( curY + d.height + pad ) > pageSize ) { + curBitmap.unlock(); + curPage++; + curBitmap = new hxd.BitmapData( pageSize, pageSize ); + curBitmap.lock(); + curBitmap.clear( ClearColor ); + pagesData.push( curBitmap ); + curX = pad; + curY = pad; + shelfHeight = 0; } - // XXX: At this point it would be really good to see the bitmap (so far) - if ( !found ) throw 'Glyphs are overflowing the bitmap. Help!'; + d.x = curX; + d.y = curY; + d.page = curPage; + if ( d.height > shelfHeight ) shelfHeight = d.height; - // Now have space that starts at x,y. - // Draw the glyph to the bitmap and save position - d.x = x; d.y = y; - for ( yy in y...(y + d.height) ) { + // Draw the glyph pixels to curBitmap + var pixLeftInByte : Int = 0; + var pixBits : Int = 0; + var pixAlpha : Int = 0; + for ( yy in 0...d.height ) { pixLeftInByte = 0; - for ( xx in x...(x + d.width) ) { - // Grab a new byte + for ( xx in 0...d.width ) { if ( pixLeftInByte == 0 ) { pixLeftInByte = pixPerByte; pixBits = d.bits.shift(); } - // Grab a pixel alpha pixAlpha = (pixBits & bppMask) >> (8 - bitsPerPixel); pixBits = pixBits << bitsPerPixel; - // Calculate actual pixel value and set pixel pixAlpha = Math.floor( pixAlpha * bppScale ) << 24; - bitmapData.setPixel( xx, yy, pixAlpha | PixelColor ); - // Advance + curBitmap.setPixel( d.x + xx, d.y + yy, pixAlpha | PixelColor ); pixLeftInByte--; } } - // Advance the start position to after the bitmap - x += d.width; + curX += d.width + pad; } - bitmapData.unlock(); - - // Create tile from bitmap data - font.tile = h2d.Tile.fromBitmap( bitmapData ); + curBitmap.unlock(); + + // Create tiles from bitmap data + var fontTiles : Array = []; + for ( bmp in pagesData ) { + var t = h2d.Tile.fromBitmap( bmp ); + fontTiles.push( t ); + bmp.dispose(); + } + font.tile = fontTiles[0]; + if ( fontTiles.length > 1 ) + font.tiles = fontTiles; // Generate glyphs for ( d in glyphData ) { - // In BDF, y-offset is offset from baseline. In FNT it appears to be offset from top - var t = font.tile.sub( d.x, d.y, d.width, d.height, d.xoffset, ascent - (d.height + d.yoffset) ); + var pageTile = fontTiles[d.page]; + var t = pageTile.sub( d.x, d.y, d.width, d.height, d.xoffset, ascent - (d.height + d.yoffset) ); var fc = new h2d.Font.FontChar( t, d.stride ); font.glyphs.set( d.code, fc ); } @@ -350,7 +342,7 @@ class BDFFont extends Resource { font.lineHeight = ascent + descent; else if ( fbbHeight >= 0 ) font.lineHeight = fbbHeight; - else{ + else { var a = font.glyphs.get( "E".code ); if ( a == null ) a = font.glyphs.get( "A".code ); @@ -381,8 +373,6 @@ class BDFFont extends Resource { fallback = font.glyphs.get( " ".code ); font.defaultChar = fallback; - // Cleanup - bitmapData.dispose(); this.glyphData = null; // No longer required } diff --git a/hxd/res/DefaultFont.hx b/hxd/res/DefaultFont.hx index 9b1395aaff..cb8361a53b 100644 --- a/hxd/res/DefaultFont.hx +++ b/hxd/res/DefaultFont.hx @@ -1,7 +1,30 @@ package hxd.res; +private class DefaultChineseFont {} + class DefaultFont { + /** + Returns the built-in Chinese pixel font (12px dot-matrix font covering 4,400+ Chinese characters and symbols). + **/ + public static function getChinese() : h2d.Font { + var engine = h3d.Engine.getCurrent(); + var fnt : h2d.Font = engine.resCache.get(DefaultChineseFont); + if( fnt == null ) { + var BYTES = hxd.res.Embed.getResource("hxd/res/defaultFontChinese.png"); + var DESC = hxd.res.Embed.getResource("hxd/res/defaultFontChinese.fnt"); + var bmp = new BitmapFont(DESC.entry); + @:privateAccess bmp.loader = BYTES.loader; + fnt = bmp.toFont(); + engine.resCache.set(DefaultChineseFont, fnt); + } + return fnt; + } + + /** + Returns the default font (ASCII Pixel Operator 12px) with automatic Chinese pixel font fallback. + English text uses the default font, and Chinese characters seamlessly render using the pixel font! + **/ public static function get() : h2d.Font { var engine = h3d.Engine.getCurrent(); var fnt : h2d.Font = engine.resCache.get(DefaultFont); @@ -11,6 +34,7 @@ class DefaultFont { var bmp = new BitmapFont(DESC.entry); @:privateAccess bmp.loader = BYTES.loader; fnt = bmp.toFont(); + fnt.fallback = getChinese(); engine.resCache.set(DefaultFont, fnt); } return fnt; diff --git a/hxd/res/defaultFontChinese.fnt b/hxd/res/defaultFontChinese.fnt new file mode 100644 index 0000000000..5f282dc32f Binary files /dev/null and b/hxd/res/defaultFontChinese.fnt differ diff --git a/hxd/res/defaultFontChinese.png b/hxd/res/defaultFontChinese.png new file mode 100644 index 0000000000..f7acf08bb5 Binary files /dev/null and b/hxd/res/defaultFontChinese.png differ