diff --git a/package.json b/package.json index ea0c889..5e37054 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "author": "zeb ", "description": "bitmap font generator", - "license": null, + "license": "see LICENSE file", "main": "./dist/electron/main.js", "scripts": { "build": "node .electron-vue/build.js && electron-builder", diff --git a/src/renderer/assets/bin-packing/packer.js b/src/renderer/assets/bin-packing/packer.js new file mode 100644 index 0000000..0388553 --- /dev/null +++ b/src/renderer/assets/bin-packing/packer.js @@ -0,0 +1,86 @@ +/****************************************************************************** + +This is a very simple binary tree based bin packing algorithm that is initialized +with a fixed width and height and will fit each block into the first node where +it fits and then split that node into 2 parts (down and right) to track the +remaining whitespace. + +Best results occur when the input blocks are sorted by height, or even better +when sorted by max(width,height). + +Inputs: +------ + + w: width of target rectangle + h: height of target rectangle + blocks: array of any objects that have .w and .h attributes + +Outputs: +------- + + marks each block that fits with a .fit attribute pointing to a + node with .x and .y coordinates + +Example: +------- + + var blocks = [ + { w: 100, h: 100 }, + { w: 100, h: 100 }, + { w: 80, h: 80 }, + { w: 80, h: 80 }, + etc + etc + ]; + + var packer = new Packer(500, 500); + packer.fit(blocks); + + for(var n = 0 ; n < blocks.length ; n++) { + var block = blocks[n]; + if (block.fit) { + Draw(block.fit.x, block.fit.y, block.w, block.h); + } + } + + +******************************************************************************/ + +Packer = function(w, h) { + this.init(w, h); +}; + +Packer.prototype = { + + init: function(w, h) { + this.root = { x: 0, y: 0, w: w, h: h }; + }, + + fit: function(blocks) { + var n, node, block; + for (n = 0; n < blocks.length; n++) { + block = blocks[n]; + if (node = this.findNode(this.root, block.w, block.h)) + block.fit = this.splitNode(node, block.w, block.h); + } + }, + + findNode: function(root, w, h) { + if (root.used) + return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); + else if ((w <= root.w) && (h <= root.h)) + return root; + else + return null; + }, + + splitNode: function(node, w, h) { + node.used = true; + node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; + node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; + return node; + } + +} + +module.exports = Packer \ No newline at end of file diff --git a/src/renderer/components/Setting.vue b/src/renderer/components/Setting.vue index 512c097..cbb92bd 100644 --- a/src/renderer/components/Setting.vue +++ b/src/renderer/components/Setting.vue @@ -3,10 +3,21 @@
-
+
+ +
+
+ + + + + +
@@ -15,7 +26,7 @@
- +
@@ -35,6 +46,31 @@ mounted () { }, computed: { + sizeHide () { + return this.autoSize ? 'none' : '' + }, + textureWidth: { + get () { return this.$store.state.Setting.textureWidth }, + set (value) { + if (Number(value) < 1) value = 1 + this.SET_TEXTURE_WIDTH(Number(value)) + this.ON_PROJ_MODIFIED() + } + }, + textureHeight: { + get () { return this.$store.state.Setting.textureHeight }, + set (value) { + this.SET_TEXTURE_HEIGHT(Number(value)) + this.ON_PROJ_MODIFIED() + } + }, + autoSize: { + get () { return this.$store.state.Setting.autoSize }, + set (value) { + this.ENABLE_AUTO_SIZE(value) + this.ON_PROJ_MODIFIED() + } + }, npower2: { get () { return this.$store.state.Setting.NPower2 }, set (value) { @@ -52,7 +88,7 @@ padding: { get () { return this.$store.state.Setting.padding }, set (value) { - this.SET_PADDING(value) + this.SET_PADDING(Number(value)) this.ON_PROJ_MODIFIED() } }, @@ -73,7 +109,8 @@ }, methods: { ...mapMutations([ - 'ENABLE_NPOWER2', 'ENABLE_SAME_WH', 'SET_PADDING', 'SET_OUTPUT_PATH', 'SET_PACK_ALGO', 'SET_OUTPUT_PATH', 'ON_PROJ_MODIFIED' + 'ENABLE_NPOWER2', 'ENABLE_SAME_WH', 'SET_PADDING', 'SET_OUTPUT_PATH', 'SET_PACK_ALGO', + 'SET_OUTPUT_PATH', 'ON_PROJ_MODIFIED', 'ENABLE_AUTO_SIZE', 'SET_TEXTURE_WIDTH', 'SET_TEXTURE_HEIGHT' ]), onClickBrowseOutputPath () { @@ -123,7 +160,12 @@ .control-item-stretch { flex-grow: 1; width: auto; - } + } + + span { + margin-left: 6px; + margin-right: 6px; + } } .form-control { diff --git a/src/renderer/store/modules/App.js b/src/renderer/store/modules/App.js index 1182cb1..ecb25a2 100644 --- a/src/renderer/store/modules/App.js +++ b/src/renderer/store/modules/App.js @@ -28,7 +28,10 @@ async function openProj (commit) { { name: 'BMFont Project File', extensions: ['bfp'] }, { name: 'All Files', extensions: ['*'] } ] - })[0] + }) + if (projPath === undefined) return + + projPath = projPath[0] try { let data = await readFile(projPath, { encoding: 'utf8' }) diff --git a/src/renderer/store/modules/BMPList.js b/src/renderer/store/modules/BMPList.js index f3d8ee0..606f386 100644 --- a/src/renderer/store/modules/BMPList.js +++ b/src/renderer/store/modules/BMPList.js @@ -1,4 +1,6 @@ +import { remote } from 'electron' import GrowingPacker from '../../assets/bin-packing/packer.growing.js' +import Packer from '../../assets/bin-packing/packer.js' import Jimp from 'jimp/es' import * as path from 'path' import * as xmlbuilder from 'xmlbuilder' @@ -11,39 +13,51 @@ const state = { bmpList: [] } -function packingImages (imgList, padding) { - let packer = new GrowingPacker() +function calculateSize (w, h, setting) { + if (setting.NPower2) { + [w, h] = expandSize(w, h) + } + if (setting.sameWH) { + let s = Math.max(w, h) + w = s + h = s + } + return [w, h] +} + +function packingImages (imgList, setting) { + let packer + let w, h + if (setting.autoSize) { + packer = new GrowingPacker() + } else { + [w, h] = calculateSize(setting.textureWidth, setting.textureHeight, setting) + packer = new Packer(w, h) + } let blocks = [] for (let img of imgList) { - blocks.push({ w: img.bitmap.width + padding, h: img.bitmap.height + padding }) + blocks.push({ w: img.bitmap.width + setting.padding, h: img.bitmap.height + setting.padding }) } packer.fit(blocks) - return blocks + if (setting.autoSize) { + [w, h] = calculateSize(packer.root.w, packer.root.h, setting) + } + return [blocks, w, h] } -function expandSize (size) { - let w = 2 ** Math.ceil(Math.log(size.w) / Math.log(2)) - let h = 2 ** Math.ceil(Math.log(size.h) / Math.log(2)) - return { w: w, h: h } +function expandSize (w, h) { + w = 2 ** Math.ceil(Math.log(w) / Math.log(2)) + h = 2 ** Math.ceil(Math.log(h) / Math.log(2)) + return [w, h] } function validateBlocks (blocks) { for (let b of blocks) { if (!b.fit || !b.fit.used) { - throw b + return false } } -} - -function calculateSize (blocks) { - let maxWidth = 0 - let maxHeight = 0 - for (let idx in blocks) { - let block = blocks[idx] - if (block.fit.x + block.fit.w > maxWidth) maxWidth = block.fit.x + block.fit.w - if (block.fit.y + block.fit.h > maxHeight) maxHeight = block.fit.y + block.fit.h - } - return { w: maxWidth, h: maxHeight } + return true } async function loadAllImages (bmpList) { @@ -116,14 +130,15 @@ const actions = { let setting = rootState.Setting try { let imgList = await loadAllImages(state.bmpList) - let blocks = packingImages(imgList, setting.padding) - validateBlocks(blocks) + let [blocks, w, h] = packingImages(imgList, setting) + if (!validateBlocks(blocks)) { + remote.dialog.showErrorBox('', '贴图太小了,不能容纳所有的字符') + return + } - let size = calculateSize(blocks) - if (setting.NPower2) size = expandSize(size) - console.log(size) + console.log(w, h) - let resultImg = await new Jimp(size.w, size.h) + let resultImg = await new Jimp(w, h) for (let idx in blocks) { let block = blocks[idx] let img = imgList[idx] @@ -134,7 +149,8 @@ const actions = { let fntPath = setting.outputPath.substring(0, setting.outputPath.length - ext.length) + '.xml' saveFNT(blocks, state.bmpList, imgList, fntPath) } catch (e) { - console.log(e) + console.dir(e) + remote.dialog.showErrorBox('', '请检查字符图片文件是否正确\n' + e.message) } } } diff --git a/src/renderer/store/modules/Setting.js b/src/renderer/store/modules/Setting.js index cf63aaf..4cdfb19 100644 --- a/src/renderer/store/modules/Setting.js +++ b/src/renderer/store/modules/Setting.js @@ -2,6 +2,9 @@ const state = { padding: 1, NPower2: true, sameWH: true, + autoSize: true, + textureWidth: 1024, + textureHeight: 1024, outputPath: 'untitiled.png', packAlgo: 'Bin-Packing' } @@ -13,6 +16,9 @@ const mutations = { if (save.padding !== undefined) state.padding = Number(save.padding) if (save.NPower2 !== undefined) state.NPower2 = save.NPower2 if (save.sameWH !== undefined) state.sameWH = save.sameWH + if (save.autoSize !== undefined) state.autoSize = save.autoSize + if (save.textureWidth !== undefined) state.textureWidth = save.textureWidth + if (save.textureHeight !== undefined) state.textureHeight = save.textureHeight if (save.outputPath !== undefined) state.outputPath = save.outputPath if (save.packAlgo !== undefined) state.packAlgo = save.packAlgo } @@ -21,9 +27,21 @@ const mutations = { state.padding = 1 state.NPower2 = true state.sameWH = true + state.autoSize = true + state.textureWidth = 1024 + state.textureHeight = 1024 state.outputPath = 'untitiled.png' state.packAlgo = 'Bin-Packing' }, + ENABLE_AUTO_SIZE (state, autoSize) { + state.autoSize = autoSize + }, + SET_TEXTURE_WIDTH (state, width) { + state.textureWidth = Number(width) + }, + SET_TEXTURE_HEIGHT (state, height) { + state.textureHeight = Number(height) + }, SET_PADDING (state, padding) { state.padding = Number(padding) }, @@ -31,7 +49,6 @@ const mutations = { state.NPower2 = enabled }, ENABLE_SAME_WH (state, enabled) { - console.log(state.sameWH, enabled) state.sameWH = enabled }, SET_OUTPUT_PATH (state, outputPath) {