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
* added automatic repair of inconsistent polygon winding (inside/outside detection)
* added toolpath cropping
* 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 simple "undo" feature for reversing model manipulations
* GCode features:
......
......@@ -39,6 +39,9 @@ TRANSFORMATIONS = {
"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)),
"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):
if progress_callback and progress_callback():
self.reset_cache()
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()
def reverse_directions(self, callback=None):
......
......@@ -58,8 +58,10 @@ class Point(object):
Otherwise the result is based on the individual x/y/z comparisons.
"""
if self.__class__ == other.__class__:
if (_is_near(self.x, other.x)) and (_is_near(self.y, other.y)) \
and (_is_near(self.z, other.z)):
if (self.id == other.id) or \
((_is_near(self.x, other.x)) and \
(_is_near(self.y, other.y)) and \
(_is_near(self.z, other.z))):
return 0
elif not _is_near(self.x, other.x):
return cmp(self.x, other.x)
......@@ -71,12 +73,19 @@ class Point(object):
return cmp(str(self), str(other))
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] \
+ 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] \
+ 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] \
+ self.z * matrix[2][2] + matrix[2][3]
+ self.z * matrix[2][2] + offsets[2]
self.x = x
self.y = y
self.z = z
......
......@@ -42,6 +42,7 @@ from pycam.Geometry.Letters import TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, \
import pycam.Geometry.Model
from pycam.Utils import ProgressCounter, check_uri_exists
from pycam.Toolpath import Bounds
import pycam.Utils.FontCache
from pycam import VERSION
import pycam.Physics.ode_physics
# this requires ODE - we import it later, if necessary
......@@ -100,6 +101,11 @@ FILTER_MODEL = (("All supported model filetypes",
FILTER_CONFIG = (("Config files", "*.conf"),)
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 = {
"enable_ode": False,
"boundary_mode": -1,
......@@ -237,37 +243,6 @@ def get_font_dir():
+ "No fonts will be available.") % FONT_DIR_FALLBACK)
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:
......@@ -295,6 +270,9 @@ class ProjectGui:
self._progress_cancel_requested = False
self._last_gtk_events_time = None
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()
gtk_build_file = get_data_file_location(GTKBUILD_FILE)
if gtk_build_file is None:
......@@ -451,7 +429,6 @@ class ProjectGui:
self.update_font_dialog_preview)
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()
......@@ -1945,14 +1922,13 @@ class ProjectGui:
if state is None:
state = not self._font_dialog_window_visible
if state:
if self._fonts is None:
if not self._fonts_cache.is_loading_complete():
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 = list(self._fonts_cache.get_font_names())
sorted_keys.sort(key=lambda x: x.upper())
for name in sorted_keys:
font_selector.append_text(name)
......@@ -1964,7 +1940,7 @@ class ProjectGui:
self.update_font_dialog_preview)
font_selector.show()
self.font_selector = font_selector
if self._fonts:
if len(self._fonts_cache) > 0:
# show the dialog only if fonts are available
if self._font_dialog_window_position:
self.font_dialog_window.move(
......@@ -2002,8 +1978,9 @@ class ProjectGui:
align = value
input_field.set_justification(justification)
font_name = self.font_selector.get_active_text()
return self._fonts[font_name].render(text, skew=skew,
line_spacing=line_space, pitch=pitch, align=align)
charset = self._fonts_cache.get_font(font_name)
return charset.render(text, skew=skew, line_spacing=line_space,
pitch=pitch, align=align)
else:
# empty text
return None
......@@ -2024,23 +2001,27 @@ class ProjectGui:
text_buffer = StringIO.StringIO()
text_model.export(comment=self.get_meta_data(),
unit=self.settings.get("unit")).write(text_buffer)
clipboard_target = "image/svg+xml"
clipboard = gtk.clipboard_get()
targets = [(clipboard_target, gtk.TARGET_OTHER_WIDGET, 0)]
def get_func(clipboard, selectiondata, info, text):
text.seek(0)
selectiondata.set("STRING", 8, text.read())
result = clipboard.set_with_data(targets, get_func,
lambda *args: None, text_buffer)
clipboard.store()
text_buffer.seek(0)
text = text_buffer.read()
self._copy_text_to_clipboard(text, CLIPBOARD_TARGETS["svg"])
def _copy_text_to_clipboard(self, text, targets):
targets = [(key, gtk.TARGET_OTHER_WIDGET, index)
for index, key in enumerate(targets)]
def get_func(clipboard, selectiondata, info, text):
# 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
def update_font_dialog_preview(self, widget=None, event=None):
if not self._fonts:
# not initialized or empty
if len(self._fonts_cache) == 0:
# empty
return
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(
"\n".join(font.get_authors()))
preview_widget = self.gui.get_object("FontDialogPreview")
......@@ -2158,8 +2139,7 @@ class ProjectGui:
columns.append(model.get_value(it, column))
content.append(" ".join(columns))
self.log_model.foreach(copy_row, content)
clipboard = gtk.Clipboard()
clipboard.set_text(os.linesep.join(content))
self.clipboard.set_text(os.linesep.join(content))
self.gui.get_object("StatusBarWarning").hide()
@gui_activity_guard
......@@ -2932,8 +2912,8 @@ class ProjectGui:
def configure_drag_and_drop(self, obj):
obj.connect("drag-data-received", self.handle_data_drop)
flags = gtk.DEST_DEFAULT_ALL
targets = [("text/uri-list", gtk.TARGET_OTHER_APP, 0),
("text/plain", gtk.TARGET_OTHER_APP, 1)]
targets = [(key, gtk.TARGET_OTHER_APP, index)
for index, key in enumerate(CLIPBOARD_TARGETS["filename_drag"])]
actions = gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_LINK | \
gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_PRIVATE | \
gtk.gdk.ACTION_ASK
......@@ -2998,6 +2978,7 @@ class ProjectGui:
if self.load_model(importer(filename,
program_locations=program_locations,
unit=self.settings.get("unit"),
fonts_cache=self._fonts_cache,
callback=self.update_progress_bar)):
self.set_model_filename(filename)
self.add_to_recent_file_list(filename)
......@@ -3565,7 +3546,7 @@ class ProjectGui:
self.menubar.set_sensitive(False)
self.task_pane.set_sensitive(False)
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)
# enable "pulse" mode for a start (in case of unknown ETA)
self.progress_bar.pulse()
......@@ -3585,7 +3566,7 @@ class ProjectGui:
if not percent is None:
percent = min(max(percent, 0.0), 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
self.progress_bar.pulse()
# update the GUI
......
This diff is collapsed.
......@@ -29,7 +29,8 @@ import os
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):
log.error("PSImporter: file (%s) does not exist" % filename)
return None
......
......@@ -52,8 +52,7 @@ def UniqueVertex(x, y, z):
vertices += 1
return Point(x, y, z)
def ImportModel(filename, use_kdtree=True, program_locations=None, unit=None,
callback=None):
def ImportModel(filename, use_kdtree=True, callback=None, **kwargs):
global vertices, edges, kdtree
vertices = 0
edges = 0
......
......@@ -81,7 +81,8 @@ def convert_eps2dxf(eps_filename, dxf_filename, location=None, unit="mm"):
process.stderr.read()))
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):
log.error("SVGImporter: file (%s) does not exist" % filename)
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):
if self._suppressed_messages_counter > 0:
# inject a message regarding the previously suppressed messages
self._last_record.msg = \
"*** %d similar messages were suppressed ***" % \
"*** skipped %d similar message(s) ***" % \
self._suppressed_messages_counter
self._handler.emit(self._last_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