Commit ba7d4da4 authored by sumpfralle's avatar sumpfralle

added support for almost all missing DXF features - incl. 3D models

added a FontCache object for incremental font loading (useful for DXF files with text elements)


git-svn-id: https://pycam.svn.sourceforge.net/svnroot/pycam/trunk@1017 bbaffbd6-741e-11dd-a85d-61de82d9cad9
parent f16bc543
...@@ -11,7 +11,9 @@ Version 0.5 - UNRELEASED ...@@ -11,7 +11,9 @@ Version 0.5 - UNRELEASED
* added automatic repair of inconsistent polygon winding (inside/outside detection) * added automatic repair of inconsistent polygon winding (inside/outside detection)
* added toolpath cropping * added toolpath cropping
* added toolpath grid pattern: clone a single toolpath in rows and columns * added toolpath grid pattern: clone a single toolpath in rows and columns
* added support for DXF features "LWPOLYLINE" and "ARC" * added support for more DXF features:
* 2D: "LWPOLYLINE", "ARC", "TEXT", "MTEXT", "CIRCLE", "POLYLINE"
* 3D: "3DFACE"
* added a configuration setting for automatically loading a custom task settings file on startup * added a configuration setting for automatically loading a custom task settings file on startup
* added a simple "undo" feature for reversing model manipulations * added a simple "undo" feature for reversing model manipulations
* GCode features: * GCode features:
......
...@@ -39,6 +39,9 @@ TRANSFORMATIONS = { ...@@ -39,6 +39,9 @@ TRANSFORMATIONS = {
"x_swap_y": ((0, 1, 0, 0), (1, 0, 0, 0), (0, 0, 1, 0)), "x_swap_y": ((0, 1, 0, 0), (1, 0, 0, 0), (0, 0, 1, 0)),
"x_swap_z": ((0, 0, 1, 0), (0, 1, 0, 0), (1, 0, 0, 0)), "x_swap_z": ((0, 0, 1, 0), (0, 1, 0, 0), (1, 0, 0, 0)),
"y_swap_z": ((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0)), "y_swap_z": ((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0)),
"xy_mirror": ((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0)),
"xz_mirror": ((1, 0, 0, 0), (0, -1, 0, 0), (0, 0, 1, 0)),
"yz_mirror": ((-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0)),
} }
......
...@@ -481,7 +481,7 @@ class ContourModel(BaseModel): ...@@ -481,7 +481,7 @@ class ContourModel(BaseModel):
if progress_callback and progress_callback(): if progress_callback and progress_callback():
self.reset_cache() self.reset_cache()
return return
log.info("The winding of %d polygons was fixed." % change_counter) log.info("The winding of %d polygon(s) was fixed." % change_counter)
self.reset_cache() self.reset_cache()
def reverse_directions(self, callback=None): def reverse_directions(self, callback=None):
......
...@@ -58,8 +58,10 @@ class Point(object): ...@@ -58,8 +58,10 @@ class Point(object):
Otherwise the result is based on the individual x/y/z comparisons. Otherwise the result is based on the individual x/y/z comparisons.
""" """
if self.__class__ == other.__class__: if self.__class__ == other.__class__:
if (_is_near(self.x, other.x)) and (_is_near(self.y, other.y)) \ if (self.id == other.id) or \
and (_is_near(self.z, other.z)): ((_is_near(self.x, other.x)) and \
(_is_near(self.y, other.y)) and \
(_is_near(self.z, other.z))):
return 0 return 0
elif not _is_near(self.x, other.x): elif not _is_near(self.x, other.x):
return cmp(self.x, other.x) return cmp(self.x, other.x)
...@@ -71,12 +73,19 @@ class Point(object): ...@@ -71,12 +73,19 @@ class Point(object):
return cmp(str(self), str(other)) return cmp(str(self), str(other))
def transform_by_matrix(self, matrix, transformed_list=None, callback=None): def transform_by_matrix(self, matrix, transformed_list=None, callback=None):
# accept 3x4 matrices as well as 3x3 matrices
offsets = []
for column in matrix:
if len(column) < 4:
offsets.append(0)
else:
offsets.append(column[3])
x = self.x * matrix[0][0] + self.y * matrix[0][1] \ x = self.x * matrix[0][0] + self.y * matrix[0][1] \
+ self.z * matrix[0][2] + matrix[0][3] + self.z * matrix[0][2] + offsets[0]
y = self.x * matrix[1][0] + self.y * matrix[1][1] \ y = self.x * matrix[1][0] + self.y * matrix[1][1] \
+ self.z * matrix[1][2] + matrix[1][3] + self.z * matrix[1][2] + offsets[1]
z = self.x * matrix[2][0] + self.y * matrix[2][1] \ z = self.x * matrix[2][0] + self.y * matrix[2][1] \
+ self.z * matrix[2][2] + matrix[2][3] + self.z * matrix[2][2] + offsets[2]
self.x = x self.x = x
self.y = y self.y = y
self.z = z self.z = z
......
...@@ -42,6 +42,7 @@ from pycam.Geometry.Letters import TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, \ ...@@ -42,6 +42,7 @@ from pycam.Geometry.Letters import TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, \
import pycam.Geometry.Model import pycam.Geometry.Model
from pycam.Utils import ProgressCounter, check_uri_exists from pycam.Utils import ProgressCounter, check_uri_exists
from pycam.Toolpath import Bounds from pycam.Toolpath import Bounds
import pycam.Utils.FontCache
from pycam import VERSION from pycam import VERSION
import pycam.Physics.ode_physics import pycam.Physics.ode_physics
# this requires ODE - we import it later, if necessary # this requires ODE - we import it later, if necessary
...@@ -100,6 +101,11 @@ FILTER_MODEL = (("All supported model filetypes", ...@@ -100,6 +101,11 @@ FILTER_MODEL = (("All supported model filetypes",
FILTER_CONFIG = (("Config files", "*.conf"),) FILTER_CONFIG = (("Config files", "*.conf"),)
FILTER_EMC_TOOL = (("EMC tool files", "*.tbl"),) FILTER_EMC_TOOL = (("EMC tool files", "*.tbl"),)
CLIPBOARD_TARGETS = {
"svg": ("image/x-inkscape-svg", "image/svg+xml"),
"filename_drag": ("text/uri-list", "text-plain"),
}
PREFERENCES_DEFAULTS = { PREFERENCES_DEFAULTS = {
"enable_ode": False, "enable_ode": False,
"boundary_mode": -1, "boundary_mode": -1,
...@@ -237,37 +243,6 @@ def get_font_dir(): ...@@ -237,37 +243,6 @@ def get_font_dir():
+ "No fonts will be available.") % FONT_DIR_FALLBACK) + "No fonts will be available.") % FONT_DIR_FALLBACK)
return None return None
def get_font_files():
font_dir = get_font_dir()
if not font_dir:
return []
log.info("Loading font files from '%s'." % font_dir)
result = []
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)
result.sort()
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,
callback=progress_counter.update)
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: class ProjectGui:
...@@ -295,6 +270,9 @@ class ProjectGui: ...@@ -295,6 +270,9 @@ class ProjectGui:
self._progress_cancel_requested = False self._progress_cancel_requested = False
self._last_gtk_events_time = None self._last_gtk_events_time = None
self._undo_states = [] self._undo_states = []
self.clipboard = gtk.clipboard_get()
self._fonts_cache = pycam.Utils.FontCache.FontCache(get_font_dir(),
callback=self.update_progress_bar)
self.gui = gtk.Builder() self.gui = gtk.Builder()
gtk_build_file = get_data_file_location(GTKBUILD_FILE) gtk_build_file = get_data_file_location(GTKBUILD_FILE)
if gtk_build_file is None: if gtk_build_file is None:
...@@ -451,7 +429,6 @@ class ProjectGui: ...@@ -451,7 +429,6 @@ class ProjectGui:
self.update_font_dialog_preview) self.update_font_dialog_preview)
self._font_dialog_window_visible = False self._font_dialog_window_visible = False
self._font_dialog_window_position = None self._font_dialog_window_position = None
self._fonts = None
# set defaults # set defaults
self.model = None self.model = None
self.toolpath = pycam.Toolpath.ToolpathList() self.toolpath = pycam.Toolpath.ToolpathList()
...@@ -1945,14 +1922,13 @@ class ProjectGui: ...@@ -1945,14 +1922,13 @@ class ProjectGui:
if state is None: if state is None:
state = not self._font_dialog_window_visible state = not self._font_dialog_window_visible
if state: if state:
if self._fonts is None: if not self._fonts_cache.is_loading_complete():
self.update_progress_bar("Initializing fonts") self.update_progress_bar("Initializing fonts")
self._fonts = load_fonts(callback=self.update_progress_bar)
# create it manually to ease access # create it manually to ease access
font_selector = gtk.combo_box_new_text() font_selector = gtk.combo_box_new_text()
self.gui.get_object("FontSelectionBox").pack_start( self.gui.get_object("FontSelectionBox").pack_start(
font_selector, expand=False, fill=False) font_selector, expand=False, fill=False)
sorted_keys = self._fonts.keys() sorted_keys = list(self._fonts_cache.get_font_names())
sorted_keys.sort(key=lambda x: x.upper()) sorted_keys.sort(key=lambda x: x.upper())
for name in sorted_keys: for name in sorted_keys:
font_selector.append_text(name) font_selector.append_text(name)
...@@ -1964,7 +1940,7 @@ class ProjectGui: ...@@ -1964,7 +1940,7 @@ class ProjectGui:
self.update_font_dialog_preview) self.update_font_dialog_preview)
font_selector.show() font_selector.show()
self.font_selector = font_selector self.font_selector = font_selector
if self._fonts: if len(self._fonts_cache) > 0:
# show the dialog only if fonts are available # show the dialog only if fonts are available
if self._font_dialog_window_position: if self._font_dialog_window_position:
self.font_dialog_window.move( self.font_dialog_window.move(
...@@ -2002,8 +1978,9 @@ class ProjectGui: ...@@ -2002,8 +1978,9 @@ class ProjectGui:
align = value align = value
input_field.set_justification(justification) input_field.set_justification(justification)
font_name = self.font_selector.get_active_text() font_name = self.font_selector.get_active_text()
return self._fonts[font_name].render(text, skew=skew, charset = self._fonts_cache.get_font(font_name)
line_spacing=line_space, pitch=pitch, align=align) return charset.render(text, skew=skew, line_spacing=line_space,
pitch=pitch, align=align)
else: else:
# empty text # empty text
return None return None
...@@ -2024,23 +2001,27 @@ class ProjectGui: ...@@ -2024,23 +2001,27 @@ class ProjectGui:
text_buffer = StringIO.StringIO() text_buffer = StringIO.StringIO()
text_model.export(comment=self.get_meta_data(), text_model.export(comment=self.get_meta_data(),
unit=self.settings.get("unit")).write(text_buffer) unit=self.settings.get("unit")).write(text_buffer)
clipboard_target = "image/svg+xml" text_buffer.seek(0)
clipboard = gtk.clipboard_get() text = text_buffer.read()
targets = [(clipboard_target, gtk.TARGET_OTHER_WIDGET, 0)] self._copy_text_to_clipboard(text, CLIPBOARD_TARGETS["svg"])
def get_func(clipboard, selectiondata, info, text):
text.seek(0) def _copy_text_to_clipboard(self, text, targets):
selectiondata.set("STRING", 8, text.read()) targets = [(key, gtk.TARGET_OTHER_WIDGET, index)
result = clipboard.set_with_data(targets, get_func, for index, key in enumerate(targets)]
lambda *args: None, text_buffer) def get_func(clipboard, selectiondata, info, text):
clipboard.store() # Inkscape for Windows strictly requires the BITMAP type
selectiondata.set(gtk.gdk.SELECTION_TYPE_BITMAP, 8, text.read())
result = self.clipboard.set_with_data(targets, get_func,
lambda *args: None, text)
self.clipboard.store()
@gui_activity_guard @gui_activity_guard
def update_font_dialog_preview(self, widget=None, event=None): def update_font_dialog_preview(self, widget=None, event=None):
if not self._fonts: if len(self._fonts_cache) == 0:
# not initialized or empty # empty
return return
font_name = self.font_selector.get_active_text() font_name = self.font_selector.get_active_text()
font = self._fonts[font_name] font = self._fonts_cache.get_font(font_name)
self.gui.get_object("FontAuthorText").set_label( self.gui.get_object("FontAuthorText").set_label(
"\n".join(font.get_authors())) "\n".join(font.get_authors()))
preview_widget = self.gui.get_object("FontDialogPreview") preview_widget = self.gui.get_object("FontDialogPreview")
...@@ -2158,8 +2139,7 @@ class ProjectGui: ...@@ -2158,8 +2139,7 @@ class ProjectGui:
columns.append(model.get_value(it, column)) columns.append(model.get_value(it, column))
content.append(" ".join(columns)) content.append(" ".join(columns))
self.log_model.foreach(copy_row, content) self.log_model.foreach(copy_row, content)
clipboard = gtk.Clipboard() self.clipboard.set_text(os.linesep.join(content))
clipboard.set_text(os.linesep.join(content))
self.gui.get_object("StatusBarWarning").hide() self.gui.get_object("StatusBarWarning").hide()
@gui_activity_guard @gui_activity_guard
...@@ -2932,8 +2912,8 @@ class ProjectGui: ...@@ -2932,8 +2912,8 @@ class ProjectGui:
def configure_drag_and_drop(self, obj): def configure_drag_and_drop(self, obj):
obj.connect("drag-data-received", self.handle_data_drop) obj.connect("drag-data-received", self.handle_data_drop)
flags = gtk.DEST_DEFAULT_ALL flags = gtk.DEST_DEFAULT_ALL
targets = [("text/uri-list", gtk.TARGET_OTHER_APP, 0), targets = [(key, gtk.TARGET_OTHER_APP, index)
("text/plain", gtk.TARGET_OTHER_APP, 1)] for index, key in enumerate(CLIPBOARD_TARGETS["filename_drag"])]
actions = gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK | \ actions = gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK | \
gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_PRIVATE | \ gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_PRIVATE | \
gtk.gdk.ACTION_ASK gtk.gdk.ACTION_ASK
...@@ -2998,6 +2978,7 @@ class ProjectGui: ...@@ -2998,6 +2978,7 @@ class ProjectGui:
if self.load_model(importer(filename, if self.load_model(importer(filename,
program_locations=program_locations, program_locations=program_locations,
unit=self.settings.get("unit"), unit=self.settings.get("unit"),
fonts_cache=self._fonts_cache,
callback=self.update_progress_bar)): callback=self.update_progress_bar)):
self.set_model_filename(filename) self.set_model_filename(filename)
self.add_to_recent_file_list(filename) self.add_to_recent_file_list(filename)
...@@ -3565,7 +3546,7 @@ class ProjectGui: ...@@ -3565,7 +3546,7 @@ class ProjectGui:
self.menubar.set_sensitive(False) self.menubar.set_sensitive(False)
self.task_pane.set_sensitive(False) self.task_pane.set_sensitive(False)
self._progress_start_time = time.time() self._progress_start_time = time.time()
self.update_progress_bar("", 0) self.update_progress_bar(text="", percent=0)
self.progress_cancel_button.set_sensitive(True) self.progress_cancel_button.set_sensitive(True)
# enable "pulse" mode for a start (in case of unknown ETA) # enable "pulse" mode for a start (in case of unknown ETA)
self.progress_bar.pulse() self.progress_bar.pulse()
...@@ -3585,7 +3566,7 @@ class ProjectGui: ...@@ -3585,7 +3566,7 @@ class ProjectGui:
if not percent is None: if not percent is None:
percent = min(max(percent, 0.0), 100.0) percent = min(max(percent, 0.0), 100.0)
self.progress_bar.set_fraction(percent/100.0) self.progress_bar.set_fraction(percent/100.0)
if (percent is None) and (self.progress_bar.get_fraction() == 0): if (not percent) and (self.progress_bar.get_fraction() == 0):
# use "pulse" mode until we reach 1% of the work to be done # use "pulse" mode until we reach 1% of the work to be done
self.progress_bar.pulse() self.progress_bar.pulse()
# update the GUI # update the GUI
......
This diff is collapsed.
...@@ -29,7 +29,8 @@ import os ...@@ -29,7 +29,8 @@ import os
log = pycam.Utils.log.get_logger() log = pycam.Utils.log.get_logger()
def import_model(filename, program_locations=None, unit="mm", callback=None): def import_model(filename, program_locations=None, unit="mm", callback=None,
**kwargs):
if not check_uri_exists(filename): if not check_uri_exists(filename):
log.error("PSImporter: file (%s) does not exist" % filename) log.error("PSImporter: file (%s) does not exist" % filename)
return None return None
......
...@@ -52,8 +52,7 @@ def UniqueVertex(x, y, z): ...@@ -52,8 +52,7 @@ def UniqueVertex(x, y, z):
vertices += 1 vertices += 1
return Point(x, y, z) return Point(x, y, z)
def ImportModel(filename, use_kdtree=True, program_locations=None, unit=None, def ImportModel(filename, use_kdtree=True, callback=None, **kwargs):
callback=None):
global vertices, edges, kdtree global vertices, edges, kdtree
vertices = 0 vertices = 0
edges = 0 edges = 0
......
...@@ -81,7 +81,8 @@ def convert_eps2dxf(eps_filename, dxf_filename, location=None, unit="mm"): ...@@ -81,7 +81,8 @@ def convert_eps2dxf(eps_filename, dxf_filename, location=None, unit="mm"):
process.stderr.read())) process.stderr.read()))
return False return False
def import_model(filename, program_locations=None, unit="mm", callback=None): def import_model(filename, program_locations=None, unit="mm", callback=None,
**kwargs):
if not check_uri_exists(filename): if not check_uri_exists(filename):
log.error("SVGImporter: file (%s) does not exist" % filename) log.error("SVGImporter: file (%s) does not exist" % filename)
return None return None
......
#!/usr/bin/env python
# -*- 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/>.
"""
import pycam.Utils.log
import os
DEFAULT_NAMES = ("normal", "default", "standard")
log = pycam.Utils.log.get_logger()
class FontCache(object):
""" The FontCache gradually loads fonts. This is more efficient than an
immeadiate initialization of all fonts for the DXF importer.
Use "get_font" for loading (incrementally) fonts until the requested font
name was found.
The functions "get_names" and "len()" trigger an immediate initialization
of all available fonts.
"""
def __init__(self, font_dir=None, callback=None):
self.font_dir = font_dir
self.fonts = {}
self.callback = callback
self._unused_font_files = list(self._get_font_files())
def is_loading_complete(self):
return len(self._unused_font_files) == 0
def _get_font_files(self):
if self.font_dir is None:
return []
log.info("Font directory: %s" % self.font_dir)
result = []
files = os.listdir(self.font_dir)
for fname in files:
filename = os.path.join(self.font_dir, fname)
if filename.lower().endswith(".cxf") and os.path.isfile(filename):
result.append(filename)
result.sort()
return result
def __len__(self):
self._load_all_files()
return len(self.fonts)
def _get_font_without_loading(self, name):
for font_name in self.fonts:
if font_name.lower() == name.lower():
return self.fonts[font_name]
else:
return None
def get_font_names(self):
self._load_all_files()
return self.fonts.keys()
def get_font(self, name):
font = self._get_font_without_loading(name)
while not font and not self.is_loading_complete():
self._load_next_file()
font = self._get_font_without_loading(name)
if font:
return font
else:
# no font with that name is available
for other_name in DEFAULT_NAMES:
font = self._get_font_without_loading(other_name)
if font:
return font
else:
if self.fonts:
# return the first (random) font in the dictionary
return self.fonts.values()[0]
def _load_all_files(self):
while not self.is_loading_complete():
self._load_next_file()
def _load_next_file(self):
if self.is_loading_complete():
return
filename = self._unused_font_files.pop(0)
charset = pycam.Importers.CXFImporter.import_font(filename,
callback=self.callback)
if not charset is None:
for name in charset.get_names():
self.fonts[name] = charset
...@@ -112,7 +112,7 @@ class RepetitionsFilter(logging.Filter): ...@@ -112,7 +112,7 @@ class RepetitionsFilter(logging.Filter):
if self._suppressed_messages_counter > 0: if self._suppressed_messages_counter > 0:
# inject a message regarding the previously suppressed messages # inject a message regarding the previously suppressed messages
self._last_record.msg = \ self._last_record.msg = \
"*** %d similar messages were suppressed ***" % \ "*** skipped %d similar message(s) ***" % \
self._suppressed_messages_counter self._suppressed_messages_counter
self._handler.emit(self._last_record) self._handler.emit(self._last_record)
self._last_record = record self._last_record = record
......
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