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()
class CadViewer(qw.QMainWindow): def __init__(self, params: Dict): super().__init__() self.doc = None self._render_params = params 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 = 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 _reset_backend(self): # clear caches self._backend = PyQtBackend(use_text_cache=True, params=self._render_params) 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): 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)) 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( 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() Frontend(self._render_context, self._backend).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 _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)
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) 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: try: self.set_document(ezdxf.readfile(path)) 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 - ' + 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(name) item.setCheckState(qc.Qt.Checked) item.setBackground(qg.QColor(layer.color)) if is_dark_color(layer.color, 0.4): item.setForeground(qg.QColor('#FFFFFF')) else: item.setForeground(qg.QColor(qg.QColor('#000000'))) 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) try: Frontend(self._render_context, self.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: self.renderer.finalize() 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(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)