Example #1
0
 def set_document(
     self,
     document: Drawing,
     auditor: Auditor,
     *,
     layout: str = "Model",
     overall_scaling_factor: float = 1.0,
 ):
     error_count = len(auditor.errors)
     if error_count > 0:
         ret = qw.QMessageBox.question(
             self,
             "Found DXF Errors",
             f'Found {error_count} errors in file "{document.filename}"\n'
             f"Load file anyway? ",
         )
         if ret == qw.QMessageBox.No:
             auditor.print_error_report(auditor.errors)
             return
     self.doc = document
     self._render_context = RenderContext(document)
     self._reset_backend(scale=overall_scaling_factor)
     self._visible_layers = None
     self._current_layout = None
     self._populate_layouts()
     self._populate_layer_list()
     self.draw_layout(layout)
     self.setWindowTitle("CAD Viewer - " + str(document.filename))
Example #2
0
    def convert_dxf2img(self,
                        names,
                        img_format=default_img_format,
                        img_res=default_img_res):
        for name in names:
            doc = ezdxf.readfile(name)
            msp = doc.modelspace()
            # Recommended: audit & repair DXF document before rendering
            auditor = doc.audit()
            # The auditor.errors attribute stores severe errors,
            # which *may* raise exceptions when rendering.
            if len(auditor.errors) != 0:
                raise exception(
                    "The DXF document is damaged and can't be converted!")
            else:
                fig = plt.figure()
                ax = fig.add_axes([0, 0, 1, 1])
                ctx = RenderContext(doc)
                ctx.set_current_layout(msp)
                ctx.current_layout.set_colors(bg='#FFFFFF')
                out = MatplotlibBackend(ax)
                Frontend(ctx, out).draw_layout(msp, finalize=True)

                img_name = re.findall(
                    "(\S+)\.", name
                )  # select the image name that is the same as the dxf file name
                first_param = ''.join(
                    img_name) + img_format  #concatenate list and string
                fig.savefig(first_param, dpi=img_res)


#if __name__ == '__main__':
#    first =  DXF2IMG()
#    first.convert_dxf2img(['slot_star.dxf'],img_format='.png')
Example #3
0
    def convert_dxf2img(self,
                        names,
                        img_format=default_img_format,
                        img_res=default_img_res,
                        clr=default_bg_color):
        for name in names:
            doc = ezdxf.readfile(name)
            msp = doc.modelspace()
            # Recommended: audit & repair DXF document before rendering
            auditor = doc.audit()
            # The auditor.errors attribute stores severe errors,
            # which *may* raise exceptions when rendering.
            if len(auditor.errors) != 0:
                raise Exception(
                    "This DXF document is damaged and can't be converted! --> ",
                    name)
                name = name = +1
            else:
                fig = plt.figure()
                ax = fig.add_axes([0, 0, 1, 1])
                ctx = RenderContext(doc)
                ctx.set_current_layout(msp)
                ezdxf.addons.drawing.properties.MODEL_SPACE_BG_COLOR = clr
                out = MatplotlibBackend(ax)
                Frontend(ctx, out).draw_layout(msp, finalize=True)

                img_name = re.findall(
                    "(\S+)\.", name
                )  # select the image name that is the same as the dxf file name
                first_param = ''.join(
                    img_name) + img_format  #concatenate list and string
                fig.savefig(first_param, dpi=img_res)
                print(name, " Converted Successfully")
Example #4
0
 def set_document(self, document: Drawing):
     self.doc = document
     self._render_context = RenderContext(document)
     self._visible_layers = None
     self._current_layout = None
     self._populate_layouts()
     self._populate_layer_list()
     self.draw_layout('Model')
Example #5
0
def _get_text_visible_when(doc: Drawing, active_layers: Set[str]) -> List[str]:
    ctx = RenderContext(doc)
    # set given layer to ON, others to OFF
    ctx.set_layers_state(active_layers, state=True)

    backend = BasicBackend()
    Frontend(ctx, backend).draw_layout(doc.modelspace())
    visible_text = [x[1] for x in backend.collector if x[0] == 'text']
    return visible_text
Example #6
0
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('dxf_file')
    parser.add_argument('output_path')
    parser.add_argument(
        "--scale",
        "-s",
        type=float,
        default=1.0,
        help="text scaling factor",
    )
    args = parser.parse_args()

    doc = ezdxf.readfile(args.dxf_file)
    layout = doc.modelspace()

    fig: plt.Figure = plt.figure()
    ax: plt.Axes = fig.add_axes((0, 0, 1, 1))
    ctx = RenderContext(layout.doc)
    layout_properties = LayoutProperties.from_layout(layout)
    out = FixedSizedTextMatplotlibBackend(ax, text_size_scale=args.scale)
    Frontend(ctx, out).draw_layout(
        layout,
        finalize=True,
        layout_properties=layout_properties,
    )
    fig.savefig(
        args.output_path,
        dpi=300,
        facecolor=ax.get_facecolor(),
        transparent=True,
    )
    plt.close(fig)
Example #7
0
def _main():
    parser = argparse.ArgumentParser(
        description='draw the given CAD file and save it to a file or view it')
    parser.add_argument('cad_file', nargs='?')
    parser.add_argument('--supported_formats', action='store_true')
    parser.add_argument('--layout', default='Model')
    parser.add_argument('--out', required=False)
    parser.add_argument('--dpi', type=int, default=300)
    parser.add_argument('--ltype', default='internal')
    args = parser.parse_args()

    if args.supported_formats:
        fig = plt.figure()
        for extension, description in fig.canvas.get_supported_filetypes().items():
            print(f'{extension}: {description}')
        sys.exit()

    if args.cad_file is None:
        print('no CAD file specified')
        sys.exit(1)

    try:
        doc = ezdxf.readfile(args.cad_file)
    except IOError:
        print(f'Not a DXF file or a generic I/O error.')
        sys.exit(2)
    except ezdxf.DXFError:
        try:
            doc, auditor = recover.readfile(args.cad_file)
        except ezdxf.DXFStructureError:
            print(f'Invalid or corrupted DXF file: {args.cad_file}')
            sys.exit(3)
    else:
        auditor = doc.audit()

    if auditor.has_errors:
        # But is most likely good enough for rendering.
        print(f'Found {len(auditor.errors)} unrecoverable errors.')
    if auditor.has_fixes:
        print(f'Fixed {len(auditor.fixes)} errors.')

    try:
        layout = doc.layouts.get(args.layout)
    except KeyError:
        print(f'Could not find layout "{args.layout}". '
              f'Valid layouts: {[l.name for l in doc.layouts]}')
        sys.exit(4)

    fig: plt.Figure = plt.figure()
    ax: plt.Axes = fig.add_axes([0, 0, 1, 1])
    ctx = RenderContext(doc)
    out = MatplotlibBackend(ax, params={'linetype_renderer': args.ltype})
    Frontend(ctx, out).draw_layout(layout, finalize=True)
    if args.out is not None:
        print(f'saving to "{args.out}"')
        fig.savefig(args.out, dpi=args.dpi)
        plt.close(fig)
    else:
        plt.show()
Example #8
0
 def set_document(self, document: Drawing, auditor: Auditor):
     error_count = len(auditor.errors)
     if error_count > 0:
         ret = qw.QMessageBox.question(
             self, 'Found DXF Errors',
             f'Found {error_count} errors in file "{document.filename}"\nLoad file anyway? '
         )
         if ret == qw.QMessageBox.No:
             auditor.print_error_report(auditor.errors)
             return
     self.doc = document
     self._render_context = RenderContext(document)
     self._reset_backend()  # clear caches
     self._visible_layers = None
     self._current_layout = None
     self._populate_layouts()
     self._populate_layer_list()
     self.draw_layout('Model')
     self.setWindowTitle('CAD Viewer - ' + str(document.filename))
Example #9
0
    def set_document(self, document: Drawing):
        self.doc = document
        self._render_context = RenderContext(document)
        self._visible_layers = None
        self._current_layout = None
        # self._populate_layouts()
        self._populate_layer_list()
        self.draw_layout('Model')
        self.model_space = self.doc.modelspace()
        self.all_dxf_entities = {}
        for entity in self.model_space.entity_space:
            self.all_dxf_entities[str(entity)] = entity
            if entity.DXFTYPE == "CIRCLE":
                if hasattr(entity.dxf, 'extrusion'):
                    print(entity.dxf.extrusion)
                    if entity.dxf.extrusion[2] == -1:
                        entity.dxf.extrusion = Vector(0, 0, 1)
                        # entity.dxf.center = Vector(-(entity.dxf.center.x), -(entity.dxf.center.y), 0)

        self.draw_origin()
        self.apply_origin_to_scene()
def main(rows: int, cols: int):
    doc = ezdxf.new()
    msp = doc.modelspace()
    create_content(msp)

    # Detecting the drawing extents by ezdxf:
    # The bounding box cache can be reused for entity filtering.
    # This cache is a lightweight object, which is compatible to the pickle
    # module, DXF entities are referenced by handle strings. (multiprocessing!)
    cache = bbox.Cache()

    # The bounding box calculation can take along time for big DXF files!
    # If flatten=0 the bounding box calculation for curves (SPLINE, ELLIPSE, ...)
    # is based on the control points of the Path class, this is less precise but
    # can speed up the calculation and for this task is a precise bounding box
    # not required.
    # This has no impact on this example which uses only straight lines
    extents = bbox.extents(msp, cache=cache, flatten=0)

    ctx = RenderContext(doc)
    for tile, render_area in enumerate(render_areas(extents, (rows, cols))):
        # Setup drawing add-on:
        fig = plt.figure(dpi=DPI)
        ax = fig.add_axes([0, 0, 1, 1])
        out = MatplotlibBackend(ax)

        ax.set_xlim(render_area.extmin.x, render_area.extmax.x)
        ax.set_ylim(render_area.extmin.y, render_area.extmax.y)

        # Disable rendering of entities outside of the render area:
        def is_intersecting_render_area(entity):
            """Returns True if entity should be rendered. """
            entity_bbox = bbox.extents([entity], cache=cache, flatten=0)
            return render_area.has_intersection(entity_bbox)

        # Finalizing invokes auto-scaling!
        Frontend(ctx, out).draw_layout(msp,
                                       finalize=False,
                                       filter_func=is_intersecting_render_area)

        # Set output size in inches:
        # width = 6 inch x 300 dpi = 1800 px
        # height = 3 inch x 300 dpi = 900 px
        fig.set_size_inches(6, 3, forward=True)

        filename = f"tile-{tile:02d}.png"
        print(f'saving tile #{tile} to "{filename}"')
        fig.savefig(DIR / filename, dpi=DPI)
        plt.close(fig)
Example #11
0
def _main():
    parser = argparse.ArgumentParser(description='draw the given CAD file and save it to a file or view it')
    parser.add_argument('cad_file', nargs='?')
    parser.add_argument('--supported_formats', action='store_true')
    parser.add_argument('--layout', default='Model')
    parser.add_argument('--out', required=False)
    parser.add_argument('--dpi', type=int, default=300)
    args = parser.parse_args()

    if args.supported_formats:
        fig = plt.figure()
        for extension, description in fig.canvas.get_supported_filetypes().items():
            print(f'{extension}: {description}')
        sys.exit()

    if args.cad_file is None:
        print('no CAD file specified')
        sys.exit(1)

    doc = ezdxf.readfile(args.cad_file)
    try:
        layout = doc.layouts.get(args.layout)
    except KeyError:
        print(f'could not find layout "{args.layout}". Valid layouts: {[l.name for l in doc.layouts]}')
        sys.exit(1)

    fig: plt.Figure = plt.figure()
    ax: plt.Axes = fig.add_axes([0, 0, 1, 1])
    ctx = RenderContext(doc)
    out = MatplotlibBackend(ax)
    Frontend(ctx, out).draw_layout(layout, finalize=True)
    if args.out is not None:
        print(f'saving to "{args.out}"')
        fig.savefig(args.out, dpi=args.dpi)
        plt.close(fig)
    else:
        plt.show()
Example #12
0
    def run(args):
        # Import on demand for a quicker startup:
        try:
            import matplotlib.pyplot as plt
        except ImportError:
            print('Matplotlib package not found.')
            sys.exit(2)

        from ezdxf.addons.drawing import RenderContext, Frontend
        from ezdxf.addons.drawing.matplotlib import MatplotlibBackend

        # Print all supported export formats:
        if args.formats:
            fig = plt.figure()
            for extension, description in fig.canvas.get_supported_filetypes(
            ).items():
                print(f'{extension}: {description}')
            sys.exit(0)

        if args.file:
            filename = args.file
        else:
            print('argument FILE is required')
            sys.exit(1)

        doc, _ = load_document(filename)
        fig = plt.figure()
        ax = fig.add_axes([0, 0, 1, 1])
        ctx = RenderContext(doc)
        out = MatplotlibBackend(ax, params={'linetype_renderer': args.ltype})
        Frontend(ctx, out).draw_layout(doc.modelspace(), finalize=True)
        if args.out is not None:
            print(f'exporting to "{args.out}"')
            fig.savefig(args.out, dpi=args.dpi)
            plt.close(fig)
        else:
            plt.show()
Example #13
0
# Copyright (c) 2020, Manfred Moitzi
# License: MIT License

import ezdxf
from pathlib import Path
import matplotlib.pyplot as plt
from ezdxf.addons.drawing import Frontend, RenderContext
from ezdxf.addons.drawing.matplotlib_backend import MatplotlibBackend
from ezdxf.math import global_bspline_interpolation

wave = [(0.0, 0.0), (0.897597901, 0.78183148), (1.79519580, 0.97492791),
        (2.69279370, 0.433883739), (3.59039160, -0.43388373),
        (4.48798950, -0.97492791), (5.38558740, -0.78183148),
        (6.28318530, 0.0)]

DIR = Path('~/Desktop/Outbox').expanduser()
FILE = 'wave'
doc = ezdxf.new()
msp = doc.modelspace()
s = global_bspline_interpolation(wave)
msp.add_spline(dxfattribs={'color': 2}).apply_construction_tool(s)

fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
backend = MatplotlibBackend(ax)
Frontend(RenderContext(doc), backend).draw_layout(msp)
fig.savefig(DIR / f'{FILE}.png', dpi=300)
Example #14
0
class CadViewer(qw.QMainWindow):
    def __init__(self):
        super().__init__()
        self.doc = None
        self._render_context = None
        self._visible_layers = None
        self._current_layout = None

        self.view = CADGraphicsViewWithOverlay()
        self.view.setScene(qw.QGraphicsScene())
        self.view.scale(1, -1)  # so that +y is up
        self.view.element_selected.connect(self._on_element_selected)
        self.view.mouse_moved.connect(self._on_mouse_moved)

        menu = self.menuBar()
        select_doc_action = qw.QAction('Select Document', self)
        select_doc_action.triggered.connect(self._select_doc)
        menu.addAction(select_doc_action)
        self.select_layout_menu = menu.addMenu('Select Layout')

        toggle_sidebar_action = qw.QAction('Toggle Sidebar', self)
        toggle_sidebar_action.triggered.connect(self._toggle_sidebar)
        menu.addAction(toggle_sidebar_action)

        self.sidebar = qw.QSplitter(qc.Qt.Vertical)
        self.layers = qw.QListWidget()
        self.layers.setStyleSheet(
            'QListWidget {font-size: 12pt;} QCheckBox {font-size: 12pt; padding-left: 5px;}'
        )
        self.sidebar.addWidget(self.layers)
        info_container = qw.QWidget()
        info_layout = qw.QVBoxLayout()
        info_layout.setContentsMargins(0, 0, 0, 0)
        self.selected_info = qw.QPlainTextEdit()
        self.selected_info.setReadOnly(True)
        info_layout.addWidget(self.selected_info)
        self.mouse_pos = qw.QLabel()
        info_layout.addWidget(self.mouse_pos)
        info_container.setLayout(info_layout)
        self.sidebar.addWidget(info_container)

        container = qw.QSplitter()
        self.setCentralWidget(container)
        container.addWidget(self.view)
        container.addWidget(self.sidebar)
        container.setCollapsible(0, False)
        container.setCollapsible(1, True)
        w = container.width()
        container.setSizes([int(3 * w / 4), int(w / 4)])

        self.setWindowTitle('CAD Viewer')
        self.resize(1600, 900)
        self.show()

    def _select_doc(self):
        path, _ = qw.QFileDialog.getOpenFileName(
            self,
            caption='Select CAD Document',
            filter='DXF Documents(*.dxf);;DWG Documents(*.dwg)')
        if path:
            try:
                if os.path.splitext(path)[1].lower() == '.dwg':
                    doc = odafc.readfile(path)
                else:
                    doc = ezdxf.readfile(path)
                self.set_document(doc)
            except IOError as e:
                qw.QMessageBox.critical(self, 'Loading Error', str(e))
            except DXFStructureError as e:
                qw.QMessageBox.critical(
                    self, 'DXF Structure Error',
                    f'Invalid DXF file "{path}": {str(e)}')

    def set_document(self, document: Drawing):
        auditor = document.audit()
        error_count = len(auditor.errors)
        if error_count > 0:
            ret = qw.QMessageBox.question(
                self, 'Found DXF Errors',
                f'Found {error_count} errors in file "{document.filename}"\nLoad file anyway? '
            )
            if ret == qw.QMessageBox.No:
                auditor.print_error_report(auditor.errors)
                return
        self.doc = document
        self._render_context = RenderContext(document)
        self._visible_layers = None
        self._current_layout = None
        self._populate_layouts()
        self._populate_layer_list()
        self.draw_layout('Model')
        self.setWindowTitle('CAD Viewer - ' + str(document.filename))

    def _populate_layer_list(self):
        self.layers.blockSignals(True)
        self.layers.clear()
        for layer in self._render_context.layers.values():
            name = layer.layer
            item = qw.QListWidgetItem()
            self.layers.addItem(item)
            checkbox = qw.QCheckBox(name)
            checkbox.setCheckState(
                qc.Qt.Checked if layer.is_visible else qc.Qt.Unchecked)
            checkbox.stateChanged.connect(self._layers_updated)
            text_color = '#FFFFFF' if is_dark_color(layer.color,
                                                    0.4) else '#000000'
            checkbox.setStyleSheet(
                f'color: {text_color}; background-color: {layer.color}')
            self.layers.setItemWidget(item, checkbox)
        self.layers.blockSignals(False)

    def _populate_layouts(self):
        self.select_layout_menu.clear()
        for layout_name in self.doc.layout_names_in_taborder():
            action = qw.QAction(layout_name, self)
            action.triggered.connect(partial(self.draw_layout, layout_name))
            self.select_layout_menu.addAction(action)

    def draw_layout(self, layout_name: str, reset_view: bool = True):
        print(f'drawing {layout_name}')
        self._current_layout = layout_name
        self.view.begin_loading()
        new_scene = qw.QGraphicsScene()
        renderer = PyQtBackend(new_scene)
        layout = self.doc.layout(layout_name)
        self._update_render_context(layout)
        try:
            Frontend(self._render_context, renderer).draw_layout(layout)
        except DXFStructureError as e:
            qw.QMessageBox.critical(
                self, 'DXF Structure Error',
                f'Abort rendering of layout "{layout_name}": {str(e)}')
        finally:
            renderer.finalize()
        self.view.end_loading(new_scene)
        self.view.buffer_scene_rect()
        if reset_view:
            self.view.fit_to_scene()

    def _update_render_context(self, layout):
        assert self._render_context
        self._render_context.set_current_layout(layout)
        # Direct modification of RenderContext.layers would be more flexible, but would also expose the internals.
        if self._visible_layers is not None:
            self._render_context.set_layers_state(self._visible_layers,
                                                  state=True)

    def resizeEvent(self, event: qg.QResizeEvent) -> None:
        self.view.fit_to_scene()

    def _layer_checkboxes(self) -> Iterable[Tuple[int, qw.QCheckBox]]:
        for i in range(self.layers.count()):
            item = self.layers.itemWidget(self.layers.item(i))
            yield i, item

    @qc.pyqtSlot(int)
    def _layers_updated(self, item_state: qc.Qt.CheckState):
        shift_held = qw.QApplication.keyboardModifiers() & qc.Qt.ShiftModifier
        if shift_held:
            for i, item in self._layer_checkboxes():
                item.blockSignals(True)
                item.setCheckState(item_state)
                item.blockSignals(False)

        self._visible_layers = set()
        for i, layer in self._layer_checkboxes():
            if layer.checkState() == qc.Qt.Checked:
                self._visible_layers.add(layer.text())
        self.draw_layout(self._current_layout, reset_view=False)

    @qc.pyqtSlot()
    def _toggle_sidebar(self):
        self.sidebar.setHidden(not self.sidebar.isHidden())

    @qc.pyqtSlot(qc.QPointF)
    def _on_mouse_moved(self, mouse_pos: qc.QPointF):
        self.mouse_pos.setText(
            f'mouse position: {mouse_pos.x():.4f}, {mouse_pos.y():.4f}\n')

    @qc.pyqtSlot(object, int)
    def _on_element_selected(self, elements: List[qw.QGraphicsItem],
                             index: int):
        if not elements:
            text = 'No element selected'
        else:
            text = f'Selected: {index + 1} / {len(elements)}    (click to cycle)\n'
            element = elements[index]
            dxf_entity = element.data(CorrespondingDXFEntity)
            if dxf_entity is None:
                text += 'No data'
            else:
                text += f'Selected Entity: {dxf_entity}\nLayer: {dxf_entity.dxf.layer}\n\nDXF Attributes:\n'
                text += _entity_attribs_string(dxf_entity)

                dxf_parent_stack = element.data(CorrespondingDXFParentStack)
                if dxf_parent_stack:
                    text += '\nParents:\n'
                    for entity in reversed(dxf_parent_stack):
                        text += f'- {entity}\n'
                        text += _entity_attribs_string(entity, indent='    ')

        self.selected_info.setPlainText(text)
Example #15
0
class CadViewer(qw.QMainWindow):
    def __init__(self):
        super().__init__()
        self.doc = None
        self._render_context = None
        self._visible_layers = None
        self._current_layout = None

        self.scene = qw.QGraphicsScene()

        self.view = CADGraphicsViewWithOverlay()
        self.view.setScene(self.scene)
        self.view.scale(1, -1)  # so that +y is up
        self.view.element_selected.connect(self._on_element_selected)

        self.renderer = PyQtBackend(self.scene)

        menu = self.menuBar()
        select_doc_action = qw.QAction('Select Document', self)
        select_doc_action.triggered.connect(self._select_doc)
        menu.addAction(select_doc_action)
        self.select_layout_menu = menu.addMenu('Select Layout')

        toggle_sidebar_action = qw.QAction('Toggle Sidebar', self)
        toggle_sidebar_action.triggered.connect(self._toggle_sidebar)
        menu.addAction(toggle_sidebar_action)

        toggle_join_polylines_action = qw.QAction('Toggle Join Polylines',
                                                  self)
        toggle_join_polylines_action.triggered.connect(
            self._toggle_join_polylines)
        menu.addAction(toggle_join_polylines_action)

        self.sidebar = qw.QSplitter(qc.Qt.Vertical)
        self.layers = qw.QListWidget()
        self.layers.setStyleSheet('font-size: 12pt')
        self.layers.itemChanged.connect(self._layers_updated)
        self.sidebar.addWidget(self.layers)
        self.info = qw.QPlainTextEdit()
        self.info.setReadOnly(True)
        self.sidebar.addWidget(self.info)

        container = qw.QSplitter()
        self.setCentralWidget(container)
        container.addWidget(self.view)
        container.addWidget(self.sidebar)
        container.setCollapsible(0, False)
        container.setCollapsible(1, True)
        w = container.width()
        container.setSizes([int(3 * w / 4), int(w / 4)])

        self.setWindowTitle('CAD Viewer')
        self.resize(1600, 900)
        self.show()

    def _select_doc(self):
        path, _ = qw.QFileDialog.getOpenFileName(
            self,
            caption='Select CAD Document',
            filter='DXF Documents (*.dxf)')
        if path:
            self.set_document(ezdxf.readfile(path))

    def set_document(self, document: Drawing):
        self.doc = document
        self._render_context = RenderContext(document)
        self._visible_layers = None
        self._current_layout = None
        self._populate_layouts()
        self._populate_layer_list()
        self.draw_layout('Model')

    def _populate_layer_list(self):
        self.layers.blockSignals(True)
        self.layers.clear()
        for layer in self._render_context.layers.values():
            name = layer.layer
            item = qw.QListWidgetItem(name)
            item.setCheckState(qc.Qt.Checked)
            item.setBackground(qg.QColor(layer.color))
            self.layers.addItem(item)
        self.layers.blockSignals(False)

    def _populate_layouts(self):
        self.select_layout_menu.clear()
        for layout_name in self.doc.layout_names_in_taborder():
            action = qw.QAction(layout_name, self)
            action.triggered.connect(partial(self.draw_layout, layout_name))
            self.select_layout_menu.addAction(action)

    def draw_layout(self, layout_name: str):
        print(f'drawing {layout_name}')
        self._current_layout = layout_name
        self.renderer.clear()
        self.view.clear()
        layout = self.doc.layout(layout_name)
        self._update_render_context(layout)
        Frontend(self._render_context, self.renderer).draw_layout(layout)
        self.view.fit_to_scene()

    def _update_render_context(self, layout):
        assert self._render_context
        self._render_context.set_current_layout(layout)
        # Direct modification of RenderContext.layers would be more flexible, but would also expose the internals.
        if self._visible_layers is not None:
            self._render_context.set_layers_state(self._visible_layers,
                                                  state=True)

    def resizeEvent(self, event: qg.QResizeEvent) -> None:
        self.view.fit_to_scene()

    @qc.pyqtSlot(qw.QListWidgetItem)
    def _layers_updated(self, _item: qw.QListWidgetItem):
        self._visible_layers = set()
        for i in range(self.layers.count()):
            layer = self.layers.item(i)
            if layer.checkState() == qc.Qt.Checked:
                self._visible_layers.add(layer.text())
        self.draw_layout(self._current_layout)

    @qc.pyqtSlot()
    def _toggle_sidebar(self):
        self.sidebar.setHidden(not self.sidebar.isHidden())

    @qc.pyqtSlot()
    def _toggle_join_polylines(self):
        self.renderer.draw_individual_polyline_elements = not self.renderer.draw_individual_polyline_elements
        self.draw_layout(self._current_layout)

    @qc.pyqtSlot(object, qc.QPointF)
    def _on_element_selected(self, element: Optional[qw.QGraphicsItem],
                             mouse_pos: qc.QPointF):
        text = f'mouse position: {mouse_pos.x():.4f}, {mouse_pos.y():.4f}\n'
        if element is None:
            text += 'No element selected'
        else:
            dxf_entity = element.data(CorrespondingDXFEntity)
            if dxf_entity is None:
                text += 'No data'
            else:
                text += f'Current Entity: {dxf_entity}\nLayer: {dxf_entity.dxf.layer}\n\nDXF Attributes:\n'
                for key, value in dxf_entity.dxf.all_existing_dxf_attribs(
                ).items():
                    text += f'- {key}: {value}\n'

                dxf_entity_stack = element.data(CorrespondingDXFEntityStack)
                if dxf_entity_stack:
                    text += '\nParents:\n'
                    for entity in reversed(dxf_entity_stack):
                        text += f'- {entity}\n'

        self.info.setPlainText(text)
Example #16
0
    def run(args):
        # Import on demand for a quicker startup:
        try:
            import matplotlib.pyplot as plt
        except ImportError:
            print("Matplotlib package not found.")
            sys.exit(2)

        from ezdxf.addons.drawing import RenderContext, Frontend
        from ezdxf.addons.drawing.matplotlib import MatplotlibBackend
        from ezdxf.addons.drawing.config import Configuration, LinePolicy

        # Print all supported export formats:
        if args.formats:
            fig = plt.figure()
            for (
                extension,
                description,
            ) in fig.canvas.get_supported_filetypes().items():
                print(f"{extension}: {description}")
            sys.exit(0)

        if args.file:
            filename = args.file
        else:
            print("argument FILE is required")
            sys.exit(1)

        doc, _ = load_document(filename)

        try:
            layout = doc.layouts.get(args.layout)
        except KeyError:
            print(
                f'Could not find layout "{args.layout}". '
                f"Valid layouts: {[l.name for l in doc.layouts]}"
            )
            sys.exit(1)

        fig = plt.figure()
        ax = fig.add_axes([0, 0, 1, 1])
        ctx = RenderContext(doc)
        out = MatplotlibBackend(ax)

        if args.all_layers_visible:
            for layer_properties in ctx.layers.values():
                layer_properties.is_visible = True

        config = Configuration.defaults()
        config = config.with_changes(
            line_policy=LinePolicy.ACCURATE
            if args.ltype == "accurate"
            else config.line_policy
        )

        if args.all_entities_visible:

            class AllVisibleFrontend(Frontend):
                def override_properties(
                    self, entity: "DXFGraphic", properties: "Properties"
                ) -> None:
                    properties.is_visible = True

            frontend = AllVisibleFrontend(ctx, out, config=config)
        else:
            frontend = Frontend(ctx, out, config=config)
        frontend.draw_layout(layout, finalize=True)

        if args.out is not None:
            print(f'exporting to "{args.out}"')
            fig.savefig(args.out, dpi=args.dpi)
            plt.close(fig)
        else:
            plt.show()
Example #17
0
    def effect(self):
        #get input file and copy it to some new temporary directory
        inputfile = self.options.inputfile
        if not os.path.exists(inputfile):
            inkex.utils.debug(
                "The input file does not exist. Please select a *.dxf or *.dwg file and try again."
            )
            exit(1)
        temp_input_dir = os.path.join(tempfile.gettempdir(), "dxfdwg_input")
        shutil.rmtree(temp_input_dir, ignore_errors=True
                      )  #remove the input directory before doing new job
        if not os.path.exists(temp_input_dir):
            os.mkdir(temp_input_dir)  #recreate blank dir
        shutil.copy2(
            inputfile, os.path.join(
                temp_input_dir,
                Path(inputfile).name))  # complete target filename given

        #Prepapre output conversion
        outputfilebase = os.path.splitext(os.path.basename(inputfile))[0]
        inputfile_ending = os.path.splitext(os.path.basename(inputfile))[1]
        temp_output_dir = os.path.join(tempfile.gettempdir(), "dxfdwg_output")
        shutil.rmtree(temp_output_dir, ignore_errors=True
                      )  #remove the output directory before doing new job
        if not os.path.exists(temp_output_dir):
            os.mkdir(temp_output_dir)

        #Prepare some more options for proceeding
        autocad_version = self.options.oda_outputformat.split("_")[0]
        autocad_format = self.options.oda_outputformat.split("_")[1]
        self.options.oda_audit_repair = "1" if self.options.oda_audit_repair else "0"  #overwrite string bool with int value
        entityspace = []
        if self.options.allentities or self.options.THREE_DFACE:
            entityspace.append("3DFACE")
        if self.options.allentities or self.options.ARC:
            entityspace.append("ARC")
        if self.options.allentities or self.options.BLOCK:
            entityspace.append("BLOCK")
        if self.options.allentities or self.options.CIRCLE:
            entityspace.append("CIRCLE")
        if self.options.allentities or self.options.ELLIPSE:
            entityspace.append("ELLIPSE")
        if self.options.allentities or self.options.LINE:
            entityspace.append("LINE")
        if self.options.allentities or self.options.LWPOLYLINE:
            entityspace.append("LWPOLYLINE")
        if self.options.allentities or self.options.POINT:
            entityspace.append("POINT")
        if self.options.allentities or self.options.POLYLINE:
            entityspace.append("POLYLINE")
        if self.options.allentities or self.options.POP_TRAFO:
            entityspace.append("POP_TRAFO")
        if self.options.allentities or self.options.SEQEND:
            entityspace.append("SEQEND")
        if self.options.allentities or self.options.SOLID:
            entityspace.append("SOLID")
        if self.options.allentities or self.options.SPLINE:
            entityspace.append("SPLINE")
        if self.options.allentities or self.options.TABLE:
            entityspace.append("TABLE")
        if self.options.allentities or self.options.VERTEX:
            entityspace.append("VERTEX")
        if self.options.allentities or self.options.VIEWPORT:
            entityspace.append("VIEWPORT")

        if self.options.allentities or self.options.THREE_DSOLID:
            entityspace.append("3DSOLID")
        if self.options.allentities or self.options.ATTRIB:
            entityspace.append("ATTRIB")
        if self.options.allentities or self.options.BODY:
            entityspace.append("BODY")
        if self.options.allentities or self.options.ARC_DIMENSION:
            entityspace.append("ARC_DIMENSION")
        if self.options.allentities or self.options.HATCH:
            entityspace.append("HATCH")
        if self.options.allentities or self.options.IMAGE:
            entityspace.append("IMAGE")
        if self.options.allentities or self.options.INSERT:
            entityspace.append("INSERT")
        if self.options.allentities or self.options.MESH:
            entityspace.append("MESH")
        if self.options.allentities or self.options.MTEXT:
            entityspace.append("MTEXT")
        if self.options.allentities or self.options.RAY:
            entityspace.append("RAY")
        if self.options.allentities or self.options.REGION:
            entityspace.append("REGION")
        if self.options.allentities or self.options.SHAPE:
            entityspace.append("SHAPE")
        if self.options.allentities or self.options.SURFACE:
            entityspace.append("SURFACE")
        if self.options.allentities or self.options.TRACE:
            entityspace.append("TRACE")
        if self.options.allentities or self.options.UNDERLAY:
            entityspace.append("UNDERLAY")
        if self.options.allentities or self.options.XLINE:
            entityspace.append("XLINE")

        #ODA to ezdxf mapping
        oda_ezdxf_mapping = []
        oda_ezdxf_mapping.append(
            ["ACAD9", "R12", "AC1004"]
        )  #this mapping is not supported directly. so we use the lowest possible which is R12
        oda_ezdxf_mapping.append(
            ["ACAD10", "R12", "AC1006"]
        )  #this mapping is not supported directly. so we use the lowest possible which is R12
        oda_ezdxf_mapping.append(["ACAD12", "R12", "AC1009"])
        oda_ezdxf_mapping.append(
            ["ACAD13", "R2000", "AC1012"]
        )  #R13 was overwritten by R2000 which points to AC1015 instead of AC1014 (see documentation)
        oda_ezdxf_mapping.append(
            ["ACAD14", "R2000", "AC1014"]
        )  #R14 was overwritten by R2000 which points to AC1015 instead of AC1014 (see documentation)
        oda_ezdxf_mapping.append(["ACAD2000", "R2000", "AC1015"])
        oda_ezdxf_mapping.append(["ACAD2004", "R2004", "AC1018"])
        oda_ezdxf_mapping.append(["ACAD2007", "R2007", "AC1021"])
        oda_ezdxf_mapping.append(["ACAD2010", "R2010", "AC1024"])
        oda_ezdxf_mapping.append(["ACAD2013", "R2013", "AC1027"])
        oda_ezdxf_mapping.append(["ACAD2018", "R2018", "AC1032"])

        ezdxf_autocad_format = None
        for oe in oda_ezdxf_mapping:
            if oe[0] == autocad_version:
                ezdxf_autocad_format = oe[1]
                break
        if ezdxf_autocad_format is None:
            inkex.errormsg("ezdxf conversion format version unknown")

        #Prepare DXF and SVG paths
        dxf_file = os.path.join(temp_output_dir, outputfilebase + ".dxf")
        svg_file = os.path.join(temp_output_dir, outputfilebase + ".svg")

        # Run ODA File Converter
        if self.options.oda_skip_dxf_to_dxf == False or inputfile_ending == ".dwg":
            # Executable test (check for proper configuration by checking mime type. Should return octet stream for a binary executable)
            if os.name == "nt" and "application/octet-stream" not in str(
                    MimeTypes().guess_type(
                        urllib.pathname2url(self.options.oda_fileconverter))):
                inkex.utils.debug(
                    "You selected to use ODA File Converter but it is not configured properly. Check for installation and path location or select 'Skip conversion from DXF to DXF'. You can download ODA Converter from 'https://www.opendesign.com/guestfiles/oda_file_converter'. You need to install it in order to use it."
                )
                exit(1)
            elif os.path.isfile(self.options.oda_fileconverter) == False:
                inkex.utils.debug(
                    "You selected to use ODA File Converter but it is not configured properly. Check for installation and path location or select 'Skip conversion from DXF to DXF'. You can download ODA Converter from 'https://www.opendesign.com/guestfiles/oda_file_converter'. You need to install it in order to use it."
                )
                exit(1)
            else:
                # Build and run ODA File Converter command
                oda_cmd = [
                    self.options.oda_fileconverter, temp_input_dir,
                    temp_output_dir, autocad_version, autocad_format, "0",
                    self.options.oda_audit_repair
                ]
                if os.name == 'nt' and self.options.oda_hidewindow:
                    info = subprocess.STARTUPINFO(
                    )  #hide the ODA File Converter window because it is annoying (does not work for Linux :-()
                    info.dwFlags = 1
                    info.wShowWindow = 0
                    proc = subprocess.Popen(oda_cmd,
                                            startupinfo=info,
                                            shell=False,
                                            stdout=PIPE,
                                            stderr=PIPE)
                else:
                    proc = subprocess.Popen(oda_cmd,
                                            shell=False,
                                            stdout=PIPE,
                                            stderr=PIPE)
                stdout, stderr = proc.communicate()
                if proc.returncode != 0 or (len(stderr) > 0 and
                                            stderr != b"Quit (core dumped)\n"):
                    inkex.utils.debug("ODAFileConverter failed: %d %s %s" %
                                      (proc.returncode, stdout, stderr))
                    if os.name != 'nt':
                        inkex.utils.debug(
                            "If the error message above contains a warning about wrong/missing Qt version please install the required version. You can get the installer from 'https://download.qt.io/archive/qt/'. Sadly you will need to create a free account to install. After installation please configure the shell script '/usr/bin/ODAFileConverter' to add a preceding line with content similar to 'LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/Qt5.14.2/5.14.2/gcc_64/lib/'."
                        )
                    exit(1)
            # check if ODA converted successfully. This is the case if no error file was created
            oda_errorfile = os.path.join(temp_output_dir,
                                         Path(inputfile).name + ".err")
            if os.path.exists(oda_errorfile):
                inkex.utils.debug(
                    "ODA File Converter failed to process the file. Cannot continue DXF/DWG import. The error message is:"
                )
                errormessage = open(oda_errorfile, 'r')
                errorlines = errormessage.readlines()
                for errorline in errorlines:
                    inkex.utils.debug(errorline.strip())
                errormessage.close()
                exit(1)

        # Do some movings/copies of skipped or processed DXF
        if self.options.oda_skip_dxf_to_dxf:  #if true we need to move the file to simulate "processed"
            shutil.move(os.path.join(temp_input_dir,
                                     Path(inputfile).name),
                        os.path.join(temp_output_dir,
                                     Path(inputfile).name))

        if self.options.oda_keepconverted_dxf:
            shutil.copy2(
                dxf_file,
                os.path.join(os.path.dirname(inputfile), outputfilebase +
                             "_oda.dxf"))  # complete target filename given

        # Preprocessing DXF to DXF (entity filter) by using ezdxf the first time
        if self.options.ezdxf_preprocessing:
            # uniconverter does not handle all entities. we parse the file to exlude stuff which lets uniconverter fail
            dxf = ezdxf.readfile(dxf_file)
            modelspace = dxf.modelspace()
            allowed_entities = []
            # supported entities by UniConverter- impossible: MTEXT TEXT INSERT and a lot of others
            query_string = str(entityspace)[1:-1].replace("'",
                                                          "").replace(",", "")
            if query_string != "":
                for e in modelspace.query(query_string):
                    allowed_entities.append(e)
            #inkex.utils.debug(ezdxf_autocad_format)
            #inkex.utils.debug(self.options.ezdxf_output_version)
            if self.options.ezdxf_output_version == "SAME":
                doc = ezdxf.new(ezdxf_autocad_format)
            else:
                doc = ezdxf.new(
                    self.options.ezdxf_output_version
                )  #use the string values from inx file. Required to match the values from ezdxf library. See Python reference
            msp = doc.modelspace()
            for e in allowed_entities:
                msp.add_foreign_entity(e)
            doc.saveas(dxf_file)
            if self.options.ezdfx_keep_preprocessed:
                shutil.copy2(
                    dxf_file,
                    os.path.join(
                        os.path.dirname(inputfile), outputfilebase +
                        "_ezdxf.dxf"))  # complete target filename given

        # Make SVG from DXF
        if self.options.dxf_to_svg_parser == "sk1":
            if os.name != "nt":
                inkex.utils.debug(
                    "You selected sk1 UniConvertor but you are not running on a Windows platform. On Linux uniconverter 1.1.X can be installed using the now obsolete Python 2.7, but it will not run correctly because you finally will fail at installing liblcms1-dev library on newer systems. That leads to uncompilable sk1libs package. Unfortunately sk1 UniConvertor 2.X does not support dxf format. So please use another DXF to SVG converter."
                )
                exit(1)
            sk1_command_ending = os.path.splitext(
                os.path.splitext(
                    os.path.basename(self.options.sk1_uniconverter))[1])[0]
            if sk1_command_ending != ".cmd":
                inkex.utils.debug(
                    "You selected sk1 UniConverter but it was not configured properly. Check the path to the executable."
                )
                exit(1)
            uniconverter_cmd = [
                self.options.sk1_uniconverter, dxf_file, svg_file
            ]
            #inkex.utils.debug(uniconverter_cmd)
            proc = subprocess.Popen(uniconverter_cmd,
                                    shell=False,
                                    stdout=PIPE,
                                    stderr=PIPE)
            stdout, stderr = proc.communicate()
            if proc.returncode != 0:
                inkex.errormsg("UniConverter failed: %d %s %s" %
                               (proc.returncode, stdout, stderr))
                if self.options.opendironerror:
                    self.openExplorer(temp_output_dir)

        elif self.options.dxf_to_svg_parser == "bjnortier":
            if which("node") is None:
                inkex.utils.debug(
                    "NodeJS executable not found on path. Please check your installation."
                )
                exit(1)
            else:
                bjnortier_cmd = [
                    "node",
                    os.path.join("node_modules", "dxf", "lib", "cli.js"),
                    dxf_file, svg_file
                ]
                #inkex.utils.debug(bjnortier_cmd)
                proc = subprocess.Popen(bjnortier_cmd,
                                        shell=False,
                                        stdout=PIPE,
                                        stderr=PIPE)
                stdout, stderr = proc.communicate()
                if proc.returncode != 0:
                    inkex.errormsg(
                        "node.js DXF to SVG conversion failed: %d %s %s" %
                        (proc.returncode, stdout, stderr))
                    if self.options.opendironerror:
                        self.openExplorer(temp_output_dir)

        elif self.options.dxf_to_svg_parser == "kabeja":
            wd = os.path.join(os.getcwd(), "kabeja")
            #inkex.utils.debug(wd)
            proc = subprocess.Popen(
                "java -jar launcher.jar -nogui -pipeline svg " + dxf_file +
                " " + svg_file,
                cwd=wd,
                shell=True,
                stdout=PIPE,
                stderr=PIPE)
            stdout, stderr = proc.communicate()
            if proc.returncode != 0:
                inkex.errormsg("kabeja failed: %d %s %s" %
                               (proc.returncode, stdout, stderr))
                if self.options.opendironerror:
                    self.openExplorer(temp_output_dir)

        elif self.options.dxf_to_svg_parser == "vpype_dxf":
            try:
                from inkex.command import inkscape
                import vpype
                from vpype_cli import execute
            except Exception as e:
                inkex.errormsg(
                    "Error importing vpype. Did you properly install the vpype and vpype-dxf python modules?"
                )
                exit(1)
            doc = vpype.Document()  #create new vpype document
            command = "dread  --quantization " + str(
                self.options.vpype_quantization)
            if self.options.vpype_simplify is True:
                command += " --simplify"
            if self.options.vpype_parallel is True:
                command += " --parallel"
            #command += " '" + inputfile + "'"
            command += " '" + dxf_file + "'"

            #inkex.errormsg(command)
            doc = execute(command, doc)
            if doc.length() == 0:
                inkex.errormsg(
                    'No lines left after vpype conversion. Conversion result is empty. Cannot continue'
                )
                exit(1)
            # save the vpype document to new svg file and close it afterwards
            output_fileIO = open(svg_file, "w", encoding="utf-8")
            vpype.write_svg(output_fileIO,
                            doc,
                            page_size=None,
                            center=False,
                            source_string='',
                            layer_label_format='%d',
                            show_pen_up=False,
                            color_mode='layer')
            output_fileIO.close()
            # convert vpype polylines/lines/polygons to regular paths again. We need to use "--with-gui" to respond to "WARNING: ignoring verb FileSave - GUI required for this verb."
            cli_output = inkscape(
                svg_file,
                "--with-gui",
                actions=
                "EditSelectAllInAllLayers;EditUnlinkClone;ObjectToPath;FileSave;FileQuit"
            )
            if len(cli_output) > 0:
                self.debug(
                    _("Inkscape returned the following output when trying to run the vpype object to path back-conversion:"
                      ))
                self.debug(cli_output)

        elif self.options.dxf_to_svg_parser == "ezdxf":
            try:
                doc = ezdxf.readfile(dxf_file)
                msp = doc.modelspace()
                #for e in msp: #loop through entities
                #    inkex.errormsg(e)
                #doc.header['$DIMSCALE'] = 0.2 does not apply to the plot :-(
                #inkex.utils.debug(doc.header['$DIMSCALE'])
                #inkex.utils.debug(doc.header['$MEASUREMENT'])
                auditor = doc.audit(
                )  #audit & repair DXF document before rendering
                # The auditor.errors attribute stores severe errors, which *may* raise exceptions when rendering.
                if len(auditor.errors) == 0:
                    fig = plt.figure()
                    ax = plt.axes([0., 0., 1., 1.], xticks=[], yticks=[])
                    #ax = plt.axes([0., 0., 1., 1.], frameon=False, xticks=[], yticks=[])
                    ax.patches = []
                    #plt.axis('off')
                    plt.margins(0, 0)
                    plt.gca().xaxis.set_major_locator(plt.NullLocator())
                    plt.gca().yaxis.set_major_locator(plt.NullLocator())
                    plt.subplots_adjust(top=1,
                                        bottom=0,
                                        right=1,
                                        left=0,
                                        hspace=0,
                                        wspace=0)
                    out = MatplotlibBackend(fig.add_axes(ax))
                    Frontend(RenderContext(doc),
                             out).draw_layout(msp, finalize=True)
                    #plt.show()
                    #fig.savefig(os.path.join(temp_output_dir, outputfilebase + ".png"), dpi=300)
                    fig.savefig(
                        svg_file
                    )  #see https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.savefig.html
            except IOError:
                inkex.errormsg("Not a DXF file or a generic I/O error.")
                exit(1)
            except ezdxf.DXFStructureError:
                inkex.errormsg("Invalid or corrupted DXF file.")
                exit(1)

        elif self.options.dxf_to_svg_parser == "legacy":
            inkex.utils.debug(
                "The selected legacy DXF to SVG parser is not supported by this extension yet. Use File > Import > *.dxf. This calls the \"dxf_input.inx\" extension."
            )
            exit(1)
        else:
            inkex.utils.debug("undefined parser")
            exit(1)

        # Write the generated SVG into InkScape's canvas
        try:
            stream = open(svg_file, 'r')
        except FileNotFoundError as e:
            inkex.utils.debug(
                "There was no SVG output generated. Cannot continue")
            exit(1)
        p = etree.XMLParser(huge_tree=True)
        doc = etree.parse(stream,
                          parser=etree.XMLParser(huge_tree=True)).getroot()
        stream.close()
        doc.set(
            'id',
            self.svg.get_unique_id("dxf_dwg_import-" +
                                   self.options.dxf_to_svg_parser + "-"))
        self.document.getroot().append(doc)

        #get children of the doc and move them one group above - we don't do this for bjnortier tool because this has different structure which we don't want to disturb
        if self.options.dxf_to_svg_parser == "sk1":
            elements = []
            emptyGroup = None
            for firstGroup in doc.getchildren():
                emptyGroup = firstGroup
                for element in firstGroup.getchildren():
                    elements.append(element)
                #break #only one cycle - could be bad idea or not
            for element in elements:
                doc.set('id', self.svg.get_unique_id('dxf_dwg_import'))
                doc.insert(doc.index(firstGroup), element)

            if emptyGroup is not None:
                emptyGroup.getparent().remove(emptyGroup)

        #empty the following vals because they destroy the size aspects of the import / make viewbox looking wrong
        if self.options.dxf_to_svg_parser == "bjnortier" or self.options.dxf_to_svg_parser == "kabeja":
            doc.set('width', '')
            doc.set('height', '')
            doc.set('viewBox', '')
            doc.getchildren()[0].set('transform', '')

        #adjust viewport and width/height to have the import at the center of the canvas
        if self.options.resizetoimport:
            bbox = inkex.elements._selected.ElementList.bounding_box(
                doc.getchildren()[0])
            if bbox is not None:
                root = self.svg.getElement('//svg:svg')
                offset = self.svg.unittouu(
                    str(self.options.extraborder) +
                    self.options.extraborder_units)
                root.set(
                    'viewBox', '%f %f %f %f' %
                    (bbox.left - offset, bbox.top - offset,
                     bbox.width + 2 * offset, bbox.height + 2 * offset))
                root.set('width', bbox.width + 2 * offset)
                root.set('height', bbox.height + 2 * offset)
Example #18
0
class CadViewer(qw.QMainWindow):
    def __init__(self, config: Configuration = Configuration.defaults()):
        super().__init__()
        self._config = config
        self.doc = None
        self._render_context = None
        self._visible_layers = None
        self._current_layout = None
        self._reset_backend()

        self.view = CADGraphicsViewWithOverlay()
        self.view.setScene(qw.QGraphicsScene())
        self.view.scale(1, -1)  # so that +y is up
        self.view.element_selected.connect(self._on_element_selected)
        self.view.mouse_moved.connect(self._on_mouse_moved)

        menu = self.menuBar()
        select_doc_action = QAction("Select Document", self)
        select_doc_action.triggered.connect(self._select_doc)
        menu.addAction(select_doc_action)
        self.select_layout_menu = menu.addMenu("Select Layout")

        toggle_sidebar_action = QAction("Toggle Sidebar", self)
        toggle_sidebar_action.triggered.connect(self._toggle_sidebar)
        menu.addAction(toggle_sidebar_action)

        self.sidebar = qw.QSplitter(qc.Qt.Vertical)
        self.layers = qw.QListWidget()
        self.layers.setStyleSheet(
            "QListWidget {font-size: 12pt;} "
            "QCheckBox {font-size: 12pt; padding-left: 5px;}")
        self.sidebar.addWidget(self.layers)
        info_container = qw.QWidget()
        info_layout = qw.QVBoxLayout()
        info_layout.setContentsMargins(0, 0, 0, 0)
        self.selected_info = qw.QPlainTextEdit()
        self.selected_info.setReadOnly(True)
        info_layout.addWidget(self.selected_info)
        self.mouse_pos = qw.QLabel()
        info_layout.addWidget(self.mouse_pos)
        info_container.setLayout(info_layout)
        self.sidebar.addWidget(info_container)

        container = qw.QSplitter()
        self.setCentralWidget(container)
        container.addWidget(self.view)
        container.addWidget(self.sidebar)
        container.setCollapsible(0, False)
        container.setCollapsible(1, True)
        w = container.width()
        container.setSizes([int(3 * w / 4), int(w / 4)])

        self.setWindowTitle("CAD Viewer")
        self.resize(1600, 900)
        self.show()

    def _reset_backend(self, scale: float = 1.0):
        backend = PyQtBackend(use_text_cache=True)
        if scale != 1.0:
            backend = BackendScaler(backend, factor=scale)
        # clear caches
        self._backend = backend

    def _select_doc(self):
        path, _ = qw.QFileDialog.getOpenFileName(
            self,
            caption="Select CAD Document",
            filter="CAD Documents (*.dxf *.DXF *.dwg *.DWG)",
        )
        if path:
            try:
                if os.path.splitext(path)[1].lower() == ".dwg":
                    doc = odafc.readfile(path)
                    auditor = doc.audit()
                else:
                    try:
                        doc = ezdxf.readfile(path)
                    except ezdxf.DXFError:
                        doc, auditor = recover.readfile(path)
                    else:
                        auditor = doc.audit()
                self.set_document(doc, auditor)
            except IOError as e:
                qw.QMessageBox.critical(self, "Loading Error", str(e))
            except DXFStructureError as e:
                qw.QMessageBox.critical(
                    self,
                    "DXF Structure Error",
                    f'Invalid DXF file "{path}": {str(e)}',
                )

    def set_document(
        self,
        document: Drawing,
        auditor: Auditor,
        *,
        layout: str = "Model",
        overall_scaling_factor: float = 1.0,
    ):
        error_count = len(auditor.errors)
        if error_count > 0:
            ret = qw.QMessageBox.question(
                self,
                "Found DXF Errors",
                f'Found {error_count} errors in file "{document.filename}"\n'
                f"Load file anyway? ",
            )
            if ret == qw.QMessageBox.No:
                auditor.print_error_report(auditor.errors)
                return
        self.doc = document
        self._render_context = RenderContext(document)
        self._reset_backend(scale=overall_scaling_factor)
        self._visible_layers = None
        self._current_layout = None
        self._populate_layouts()
        self._populate_layer_list()
        self.draw_layout(layout)
        self.setWindowTitle("CAD Viewer - " + str(document.filename))

    def _populate_layer_list(self):
        self.layers.blockSignals(True)
        self.layers.clear()
        for layer in self._render_context.layers.values():
            name = layer.layer
            item = qw.QListWidgetItem()
            self.layers.addItem(item)
            checkbox = qw.QCheckBox(name)
            checkbox.setCheckState(
                qc.Qt.Checked if layer.is_visible else qc.Qt.Unchecked)
            checkbox.stateChanged.connect(self._layers_updated)
            text_color = ("#FFFFFF"
                          if is_dark_color(layer.color, 0.4) else "#000000")
            checkbox.setStyleSheet(
                f"color: {text_color}; background-color: {layer.color}")
            self.layers.setItemWidget(item, checkbox)
        self.layers.blockSignals(False)

    def _populate_layouts(self):
        self.select_layout_menu.clear()
        for layout_name in self.doc.layout_names_in_taborder():
            action = QAction(layout_name, self)
            action.triggered.connect(
                lambda: self.draw_layout(layout_name, reset_view=True))
            self.select_layout_menu.addAction(action)

    def draw_layout(
        self,
        layout_name: str,
        reset_view: bool = True,
    ):
        print(f"drawing {layout_name}")
        self._current_layout = layout_name
        self.view.begin_loading()
        new_scene = qw.QGraphicsScene()
        self._backend.set_scene(new_scene)
        layout = self.doc.layout(layout_name)
        self._update_render_context(layout)
        try:
            start = time.perf_counter()
            self.create_frontend().draw_layout(layout)
            duration = time.perf_counter() - start
            print(f"took {duration:.4f} seconds")
        except DXFStructureError as e:
            qw.QMessageBox.critical(
                self,
                "DXF Structure Error",
                f'Abort rendering of layout "{layout_name}": {str(e)}',
            )
        finally:
            self._backend.finalize()
        self.view.end_loading(new_scene)
        self.view.buffer_scene_rect()
        if reset_view:
            self.view.fit_to_scene()

    def create_frontend(self):
        return Frontend(
            self._render_context,
            self._backend,
            self._config,
        )

    def _update_render_context(self, layout):
        assert self._render_context
        self._render_context.set_current_layout(layout)
        # Direct modification of RenderContext.layers would be more flexible,
        # but would also expose the internals.
        if self._visible_layers is not None:
            self._render_context.set_layers_state(self._visible_layers,
                                                  state=True)

    def resizeEvent(self, event: qg.QResizeEvent) -> None:
        self.view.fit_to_scene()

    def _layer_checkboxes(self) -> Iterable[Tuple[int, qw.QCheckBox]]:
        for i in range(self.layers.count()):
            item = self.layers.itemWidget(self.layers.item(i))
            yield i, item  # type: ignore

    @Slot(int)  # type: ignore
    def _layers_updated(self, item_state: qc.Qt.CheckState):
        shift_held = qw.QApplication.keyboardModifiers() & qc.Qt.ShiftModifier
        if shift_held:
            for i, item in self._layer_checkboxes():
                item.blockSignals(True)
                item.setCheckState(item_state)
                item.blockSignals(False)

        self._visible_layers = set()
        for i, layer in self._layer_checkboxes():
            if layer.checkState() == qc.Qt.Checked:
                self._visible_layers.add(layer.text())
        self.draw_layout(self._current_layout, reset_view=False)

    @Slot()
    def _toggle_sidebar(self):
        self.sidebar.setHidden(not self.sidebar.isHidden())

    @Slot(qc.QPointF)
    def _on_mouse_moved(self, mouse_pos: qc.QPointF):
        self.mouse_pos.setText(
            f"mouse position: {mouse_pos.x():.4f}, {mouse_pos.y():.4f}\n")

    @Slot(object, int)
    def _on_element_selected(self, elements: List[qw.QGraphicsItem],
                             index: int):
        if not elements:
            text = "No element selected"
        else:
            text = (
                f"Selected: {index + 1} / {len(elements)}    (click to cycle)\n"
            )
            element = elements[index]
            dxf_entity = element.data(CorrespondingDXFEntity)
            if dxf_entity is None:
                text += "No data"
            else:
                text += (f"Selected Entity: {dxf_entity}\n"
                         f"Layer: {dxf_entity.dxf.layer}\n\nDXF Attributes:\n")
                text += _entity_attribs_string(dxf_entity)

                dxf_parent_stack = element.data(CorrespondingDXFParentStack)
                if dxf_parent_stack:
                    text += "\nParents:\n"
                    for entity in reversed(dxf_parent_stack):
                        text += f"- {entity}\n"
                        text += _entity_attribs_string(entity, indent="    ")

        self.selected_info.setPlainText(text)
Example #19
0
def _main():
    parser = argparse.ArgumentParser(
        description="draw the given CAD file and save it to a file or view it"
    )
    parser.add_argument("cad_file", nargs="?")
    parser.add_argument("--supported_formats", action="store_true")
    parser.add_argument("--layout", default="Model")
    parser.add_argument("--out", required=False)
    parser.add_argument("--dpi", type=int, default=300)
    parser.add_argument("--ltype", default="internal")
    args = parser.parse_args()

    if args.supported_formats:
        fig = plt.figure()
        for (
            extension,
            description,
        ) in fig.canvas.get_supported_filetypes().items():
            print(f"{extension}: {description}")
        sys.exit()

    if args.cad_file is None:
        print("no CAD file specified")
        sys.exit(1)

    try:
        doc = ezdxf.readfile(args.cad_file)
    except IOError:
        print(f"Not a DXF file or a generic I/O error.")
        sys.exit(2)
    except ezdxf.DXFError:
        try:
            doc, auditor = recover.readfile(args.cad_file)
        except ezdxf.DXFStructureError:
            print(f"Invalid or corrupted DXF file: {args.cad_file}")
            sys.exit(3)
    else:
        auditor = doc.audit()

    if auditor.has_errors:
        # But is most likely good enough for rendering.
        print(f"Found {len(auditor.errors)} unrecoverable errors.")
    if auditor.has_fixes:
        print(f"Fixed {len(auditor.fixes)} errors.")

    try:
        layout = doc.layouts.get(args.layout)
    except KeyError:
        print(
            f'Could not find layout "{args.layout}". '
            f"Valid layouts: {[l.name for l in doc.layouts]}"
        )
        sys.exit(4)

    # setup drawing add-on configuration
    config = Configuration.defaults()
    config = config.with_changes(
        line_policy=LinePolicy.ACCURATE
        if args.ltype == "ezdxf"
        else config.line_policy
    )

    fig: plt.Figure = plt.figure(dpi=args.dpi)
    ax: plt.Axes = fig.add_axes([0, 0, 1, 1])
    ctx = RenderContext(doc)
    out = MatplotlibBackend(ax)
    Frontend(ctx, out, config=config).draw_layout(layout, finalize=True)
    if args.out is not None:
        print(f'saving to "{args.out}"')
        fig.savefig(args.out, dpi=args.dpi)
        plt.close(fig)
    else:
        plt.show()
Example #20
0
def ctx(doc):
    return RenderContext(doc)
Example #21
0
class MainWindow(qw.QMainWindow, Ui_MainWindow):
    def __init__(self, controller):
        super().__init__()
        self.controller = controller
        self.setupUi(self)
        self.scene = qw.QGraphicsScene()
        items = self.scene.items()
        for item in items:
            item.transformations()
        self.view = self.graphicsView
        self.view.setScene(self.scene)
        self.view.scale(1, -1)  # so that +y is up
        self.view.element_selected.connect(self._on_element_selected)
        self.actionImport_DXF.triggered.connect(self.import_dxf)
        self.actionNew_Origin.triggered.connect(self.show_origin_dialog)
        self.actionNew_Setup.triggered.connect(self.show_new_setup_dialog)
        self.new_face.triggered.connect(self.show_new_face_feature_dialog)
        self.new_drill.triggered.connect(self.show_new_drill_feature_dialog)
        self.new_slot.triggered.connect(self.show_new_slot_feature_dialog)
        self.treeView.setContextMenuPolicy(qc.Qt.CustomContextMenu)
        self.treeView.customContextMenuRequested.connect(
            self.show_r_click_tree_menu)
        self.position_widget = PositionWidget()
        self.statusbar.addWidget(self.position_widget)
        self.position_widget.change_current_origin.connect(
            self.change_current_origin)
        self.position_widget.change_active_setup.connect(
            self.change_active_setup)
        self.save_nc_button.clicked.connect(self.save_nc_file)
        self.actionPost.triggered.connect(self.post_file)
        self.actionSet_DB.triggered.connect(
            self.controller.show_connect_dialog)
        self.actionGeometry_Palette.triggered.connect(
            self.show_geometry_palette)
        self.geometry_palette_dialog = None
        self.origin_dialog = None
        self.setup_dialog = None
        self.feature_dialog = None
        self.tree_r_click_menu = None
        self.picker_axis = None
        self.all_dxf_entities = {}
        self.selected_dxf_entities = []
        self.model_space = None
        self.right_click_point = None
        self.picked_item_vector = None
        self.scene_origin = None

        self.image = qg.QPixmap('img/splash.png')

        self.editor_setup = None
        self.editor_origin = None
        self.editor_feature = None

        self.renderer = PyQtBackend(self.scene)
        self.doc = None
        self._render_context = None
        self._visible_layers = None
        self._current_layout = None

        self.scene_shift = (0, 0, 0)
        self.hidden_dialogs = []

    def post_file(self):
        self.get_all_dxf_entities()
        self.controller.update_machine()
        self.controller.populate_operation_list()
        if hasattr(self.controller.current_camo_file, 'operations'):
            if len(self.controller.current_camo_file.operations) > 0:
                code_builder = CodeBuilder(self.controller)
                self.controller.nc_output = code_builder.post()
                self.nc_output_edit.setText(self.controller.nc_output)
                self.tabWidget.setCurrentWidget(self.code_tab)
        else:
            show_error_message(
                'No features',
                'please have active setup with features to post NC code')

    def save_nc_file(self):
        default_filename = self.controller.current_camo_file.filename
        if default_filename:
            default_filename = default_filename.replace('.camo', '.NC')
        filename, _ = qw.QFileDialog.getSaveFileName(self,
                                                     'Save .NC File',
                                                     default_filename,
                                                     filter='*.NC')
        if filename:
            with open(filename, 'w') as file:
                file.writelines(self.nc_output_edit.toPlainText())

    def get_all_dxf_entities(self):
        msp = self.controller.current_camo_file.dxf_doc.modelspace()
        self.all_dxf_entities = {}
        for entity in msp:
            self.all_dxf_entities[str(entity)] = entity

    def show_geometry_palette(self):
        self.geometry_palette_dialog = GeoPaletteDialog()
        self.geometry_palette_dialog.show()

    def show_r_click_tree_menu(self, point):
        self.right_click_point = point
        self.tree_r_click_menu = qw.QMenu()
        edit_tree_item_action = qw.QAction('Edit', self)
        edit_tree_item_action.triggered.connect(self.edit_tree_item)
        delete_tree_item_action = qw.QAction('Delete', self)
        delete_tree_item_action.triggered.connect(self.delete_tree_item)
        self.tree_r_click_menu.addAction(edit_tree_item_action)
        self.tree_r_click_menu.addAction(delete_tree_item_action)
        self.tree_r_click_menu.exec(self.treeView.mapToGlobal(point))
        print('rclick')

    def get_right_clicked_tree_item(self):
        index = self.treeView.indexAt(self.right_click_point)
        node = index.internalPointer()
        return index, node

    def edit_tree_item(self, ):
        _, node = self.get_right_clicked_tree_item()
        if node.type == ct.OSetup:
            self.show_edit_setup_dialog(node._data[1])
        elif node.type == ct.OOrigin:
            self.show_edit_origin_dialog(node._data[1])
        elif node.type == ct.OFeature:
            self.show_edit_feature_dialog(node._data[1])

        print(node)

    def delete_tree_item(self):
        selected_node = self.treeView.indexAt(self.right_click_point)
        item = selected_node.internalPointer()
        print(item)
        item = item._data[1]
        if isinstance(item, Setup):
            self.controller.current_camo_file.setups.remove(item)
        elif isinstance(item, Origin):
            self.controller.current_camo_file.origins.remove(item)
        elif isinstance(item, PartFeature):
            self.controller.current_camo_file.features.remove(item)
        elif hasattr(item, 'DXFTYPE'):
            self.controller.current_camo_file.dxf_doc.modelspace(
            ).delete_entity(item)
        else:
            return
        self.set_document(self.controller.current_camo_file.dxf_doc)
        self.controller.build_file_tree_model()

    def populate_origin_comboBox(self):
        self.position_widget.origin_combo.clear()
        for origin in self.controller.current_camo_file.origins:
            self.position_widget.origin_combo.addItem(origin.name, origin)

    def populate_active_setup_combo(self):
        self.position_widget.active_setup_combo.clear()
        for setup in self.controller.current_camo_file.setups:
            self.position_widget.active_setup_combo.addItem(setup.name, setup)

    def change_current_origin(self, origin):
        if origin:
            self.controller.current_camo_file.active_origin = origin
            self.apply_origin_to_scene()

    def apply_origin_to_scene(self):
        self.reset_scene_shift()
        x = self.controller.current_camo_file.active_origin.x
        y = self.controller.current_camo_file.active_origin.y
        a = self.controller.current_camo_file.active_origin.angle
        self.scene_shift = (x, y, a)
        self.graphicsView.rotate(a)
        self.scene_origin.setX(x)
        self.scene_origin.setY(y)
        self.scene_origin.setRotation(-a)

    def reset_scene_shift(self):
        x, y, a = self.scene_shift
        self.graphicsView.rotate(360 - a)
        self.scene_origin.setX(-x)
        self.scene_origin.setY(-y)
        self.scene_origin.setRotation(a)
        self.scene_shift = (0, 0, 0)

    def change_active_setup(self, setup):
        if setup:
            self.controller.current_camo_file.active_setup = setup
            self.controller.populate_operation_list()

    def show_origin_dialog(self):
        self.origin_dialog = OriginDialog()
        self.origin_dialog.find_x.clicked.connect(self.find_x)
        self.origin_dialog.find_y.clicked.connect(self.find_y)
        self.origin_dialog.buttonBox.accepted.connect(self.add_new_origin)
        self.origin_dialog.show()

    def find_x(self):
        self.origin_dialog.hide()
        self.picker_axis = 'x'
        self.graphicsView.graphics_view_clicked.connect(
            self.dxf_entity_clicked_origin)

    def find_y(self):
        self.origin_dialog.hide()
        self.picker_axis = 'y'
        self.graphicsView.graphics_view_clicked.connect(
            self.dxf_entity_clicked_origin)

    def dxf_entity_clicked_origin(self, item):
        item_data = item.data(0)
        picked_item_vector = None
        if item_data.DXFTYPE == 'LINE':
            picked_item_vector = item_data.dxf.start
        elif item_data.DXFTYPE == 'CIRCLE':
            picked_item_vector = item_data.dxf.center
        elif item_data.DXFTYPE == 'ARC':
            picked_item_vector = item_data.dxf.center

        if picked_item_vector:
            if self.picker_axis == 'x':
                self.origin_dialog.origin_x.setText(
                    str(round(picked_item_vector.x, 4)))
            elif self.picker_axis == 'y':
                self.origin_dialog.origin_y.setText(
                    str(round(picked_item_vector.y, 4)))
        self.origin_dialog.showNormal()
        self.graphicsView.graphics_view_clicked.disconnect(
            self.dxf_entity_clicked_origin)

    def show_edit_origin_dialog(self, origin):
        self.show_origin_dialog()
        self.editor_origin = origin
        self.origin_dialog.buttonBox.accepted.disconnect(self.add_new_origin)
        self.origin_dialog.buttonBox.accepted.connect(self.edit_origin)
        self.origin_dialog.origin_name_input.setText(origin.name)
        self.origin_dialog.origin_x.setText(str(origin.x))
        self.origin_dialog.origin_y.setText(str(origin.y))
        self.origin_dialog.angle_input.setText(str(origin.angle))
        self.origin_dialog.wfo_spinBox.setValue(origin.wfo_num)

    def add_new_origin(self):
        name, wfo, x, y, angle = self.clean_origin_values_from_dialog()
        origin = Origin(name, wfo, x, y, angle)
        self.controller.current_camo_file.origins.append(origin)
        self.controller.build_file_tree_model()
        self.populate_origin_comboBox()

    def edit_origin(self):
        name, wfo, x, y, angle = self.clean_origin_values_from_dialog()

        self.editor_origin.name = name
        self.editor_origin.x = x
        self.editor_origin.y = y
        self.editor_origin.angle = angle
        self.editor_origin.wfo_num = wfo
        self.editor_origin.angle = angle

        self.controller.build_file_tree_model()

    def clean_origin_values_from_dialog(self):
        name = self.origin_dialog.origin_name_input.text()
        x = self.origin_dialog.origin_x.text()
        y = self.origin_dialog.origin_y.text()
        angle = self.origin_dialog.angle_input.text()
        wfo = self.origin_dialog.wfo_spinBox.value()
        if x == '':
            x = 0
        if y == '':
            y = 0
        if angle == '':
            angle = 0
        return name, wfo, float(x), float(y), float(angle)

    def show_new_setup_dialog(self):
        if len(self.controller.current_camo_file.origins) < 1:
            show_error_message('Error', 'Please add origin')
            return
        self.setup_dialog = SetupDialog(self.controller)
        self.setup_dialog.buttonBox.accepted.connect(self.add_new_setup)
        self.setup_dialog.show()

    def show_edit_feature_dialog(self, feature):
        self.feature_dialog = FeatureDialog(self.controller)
        self.editor_feature = feature
        base_feature = self.controller.session.query(Feature).filter(
            Feature.id == self.editor_feature.base_feature_id).one()
        print(base_feature)

    def show_edit_setup_dialog(self, setup):
        self.setup_dialog = SetupDialog(self.controller)
        self.editor_setup = setup
        self.setup_dialog.buttonBox.accepted.connect(self.edit_setup)
        self.setup_dialog.machine_combo.setCurrentIndex(
            self.setup_dialog.machine_combo.findText(
                setup.get_machine(self.controller.session).name))
        self.setup_dialog.origin_combo.setCurrentIndex(
            self.setup_dialog.origin_combo.findText(setup.origin.name))
        self.setup_dialog.clearance_spinbox.setValue(setup.clearance_plane)
        self.setup_dialog.setup_name_input.setText(setup.name)
        self.setup_dialog.program_number_spinbox.setValue(setup.program_number)
        # setup.qb_setup_id = ''
        self.setup_dialog.setup_id_edit.setText(setup.qb_setup_id)
        self.setup_dialog.show()

    def add_new_setup(self):
        name = self.setup_dialog.setup_name_input.text()
        machine = get_combo_data(self.setup_dialog.machine_combo)
        origin = get_combo_data(self.setup_dialog.origin_combo)
        clearance = self.setup_dialog.clearance_spinbox.value()
        program_number = self.setup_dialog.program_number_spinbox.value()
        setup_id = self.setup_dialog.setup_id_edit.text()
        setup = Setup(name, machine.id, origin, clearance, program_number,
                      setup_id)
        self.controller.current_camo_file.setups.append(setup)
        self.controller.build_file_tree_model()
        self.populate_active_setup_combo()

    def edit_setup(self):
        self.editor_setup.name = self.setup_dialog.setup_name_input.text()
        self.editor_setup.machine = get_combo_data(
            self.setup_dialog.machine_combo)
        self.editor_setup.machine_id = self.editor_setup.machine.id
        self.editor_setup.origin = self.setup_dialog.origin_combo.itemData(
            self.setup_dialog.origin_combo.currentIndex())
        self.editor_setup.clearance_plane = self.setup_dialog.clearance_spinbox.value(
        )
        self.editor_setup.program_number = self.setup_dialog.program_number_spinbox.value(
        )
        self.editor_setup.qb_setup_id = self.setup_dialog.setup_id_edit.text()
        self.controller.build_file_tree_model()

    def show_feature_dialog(self):
        if len(self.controller.current_camo_file.setups) < 1:
            show_error_message('Error', 'Please add setup')
            return
        self.feature_dialog = FeatureDialog(self.controller)
        for setup in self.controller.current_camo_file.setups:
            self.feature_dialog.setup_combo.addItem(setup.name, setup)
        self.feature_dialog.buttonBox.rejected.connect(
            self.feature_dialog.reject)
        self.feature_dialog.base_feature_combo.currentIndexChanged.connect(
            self.populate_depth_inputs)
        self.feature_dialog.show()

    def populate_filtered_base_feature_list(self, features):
        for feature in features:
            self.feature_dialog.base_feature_combo.addItem(
                feature.name, feature)

    def show_face_feature_dialog(self):
        self.show_feature_dialog()
        if self.feature_dialog:
            filtered_list = [
                feature for feature in self.controller.feature_list.features
                if feature.feature_type.feature_type == 'Facing'
            ]
            self.populate_filtered_base_feature_list(filtered_list)
            face_widget = FaceWidget()
            self.feature_dialog.frame_layout.addWidget(face_widget)

    def show_drill_feature_dialog(self):
        self.show_feature_dialog()
        if self.feature_dialog:
            filtered_list = [
                feature for feature in self.controller.feature_list.features
                if feature.feature_type.feature_type == 'Drilling'
            ]
            self.populate_filtered_base_feature_list(filtered_list)
            self.feature_dialog.drill_widget = DrillWidget()
            self.feature_dialog.frame_layout.addWidget(
                self.feature_dialog.drill_widget)
            self.feature_dialog.drill_widget.select_sample_circle.clicked.connect(
                self.find_circle_diameter)
            self.feature_dialog.drill_widget.find_cirlces_button.clicked.connect(
                self.find_circles)

    def show_slot_feature_dialog(self):
        filtered_list = [
            feature for feature in self.controller.feature_list.features
            if feature.feature_type.feature_type == 'Slotting'
        ]
        if len(filtered_list) < 1:
            show_error_message('Error', 'Please add Slot to database Features')
            return
        self.show_feature_dialog()
        if self.feature_dialog:
            self.populate_filtered_base_feature_list(filtered_list)
            self.feature_dialog.slot_widget = SlotWidget()
            self.feature_dialog.frame_layout.addWidget(
                self.feature_dialog.slot_widget)
            self.feature_dialog.slot_widget.add_line_button.clicked.connect(
                self.add_slot_line)

    def show_new_face_feature_dialog(self):
        self.show_face_feature_dialog()
        self.feature_dialog.buttonBox.accepted.connect(self.add_face_feature)

    def show_new_drill_feature_dialog(self):
        self.show_drill_feature_dialog()
        if self.feature_dialog:
            self.feature_dialog.buttonBox.accepted.connect(
                self.add_drill_feature)

    def show_new_slot_feature_dialog(self):
        self.show_slot_feature_dialog()
        if self.feature_dialog:
            self.feature_dialog.selected_slot_lines = []
            self.feature_dialog.buttonBox.accepted.connect(
                self.add_slot_feature)

    def populate_depth_inputs(self, index):
        feature = get_combo_data_index(self.feature_dialog.base_feature_combo,
                                       index)

        # for i in reversed(range(self.feature_dialog.depth_groupBox.layout().count())):
        #     self.feature_dialog.depth_groupBox.layout().itemAt(i).widget().setParent(None)
        clearLayout(self.feature_dialog.depth_groupBox.layout())

        for op in feature.operations:
            print(str(op.camo_op.op_type))
            depth_widget = DepthWidget(op.camo_op.op_type)
            self.feature_dialog.depth_groupBox.layout().addWidget(depth_widget)

    def add_face_feature(self):
        base_feature = get_combo_data(self.feature_dialog.base_feature_combo)
        print(base_feature)
        setup = get_combo_data(self.feature_dialog.setup_combo)
        print(setup)
        depths = get_depths_from_layout(
            self.feature_dialog.depth_groupBox.layout())
        feature = PartFeature(base_feature.id, setup, depths=depths)
        self.controller.current_camo_file.features.append(feature)
        self.controller.build_file_tree_model()

    def add_slot_feature(self):
        base_feature = get_combo_data(self.feature_dialog.base_feature_combo)
        setup = get_combo_data(self.feature_dialog.setup_combo)
        geo = self.feature_dialog.selected_slot_lines
        if len(geo) > 0:
            depths = get_depths_from_layout(
                self.feature_dialog.depth_groupBox.layout())
            feature = PartFeature(base_feature.id,
                                  setup,
                                  geometry=geo,
                                  depths=depths)
            self.controller.current_camo_file.features.append(feature)
            self.controller.build_file_tree_model()

    def add_drill_feature(self):
        base_feature = get_combo_data(self.feature_dialog.base_feature_combo)
        setup = get_combo_data(self.feature_dialog.setup_combo)
        geo = self.feature_dialog.drill_widget.selected_circles
        depths = get_depths_from_layout(
            self.feature_dialog.depth_groupBox.layout())
        feature = PartFeature(base_feature.id,
                              setup,
                              geometry=geo,
                              depths=depths)
        self.controller.current_camo_file.features.append(feature)
        self.controller.build_file_tree_model()

    def add_slot_line(self):
        self.feature_dialog.hide()
        self.graphicsView.graphics_view_clicked.connect(
            self.dxf_entity_clicked_slot_line)

    def dxf_entity_clicked_slot_line(self, item):
        item_data = item.data(0)
        line = None
        if item_data.DXFTYPE == 'LINE':
            line = item_data
        if line:
            self.feature_dialog.selected_slot_lines.append(str(line))
            self.feature_dialog.slot_widget.line_list.addItem(str(line))
            self.feature_dialog.show()
            self.graphicsView.graphics_view_clicked.disconnect(
                self.dxf_entity_clicked_slot_line)

    def find_circle_diameter(self):
        self.feature_dialog.hide()
        self.graphicsView.graphics_view_clicked.connect(
            self.dxf_entity_clicked_circle_diameter)

    def find_circles(self):
        diameter = self.feature_dialog.drill_widget.diameter_edit.text()
        try:

            diameter = float(diameter)
            assert diameter > 0
        except:
            show_error_message('Error', 'No diameter given')
            return
        circles = [
            entity
            for entity in self.controller.current_camo_file.dxf_doc.modelspace(
            ).entity_space.entities if entity.DXFTYPE == 'CIRCLE'
        ]
        filtered_circles = [
            circle for circle in circles
            if math.isclose(circle.dxf.radius * 2, diameter, rel_tol=.0002)
        ]
        self.feature_dialog.drill_widget.geometry_list.clear()
        self.feature_dialog.drill_widget.selected_circles = [
            str(x) for x in filtered_circles
        ]
        for circle in filtered_circles:
            item = qw.QListWidgetItem(str(circle))
            item.circle = circle
            self.feature_dialog.drill_widget.geometry_list.addItem(item)
        self.feature_dialog.drill_widget.lcdNumber.display(
            len(filtered_circles))

    def dxf_entity_clicked_circle_diameter(self, item):
        item_data = item.data(0)
        diameter = None
        if item_data.DXFTYPE == 'CIRCLE':
            diameter = item_data.dxf.radius * 2
        elif item_data.DXFTYPE == 'ARC':
            diameter = item_data.dxf.radius * 2

        if diameter:
            self.feature_dialog.drill_widget.diameter_edit.setText(
                str(round(diameter, 4)))
            self.feature_dialog.show()
            self.graphicsView.graphics_view_clicked.disconnect(
                self.dxf_entity_clicked_circle_diameter)

        print(diameter)

    # def fill_dxf_select_list(self, element: Optional[qw.QGraphicsItem]):
    #     self.selected_dxf_entities.append(element.data(0))
    #     print(element)

    def import_dxf(self):
        path, _ = qw.QFileDialog.getOpenFileName(
            self,
            caption='Select CAD Document',
            filter='DXF Documents (*.dxf)')
        if path:
            incoming_dxf = ezdxf.readfile(path)
            importer = Importer(incoming_dxf,
                                self.controller.current_camo_file.dxf_doc)
            # import all entities from source modelspace into modelspace of the target drawing
            importer.import_modelspace()
            importer.finalize()
            self.set_document(self.controller.current_camo_file.dxf_doc)
            self.controller.build_file_tree_model()

    def set_document(self, document: Drawing):
        self.doc = document
        self._render_context = RenderContext(document)
        self._visible_layers = None
        self._current_layout = None
        # self._populate_layouts()
        self._populate_layer_list()
        self.draw_layout('Model')
        self.model_space = self.doc.modelspace()
        self.all_dxf_entities = {}
        for entity in self.model_space.entity_space:
            self.all_dxf_entities[str(entity)] = entity
            if entity.DXFTYPE == "CIRCLE":
                if hasattr(entity.dxf, 'extrusion'):
                    print(entity.dxf.extrusion)
                    if entity.dxf.extrusion[2] == -1:
                        entity.dxf.extrusion = Vector(0, 0, 1)
                        # entity.dxf.center = Vector(-(entity.dxf.center.x), -(entity.dxf.center.y), 0)

        self.draw_origin()
        self.apply_origin_to_scene()

    def _populate_layer_list(self):
        self.layers.blockSignals(True)
        self.layers.clear()
        for layer in self._render_context.layers.values():
            name = layer.layer
            item = qw.QListWidgetItem(name)
            item.setCheckState(qc.Qt.Checked)
            item.setBackground(qg.QColor(layer.color))
            self.layers.addItem(item)
        self.layers.blockSignals(False)

    def draw_origin(self):
        pen = qg.QPen(qg.QColor('#e533e5'), 2)
        pen.setCosmetic(True)  # changes width depending on zoom
        pen.setJoinStyle(qc.Qt.RoundJoin)

        length = self.scene.itemsBoundingRect().width() / 16
        diameter = length / 10

        origin_item = qw.QGraphicsItemGroup()

        circle = qw.QGraphicsEllipseItem(
            qc.QRectF(-diameter / 2, -diameter / 2, diameter, diameter))
        circle.setPen(pen)
        circle.setEnabled(False)
        origin_item.addToGroup(circle)

        x_line = qw.QGraphicsLineItem(
            0,
            0,
            length,
            0,
        )
        x_line.setPen(pen)
        origin_item.addToGroup(x_line)

        y_line = qw.QGraphicsLineItem(0, 0, 0, length)
        y_line.setPen(pen)
        origin_item.addToGroup(y_line)

        origin_item.setEnabled(False)
        origin_item.setZValue(-1)
        self.scene_origin = origin_item
        self.scene.addItem(self.scene_origin)

    def draw_layout(self, layout_name: str):
        print(f'drawing {layout_name}')
        self._current_layout = layout_name
        self.renderer.clear()
        self.view.clear()
        layout = self.doc.layout(layout_name)
        self._update_render_context(layout)
        Frontend(self._render_context, self.renderer).draw_layout(layout)
        self.view.fit_to_scene()

    def _update_render_context(self, layout):
        assert self._render_context
        self._render_context.set_current_layout(layout)
        # Direct modification of RenderContext.layers would be more flexible, but would also expose the internals.
        if self._visible_layers is not None:
            self._render_context.set_layers_state(self._visible_layers,
                                                  state=True)

    def resizeEvent(self, event: qg.QResizeEvent) -> None:
        self.view.fit_to_scene()

    @qc.pyqtSlot(qw.QListWidgetItem)
    def _layers_updated(self, _item: qw.QListWidgetItem):
        self._visible_layers = set()
        for i in range(self.layers.count()):
            layer = self.layers.item(i)
            if layer.checkState() == qc.Qt.Checked:
                self._visible_layers.add(layer.text())
        self.draw_layout(self._current_layout)

    @qc.pyqtSlot()
    def _toggle_sidebar(self):
        self.sidebar.setHidden(not self.sidebar.isHidden())

    @qc.pyqtSlot()
    def _toggle_join_polylines(self):
        self.renderer.draw_individual_polyline_elements = not self.renderer.draw_individual_polyline_elements
        self.draw_layout(self._current_layout)

    @qc.pyqtSlot(object, qc.QPointF)
    def _on_element_selected(self, element: Optional[qw.QGraphicsItem],
                             mouse_pos: qc.QPointF):
        shifted_position = (mouse_pos.x() - self.scene_shift[0],
                            mouse_pos.y() - self.scene_shift[1])
        x, y = shifted_position
        x, y = rotate_point((x, y), -self.scene_shift[2])

        # rotate also here
        text = f'mouse position: {shifted_position[0]:.4f}, {shifted_position[1]:.4f}\n'
        self.position_widget.xlabel.setText(f'X: {x:.4f}')
        self.position_widget.ylabel.setText(f'Y: {y:.4f}')
        if element is None:
            text += 'No element selected'
        else:
            dxf_entity = element.data(CorrespondingDXFEntity)
            if dxf_entity is None:
                text += 'No data'
            else:
                text += f'Current Entity: {dxf_entity}\nLayer: {dxf_entity.dxf.layer}\n\nDXF Attributes:\n'
                attribs = dxf_entity.dxf.all_existing_dxf_attribs()
                for key, value in attribs.items():
                    text += f'- {key}: {value}\n'
                dxf_entity_stack = element.data(CorrespondingDXFEntityStack)

                # model = self.controller.main_window.treeView.model()
                # view = self.treeView
                # if model and model.indexes:     this doe the tree select based on hover
                #     view.setCurrentIndex(model.indexes[str(dxf_entity)])

                if dxf_entity_stack:
                    text += '\nParents:\n'
                    for entity in reversed(dxf_entity_stack):
                        text += f'- {entity}\n'

        self.info.setPlainText(text)