Commit fff20cc9 authored by sumpfralle's avatar sumpfralle

added support for single-line-fonts (taken from QCAD)


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@821 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent 81c0d469
......@@ -2,6 +2,7 @@ Version 0.4.1 - UNRELEASED
* added support for EPS/PS contour files
* added adaptive positioning for DropCutter strategy (improves precision)
* allow conventional/climb milling style for ContourFollow and Engrave strategies
* added support for single-line fonts text (based on fonts from QCAD)
* visualize movements up to safety height properly
* unify DropCutter behaviour for models that are higher than the defined bounding box
* always move up to safety height in this case
......
......@@ -19,6 +19,9 @@
<separator />
<menuitem action="GeneralSettings"/>
</menu>
<menu action="ExtrasMenu">
<menuitem action="ShowFontDialog"/>
</menu>
<menu action="WindowMenu">
<menuitem action="Toggle3DView"/>
<menuitem action="ToggleLogWindow"/>
......
......@@ -6930,4 +6930,203 @@ Please read the description of the Server Mode (linked below) to understand the
<object class="GtkAction" id="HelpServerMode">
<property name="label">Server Mode</property>
</object>
<object class="GtkDialog" id="FontDialog">
<property name="border_width">5</property>
<property name="title" translatable="yes">Engrave text</property>
<property name="window_position">center</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="transient_for">ProjectWindow</property>
<property name="has_separator">False</property>
<child internal-child="vbox">
<object class="GtkVBox" id="dialog-vbox8">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkLabel" id="FontDialogProperties">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Font selection:&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="FontSelectionBox">
<property name="visible">True</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkHScale" id="FontSideShift">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Skew the text to the left or right side. The value is given in percent based on the height of the text.</property>
<property name="adjustment">FontSideSkewValue</property>
<property name="digits">0</property>
<property name="value_pos">bottom</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator5">
<property name="visible">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="FontDialogInputLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Text:&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkViewport" id="FontDialogInputViewPort">
<property name="visible">True</property>
<property name="resize_mode">queue</property>
<child>
<object class="GtkTextView" id="FontDialogInput">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="border_width">3</property>
<property name="wrap_mode">word</property>
<property name="buffer">FontDialogInputBuffer</property>
<property name="accepts_tab">False</property>
</object>
</child>
</object>
<packing>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkHSeparator" id="hseparator14">
<property name="visible">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="FontDialogPreviewLabel">
<property name="visible">True</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&lt;b&gt;Preview:&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkViewport" id="FontDialogPreviewViewPort">
<property name="height_request">120</property>
<property name="visible">True</property>
<property name="resize_mode">queue</property>
<property name="shadow_type">etched-in</property>
<child>
<object class="GtkDrawingArea" id="FontDialogPreview">
<property name="visible">True</property>
</object>
</child>
</object>
<packing>
<property name="position">7</property>
</packing>
</child>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="dialog-action_area7">
<property name="visible">True</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="FontDialogCancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="FontDialogSave">
<property name="label">gtk-save-as</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="FontDialogApply">
<property name="label">gtk-apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="pack_type">end</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">FontDialogCancel</action-widget>
<action-widget response="0">FontDialogSave</action-widget>
<action-widget response="0">FontDialogApply</action-widget>
</action-widgets>
</object>
<object class="GtkAdjustment" id="FontSideSkewValue">
<property name="lower">-100</property>
<property name="upper">100</property>
<property name="step_increment">5</property>
</object>
<object class="GtkAction" id="ShowFontDialog">
<property name="label">_Engrave text</property>
<property name="stock_id">gtk-select-font</property>
<property name="always_show_image">True</property>
</object>
<object class="GtkTextBuffer" id="FontDialogInputBuffer"/>
<object class="GtkAction" id="ExtrasMenu">
<property name="label">E_xtras</property>
</object>
</interface>
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2008-2010 Lode Leroy
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
This file is part of PyCAM.
PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from pycam.Geometry import TransformableContainer
from pycam.Geometry.Model import ContourModel
from pycam.Geometry.Line import Line
from pycam.Geometry.Point import Point
class Letter(TransformableContainer):
def __init__(self, lines):
self.lines = lines
def minx(self):
return min([line.minx for line in self.lines])
def maxx(self):
return max([line.maxx for line in self.lines])
def miny(self):
return min([line.miny for line in self.lines])
def maxy(self):
return max([line.maxy for line in self.lines])
def get_positioned_lines(self, base_point, skew=None):
result = []
# TODO: calculate skew
for line in self.lines:
new_line = Line(line.p1.add(base_point), line.p2.add(base_point))
result.append(new_line)
return result
class Charset(object):
def __init__(self, name=None, author=None, letterspacing=3.0,
wordspacing=6.75, linespacingfactor=1.0, encoding=None):
self.letters = {}
self.letterspacing = letterspacing
self.wordspacing = wordspacing
self.linespacingfactor = linespacingfactor
self.default_linespacing = 1.6
self.default_height = 10.0
if name is None:
self.names = []
else:
if isinstance(name, (list, set, tuple)):
self.names = name
else:
self.names = [name]
if author is None:
self.authors = []
else:
if isinstance(author, (list, set, tuple)):
self.authors = author
else:
self.authors = [author]
if encoding is None:
self.encoding = "iso-8859-1"
else:
self.encoding = encoding
def add_character(self, character, lines):
if len(lines) > 0:
self.letters[character] = Letter(lines)
def get_names(self):
return self.names
def render(self, text, origin=None, skew=0):
result = ContourModel()
if origin is None:
origin = Point(0, 0, 0)
base = origin
for line in text.splitlines():
line_height = self.default_height
for character in line:
if character == " ":
base = base.add(Point(self.wordspacing, 0, 0))
elif character in self.letters:
charset_letter = self.letters[character]
for line in charset_letter.get_positioned_lines(base):
result.append(line)
# update line height
line_height = max(line_height, charset_letter.maxy())
# shift the base position
base = base.add(Point(
charset_letter.maxx() + self.letterspacing, 0, 0))
else:
# unknown character - add a small whitespace
base = base.add(Point(self.letterspacing, 0, 0))
# go to the next line
line_spacing = line_height * self.default_linespacing * self.linespacingfactor
base = Point(origin.x, base.y - line_spacing, origin.z)
return result
......@@ -23,9 +23,9 @@ along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
__all__ = ["utils", "Line", "Model", "Path", "Plane", "Point", "Triangle",
"PolygonExtractor", "TriangleKdtree", "intersection", "kdtree",
"Matrix", "Polygon"]
"Matrix", "Polygon", "Letters"]
from pycam.Geometry.utils import epsilon
from pycam.Geometry.utils import epsilon, ceil
import math
......@@ -81,6 +81,41 @@ def get_angle_pi(p1, p2, p3, up_vector, pi_factor=False):
else:
return angle
def get_points_of_arc(center, radius, a1, a2, plane=None, cords=16):
""" return the points for an approximated arc
@param center: center of the circle
@type center: pycam.Geometry.Point.Point
@param radius: radius of the arc
@type radius: float
@param a1: angle of the start (in degree)
@type a1: float
@param a2: angle of the end (in degree)
@type a2: float
@param plane: the plane of the circle (default: xy-plane)
@type plane: pycam.Geometry.Plane.Plane
@param cords: number of lines for a full circle
@type cords: int
@return: a list of lines approximating the arc
@rtype: list(pycam.Geometry.Line.Line)
"""
# TODO: implement 3D arc and respect "plane"
a1 = math.pi * a1 / 180
a2 = math.pi * a2 / 180
angle_diff = a2 - a1
if angle_diff < 0:
angle_diff += 2 * math.pi
num_of_segments = ceil(angle_diff / (2 * math.pi) * cords)
angle_segment = angle_diff / num_of_segments
points = []
get_angle_point = lambda angle: (
center.x + radius * math.cos(angle),
center.y + radius * math.sin(angle))
points.append(get_angle_point(a1))
for index in range(num_of_segments):
points.append(get_angle_point(a1 + angle_segment * (index + 1)))
return points
class TransformableContainer(object):
""" a base class for geometrical objects containing other elements
......
......@@ -28,11 +28,13 @@ import pycam.Gui.Settings
import pycam.Cutters
import pycam.Toolpath.Generator
import pycam.Toolpath
import pycam.Importers.CXFImporter
import pycam.Importers
import pycam.Utils.log
import pycam.Utils
from pycam.Geometry.utils import sqrt
from pycam.Gui.OpenGLTools import ModelViewWindowGL
from pycam.Utils import ProgressCounter
from pycam.Toolpath import Bounds
from pycam import VERSION
import pycam.Physics.ode_physics
......@@ -56,6 +58,8 @@ DATA_DIR_ENVIRON_KEY = "PYCAM_DATA_DIR"
DATA_BASE_DIRS = [os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
os.pardir, "share", "gtk-interface"),
os.path.join(sys.prefix, "share", "pycam", "ui")]
# TODO: improve this definition of the fonts' location
FONT_DIRS = [os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "share", "fonts")]
# necessary for "pyinstaller"
if "_MEIPASS2" in os.environ:
DATA_BASE_DIRS.insert(0, os.environ["_MEIPASS2"])
......@@ -154,6 +158,32 @@ def get_filters_from_list(filter_list, file_filter=True):
result.append(file_filter)
return result
def get_font_files():
result = []
for font_dir in FONT_DIRS:
files = os.listdir(font_dir)
for fname in files:
filename = os.path.join(font_dir, fname)
if filename.lower().endswith(".cxf") and os.path.isfile(filename):
result.append(filename)
return result
def load_fonts(callback=None):
fonts = {}
all_font_files = get_font_files()
if not callback is None:
progress_counter = ProgressCounter(len(all_font_files), callback)
else:
progress_counter = None
for font_file in all_font_files:
charset = pycam.Importers.CXFImporter.import_font(font_file)
if (not progress_counter is None) and progress_counter.increment():
break
if not charset is None:
for name in charset.get_names():
fonts[name] = charset
return fonts
class ProjectGui:
......@@ -217,6 +247,7 @@ class ProjectGui:
("Toggle3DView", self.toggle_3d_view, None, "<Control><Shift>v"),
("ToggleLogWindow", self.toggle_log_window, None, "<Control>l"),
("ToggleProcessPoolWindow", self.toggle_process_pool_window, None, None),
("ShowFontDialog", self.toggle_font_dialog_window, None, "<Control><Shift>t"),
("HelpIntroduction", self.show_help, "Introduction", "F1"),
("HelpSupportedFormats", self.show_help, "SupportedFormats", None),
("HelpModelTransformations", self.show_help, "ModelTransformations", None),
......@@ -286,6 +317,21 @@ class ProjectGui:
self.gui.get_object("ProcessPoolWindowClose").connect("clicked", self.toggle_process_pool_window, False)
self.gui.get_object("ProcessPoolRefreshInterval").set_value(3)
self.process_pool_model = self.gui.get_object("ProcessPoolStatisticsModel")
# "font dialog" window
self.font_dialog_window = self.gui.get_object("FontDialog")
self.font_dialog_window.connect("delete-event",
self.toggle_font_dialog_window, False)
self.font_dialog_window.connect("destroy",
self.toggle_font_dialog_window, False)
self.gui.get_object("FontDialogCancel").connect("clicked",
self.toggle_font_dialog_window, False)
self.gui.get_object("FontDialogApply").connect("clicked",
self.import_from_font_dialog)
self.gui.get_object("FontDialogSave").connect("clicked",
self.export_from_font_dialog)
self._font_dialog_window_visible = False
self._font_dialog_window_position = None
self._fonts = None
# set defaults
self.model = None
self.toolpath = pycam.Toolpath.ToolpathList()
......@@ -1382,6 +1428,60 @@ class ProjectGui:
obj.set_value(default_value)
self.gui.get_object("ExportEMCToolDefinition").set_sensitive(len(self.tool_list) > 0)
@progress_activity_guard
@gui_activity_guard
def toggle_font_dialog_window(self, widget=None, event=None, state=None):
# only "delete-event" uses four arguments
# TODO: unify all these "toggle" functions for different windows into one single function (including storing the position)
if state is None:
state = event
if state is None:
state = not self._font_dialog_window_visible
if state:
if self._fonts is None:
self.update_progress_bar("Initializing fonts")
self._fonts = load_fonts(callback=self.update_progress_bar)
# create it manually to ease access
font_selector = gtk.combo_box_new_text()
self.gui.get_object("FontSelectionBox").pack_start(
font_selector, expand=False, fill=False)
sorted_keys = self._fonts.keys()
sorted_keys.sort()
for name in sorted_keys:
font_selector.append_text(name)
if sorted_keys:
font_selector.set_active(0)
else:
# TODO: show some warning - no fonts found
pass
font_selector.show()
self.font_selector = font_selector
if self._font_dialog_window_position:
self.font_dialog_window.move(
*self._font_dialog_window_position)
self.font_dialog_window.show()
else:
self._font_dialog_window_position = \
self.font_dialog_window.get_position()
self.font_dialog_window.hide()
self._font_dialog_window_visible = state
# don't close the window - just hide it (for "delete-event")
return True
@gui_activity_guard
def import_from_font_dialog(self, widget=None):
text_buffer = self.gui.get_object("FontDialogInput").get_buffer()
text = text_buffer.get_text(text_buffer.get_start_iter(),
text_buffer.get_end_iter())
font_name = self.font_selector.get_active_text()
new_model = self._fonts[font_name].render(text)
self.load_model(new_model)
self.append_to_queue(self.toggle_font_dialog_window)
@gui_activity_guard
def export_from_font_dialog(self, widget=None):
self.append_to_queue(self.toggle_font_dialog_window)
@gui_activity_guard
def toggle_about_window(self, widget=None, event=None, state=None):
# only "delete-event" uses four arguments
......@@ -2660,12 +2760,15 @@ class ProjectGui:
# update the GUI
current_time = time.time()
# Don't update the GUI more often than once per second.
# Exception: text-only updates
# This restriction improves performance and reduces the
# "snappiness" of the GUI.
if (self._last_gtk_events_time is None) \
or (percent is None) \
or (self._last_gtk_events_time + 1 < current_time):
while gtk.events_pending():
gtk.main_iteration()
if not percent is None:
self._last_gtk_events_time = current_time
# return if the user requested a break
return self._progress_cancel_requested
......
# -*- coding: utf-8 -*-
"""
$Id$
Copyright 2010 Lars Kruse <devel@sumpfralle.de>
This file is part of PyCAM.
PyCAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
PyCAM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PyCAM. If not, see <http://www.gnu.org/licenses/>.
"""
from pycam.Geometry.Letters import Charset
from pycam.Geometry.Line import Line
from pycam.Geometry.Point import Point
from pycam.Geometry import get_points_of_arc
import pycam.Utils.log
log = pycam.Utils.log.get_logger()
class _CXFParseError(BaseException):
pass
class _LineFeeder(object):
def __init__(self, items):
self.items = items
self.index = 0
def consume(self):
if not self.is_empty():
result = self.get()
self.index += 1
else:
result = None
return result
def get(self):
if not self.is_empty():
return self.items[self.index].strip()
else:
return None
def is_empty(self):
return self.index >= len(self.items)
def get_index(self):
return self.index + 1
class CXFParser(object):
META_KEYWORDS = ("letterspacing", "wordspacing", "linespacingfactor",
"encoding")
META_KEYWORDS_MULTI = ("author", "name")
def __init__(self, stream):
self.letters = {}
self.meta = {}
finished = False
feeder = _LineFeeder(stream.readlines())
while not feeder.is_empty():
line = feeder.consume()
if not line:
# ignore
pass
elif line.startswith("#"):
# comment or meta data
content = line[1:].split(":", 1)
if len(content) == 2:
key = content[0].lower().strip()
value = content[1].strip()
if key in self.META_KEYWORDS:
try:
if key != "encoding":
self.meta[key] = float(value)
else:
self.meta[key] = value
except ValueError:
raise _CXFParseError("Invalid meta information " \
+ "in line %d" % feeder.get_index())
elif key in self.META_KEYWORDS_MULTI:
if key in self.meta:
self.meta[key].append(value)
else:
self.meta[key] = [value]
else:
# unknown -> ignore
pass
elif line.startswith("["):
if (len(line) >= 3) and (line[2] == "]"):
# single character
character = line[1]
elif (len(line) >= 6) and (line[5] == "]"):
# unicode character (e.g. "[1ae4]")
try:
character = unichr(int(line[1:5], 16))
except ValueError:
raise _CXFParseError("Failed to parse unicode " \
+ "character at line %d" % feeder.get_index())
elif (len(line) > 3) and (line.find("]") > 2):
# read UTF8 (qcad 1 compatibility)
end_bracket = line.find("] ")
text = line[1:end_bracket]
character = text.decode("utf-8")[0]
else:
# unknown format
raise _CXFParseError("Failed to parse character at line " \
+ "%d" % feeder.get_index())
# parse the following lines up to the next empty line
char_definition = []
while not feeder.is_empty() and (len(feeder.get()) > 0):
line = feeder.consume()
coords = [float(value) for value in line[2:].split(",")]
type_char = line[0].upper()
if (type_char == "L") and (len(coords) == 4):
# line
p1 = Point(coords[0], coords[1], 0)
p2 = Point(coords[2], coords[3], 0)
char_definition.append(Line(p1, p2))
elif (type_char == "A") and (len(coords) == 5):
# arc
previous = None
center = Point(coords[0], coords[1], 0)
radius = coords[2]
start_angle, end_angle = coords[3], coords[4]
for p in get_points_of_arc(center, radius, start_angle,
end_angle):
current = Point(p[0], p[1], 0)
if not previous is None:
char_definition.append(Line(previous, current))
previous = current
else:
raise _CXFParseError("Failed to read item coordinates" \
+ " in line %d" % feeder.get_index())
self.letters[character] = char_definition
else:
# unknown line format
raise _CXFParseError("Failed to parse unknown content in " \
+ "line %d" % feeder.get_index())
def import_font(filename, program_locations=None, unit=None):
try:
infile = open(filename,"r")
except IOError, err_msg:
log.error("CXFImporter: Failed to read file (%s): %s" \
% (filename, err_msg))
return None
try:
parsed_font = CXFParser(infile)
except _CXFParseError, err_msg:
log.error("CFXImporter: Skipped font defintion file '%s'. Reason: %s" \
% (filename, err_msg))
return None
charset = Charset(**parsed_font.meta)
for key, value in parsed_font.letters.iteritems():
charset.add_character(key, value)
log.info("CXFImporter: Imported CXF font from '%s': %d letters" \
% (filename, len(parsed_font.letters)))
infile.close()
return charset
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment