Import_3ds: Improved distance cue node setup
[blender-addons.git] / io_import_dxf / __init__.py
blob30c7081b2ba6a8e75b334779033671e6fd946a2e
1 # SPDX-FileCopyrightText: 2014-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 import os
7 from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty, CollectionProperty
8 from .dxfimport.do import Do, Indicator
9 from .transverse_mercator import TransverseMercator
10 from pathlib import Path
12 try:
13 from pyproj import Proj, transform
14 PYPROJ = True
15 except:
16 PYPROJ = False
18 bl_info = {
19 "name": "Import AutoCAD DXF Format (.dxf)",
20 "author": "Lukas Treyer, Manfred Moitzi (support + dxfgrabber library), Vladimir Elistratov, Bastien Montagne, Remigiusz Fiedler (AKA migius)",
21 "version": (0, 9, 8),
22 "blender": (2, 80, 0),
23 "location": "File > Import > AutoCAD DXF",
24 "description": "Import files in the Autocad DXF format (.dxf)",
25 "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/scene_dxf.html",
26 "category": "Import-Export",
30 proj_none_items = (
31 ('NONE', "None", "No Coordinate System is available / will be set"),
33 proj_user_items = (
34 ('USER', "User Defined", "Define the EPSG code"),
36 proj_tmerc_items = (
37 ('TMERC', "Transverse Mercator", "Mercator Projection using a lat/lon coordinate as its geo-reference"),
39 proj_epsg_items = (
40 ('EPSG:4326', "WGS84", "World Geodetic System 84; default for lat / lon; EPSG:4326"),
41 ('EPSG:3857', "Spherical Mercator", "Webbrowser mapping service standard (Google, OpenStreetMap, ESRI); EPSG:3857"),
42 ('EPSG:27700', "National Grid U.K",
43 "Ordnance Survey National Grid reference system used in Great Britain; EPSG:27700"),
44 ('EPSG:2154', "France (Lambert 93)", "Lambert Projection for France; EPSG:2154"),
45 ('EPSG:5514', "Czech Republic & Slovakia", "Coordinate System for Czech Republic and Slovakia; EPSG:5514"),
46 ('EPSG:5243', "LLC Germany", "Projection for Germany; EPSG:5243"),
47 ('EPSG:28992', "Amersfoort Netherlands", "Amersfoort / RD New -- Netherlands; EPSG:28992"),
48 ('EPSG:21781', "Swiss CH1903 / LV03", "Switzerland and Lichtenstein; EPSG:21781"),
49 ('EPSG:5880', "Brazil Polyconic", "Cartesian 2D; Central, South America; EPSG:5880 "),
50 ('EPSG:42103', "LCC USA", "Lambert Conformal Conic Projection; EPSG:42103"),
51 ('EPSG:3350', "Russia: Pulkovo 1942 / CS63 zone C0", "Russian Federation - onshore and offshore; EPSG:3350"),
52 ('EPSG:22293', "Cape / Lo33 South Africa", "South Africa; EPSG:22293"),
53 ('EPSG:27200', "NZGD49 / New Zealand Map Grid", "NZGD49 / New Zealand Map Grid; EPSG:27200"),
54 ('EPSG:3112', "GDA94 Australia Lambert", "GDA94 / Geoscience Australia Lambert; EPSG:3112"),
55 ('EPSG:24378', "India zone I", "Kalianpur 1975 / India zone I; EPSG:24378"),
56 ('EPSG:2326', "Hong Kong 1980 Grid System", "Hong Kong 1980 Grid System; EPSG:2326"),
57 ('EPSG:3414', "SVY21 / Singapore TM", "SVY21 / Singapore TM; EPSG:3414"),
60 proj_epsg_dict = {e[0]: e[1] for e in proj_epsg_items}
62 __version__ = '.'.join([str(s) for s in bl_info['version']])
64 BY_LAYER = 0
65 BY_DXFTYPE = 1
66 BY_CLOSED_NO_BULGE_POLY = 2
67 SEPARATED = 3
68 LINKED_OBJECTS = 4
69 GROUP_INSTANCES = 5
70 BY_BLOCKS = 6
72 merge_map = {"BY_LAYER": BY_LAYER, "BY_TYPE": BY_DXFTYPE,
73 "BY_CLOSED_NO_BULGE_POLY": BY_CLOSED_NO_BULGE_POLY, "BY_BLOCKS": BY_BLOCKS}
75 T_Merge = True
76 T_ImportText = True
77 T_ImportLight = True
78 T_ExportAcis = False
79 T_MergeLines = True
80 T_OutlinerGroups = True
81 T_Bbox = True
82 T_CreateNewScene = False
83 T_Recenter = False
84 T_ThicknessBevel = True
85 T_import_atts = True
86 T_Collection = False
88 RELEASE_TEST = False
89 DEBUG = False
92 def is_ref_scene(scene):
93 return "latitude" in scene and "longitude" in scene
96 def read(report, filename, obj_merge=BY_LAYER, import_text=True, import_light=True, export_acis=True, merge_lines=True,
97 do_bbox=True, block_rep=LINKED_OBJECTS, new_scene=None, new_collection=None, recenter=False, projDXF=None, projSCN=None,
98 thicknessWidth=True, but_group_by_att=True, dxf_unit_scale=1.0):
99 # import dxf and export nurbs types to sat/sab files
100 # because that's how autocad stores nurbs types in a dxf...
101 do = Do(filename, obj_merge, import_text, import_light, export_acis, merge_lines, do_bbox, block_rep, recenter,
102 projDXF, projSCN, thicknessWidth, but_group_by_att, dxf_unit_scale)
104 errors = do.entities(Path(filename.name).stem, new_scene, new_collection)
106 # display errors
107 for error in errors:
108 report({'ERROR', 'INFO'}, error)
110 # inform the user about the sat/sab files
111 if len(do.acis_files) > 0:
112 report({'INFO'}, "Exported %d NURBS objects to sat/sab files next to your DXF file" % len(do.acis_files))
115 def display_groups_in_outliner():
116 outliners = (a for a in bpy.context.screen.areas if a.type == "OUTLINER")
117 for outliner in outliners:
118 pass
119 #outliner.spaces[0].display_mode = "GROUPS"
122 # Update helpers (must be globals to be re-usable).
123 def _update_use_georeferencing_do(self, context):
124 if not self.create_new_scene:
125 scene = context.scene
126 # Try to get Scene SRID (ESPG) data from current scene.
127 srid = scene.get("SRID", None)
128 if srid is not None:
129 self.internal_using_scene_srid = True
130 srid = srid.upper()
131 if srid == 'TMERC':
132 self.proj_scene = 'TMERC'
133 self.merc_scene_lat = scene.get('latitude', 0)
134 self.merc_scene_lon = scene.get('longitude', 0)
135 else:
136 if srid in (p[0] for p in proj_epsg_items):
137 self.proj_scene = srid
138 else:
139 self.proj_scene = 'USER'
140 self.epsg_scene_user = srid
141 else:
142 self.internal_using_scene_srid = False
143 else:
144 self.internal_using_scene_srid = False
147 def _recenter_allowed(self):
148 scene = bpy.context.scene
149 conditional_requirement = self.proj_scene == 'TMERC' if PYPROJ else self.dxf_indi == "SPHERICAL"
150 return not (
151 self.use_georeferencing and
153 conditional_requirement or
154 (not self.create_new_scene and is_ref_scene(scene))
159 def _set_recenter(self, value):
160 self.recenter = value if _recenter_allowed(self) else False
163 def _update_proj_scene_do(self, context):
164 # make sure scene EPSG is not None if DXF EPSG is not None
165 if self.proj_scene == 'NONE' and self.proj_dxf != 'NONE':
166 self.proj_scene = self.proj_dxf
169 def _update_import_atts_do(self, context):
170 mo = merge_map[self.merge_options]
171 if mo == BY_CLOSED_NO_BULGE_POLY or mo == BY_BLOCKS:
172 self.import_atts = False
173 self.represent_thickness_and_width = False
174 elif self.represent_thickness_and_width and self.merge:
175 self.import_atts = True
176 elif not self.merge:
177 self.import_atts = False
180 class IMPORT_OT_dxf(bpy.types.Operator):
181 """Import from DXF file format (.dxf)"""
182 bl_idname = "import_scene.dxf"
183 bl_description = 'Import from DXF file format (.dxf)'
184 bl_label = "Import DXf v." + __version__
185 bl_space_type = 'PROPERTIES'
186 bl_region_type = 'WINDOW'
187 bl_options = {'UNDO'}
189 filepath: StringProperty(
190 name="input file",
191 subtype='FILE_PATH'
194 filename_ext = ".dxf"
196 files: CollectionProperty(
197 type=bpy.types.OperatorFileListElement,
198 options={'HIDDEN', 'SKIP_SAVE'}
201 directory: StringProperty(
202 subtype='DIR_PATH'
205 filter_glob: StringProperty(
206 default="*.dxf",
207 options={'HIDDEN'},
210 def _update_merge(self, context):
211 _update_import_atts_do(self, context)
212 merge: BoolProperty(
213 name="Merged Objects",
214 description="Merge DXF entities to Blender objects",
215 default=T_Merge,
216 update=_update_merge
219 def _update_merge_options(self, context):
220 _update_import_atts_do(self, context)
222 merge_options: EnumProperty(
223 name="Merge",
224 description="Merge multiple DXF entities into one Blender object",
225 items=[('BY_LAYER', "By Layer", "Merge DXF entities of a layer to an object"),
226 ('BY_TYPE', "By Layer AND DXF-Type", "Merge DXF entities by type AND layer"),
227 ('BY_CLOSED_NO_BULGE_POLY', "By Layer AND closed no-bulge polys", "Polys can have a transformation attribute that makes DXF polys resemble Blender mesh faces quite a bit. Merging them results in one MESH object."),
228 ('BY_BLOCKS', "By Layer AND DXF-Type AND Blocks", "Merging blocks results in all uniformly scaled blocks being referenced by a dupliface mesh instead of object containers. Non-uniformly scaled blocks will be imported as indicated by 'Blocks As'.")],
229 default='BY_LAYER',
230 update=_update_merge_options
233 merge_lines: BoolProperty(
234 name="Combine LINE entities to polygons",
235 description="Checks if lines are connect on start or end and merges them to a polygon",
236 default=T_MergeLines
239 import_text: BoolProperty(
240 name="Import Text",
241 description="Import DXF Text Entities MTEXT and TEXT",
242 default=T_ImportText,
245 import_light: BoolProperty(
246 name="Import Lights",
247 description="Import DXF Text Entity LIGHT",
248 default=T_ImportLight
251 export_acis: BoolProperty(
252 name="Export ACIS Entities",
253 description="Export Entities consisting of ACIS code to ACIS .sat/.sab files",
254 default=T_ExportAcis
257 outliner_groups: BoolProperty(
258 name="Display Groups in Outliner(s)",
259 description="Make all outliners in current screen layout show groups",
260 default=T_OutlinerGroups
263 do_bbox: BoolProperty(
264 name="Parent Blocks to Bounding Boxes",
265 description="Create a bounding box for blocks with more than one object (faster without)",
266 default=T_Bbox
269 scene_options: EnumProperty(
270 name="Scene",
271 description="Select the import method",
272 items=[('CURRENT_SCENE', "Current", "All DXF files in the current scene."),
273 ('NEW_SCENE', "New", "Each DXF file in a new scene."),
274 ('NEW_UNIQUE_SCENE', "Unique", "All DXF files in a new collection.")],
275 default='CURRENT_SCENE',
278 collection_options: EnumProperty(
279 name="Collection",
280 description="Select the import method",
281 items=[('CURRENT_COLLECTION', "Current", "All DXF files in the current scene collection."),
282 ('NEW_COLLECTION', "New", "Each DXF file in a new collection."),
283 ('SCENE_COLLECTION', "Scene", "All DXF files in the scene collection.")],
284 default='CURRENT_COLLECTION',
287 block_options: EnumProperty(
288 name="Blocks As",
289 description="Select the representation of DXF blocks: linked objects or group instances",
290 items=[('LINKED_OBJECTS', "Linked Objects", "Block objects get imported as linked objects"),
291 ('GROUP_INSTANCES', "Group Instances", "Block objects get imported as group instances")],
292 default='LINKED_OBJECTS',
296 def _update_create_new_scene(self, context):
297 _update_use_georeferencing_do(self, context)
298 _set_recenter(self, self.recenter)
299 create_new_scene: BoolProperty(
300 name="Import DXF to new scene",
301 description="Creates a new scene with the name of the imported file",
302 default=T_CreateNewScene,
303 update=_update_create_new_scene,
306 recenter: BoolProperty(
307 name="Center geometry to scene",
308 description="Moves geometry to the center of the scene",
309 default=T_Recenter,
312 def _update_thickness_width(self, context):
313 _update_import_atts_do(self, context)
314 represent_thickness_and_width: BoolProperty(
315 name="Represent line thickness/width",
316 description="Map thickness and width of lines to Bevel objects and extrusion attribute",
317 default=T_ThicknessBevel,
318 update=_update_thickness_width
321 import_atts: BoolProperty(
322 name="Merge by attributes",
323 description="If 'Merge objects' is on but thickness and width are not chosen to be represented, with this "
324 "option object still can be merged by thickness, with, subd and extrusion attributes "
325 "(extrusion = transformation matrix of DXF objects)",
326 default=T_import_atts
329 # geo referencing
331 def _update_use_georeferencing(self, context):
332 _update_use_georeferencing_do(self, context)
333 _set_recenter(self, self.recenter)
334 use_georeferencing: BoolProperty(
335 name="Geo Referencing",
336 description="Project coordinates to a given coordinate system or reference point",
337 default=True,
338 update=_update_use_georeferencing,
341 def _update_dxf_indi(self, context):
342 _set_recenter(self, self.recenter)
343 dxf_indi: EnumProperty(
344 name="DXF coordinate type",
345 description="Indication for spherical or euclidean coordinates",
346 items=[('EUCLIDEAN', "Euclidean", "Coordinates in x/y"),
347 ('SPHERICAL', "Spherical", "Coordinates in lat/lon")],
348 default='EUCLIDEAN',
349 update=_update_dxf_indi,
352 # Note: FloatProperty is not precise enough, e.g. 1.0 becomes 0.999999999. Python is more precise here (it uses
353 # doubles internally), so we store it as string here and convert to number with py's float() func.
354 dxf_scale: StringProperty(
355 name="Unit Scale",
356 description="Coordinates are assumed to be in meters; deviation must be indicated here",
357 default="1.0"
360 def _update_proj(self, context):
361 _update_proj_scene_do(self, context)
362 _set_recenter(self, self.recenter)
363 if PYPROJ:
364 pitems = proj_none_items + proj_user_items + proj_epsg_items
365 proj_dxf: EnumProperty(
366 name="DXF SRID",
367 description="The coordinate system for the DXF file (check http://epsg.io)",
368 items=pitems,
369 default='NONE',
370 update=_update_proj,
373 epsg_dxf_user: StringProperty(name="EPSG-Code", default="EPSG")
374 merc_dxf_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
375 merc_dxf_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)
377 pitems = proj_none_items + ((proj_user_items + proj_tmerc_items + proj_epsg_items) if PYPROJ else proj_tmerc_items)
378 proj_scene: EnumProperty(
379 name="Scn SRID",
380 description="The coordinate system for the Scene (check http://epsg.io)",
381 items=pitems,
382 default='NONE',
383 update=_update_proj,
386 epsg_scene_user: StringProperty(name="EPSG-Code", default="EPSG")
387 merc_scene_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
388 merc_scene_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)
390 # internal use only!
391 internal_using_scene_srid: BoolProperty(default=False, options={'HIDDEN'})
393 def draw(self, context):
394 layout = self.layout
395 scene = context.scene
397 # Import options
398 layout.label(text="Import Options:")
399 box = layout.box()
400 box.prop(self, "scene_options")
401 box.prop(self, "collection_options")
403 # merge options
404 layout.label(text="Merge Options:")
405 box = layout.box()
406 sub = box.row()
407 #sub.enabled = merge_map[self.merge_options] != BY_BLOCKS
408 sub.prop(self, "block_options")
409 box.prop(self, "do_bbox")
410 box.prop(self, "merge")
411 sub = box.row()
412 sub.enabled = self.merge
413 sub.prop(self, "merge_options")
414 box.prop(self, "merge_lines")
416 # general options
417 layout.label(text="Line thickness and width:")
418 box = layout.box()
419 box.enabled = not merge_map[self.merge_options] == BY_CLOSED_NO_BULGE_POLY
420 box.prop(self, "represent_thickness_and_width")
421 sub = box.row()
422 sub.enabled = (not self.represent_thickness_and_width and self.merge)
423 sub.prop(self, "import_atts")
425 # optional objects
426 layout.label(text="Optional Objects:")
427 box = layout.box()
428 box.prop(self, "import_text")
429 box.prop(self, "import_light")
430 box.prop(self, "export_acis")
432 # view options
433 layout.label(text="View Options:")
434 box = layout.box()
435 box.prop(self, "outliner_groups")
436 sub = box.row()
437 sub.enabled = _recenter_allowed(self)
438 sub.prop(self, "recenter")
440 # geo referencing
441 layout.prop(self, "use_georeferencing", text="Geo Referencing:")
442 box = layout.box()
443 box.enabled = self.use_georeferencing
444 self.draw_pyproj(box, context.scene) if PYPROJ else self.draw_merc(box)
446 def draw_merc(self, box):
447 box.label(text="DXF File:")
448 box.prop(self, "dxf_indi")
449 box.prop(self, "dxf_scale")
451 sub = box.column()
452 sub.enabled = not _recenter_allowed(self)
453 sub.label(text="Geo Reference:")
454 sub = box.column()
455 sub.enabled = not _recenter_allowed(self)
456 if is_ref_scene(bpy.context.scene):
457 sub.enabled = False
458 sub.prop(self, "merc_scene_lat", text="Lat")
459 sub.prop(self, "merc_scene_lon", text="Lon")
461 def draw_pyproj(self, box, scene):
462 valid_dxf_srid = True
464 # DXF SCALE
465 box.prop(self, "dxf_scale")
467 # EPSG DXF
468 box.alert = (self.proj_scene != 'NONE' and (not valid_dxf_srid or self.proj_dxf == 'NONE'))
469 box.prop(self, "proj_dxf")
470 box.alert = False
471 if self.proj_dxf == 'USER':
472 try:
473 Proj(init=self.epsg_dxf_user)
474 except:
475 box.alert = True
476 valid_dxf_srid = False
477 box.prop(self, "epsg_dxf_user")
478 box.alert = False
480 box.separator()
482 # EPSG SCENE
483 col = box.column()
484 # Only info in case of pre-defined EPSG from current scene.
485 if self.internal_using_scene_srid:
486 col.enabled = False
488 col.prop(self, "proj_scene")
490 if self.proj_scene == 'USER':
491 try:
492 Proj(init=self.epsg_scene_user)
493 except Exception as e:
494 col.alert = True
495 col.prop(self, "epsg_scene_user")
496 col.alert = False
497 col.label(text="") # Placeholder.
498 elif self.proj_scene == 'TMERC':
499 col.prop(self, "merc_scene_lat", text="Lat")
500 col.prop(self, "merc_scene_lon", text="Lon")
501 else:
502 col.label(text="") # Placeholder.
503 col.label(text="") # Placeholder.
505 # user info
506 if self.proj_scene != 'NONE':
507 if not valid_dxf_srid:
508 box.label(text="DXF SRID not valid", icon="ERROR")
509 if self.proj_dxf == 'NONE':
510 box.label(text="", icon='ERROR')
511 box.label(text="DXF SRID must be set, otherwise")
512 if self.proj_scene == 'USER':
513 code = self.epsg_scene_user
514 else:
515 code = self.proj_scene
516 box.label(text='Scene SRID %r is ignored!' % code)
518 def execute(self, context):
519 block_map = {"LINKED_OBJECTS": LINKED_OBJECTS, "GROUP_INSTANCES": GROUP_INSTANCES}
520 merge_options = SEPARATED
521 if self.merge:
522 merge_options = merge_map[self.merge_options]
523 scene = bpy.context.scene
524 if self.create_new_scene:
525 scene = bpy.data.scenes.new(os.path.basename(self.filepath).replace(".dxf", ""))
527 proj_dxf = None
528 proj_scn = None
529 dxf_unit_scale = 1.0
530 if self.use_georeferencing:
531 dxf_unit_scale = float(self.dxf_scale.replace(",", "."))
532 if PYPROJ:
533 if self.proj_dxf != 'NONE':
534 if self.proj_dxf == 'USER':
535 proj_dxf = Proj(init=self.epsg_dxf_user)
536 else:
537 proj_dxf = Proj(init=self.proj_dxf)
538 if self.proj_scene != 'NONE':
539 if self.proj_scene == 'USER':
540 proj_scn = Proj(init=self.epsg_scene_user)
541 elif self.proj_scene == 'TMERC':
542 proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
543 else:
544 proj_scn = Proj(init=self.proj_scene)
545 else:
546 proj_dxf = Indicator(self.dxf_indi)
547 proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
549 scene = bpy.context.scene
550 if self.create_new_scene:
551 scene = bpy.data.scenes.new(Path(file.name).stem)
553 for file in self.files:
555 match self.scene_options:
556 case 'NEW_SCENE':
557 scene = bpy.data.scenes.new(Path(file.name).stem)
558 case 'NEW_UNIQUE_SCENE':
559 scene_name="DXF Import"
560 if bpy.data.scenes.get(scene_name): scene=bpy.data.scenes[scene_name]
561 else: scene = bpy.data.scenes.new(scene_name)
562 case _:
563 scene = bpy.context.scene
565 match self.collection_options:
566 case 'NEW_COLLECTION':
567 collection = bpy.data.collections.new(Path(file.name).stem)
568 scene.collection.children.link(collection)
569 case 'SCENE_COLLECTION':
570 collection = scene.collection
571 case _:
572 collection = bpy.context.collection
573 if collection != scene.collection and collection.name not in scene.collection.children: scene.collection.children.link(collection)
575 if RELEASE_TEST:
576 # for release testing
577 from . import test
578 test.test()
579 else:
580 read(self.report, Path(self.directory, file.name), merge_options, self.import_text, self.import_light, self.export_acis,
581 self.merge_lines, self.do_bbox, block_map[self.block_options], scene, collection, self.recenter,
582 proj_dxf, proj_scn, self.represent_thickness_and_width, self.import_atts, dxf_unit_scale)
584 if self.outliner_groups:
585 display_groups_in_outliner()
587 return {'FINISHED'}
589 def invoke(self, context, event):
590 # Force first update...
591 self._update_use_georeferencing(context)
593 wm = context.window_manager
594 wm.fileselect_add(self)
595 return {'RUNNING_MODAL'}
598 def menu_func(self, context):
599 self.layout.operator(IMPORT_OT_dxf.bl_idname, text="AutoCAD DXF (.dxf)")
602 def register():
603 bpy.utils.register_class(IMPORT_OT_dxf)
604 bpy.types.TOPBAR_MT_file_import.append(menu_func)
607 def unregister():
608 bpy.utils.unregister_class(IMPORT_OT_dxf)
609 bpy.types.TOPBAR_MT_file_import.remove(menu_func)
612 if __name__ == "__main__":
613 register()