Fix T71100: Node Wrangler creates nodes on linked node trees
[blender-addons.git] / io_import_dxf / __init__.py
blobecda11e42b40453816397253f51b5d55191bc769
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
4 import os
5 from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty
6 from .dxfimport.do import Do, Indicator
7 from .transverse_mercator import TransverseMercator
10 try:
11 from pyproj import Proj, transform
12 PYPROJ = True
13 except:
14 PYPROJ = False
16 bl_info = {
17 "name": "Import AutoCAD DXF Format (.dxf)",
18 "author": "Lukas Treyer, Manfred Moitzi (support + dxfgrabber library), Vladimir Elistratov, Bastien Montagne, Remigiusz Fiedler (AKA migius)",
19 "version": (0, 9, 6),
20 "blender": (2, 80, 0),
21 "location": "File > Import > AutoCAD DXF",
22 "description": "Import files in the Autocad DXF format (.dxf)",
23 "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/scene_dxf.html",
24 "category": "Import-Export",
28 proj_none_items = (
29 ('NONE', "None", "No Coordinate System is available / will be set"),
31 proj_user_items = (
32 ('USER', "User Defined", "Define the EPSG code"),
34 proj_tmerc_items = (
35 ('TMERC', "Transverse Mercator", "Mercator Projection using a lat/lon coordinate as its geo-reference"),
37 proj_epsg_items = (
38 ('EPSG:4326', "WGS84", "World Geodetic System 84; default for lat / lon; EPSG:4326"),
39 ('EPSG:3857', "Spherical Mercator", "Webbrowser mapping service standard (Google, OpenStreetMap, ESRI); EPSG:3857"),
40 ('EPSG:27700', "National Grid U.K",
41 "Ordnance Survey National Grid reference system used in Great Britain; EPSG:27700"),
42 ('EPSG:2154', "France (Lambert 93)", "Lambert Projection for France; EPSG:2154"),
43 ('EPSG:5514', "Czech Republic & Slovakia", "Coordinate System for Czech Republic and Slovakia; EPSG:5514"),
44 ('EPSG:5243', "LLC Germany", "Projection for Germany; EPSG:5243"),
45 ('EPSG:28992', "Amersfoort Netherlands", "Amersfoort / RD New -- Netherlands; EPSG:28992"),
46 ('EPSG:21781', "Swiss CH1903 / LV03", "Switzerland and Lichtenstein; EPSG:21781"),
47 ('EPSG:5880', "Brazil Polyconic", "Cartesian 2D; Central, South America; EPSG:5880 "),
48 ('EPSG:42103', "LCC USA", "Lambert Conformal Conic Projection; EPSG:42103"),
49 ('EPSG:3350', "Russia: Pulkovo 1942 / CS63 zone C0", "Russian Federation - onshore and offshore; EPSG:3350"),
50 ('EPSG:22293', "Cape / Lo33 South Africa", "South Africa; EPSG:22293"),
51 ('EPSG:27200', "NZGD49 / New Zealand Map Grid", "NZGD49 / New Zealand Map Grid; EPSG:27200"),
52 ('EPSG:3112', "GDA94 Australia Lambert", "GDA94 / Geoscience Australia Lambert; EPSG:3112"),
53 ('EPSG:24378', "India zone I", "Kalianpur 1975 / India zone I; EPSG:24378"),
54 ('EPSG:2326', "Hong Kong 1980 Grid System", "Hong Kong 1980 Grid System; EPSG:2326"),
55 ('EPSG:3414', "SVY21 / Singapore TM", "SVY21 / Singapore TM; EPSG:3414"),
58 proj_epsg_dict = {e[0]: e[1] for e in proj_epsg_items}
60 __version__ = '.'.join([str(s) for s in bl_info['version']])
62 BY_LAYER = 0
63 BY_DXFTYPE = 1
64 BY_CLOSED_NO_BULGE_POLY = 2
65 SEPARATED = 3
66 LINKED_OBJECTS = 4
67 GROUP_INSTANCES = 5
68 BY_BLOCKS = 6
70 merge_map = {"BY_LAYER": BY_LAYER, "BY_TYPE": BY_DXFTYPE,
71 "BY_CLOSED_NO_BULGE_POLY": BY_CLOSED_NO_BULGE_POLY, "BY_BLOCKS": BY_BLOCKS}
73 T_Merge = True
74 T_ImportText = True
75 T_ImportLight = True
76 T_ExportAcis = False
77 T_MergeLines = True
78 T_OutlinerGroups = True
79 T_Bbox = True
80 T_CreateNewScene = False
81 T_Recenter = False
82 T_ThicknessBevel = True
83 T_import_atts = True
85 RELEASE_TEST = False
86 DEBUG = False
89 def is_ref_scene(scene):
90 return "latitude" in scene and "longitude" in scene
93 def read(report, filename, obj_merge=BY_LAYER, import_text=True, import_light=True, export_acis=True, merge_lines=True,
94 do_bbox=True, block_rep=LINKED_OBJECTS, new_scene=None, recenter=False, projDXF=None, projSCN=None,
95 thicknessWidth=True, but_group_by_att=True, dxf_unit_scale=1.0):
96 # import dxf and export nurbs types to sat/sab files
97 # because that's how autocad stores nurbs types in a dxf...
98 do = Do(filename, obj_merge, import_text, import_light, export_acis, merge_lines, do_bbox, block_rep, recenter,
99 projDXF, projSCN, thicknessWidth, but_group_by_att, dxf_unit_scale)
101 errors = do.entities(os.path.basename(filename).replace(".dxf", ""), new_scene)
103 # display errors
104 for error in errors:
105 report({'ERROR', 'INFO'}, error)
107 # inform the user about the sat/sab files
108 if len(do.acis_files) > 0:
109 report({'INFO'}, "Exported %d NURBS objects to sat/sab files next to your DXF file" % len(do.acis_files))
112 def display_groups_in_outliner():
113 outliners = (a for a in bpy.context.screen.areas if a.type == "OUTLINER")
114 for outliner in outliners:
115 pass
116 #outliner.spaces[0].display_mode = "GROUPS"
119 # Update helpers (must be globals to be re-usable).
120 def _update_use_georeferencing_do(self, context):
121 if not self.create_new_scene:
122 scene = context.scene
123 # Try to get Scene SRID (ESPG) data from current scene.
124 srid = scene.get("SRID", None)
125 if srid is not None:
126 self.internal_using_scene_srid = True
127 srid = srid.upper()
128 if srid == 'TMERC':
129 self.proj_scene = 'TMERC'
130 self.merc_scene_lat = scene.get('latitude', 0)
131 self.merc_scene_lon = scene.get('longitude', 0)
132 else:
133 if srid in (p[0] for p in proj_epsg_items):
134 self.proj_scene = srid
135 else:
136 self.proj_scene = 'USER'
137 self.epsg_scene_user = srid
138 else:
139 self.internal_using_scene_srid = False
140 else:
141 self.internal_using_scene_srid = False
144 def _recenter_allowed(self):
145 scene = bpy.context.scene
146 conditional_requirement = self.proj_scene == 'TMERC' if PYPROJ else self.dxf_indi == "SPHERICAL"
147 return not (
148 self.use_georeferencing and
150 conditional_requirement or
151 (not self.create_new_scene and is_ref_scene(scene))
156 def _set_recenter(self, value):
157 self.recenter = value if _recenter_allowed(self) else False
160 def _update_proj_scene_do(self, context):
161 # make sure scene EPSG is not None if DXF EPSG is not None
162 if self.proj_scene == 'NONE' and self.proj_dxf != 'NONE':
163 self.proj_scene = self.proj_dxf
166 def _update_import_atts_do(self, context):
167 mo = merge_map[self.merge_options]
168 if mo == BY_CLOSED_NO_BULGE_POLY or mo == BY_BLOCKS:
169 self.import_atts = False
170 self.represent_thickness_and_width = False
171 elif self.represent_thickness_and_width and self.merge:
172 self.import_atts = True
173 elif not self.merge:
174 self.import_atts = False
177 class IMPORT_OT_dxf(bpy.types.Operator):
178 """Import from DXF file format (.dxf)"""
179 bl_idname = "import_scene.dxf"
180 bl_description = 'Import from DXF file format (.dxf)'
181 bl_label = "Import DXf v." + __version__
182 bl_space_type = 'PROPERTIES'
183 bl_region_type = 'WINDOW'
184 bl_options = {'UNDO'}
186 filepath: StringProperty(
187 name="input file",
188 subtype='FILE_PATH'
191 filename_ext = ".dxf"
193 filter_glob: StringProperty(
194 default="*.dxf",
195 options={'HIDDEN'},
198 def _update_merge(self, context):
199 _update_import_atts_do(self, context)
200 merge: BoolProperty(
201 name="Merged Objects",
202 description="Merge DXF entities to Blender objects",
203 default=T_Merge,
204 update=_update_merge
207 def _update_merge_options(self, context):
208 _update_import_atts_do(self, context)
210 merge_options: EnumProperty(
211 name="Merge",
212 description="Merge multiple DXF entities into one Blender object",
213 items=[('BY_LAYER', "By Layer", "Merge DXF entities of a layer to an object"),
214 ('BY_TYPE', "By Layer AND DXF-Type", "Merge DXF entities by type AND layer"),
215 ('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."),
216 ('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'.")],
217 default='BY_LAYER',
218 update=_update_merge_options
221 merge_lines: BoolProperty(
222 name="Combine LINE entities to polygons",
223 description="Checks if lines are connect on start or end and merges them to a polygon",
224 default=T_MergeLines
227 import_text: BoolProperty(
228 name="Import Text",
229 description="Import DXF Text Entities MTEXT and TEXT",
230 default=T_ImportText,
233 import_light: BoolProperty(
234 name="Import Lights",
235 description="Import DXF Text Entity LIGHT",
236 default=T_ImportLight
239 export_acis: BoolProperty(
240 name="Export ACIS Entities",
241 description="Export Entities consisting of ACIS code to ACIS .sat/.sab files",
242 default=T_ExportAcis
245 outliner_groups: BoolProperty(
246 name="Display Groups in Outliner(s)",
247 description="Make all outliners in current screen layout show groups",
248 default=T_OutlinerGroups
251 do_bbox: BoolProperty(
252 name="Parent Blocks to Bounding Boxes",
253 description="Create a bounding box for blocks with more than one object (faster without)",
254 default=T_Bbox
259 block_options: EnumProperty(
260 name="Blocks As",
261 description="Select the representation of DXF blocks: linked objects or group instances",
262 items=[('LINKED_OBJECTS', "Linked Objects", "Block objects get imported as linked objects"),
263 ('GROUP_INSTANCES', "Group Instances", "Block objects get imported as group instances")],
264 default='LINKED_OBJECTS',
268 def _update_create_new_scene(self, context):
269 _update_use_georeferencing_do(self, context)
270 _set_recenter(self, self.recenter)
271 create_new_scene: BoolProperty(
272 name="Import DXF to new scene",
273 description="Creates a new scene with the name of the imported file",
274 default=T_CreateNewScene,
275 update=_update_create_new_scene,
278 recenter: BoolProperty(
279 name="Center geometry to scene",
280 description="Moves geometry to the center of the scene",
281 default=T_Recenter,
284 def _update_thickness_width(self, context):
285 _update_import_atts_do(self, context)
286 represent_thickness_and_width: BoolProperty(
287 name="Represent line thickness/width",
288 description="Map thickness and width of lines to Bevel objects and extrusion attribute",
289 default=T_ThicknessBevel,
290 update=_update_thickness_width
293 import_atts: BoolProperty(
294 name="Merge by attributes",
295 description="If 'Merge objects' is on but thickness and width are not chosen to be represented, with this "
296 "option object still can be merged by thickness, with, subd and extrusion attributes "
297 "(extrusion = transformation matrix of DXF objects)",
298 default=T_import_atts
301 # geo referencing
303 def _update_use_georeferencing(self, context):
304 _update_use_georeferencing_do(self, context)
305 _set_recenter(self, self.recenter)
306 use_georeferencing: BoolProperty(
307 name="Geo Referencing",
308 description="Project coordinates to a given coordinate system or reference point",
309 default=True,
310 update=_update_use_georeferencing,
313 def _update_dxf_indi(self, context):
314 _set_recenter(self, self.recenter)
315 dxf_indi: EnumProperty(
316 name="DXF coordinate type",
317 description="Indication for spherical or euclidean coordinates",
318 items=[('EUCLIDEAN', "Euclidean", "Coordinates in x/y"),
319 ('SPHERICAL', "Spherical", "Coordinates in lat/lon")],
320 default='EUCLIDEAN',
321 update=_update_dxf_indi,
324 # Note: FloatProperty is not precise enough, e.g. 1.0 becomes 0.999999999. Python is more precise here (it uses
325 # doubles internally), so we store it as string here and convert to number with py's float() func.
326 dxf_scale: StringProperty(
327 name="Unit Scale",
328 description="Coordinates are assumed to be in meters; deviation must be indicated here",
329 default="1.0"
332 def _update_proj(self, context):
333 _update_proj_scene_do(self, context)
334 _set_recenter(self, self.recenter)
335 if PYPROJ:
336 pitems = proj_none_items + proj_user_items + proj_epsg_items
337 proj_dxf: EnumProperty(
338 name="DXF SRID",
339 description="The coordinate system for the DXF file (check http://epsg.io)",
340 items=pitems,
341 default='NONE',
342 update=_update_proj,
345 epsg_dxf_user: StringProperty(name="EPSG-Code", default="EPSG")
346 merc_dxf_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
347 merc_dxf_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)
349 pitems = proj_none_items + ((proj_user_items + proj_tmerc_items + proj_epsg_items) if PYPROJ else proj_tmerc_items)
350 proj_scene: EnumProperty(
351 name="Scn SRID",
352 description="The coordinate system for the Scene (check http://epsg.io)",
353 items=pitems,
354 default='NONE',
355 update=_update_proj,
358 epsg_scene_user: StringProperty(name="EPSG-Code", default="EPSG")
359 merc_scene_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
360 merc_scene_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)
362 # internal use only!
363 internal_using_scene_srid: BoolProperty(default=False, options={'HIDDEN'})
365 def draw(self, context):
366 layout = self.layout
367 scene = context.scene
369 # merge options
370 layout.label(text="Merge Options:")
371 box = layout.box()
372 sub = box.row()
373 #sub.enabled = merge_map[self.merge_options] != BY_BLOCKS
374 sub.prop(self, "block_options")
375 box.prop(self, "do_bbox")
376 box.prop(self, "merge")
377 sub = box.row()
378 sub.enabled = self.merge
379 sub.prop(self, "merge_options")
380 box.prop(self, "merge_lines")
382 # general options
383 layout.label(text="Line thickness and width:")
384 box = layout.box()
385 box.enabled = not merge_map[self.merge_options] == BY_CLOSED_NO_BULGE_POLY
386 box.prop(self, "represent_thickness_and_width")
387 sub = box.row()
388 sub.enabled = (not self.represent_thickness_and_width and self.merge)
389 sub.prop(self, "import_atts")
391 # optional objects
392 layout.label(text="Optional Objects:")
393 box = layout.box()
394 box.prop(self, "import_text")
395 box.prop(self, "import_light")
396 box.prop(self, "export_acis")
398 # view options
399 layout.label(text="View Options:")
400 box = layout.box()
401 box.prop(self, "outliner_groups")
402 box.prop(self, "create_new_scene")
403 sub = box.row()
404 sub.enabled = _recenter_allowed(self)
405 sub.prop(self, "recenter")
407 # geo referencing
408 layout.prop(self, "use_georeferencing", text="Geo Referencing:")
409 box = layout.box()
410 box.enabled = self.use_georeferencing
411 self.draw_pyproj(box, context.scene) if PYPROJ else self.draw_merc(box)
413 def draw_merc(self, box):
414 box.label(text="DXF File:")
415 box.prop(self, "dxf_indi")
416 box.prop(self, "dxf_scale")
418 sub = box.column()
419 sub.enabled = not _recenter_allowed(self)
420 sub.label(text="Geo Reference:")
421 sub = box.column()
422 sub.enabled = not _recenter_allowed(self)
423 if is_ref_scene(bpy.context.scene):
424 sub.enabled = False
425 sub.prop(self, "merc_scene_lat", text="Lat")
426 sub.prop(self, "merc_scene_lon", text="Lon")
428 def draw_pyproj(self, box, scene):
429 valid_dxf_srid = True
431 # DXF SCALE
432 box.prop(self, "dxf_scale")
434 # EPSG DXF
435 box.alert = (self.proj_scene != 'NONE' and (not valid_dxf_srid or self.proj_dxf == 'NONE'))
436 box.prop(self, "proj_dxf")
437 box.alert = False
438 if self.proj_dxf == 'USER':
439 try:
440 Proj(init=self.epsg_dxf_user)
441 except:
442 box.alert = True
443 valid_dxf_srid = False
444 box.prop(self, "epsg_dxf_user")
445 box.alert = False
447 box.separator()
449 # EPSG SCENE
450 col = box.column()
451 # Only info in case of pre-defined EPSG from current scene.
452 if self.internal_using_scene_srid:
453 col.enabled = False
455 col.prop(self, "proj_scene")
457 if self.proj_scene == 'USER':
458 try:
459 Proj(init=self.epsg_scene_user)
460 except Exception as e:
461 col.alert = True
462 col.prop(self, "epsg_scene_user")
463 col.alert = False
464 col.label(text="") # Placeholder.
465 elif self.proj_scene == 'TMERC':
466 col.prop(self, "merc_scene_lat", text="Lat")
467 col.prop(self, "merc_scene_lon", text="Lon")
468 else:
469 col.label(text="") # Placeholder.
470 col.label(text="") # Placeholder.
472 # user info
473 if self.proj_scene != 'NONE':
474 if not valid_dxf_srid:
475 box.label(text="DXF SRID not valid", icon="ERROR")
476 if self.proj_dxf == 'NONE':
477 box.label(text="", icon='ERROR')
478 box.label(text="DXF SRID must be set, otherwise")
479 if self.proj_scene == 'USER':
480 code = self.epsg_scene_user
481 else:
482 code = self.proj_scene
483 box.label(text='Scene SRID %r is ignored!' % code)
485 def execute(self, context):
486 block_map = {"LINKED_OBJECTS": LINKED_OBJECTS, "GROUP_INSTANCES": GROUP_INSTANCES}
487 merge_options = SEPARATED
488 if self.merge:
489 merge_options = merge_map[self.merge_options]
490 scene = bpy.context.scene
491 if self.create_new_scene:
492 scene = bpy.data.scenes.new(os.path.basename(self.filepath).replace(".dxf", ""))
494 proj_dxf = None
495 proj_scn = None
496 dxf_unit_scale = 1.0
497 if self.use_georeferencing:
498 dxf_unit_scale = float(self.dxf_scale.replace(",", "."))
499 if PYPROJ:
500 if self.proj_dxf != 'NONE':
501 if self.proj_dxf == 'USER':
502 proj_dxf = Proj(init=self.epsg_dxf_user)
503 else:
504 proj_dxf = Proj(init=self.proj_dxf)
505 if self.proj_scene != 'NONE':
506 if self.proj_scene == 'USER':
507 proj_scn = Proj(init=self.epsg_scene_user)
508 elif self.proj_scene == 'TMERC':
509 proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
510 else:
511 proj_scn = Proj(init=self.proj_scene)
512 else:
513 proj_dxf = Indicator(self.dxf_indi)
514 proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
516 if RELEASE_TEST:
517 # for release testing
518 from . import test
519 test.test()
520 else:
521 read(self.report, self.filepath, merge_options, self.import_text, self.import_light, self.export_acis,
522 self.merge_lines, self.do_bbox, block_map[self.block_options], scene, self.recenter,
523 proj_dxf, proj_scn, self.represent_thickness_and_width, self.import_atts, dxf_unit_scale)
525 if self.outliner_groups:
526 display_groups_in_outliner()
528 return {'FINISHED'}
530 def invoke(self, context, event):
531 # Force first update...
532 self._update_use_georeferencing(context)
534 wm = context.window_manager
535 wm.fileselect_add(self)
536 return {'RUNNING_MODAL'}
539 def menu_func(self, context):
540 self.layout.operator(IMPORT_OT_dxf.bl_idname, text="AutoCAD DXF")
543 def register():
544 bpy.utils.register_class(IMPORT_OT_dxf)
545 bpy.types.TOPBAR_MT_file_import.append(menu_func)
548 def unregister():
549 bpy.utils.unregister_class(IMPORT_OT_dxf)
550 bpy.types.TOPBAR_MT_file_import.remove(menu_func)
553 if __name__ == "__main__":
554 register()