kopia lustrzana https://github.com/inkstitch/inkstitch
522 wiersze
21 KiB
Python
522 wiersze
21 KiB
Python
# Authors: see git history
|
|
#
|
|
# Copyright (c) 2025 Authors
|
|
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
|
|
|
import json
|
|
from collections import defaultdict
|
|
from copy import deepcopy
|
|
from itertools import combinations_with_replacement
|
|
from os import path
|
|
|
|
import wx
|
|
import wx.adv
|
|
from inkex import errormsg
|
|
|
|
from ...elements import nodes_to_elements
|
|
from ...i18n import _
|
|
from ...lettering import get_font_list
|
|
from ...lettering.font_variant import FontVariant
|
|
from ...lettering.categories import FONT_CATEGORIES
|
|
from ...stitch_plan import stitch_groups_to_stitch_plan
|
|
from ...svg.tags import SVG_PATH_TAG
|
|
from ...utils.settings import global_settings
|
|
from ...utils.threading import ExitThread, check_stop_flag
|
|
from .. import PreviewRenderer
|
|
from . import HelpPanel, SettingsPanel
|
|
|
|
LETTER_CASE = {0: '', 1: 'upper', 2: 'lower'}
|
|
|
|
|
|
class LetteringEditJsonPanel(wx.Panel):
|
|
|
|
def __init__(self, parent, simulator, layer, metadata=None, background_color='white'):
|
|
self.parent = parent
|
|
self.simulator = simulator
|
|
self.layer = layer
|
|
self.metadata = metadata or dict()
|
|
self.background_color = background_color
|
|
|
|
self.fonts = None
|
|
self.font = None
|
|
self.default_variant = None
|
|
self.font_meta = defaultdict(list)
|
|
self.glyphs = []
|
|
self.kerning_pairs = None
|
|
self.kerning_combinations = []
|
|
self.horiz_adv_x = {}
|
|
|
|
self.text_before = ''
|
|
self.text_after = ''
|
|
|
|
self.last_notebook_selection = 4
|
|
|
|
super().__init__(parent, wx.ID_ANY)
|
|
|
|
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
|
|
|
|
# preview
|
|
self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered)
|
|
|
|
notebook_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.notebook = wx.Notebook(self, wx.ID_ANY)
|
|
notebook_sizer.Add(self.notebook, 1, wx.EXPAND, 0)
|
|
|
|
self.settings = wx.Panel(self.notebook, wx.ID_ANY)
|
|
self.settings_panel = SettingsPanel(self.notebook)
|
|
self.notebook.AddPage(self.settings_panel, _("Settings"))
|
|
self.notebook.AddPage(HelpPanel(self.notebook), _("Help"))
|
|
|
|
self.SetSizer(notebook_sizer)
|
|
|
|
self.set_font_list()
|
|
select_font = global_settings['last_font']
|
|
self.settings_panel.font_chooser.SetValue(select_font)
|
|
self.on_font_changed()
|
|
|
|
self.SetSizeHints(notebook_sizer.CalcMin())
|
|
self.Layout()
|
|
|
|
def on_text_before_changed(self, event):
|
|
self.text_before = event.GetEventObject().GetValue()
|
|
self.update_preview()
|
|
|
|
def on_text_after_changed(self, event):
|
|
self.text_after = event.GetEventObject().GetValue()
|
|
self.update_preview()
|
|
|
|
def on_glyphlist_update(self, event=None):
|
|
item = event.GetItem()
|
|
value = None
|
|
try:
|
|
value = float(item.GetText())
|
|
except ValueError:
|
|
pass
|
|
if value == self.font_meta['horiz_adv_x_default']:
|
|
self.settings_panel.glyph_list.CheckItem(event.Index)
|
|
else:
|
|
self.settings_panel.glyph_list.CheckItem(event.Index, False)
|
|
self.update_preview()
|
|
event.Skip()
|
|
|
|
def on_kerning_update(self, event=None):
|
|
self.update_preview()
|
|
event.Skip()
|
|
|
|
def on_kerning_list_select(self, event=None):
|
|
self.update_preview()
|
|
event.Skip()
|
|
|
|
def on_horiz_adv_x_default_changed(self, event=None):
|
|
self.font_meta['horiz_adv_x_default'] = event.GetValue()
|
|
glyph_list = self.settings_panel.glyph_list
|
|
for i in range(glyph_list.ItemCount):
|
|
selected = glyph_list.IsItemChecked(i)
|
|
glyph = glyph_list.GetItem(i, 1).Text
|
|
if selected:
|
|
self.horiz_adv_x[glyph] = self.font_meta['horiz_adv_x_default']
|
|
self.update_preview()
|
|
|
|
def on_font_meta_value_changed(self, name, needs_update, event=None):
|
|
self.font_meta[name] = event.GetEventObject().GetValue()
|
|
if needs_update:
|
|
self.update_preview()
|
|
|
|
def on_keyword_changed(self, event=None):
|
|
keywords = []
|
|
selections = self.settings_panel.font_info.keywords.GetSelections()
|
|
for selection in selections:
|
|
cat_name = self.settings_panel.font_info.keywords.GetString(selection)
|
|
for category in FONT_CATEGORIES:
|
|
if cat_name == category.name:
|
|
keywords.append(category.id)
|
|
self.font_meta['keywords'] = keywords
|
|
|
|
def on_combine_indices_changed(self, event=None):
|
|
indices = self.settings_panel.font_settings.combine_at_sort_indices.GetValue()
|
|
if not indices:
|
|
self.font_meta['combine_at_sort_indices'] = ''
|
|
return
|
|
indices = indices.split(',')
|
|
try:
|
|
indices = [int(i) for i in indices]
|
|
except ValueError:
|
|
self.settings_panel.font_settings.combine_at_sort_indices.SetForegroundColour('red')
|
|
return
|
|
self.settings_panel.font_settings.combine_at_sort_indices.SetForegroundColour(wx.NullColour)
|
|
self.font_meta['combine_at_sort_indices'] = indices
|
|
|
|
def on_default_variant_change(self, event=None):
|
|
selection = self.settings_panel.font_info.default_variant.GetSelection()
|
|
value = '→'
|
|
if selection == 1:
|
|
value = '←'
|
|
elif selection == 2:
|
|
value = '↓'
|
|
elif selection == 3:
|
|
value = '↑'
|
|
self.font_meta['default_variant'] = value
|
|
self.update_preview()
|
|
|
|
def on_text_direction_changed(self, event=None):
|
|
selection = self.settings_panel.font_info.text_direction.GetSelection()
|
|
value = 'ltr'
|
|
if selection == 1:
|
|
value = 'rtl'
|
|
self.font_meta['text_direction'] = value
|
|
self.update_preview()
|
|
|
|
def on_letter_case_change(self, event=None):
|
|
selection = self.settings_panel.font_settings.letter_case.GetSelection()
|
|
value = ''
|
|
if selection == 1:
|
|
value = 'upper'
|
|
elif selection == 2:
|
|
value = 'lower'
|
|
self.font_meta['letter_case'] = value
|
|
|
|
def set_font_list(self):
|
|
self.fonts = {}
|
|
font_list = get_font_list()
|
|
for font in font_list:
|
|
self.fonts[font.marked_custom_font_name] = font
|
|
image = font.preview_image
|
|
if image is not None:
|
|
image = wx.Image(image)
|
|
# Windows requires all images to have the exact same size
|
|
image.Rescale(300, 20, quality=wx.IMAGE_QUALITY_HIGH)
|
|
self.settings_panel.font_chooser.Append(font.marked_custom_font_name, wx.Bitmap(image))
|
|
else:
|
|
self.settings_panel.font_chooser.Append(font.marked_custom_font_name)
|
|
|
|
def get_active_kerning_pair(self):
|
|
kerning_list = self.settings_panel.kerning_list
|
|
selection = kerning_list.GetFirstSelected()
|
|
if selection == -1:
|
|
return ''
|
|
kerning_pair = kerning_list.GetItem(selection, 0).Text
|
|
kerning = float(kerning_list.GetItem(selection, 1).Text)
|
|
if kerning_list.GetItem(selection, 2).Text:
|
|
try:
|
|
kerning = float(kerning_list.GetItem(selection, 2).Text)
|
|
self.kerning_pairs[kerning_pair] = float(kerning)
|
|
except (ValueError, IndexError):
|
|
pass
|
|
return kerning_pair
|
|
|
|
def on_glyph_item_checked(self, event=None):
|
|
self.get_active_glyph(event.Index)
|
|
self.update_preview()
|
|
|
|
def get_active_glyph(self, index=None):
|
|
glyph_list = self.settings_panel.glyph_list
|
|
if index is not None:
|
|
selection = index
|
|
else:
|
|
selection = glyph_list.GetFirstSelected()
|
|
if selection == -1:
|
|
return ''
|
|
glyph = glyph_list.GetItem(selection, 1).Text
|
|
if glyph_list.IsItemChecked(selection):
|
|
self.horiz_adv_x[glyph] = self.font_meta['horiz_adv_x_default']
|
|
return glyph
|
|
horiz_adv_x = float(glyph_list.GetItem(selection, 2).Text)
|
|
if glyph_list.GetItem(selection, 3).Text:
|
|
try:
|
|
horiz_adv_x = float(glyph_list.GetItem(selection, 3).Text)
|
|
self.horiz_adv_x[glyph] = float(horiz_adv_x)
|
|
except (ValueError, IndexError):
|
|
pass
|
|
return glyph
|
|
|
|
def on_font_changed(self, event=None):
|
|
selected_font = self.settings_panel.font_chooser.GetValue()
|
|
if selected_font:
|
|
self.font = self.fonts[selected_font]
|
|
else:
|
|
first = list(self.fonts.values())[0].marked_custom_font_name
|
|
self.font = self.fonts[first]
|
|
self.settings_panel.font_chooser.SetValue(first)
|
|
global_settings['last_font'] = self.font.marked_custom_font_name
|
|
self.kerning_pairs = self.font.kerning_pairs
|
|
self.font._load_variants()
|
|
self.default_variant = self.font.variants[self.font.json_default_variant]
|
|
self.glyphs = list(self.default_variant.glyphs.keys())
|
|
self.glyphs.sort()
|
|
self.horiz_adv_x = self.font.horiz_adv_x
|
|
|
|
kerning_combinations = combinations_with_replacement(self.glyphs, 2)
|
|
self.kerning_combinations = []
|
|
for combination in kerning_combinations:
|
|
self.kerning_combinations.append(f'{combination[0]} {combination[1]}')
|
|
self.kerning_combinations.append(f'{combination[1]} {combination[0]}')
|
|
self.kerning_combinations = list(set(self.kerning_combinations))
|
|
self.kerning_combinations.sort()
|
|
|
|
self.update_legacy_kerning_pairs()
|
|
self.update_settings()
|
|
self.update_kerning_list()
|
|
self.update_filter_list()
|
|
self.update_glyph_list()
|
|
self.update_preview()
|
|
|
|
def update_legacy_kerning_pairs(self):
|
|
new_list = defaultdict(list)
|
|
for kerning_pair, value in self.kerning_pairs.items():
|
|
if " " in kerning_pair:
|
|
# legacy kerning pairs do not use a space
|
|
return
|
|
if len(kerning_pair) < 2:
|
|
continue
|
|
pair = f'{kerning_pair[0]} {kerning_pair[1]}'
|
|
new_list[pair] = value
|
|
self.kerning_pairs = new_list
|
|
|
|
def update_settings(self):
|
|
# reset font_meta
|
|
self.font_meta = defaultdict(list)
|
|
self.font_meta['name'] = self.font.name
|
|
self.font_meta['description'] = self.font.metadata['description'] # untranslated description
|
|
self.font_meta['default_variant'] = self.font.json_default_variant
|
|
self.font_meta['text_direction'] = self.font.text_direction
|
|
self.font_meta['keywords'] = self.font.keywords
|
|
self.font_meta['default_glyph'] = self.font.default_glyph
|
|
self.font_meta['auto_satin'] = self.font.auto_satin
|
|
self.font_meta['letter_case'] = self.font.letter_case
|
|
self.font_meta['reversible'] = self.font.reversible
|
|
self.font_meta['sortable'] = self.font.sortable
|
|
self.font_meta['combine_at_sort_indices'] = self.font.combine_at_sort_indices
|
|
self.font_meta['leading'] = self.font.leading
|
|
self.font_meta['size'] = self.font.size
|
|
self.font_meta['max_scale'] = self.font.max_scale
|
|
self.font_meta['min_scale'] = self.font.min_scale
|
|
self.font_meta['horiz_adv_x_default'] = self.font.horiz_adv_x_default
|
|
self.font_meta['horiz_adv_x_space'] = self.font.word_spacing
|
|
|
|
# update ctrl
|
|
self.settings_panel.font_info.name.ChangeValue(self.font.name)
|
|
self.settings_panel.font_info.description.ChangeValue(self.font.metadata['description'])
|
|
selection = ['→', '←', '↓', '↑'].index(self.font.json_default_variant)
|
|
self.settings_panel.font_info.default_variant.SetSelection(selection)
|
|
selection = ['ltr', 'rtl'].index(self.font.text_direction)
|
|
self.settings_panel.font_info.text_direction.SetSelection(selection)
|
|
self.settings_panel.font_info.keywords.SetSelection(-1)
|
|
for category in FONT_CATEGORIES:
|
|
if category.id in self.font.keywords:
|
|
self.settings_panel.font_info.keywords.SetStringSelection(category.name)
|
|
self.settings_panel.font_settings.default_glyph.ChangeValue(self.font.default_glyph)
|
|
self.settings_panel.font_settings.auto_satin.SetValue(self.font.auto_satin)
|
|
selection = list(LETTER_CASE.keys())[list(LETTER_CASE.values()).index(self.font.letter_case)]
|
|
self.settings_panel.font_settings.letter_case.SetSelection(selection)
|
|
self.settings_panel.font_settings.reversible.SetValue(self.font.reversible)
|
|
self.settings_panel.font_settings.sortable.SetValue(self.font.sortable)
|
|
self.settings_panel.font_settings.combine_at_sort_indices.ChangeValue(
|
|
', '.join([str(i) for i in self.font.combine_at_sort_indices])
|
|
)
|
|
self.settings_panel.font_kerning.leading.SetValue(self.font.leading)
|
|
self.settings_panel.font_kerning.size.SetValue(self.font.size)
|
|
self.settings_panel.font_kerning.max_scale.SetValue(self.font.max_scale)
|
|
self.settings_panel.font_kerning.min_scale.SetValue(self.font.min_scale)
|
|
if self.font.horiz_adv_x_default is None:
|
|
self.settings_panel.font_kerning.horiz_adv_x_default.SetValue(0.0)
|
|
else:
|
|
self.settings_panel.font_kerning.horiz_adv_x_default.SetValue(self.font.horiz_adv_x_default)
|
|
self.settings_panel.font_kerning.horiz_adv_x_space.SetValue(self.font.word_spacing)
|
|
|
|
def update_filter_list(self):
|
|
# Update filter list
|
|
self.settings_panel.kerning_filter.Clear()
|
|
choices = [' '] + self.glyphs
|
|
self.settings_panel.kerning_filter.AppendItems(choices)
|
|
self.settings_panel.kerning_filter.update_choices(choices)
|
|
|
|
def update_kerning_list(self, filter_value=None):
|
|
kerning_list = self.settings_panel.kerning_list
|
|
# Add the rows
|
|
kerning_list.ClearAll()
|
|
# Add some columns
|
|
kerning_list.AppendColumn("Kerning pair", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
kerning_list.AppendColumn("Current kerning", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
kerning_list.AppendColumn("New kerning", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
for kerning_pair in self.kerning_combinations:
|
|
if filter_value is not None and filter_value.strip() not in kerning_pair:
|
|
continue
|
|
if self.font_meta['text_direction'] == 'rtl':
|
|
pair = kerning_pair.split()
|
|
kerning_pair = ' '.join(pair[::-1])
|
|
index = kerning_list.InsertItem(kerning_list.GetItemCount(), kerning_pair)
|
|
# kerning_list.SetItem(index, 0, kerning_pair)
|
|
kerning_list.SetItem(index, 1, str(self.kerning_pairs.get(kerning_pair, 0.0)))
|
|
if kerning_list.GetItemCount() != 0:
|
|
kerning_list.Select(0)
|
|
kerning_list.Focus(0)
|
|
|
|
def update_glyph_list(self):
|
|
glyph_list = self.settings_panel.glyph_list
|
|
# Add the rows
|
|
glyph_list.ClearAll()
|
|
# Add some columns
|
|
glyph_list.AppendColumn("Use default", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
glyph_list.AppendColumn("Glyph", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
glyph_list.AppendColumn("Current horizontal advance", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
glyph_list.AppendColumn("New horizontal advance", width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
horiz_adv_x_default = self.font.horiz_adv_x_default
|
|
for glyph in self.glyphs:
|
|
index = glyph_list.InsertItem(glyph_list.GetItemCount(), '')
|
|
horiz_adv = self.font.horiz_adv_x.get(glyph, horiz_adv_x_default)
|
|
if horiz_adv == horiz_adv_x_default:
|
|
glyph_list.CheckItem(index)
|
|
glyph_list.SetItem(index, 1, glyph)
|
|
glyph_list.SetItem(index, 2, str(horiz_adv))
|
|
if glyph_list.GetItemCount() != 0:
|
|
glyph_list.Select(0)
|
|
glyph_list.Focus(0)
|
|
|
|
def apply(self, event):
|
|
json_file = path.join(self.font.path, 'font.json')
|
|
|
|
if not path.isfile(json_file) or not path.isfile(json_file):
|
|
errormsg(_("Could not read json file."))
|
|
return
|
|
|
|
with open(json_file, 'r') as font_data:
|
|
data = json.load(font_data)
|
|
|
|
horiz_adv_x_default = self.font_meta['horiz_adv_x_default']
|
|
if horiz_adv_x_default == 0:
|
|
horiz_adv_x_default = None
|
|
|
|
for key, val in self.font_meta.items():
|
|
data[key] = val
|
|
horiz_adv_x = {key: val for key, val in self.horiz_adv_x.items() if val != self.font_meta['horiz_adv_x_default']}
|
|
kerning_pairs = {key: val for key, val in self.kerning_pairs.items() if val != 0}
|
|
data['horiz_adv_x'] = horiz_adv_x
|
|
data['kerning_pairs'] = kerning_pairs
|
|
data['glyphs'] = self.glyphs
|
|
|
|
# write data to font.json into the same directory as the font file
|
|
with open(json_file, 'w', encoding="utf8") as font_data:
|
|
json.dump(data, font_data, indent=4, ensure_ascii=False)
|
|
|
|
self.GetTopLevelParent().Close()
|
|
|
|
def cancel(self, event):
|
|
self.GetTopLevelParent().Close()
|
|
|
|
def update_preview(self, event=None):
|
|
self.preview_renderer.update()
|
|
|
|
def update_lettering(self):
|
|
del self.layer[:]
|
|
|
|
if self.settings_panel.notebook.GetSelection() in [3, 4]:
|
|
self.last_notebook_selection = self.settings_panel.notebook.GetSelection()
|
|
|
|
if self.last_notebook_selection == 3:
|
|
text = self.get_active_glyph()
|
|
else:
|
|
kerning = self.get_active_kerning_pair()
|
|
kerning = kerning.split()
|
|
text = ''.join(kerning)
|
|
if self.font_meta['text_direction'] == 'rtl':
|
|
text = ''.join(kerning[::-1])
|
|
if not text:
|
|
return
|
|
|
|
position_x = self._render_text(self.text_before, 0, True)
|
|
position_x = self._render_text(text, position_x, False)
|
|
self._render_text(self.text_after, position_x, True)
|
|
|
|
if self.default_variant.variant == FontVariant.RIGHT_TO_LEFT:
|
|
self.layer[:] = reversed(self.layer)
|
|
for group in self.layer:
|
|
group[:] = reversed(group)
|
|
|
|
def _render_text(self, text, position_x, use_character_position):
|
|
words = text.split()
|
|
for i, word in enumerate(words):
|
|
glyphs = []
|
|
skip = []
|
|
previous_is_binding = False
|
|
for i, character in enumerate(word):
|
|
if i in skip:
|
|
continue
|
|
if use_character_position:
|
|
glyph, glyph_len, previous_is_binding = self.default_variant.get_next_glyph(word, i, previous_is_binding)
|
|
else:
|
|
glyph, glyph_len = self.default_variant.get_glyph(character, word[i:])
|
|
glyphs.append(glyph)
|
|
skip = list(range(i, i+glyph_len))
|
|
|
|
last_character = None
|
|
for glyph in glyphs:
|
|
if glyph is None:
|
|
position_x += self.font_meta['horiz_adv_x_space']
|
|
last_character = None
|
|
continue
|
|
|
|
position_x = self._render_glyph(glyph, position_x, glyph.name, last_character)
|
|
last_character = glyph.name
|
|
position_x += self.font_meta['horiz_adv_x_space']
|
|
position_x -= self.font_meta['horiz_adv_x_space']
|
|
return position_x
|
|
|
|
def _render_glyph(self, glyph, position_x, character, last_character):
|
|
node = deepcopy(glyph.node)
|
|
if last_character is not None:
|
|
if self.font_meta['text_direction'] != 'rtl':
|
|
position_x += glyph.min_x - self.kerning_pairs.get(f'{last_character} {character}', 0)
|
|
else:
|
|
position_x += glyph.min_x - self.kerning_pairs.get(f'{character} {last_character}', 0)
|
|
|
|
transform = f"translate({position_x}, 0)"
|
|
node.set('transform', transform)
|
|
|
|
horiz_adv_x_default = self.font_meta['horiz_adv_x_default']
|
|
if horiz_adv_x_default in [0, None]:
|
|
horiz_adv_x_default = glyph.width + glyph.min_x
|
|
|
|
position_x += self.font.horiz_adv_x.get(character, horiz_adv_x_default) - glyph.min_x
|
|
|
|
self.font._update_commands(node, glyph)
|
|
self.font._update_clips(self.layer, node, glyph)
|
|
|
|
# this is used to recognize a glyph layer later in the process
|
|
# because this is not unique it will be overwritten by inkscape when inserted into the document
|
|
node.set("id", "glyph")
|
|
self.layer.add(node)
|
|
return position_x
|
|
|
|
def render_stitch_plan(self):
|
|
stitch_groups = []
|
|
try:
|
|
self.update_lettering()
|
|
elements = nodes_to_elements(self.layer.iterdescendants(SVG_PATH_TAG))
|
|
last_stitch_group = None
|
|
for element in elements:
|
|
check_stop_flag()
|
|
stitch_groups.extend(element.embroider(last_stitch_group))
|
|
if stitch_groups:
|
|
last_stitch_group = stitch_groups[-1]
|
|
|
|
if stitch_groups:
|
|
return stitch_groups_to_stitch_plan(
|
|
stitch_groups,
|
|
collapse_len=self.metadata['collapse_len_mm'],
|
|
min_stitch_len=self.metadata['min_stitch_len_mm']
|
|
)
|
|
except SystemExit:
|
|
raise
|
|
except ExitThread:
|
|
raise
|
|
except Exception:
|
|
raise
|
|
# Ignore errors. This can be things like incorrect paths for
|
|
# satins or division by zero caused by incorrect param values.
|
|
pass
|
|
|
|
def on_stitch_plan_rendered(self, stitch_plan):
|
|
self.simulator.stop()
|
|
self.simulator.load(stitch_plan)
|
|
self.simulator.go()
|