2V0-g: port the .fnt font pipeline over a FreeType GDI emulation
CGraphicText, CGraphicFontTexture and CGraphicDib are the 40250 bodies verbatim; CGraphicTextInstance is verbatim except that its D3D draws become UI render commands (one image quad per glyph, Bar for cursor/underline). - platform/Win32Gdi.cpp: the GDI subset those files call, over vendored FreeType 2.13.3, following GDI's LOGFONT cell height, win metrics and gasp rules; system font lookup with substitute and CJK fallback faces - Win32Crt: MultiByteToWideChar / WideCharToMultiByte (UTF-8 and 1252 by hand, other code pages through iconv) - CGraphicImageTexture memory textures: glyph pages reach Godot as "mem:<id>@<revision>"; Metin2PythonHost.memory_texture + Canvas cache - Util.cpp code page / font face table, SetDefaultCodePage at boot, DefaultFont_* block, fnt resource type, GetMaxTextureWidth/Height - TextTag, StringCodec, Arabic, Vietnamese copied from 40250 - tests: port.text (Tahoma 12 metrics, alignment, CP949), port.app_loop and python_host_test.gd now check glyph quads instead of text commands Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
141206cb69
commit
afe63bfa61
@@ -133,14 +133,32 @@ wndMgr.Show(_ui_clip)
|
||||
surface.free()
|
||||
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_bar)") == "", "Bar 窗口已销毁")
|
||||
_ck(Metin2PythonHost.run_line("grp.RestoreViewport(); wndMgr.Destroy(_ui_clip)") == "", "裁剪窗口已销毁")
|
||||
_ck(Metin2PythonHost.run_line("_ui_text_probe = _UIProbe(); _ui_text = wndMgr.RegisterTextLine(_ui_text_probe, 'UI'); wndMgr.SetWindowPosition(_ui_text, 70, 80); wndMgr.SetText(_ui_text, 'Hello'); wndMgr.SetFontColor(_ui_text, 1.0, 0.5, 0.0); wndMgr.Show(_ui_text)") == "",
|
||||
"40250 TextLine 已创建")
|
||||
_ck(Metin2PythonHost.run_line("_ui_text_probe = _UIProbe(); _ui_text = wndMgr.RegisterTextLine(_ui_text_probe, 'UI'); wndMgr.SetWindowPosition(_ui_text, 70, 80); wndMgr.SetFontName(_ui_text, 'Tahoma:12'); wndMgr.SetText(_ui_text, 'Hello'); wndMgr.SetFontColor(_ui_text, 1.0, 0.5, 0.0); wndMgr.Show(_ui_text)") == "",
|
||||
"40250 TextLine 已创建(和 ui.TextLine.__init__ 一样先 SetFontName)")
|
||||
draws = Metin2PythonHost.ui_render_commands()
|
||||
var has_text := false
|
||||
for draw in draws:
|
||||
if draw["kind"] == "text" and draw["text"] == "Hello" and draw["x1"] == 70.0 and draw["y1"] == 80.0:
|
||||
has_text = true
|
||||
_ck(has_text, "40250 TextLine 文本进入 Godot Canvas 命令队列")
|
||||
var glyphs: Array = draws.filter(func(d): return d["kind"] == "image" and String(d["text"]).begins_with("mem:"))
|
||||
var min_x := INF
|
||||
for draw in glyphs:
|
||||
min_x = minf(min_x, draw["quad"][0].x)
|
||||
_ck(glyphs.size() == 5 and absf(min_x - 70.0) <= 1.5,
|
||||
"40250 TextLine 经 CGraphicFontTexture 字形页画成 5 个字形四边形(%d 个,左边 %.1f)" % [glyphs.size(), min_x])
|
||||
if not glyphs.is_empty():
|
||||
var page: Image = Metin2PythonHost.memory_texture(glyphs[0]["text"], -1)
|
||||
var inked := false
|
||||
if page != null:
|
||||
for y in page.get_height():
|
||||
for x in page.get_width():
|
||||
if page.get_pixel(x, y).a > 0.0:
|
||||
inked = true
|
||||
break
|
||||
if inked:
|
||||
break
|
||||
_ck(inked, "字形页(mem: 内存纹理)有字形像素")
|
||||
var page_surface := preload("res://python_ui_surface.gd").new()
|
||||
var page_texture: Texture2D = page_surface._texture(glyphs[0]["text"])
|
||||
_ck(page_texture != null and page_texture == page_surface._texture(glyphs[0]["text"]),
|
||||
"Canvas 按 mem:<id> 缓存字形页纹理")
|
||||
page_surface.free()
|
||||
_ck(Metin2PythonHost.run_line("wndMgr.Destroy(_ui_text)") == "", "TextLine 已销毁")
|
||||
var image_path := "locale/en/ui/vkey/key_backspace.tga"
|
||||
if Metin2Pack.exists(image_path):
|
||||
@@ -193,16 +211,21 @@ func _run_app_loop() -> void:
|
||||
reached = true
|
||||
break
|
||||
_ck(reached, "帧推进后进入 LoginWindow")
|
||||
var glyphs_before := _glyph_quads()
|
||||
_ck(Metin2PythonHost.run_line("_stream.popupWindow.Open('POPUP_PROBE')\nassert _stream.popupWindow.IsShow()") == "", "PopupDialog 已打开")
|
||||
Metin2PythonHost.ui_update()
|
||||
var has_popup := false
|
||||
for draw in Metin2PythonHost.ui_render_commands():
|
||||
if draw["kind"] == "text" and draw["text"] == "POPUP_PROBE":
|
||||
has_popup = true
|
||||
_ck(has_popup, "弹窗文字进入 Godot Canvas 命令队列")
|
||||
_ck(_glyph_quads() >= glyphs_before + 11, "弹窗文字的字形进入 Godot Canvas 命令队列")
|
||||
Metin2PythonHost.ui_key(1, true) # DIK_ESCAPE
|
||||
Metin2PythonHost.ui_key(1, false)
|
||||
Metin2PythonHost.ui_update()
|
||||
_ck(Metin2PythonHost.run_line("assert not _stream.popupWindow.IsShow()") == "", "ESC 关闭弹窗")
|
||||
Metin2PythonHost.stop()
|
||||
_ck(not Metin2PythonHost.is_running() and not Metin2PythonHost.is_app_looping(), "stop() 结束 app.Loop() 并收尾")
|
||||
|
||||
# CGraphicTextInstance draws one image quad per glyph from a "mem:<id>@<rev>" font page.
|
||||
func _glyph_quads() -> int:
|
||||
var count := 0
|
||||
for draw in Metin2PythonHost.ui_render_commands():
|
||||
if draw["kind"] == "image" and String(draw["text"]).begins_with("mem:"):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@@ -20,6 +20,10 @@ const KEY_TO_DIK := {
|
||||
KEY_RIGHT: 0xcd, KEY_DOWN: 0xd0,
|
||||
}
|
||||
|
||||
# "mem:<id>" → [revision, ImageTexture]: the CGraphicFontTexture glyph pages, refetched when the
|
||||
# "@<revision>" of a command's name moves on (a glyph was added to the page).
|
||||
var _memory_textures := {}
|
||||
|
||||
# True when this surface started system.py itself (run_app) and so owns the interpreter's lifetime.
|
||||
var _owns_app := false
|
||||
|
||||
@@ -106,20 +110,34 @@ func _draw() -> void:
|
||||
var segment := _clip_line(p1, p2, clip)
|
||||
if segment.size() == 2:
|
||||
draw_line(segment[0], segment[1], color)
|
||||
elif command["kind"] == "text":
|
||||
var font: Font = ThemeDB.fallback_font
|
||||
if font != null:
|
||||
var baseline := p1 + Vector2(0, font.get_ascent(14))
|
||||
if clip.has_point(p1):
|
||||
draw_string(font, baseline, command["text"], HORIZONTAL_ALIGNMENT_LEFT,
|
||||
clip.end.x - p1.x, 14, color)
|
||||
elif command["kind"] == "image":
|
||||
var texture: Texture2D = UiAssets.load_tex(AssetRoot.path(), command["text"])
|
||||
var texture: Texture2D = _texture(command["text"])
|
||||
if texture != null:
|
||||
var clipped := _clip_quad(command["quad"], command["uv"], clip)
|
||||
if clipped[0].size() >= 3:
|
||||
draw_polygon(clipped[0], PackedColorArray([color]), clipped[1], texture)
|
||||
|
||||
func _texture(name: String) -> Texture2D:
|
||||
if not name.begins_with("mem:"):
|
||||
return UiAssets.load_tex(AssetRoot.path(), name)
|
||||
var at := name.find("@")
|
||||
var key := name.substr(0, at) if at >= 0 else name
|
||||
var revision := int(name.substr(at + 1)) if at >= 0 else 0
|
||||
var entry: Array = _memory_textures.get(key, [])
|
||||
if not entry.is_empty() and entry[0] == revision:
|
||||
return entry[1]
|
||||
var image: Image = Metin2PythonHost.memory_texture(name, -1)
|
||||
if image == null:
|
||||
_memory_textures.erase(key)
|
||||
return null
|
||||
var texture: ImageTexture = entry[1] if not entry.is_empty() else null
|
||||
if texture != null and texture.get_size() == Vector2(image.get_size()):
|
||||
texture.update(image)
|
||||
else:
|
||||
texture = ImageTexture.create_from_image(image)
|
||||
_memory_textures[key] = [revision, texture]
|
||||
return texture
|
||||
|
||||
# 40250's image quad (TL, TR, BL, BR) clipped to the viewport. The quad is a
|
||||
# parallelogram, so texture coordinates are an affine function of position and are recomputed for
|
||||
# the clipped outline. Returns [points, uvs]; empty points when nothing is visible.
|
||||
|
||||
Reference in New Issue
Block a user