Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 187 additions & 66 deletions GodotExport.as
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package {
package {
import com.adobe.images.PNGEncoder;
import flash.display.*;
import flash.events.*;
Expand All @@ -11,6 +11,7 @@ package {
import flash.text.TextField;
import flash.text.TextFormat;
import flash.utils.setTimeout;
import flash.display.StageQuality;

public class GodotExport extends Sprite {
public static var _root:DisplayObjectContainer;
Expand Down Expand Up @@ -64,6 +65,81 @@ package {
private var bitmapDataCache:Dictionary = new Dictionary();
private var atlasRects:Dictionary;

// --- Quality knobs ---
private const EXPORT_SCALE:Number = 2.0; // 1.0 = uit; 2.0 of 3.0 higher resolution sharper
private const ATLAS_PADDING:int = 2; // 2px extrude rondom in atlas

// Kopieer src naar out met padding + extruded edges (voor single textures)
private function extrudeBitmapEdges(src:BitmapData, padding:int = 2):BitmapData {
if (padding <= 0) return src.clone();

var out:BitmapData = new BitmapData(src.width + padding*2, src.height + padding*2, true, 0x00000000);
out.lock();

// 1) midden
out.copyPixels(src, src.rect, new Point(padding, padding));

// 2) randen (repliceer 1px strips)
for (var i:int = 1; i <= padding; i++) {
// top
out.copyPixels(src, new Rectangle(0, 0, src.width, 1), new Point(padding, padding - i));
// bottom
out.copyPixels(src, new Rectangle(0, src.height - 1, src.width, 1), new Point(padding, padding + src.height - 1 + i));
// left
out.copyPixels(src, new Rectangle(0, 0, 1, src.height), new Point(padding - i, padding));
// right
out.copyPixels(src, new Rectangle(src.width - 1, 0, 1, src.height), new Point(padding + src.width - 1 + i, padding));
}

// 3) hoeken (vul met hoekpixels)
var tl:uint = src.getPixel32(0, 0);
var tr:uint = src.getPixel32(src.width - 1, 0);
var bl:uint = src.getPixel32(0, src.height - 1);
var br:uint = src.getPixel32(src.width - 1, src.height - 1);

for (var y:int = 0; y < padding; y++) {
for (var x:int = 0; x < padding; x++) {
out.setPixel32(x, y, tl); // top-left
out.setPixel32(out.width-1-x, y, tr); // top-right
out.setPixel32(x, out.height-1-y, bl); // bottom-left
out.setPixel32(out.width-1-x, out.height-1-y, br); // bottom-right
}
}

out.unlock();
return out;
}

// Schrijf bd in atlas op (dx,dy) en extrude randen in de atlasbuffer
private function blitWithExtrude(atlas:BitmapData, bd:BitmapData, dx:int, dy:int, padding:int = 2):void {
// midden
atlas.copyPixels(bd, bd.rect, new Point(dx, dy));

// randen
for (var i:int = 1; i <= padding; i++) {
atlas.copyPixels(bd, new Rectangle(0, 0, bd.width, 1), new Point(dx, dy - i)); // top
atlas.copyPixels(bd, new Rectangle(0, bd.height - 1, bd.width, 1), new Point(dx, dy + bd.height - 1 + i)); // bottom
atlas.copyPixels(bd, new Rectangle(0, 0, 1, bd.height), new Point(dx - i, dy)); // left
atlas.copyPixels(bd, new Rectangle(bd.width - 1, 0, 1, bd.height), new Point(dx + bd.width - 1 + i, dy)); // right
}

// hoeken
var tl:uint = bd.getPixel32(0, 0);
var tr:uint = bd.getPixel32(bd.width - 1, 0);
var bl:uint = bd.getPixel32(0, bd.height - 1);
var br:uint = bd.getPixel32(bd.width - 1, bd.height - 1);

for (var y:int = 0; y < padding; y++) {
for (var x:int = 0; x < padding; x++) {
atlas.setPixel32(dx - 1 - x, dy - 1 - y, tl);
atlas.setPixel32(dx + bd.width + x, dy - 1 - y, tr);
atlas.setPixel32(dx - 1 - x, dy + bd.height + y, bl);
atlas.setPixel32(dx + bd.width + x, dy + bd.height + y, br);
}
}
}


public function GodotExport() {
if (File.desktopDirectory) {
outputFolder = File.desktopDirectory.resolvePath("SWF_Export");
Expand Down Expand Up @@ -327,25 +403,23 @@ package {
fs.writeUTFBytes(tscnContent);
fs.close();
}

private function generateAtlas():void {
// Count items in cache
var keyCount:int = 0;
for (var key:String in bitmapDataCache) {
keyCount++;
}
if (keyCount == 0) {
return;
}
for (var key:String in bitmapDataCache) keyCount++;
if (keyCount == 0) return;

atlasRects = new Dictionary();
var atlases:Array = [];

var currentAtlasIndex:int = 0;
var currentX:int = 0;
var currentY:int = 0;
var currentRowHeight:int = 0;

var createNewAtlas = function():void {
var P:int = Math.max(0, ATLAS_PADDING); // padding around each sprite in the atlas

var createNewAtlas:Function = function():void {
atlases.push(new BitmapData(MAX_ATLAS_WIDTH, MAX_ATLAS_HEIGHT, true, 0x00000000));
currentX = 0;
currentY = 0;
Expand All @@ -358,36 +432,45 @@ package {
var item:Object = bitmapDataCache[id];
var bd:BitmapData = item.bd;

if (currentX + bd.width > MAX_ATLAS_WIDTH) {
// Space needed including padding (extrude area lives in padding)
var neededW:int = bd.width + P * 2;
var neededH:int = bd.height + P * 2;

// Move to next row if needed
if (currentX + neededW > MAX_ATLAS_WIDTH) {
currentX = 0;
currentY += currentRowHeight;
currentRowHeight = 0;
}

if (currentY + bd.height > MAX_ATLAS_HEIGHT) {
// Need a new atlas?
if (currentY + neededH > MAX_ATLAS_HEIGHT) {
currentAtlasIndex++;
createNewAtlas();
}

var atlas:BitmapData = atlases[currentAtlasIndex];
var destPoint:Point = new Point(currentX, currentY);
var innerX:int = currentX + P; // where the actual image (without padding) begins
var innerY:int = currentY + P;

try {
bd.lock();
atlas.copyPixels(bd, bd.rect, destPoint);
// Blit with edge extrusion into atlas (fills padding area with replicated edge pixels)
blitWithExtrude(atlas, bd, innerX, innerY, P);
bd.unlock();
} catch (e:Error) {
throw new Error("Failed during copyPixels in generateAtlas for texture ID '" + id + "'. Original error: " + e.message);
throw new Error("Failed during blitWithExtrude in generateAtlas for texture ID '" + id + "'. Original error: " + e.message);
}

// Store only the inner rect (without padding) for Godot region_rect
atlasRects[id] = {
rect: new Rectangle(currentX, currentY, bd.width, bd.height),
rect: new Rectangle(innerX, innerY, bd.width, bd.height),
atlasIndex: currentAtlasIndex
};

currentX += bd.width;
if (bd.height > currentRowHeight) {
currentRowHeight = bd.height;
}
// Advance packing cursor
currentX += neededW;
if (neededH > currentRowHeight) currentRowHeight = neededH;
}

// --- Save atlases and create ExtResources ---
Expand All @@ -396,12 +479,13 @@ package {
var atlasPath:String = "textures/texture_atlas_" + i + ".png";
var file:File = outputFolder.resolvePath(atlasPath);
if (!file.parent.exists) file.parent.createDirectory();

var fs:FileStream = new FileStream();
fs.open(file, FileMode.WRITE);
try {
fs.writeBytes(PNGEncoder.encode(atlasBitmap));
} catch (e:Error) {
fs.close();
throw new Error("Failed during PNGEncoder.encode in generateAtlas for atlas #" + i + ". Original error: " + e.message);
}
fs.close();
Expand All @@ -418,7 +502,7 @@ package {
for (var j:int = 0; j < nodeData.nodeList.length; j++) {
var nodeString:String = nodeData.nodeList[j];

// Replace texture placeholder
// texture placeholder -> atlas resource
var textureRegex:RegExp = /texture = ATLAS_TEXTURE_PLACEHOLDER_FOR_ID_([a-zA-Z0-9_]+)/;
var textureMatch:Object = textureRegex.exec(nodeString);
if (textureMatch) {
Expand All @@ -430,24 +514,25 @@ package {
}
}

// Replace rect placeholder
// region placeholder -> inner rect (without padding)
var rectRegex:RegExp = /region_rect = ATLAS_RECT_PLACEHOLDER_FOR_ID_([a-zA-Z0-9_]+)/;
var rectMatch:Object = rectRegex.exec(nodeString);
if (rectMatch) {
var rectTextureId:String = rectMatch[1];
if (rectTextureId in atlasRects) {
var rectAtlasInfo:Object = atlasRects[rectTextureId];
var rect:Rectangle = rectAtlasInfo.rect;
var rectReplacement:String = "region_rect = Rect2(" + rect.x + ", " + rect.y + ", " + rect.width + ", " + rect.height + ")";
var r:Rectangle = rectAtlasInfo.rect;
var rectReplacement:String = "region_rect = Rect2(" + r.x + ", " + r.y + ", " + r.width + ", " + r.height + ")";
nodeString = nodeString.replace(rectMatch[0], rectReplacement);
}
}

nodeData.nodeList[j] = nodeString;
}
}
}


private function showConvertingMessage():void {
// Supprimer ancien message s'il existe
if (conversionMsgContainer && contains(conversionMsgContainer)) {
Expand Down Expand Up @@ -690,7 +775,7 @@ package {
_st += '[node name="'+nodeName+'" type="Sprite2D" parent="' + _parent_path+'"]\n';
_st += 'position = Vector2('+Math.ceil(_posXFinal)+','+Math.ceil(_posYFinal)+')\n';
_st += 'rotation = '+ GodotExport.getTrueRotationRadians(obj) +'\n';
_st += 'scale = Vector2('+Math.abs(_scale.x)+','+Math.abs(_scale.y)+')\n'
_st += 'scale = Vector2('+ (Math.abs(_scale.x)/EXPORT_SCALE) +','+ (Math.abs(_scale.y)/EXPORT_SCALE) +')\n';
_st += 'flip_h = '+ (_scale.x < 0)+'\n';
if (atlasEnabled) {
_st += 'texture = ATLAS_TEXTURE_PLACEHOLDER_FOR_ID_' + _idTex + '\n';
Expand Down Expand Up @@ -788,71 +873,107 @@ package {
return _bool;
}

private function exportSprite(obj:DisplayObject, nodeName:String):String {
var marginX:int = parseInt(marginXInput.text) || 0;
var marginY:int = parseInt(marginYInput.text) || 0;
private function exportSprite(obj:DisplayObject, nodeName:String):String {
// UI margins
var uiMarginX:int = parseInt(marginXInput.text) || 0;
var uiMarginY:int = parseInt(marginYInput.text) || 0;

// Voor single textures willen we altijd wat extra ruimte voor bleed
var padX:int = uiMarginX + (atlasEnabled ? 0 : ATLAS_PADDING);
var padY:int = uiMarginY + (atlasEnabled ? 0 : ATLAS_PADDING);

// Nauwkeurige bounds in local space
var bounds:Rectangle = getRealBounds(obj);
var w:int = Math.max(1, Math.ceil(bounds.width)) + (marginX * 2);
var h:int = Math.max(1, Math.ceil(bounds.height)) + (marginY * 2);

if (w > 8191 || h > 8191) {
throw new Error("Object '" + nodeName + "' is too large to be exported. Its dimensions (" + w + "x" + h + ") exceed the maximum texture size of 8191px.");
}
// Supersampling: render groter, daarna in Godot weer kleiner schalen (zie createShapeNode patch)
var renderW:int = Math.max(1, Math.ceil(bounds.width * EXPORT_SCALE)) + (padX * 2);
var renderH:int = Math.max(1, Math.ceil(bounds.height * EXPORT_SCALE)) + (padY * 2);

if (atlasEnabled && (w > MAX_ATLAS_WIDTH || h > MAX_ATLAS_HEIGHT)) {
throw new Error("Object '" + nodeName + "' (" + w + "x" + h + ") is too large to fit in the texture atlas (max " + MAX_ATLAS_WIDTH + "x" + MAX_ATLAS_HEIGHT + "). Please disable the 'Single Texture' option or reduce the object's size.");
if (renderW > 8191 || renderH > 8191) {
throw new Error("Object '" + nodeName + "' is too large to be exported at this scale (" + renderW + "x" + renderH + ").");
}
if (atlasEnabled && (renderW > MAX_ATLAS_WIDTH || renderH > MAX_ATLAS_HEIGHT)) {
throw new Error("Object '" + nodeName + "' (" + renderW + "x" + renderH + ") is too large for the atlas.");
}

var _id : String = '';

var id:String = '';
var bd:BitmapData;

try {
bd = new BitmapData(w, h, true, 0x00000000);
var matrix:Matrix = new Matrix();
matrix.translate(-bounds.x + marginX, -bounds.y + marginY);
bd.draw(obj, matrix, null, null, null, true);
bd = new BitmapData(renderW, renderH, true, 0x00000000);

// Tekenen met BEST kwaliteit + pixelsnapping
var oldQ:String = stage ? stage.quality : null;
if (stage) stage.quality = StageQuality.BEST;

var m:Matrix = new Matrix();
// centreer het object in de bitmap
m.translate(-bounds.x, -bounds.y);
m.scale(EXPORT_SCALE, EXPORT_SCALE);
m.translate(padX, padY);

// pixel snap de vertaling om “softere” randen door subpixels te voorkomen
m.tx = Math.round(m.tx);
m.ty = Math.round(m.ty);

bd.draw(obj, m, null, null, null, true); // smoothing=true is ok; StageQuality doet het antialias-werk voor vector

if (stage && oldQ) stage.quality = oldQ;
} catch (e:Error) {
throw new Error("Failed during BitmapData creation/draw in exportSprite for node '" + nodeName + "'. Original error: " + e.message);
throw new Error("Failed during BitmapData draw in exportSprite for node '" + nodeName + "'. " + e.message);
}

// Check for empty bitmap
// Lege bitmap skippen
var colorBounds:Rectangle = bd.getColorBoundsRect(0xFF000000, 0x000000, false);
if (colorBounds == null) {
return null; // Return null for empty sprites
if (colorBounds == null || colorBounds.isEmpty()) {
return null;
}

// Cache-dedupe op content
var pngBytes:ByteArray;
var writeBD:BitmapData = bd;

// Voor single textures meteen extrude-bleed toevoegen (atlas doet dat later in generateAtlas)
if (!atlasEnabled && ATLAS_PADDING > 0) {
writeBD = extrudeBitmapEdges(bd, ATLAS_PADDING);
}
var _png:ByteArray;

try {
_png = PNGEncoder.encode(bd);
pngBytes = PNGEncoder.encode(writeBD);
} catch (e:Error) {
throw new Error("Failed during PNGEncoder.encode in exportSprite for node '" + nodeName + "'. Original error: " + e.message);
throw new Error("Failed during PNGEncoder.encode in exportSprite for node '" + nodeName + "'. " + e.message);
}
var _pngSt : String = _png.toString();
_id = textureID + '_' + generateUIDTex();
if(clipNameToTex.hasOwnProperty(_pngSt) == true) {
_id = clipNameToTex[_pngSt];
var pngKey:String = pngBytes.toString();

id = textureID + '_' + generateUIDTex();
if (clipNameToTex.hasOwnProperty(pngKey)) {
id = clipNameToTex[pngKey];
} else {
clipNameToTex[_pngSt] = _id;
clipNameToTex[pngKey] = id;

if (atlasEnabled) {
bitmapDataCache[_id] = {bd: bd.clone(), nodeName: nodeName};
// In atlasmodus bewaren we de strakke (niet-ge-extrude) bd, extrude gebeurt in generateAtlas()
bitmapDataCache[id] = { bd: bd.clone(), nodeName: nodeName };
} else {
var _path : String = "textures/" + nodeName + ".png";
var file:File = outputFolder.resolvePath(_path);
var path:String = "textures/" + nodeName + ".png";
var file:File = outputFolder.resolvePath(path);
if (!file.parent.exists) file.parent.createDirectory();

var fs:FileStream = new FileStream();
fs.open(file, FileMode.WRITE);
fs.writeBytes(_png);
fs.close();
var _uuid : String = generateUID();
_path = outputFolderAnimSt+'/' +_path;
var _tex : String = '[ext_resource type="Texture2D" uid="uid://'+ _uuid+'" path="res://' + _path + '" id="' + _id + '"]\n';
listTexture.push(_tex);
fs.writeBytes(pngBytes);
fs.close();

var uuid:String = generateUID();
path = outputFolderAnimSt + '/' + path;
var tex:String = '[ext_resource type="Texture2D" uid="uid://' + uuid + '" path="res://' + path + '" id="' + id + '"]\n';
listTexture.push(tex);
}
textureID++;
}
return _id;
return id;
}


public static function getRealBounds(obj:DisplayObject):Rectangle {
var bounds:Rectangle = obj.getBounds(obj);
Expand Down