Remove bl_options from menus which caused tests to fail
[blender-addons.git] / io_import_dxf / __init__.py
blob6d2fa332a8d553fed94d96602efd5637dc29df6b
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
8 from .dxfimport.do import Do, Indicator
9 from .transverse_mercator import TransverseMercator
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
87 RELEASE_TEST = False
88 DEBUG = False
91 def is_ref_scene(scene):
92 return "latitude" in scene and "longitude" in scene
95 def read(report, filename, obj_merge=BY_LAYER, import_text=True, import_light=True, export_acis=True, merge_lines=True,
96 do_bbox=True, block_rep=LINKED_OBJECTS, new_scene=None, recenter=False, projDXF=None, projSCN=None,
97 thicknessWidth=True, but_group_by_att=True, dxf_unit_scale=1.0):
98 # import dxf and export nurbs types to sat/sab files
99 # because that's how autocad stores nurbs types in a dxf...
100 do = Do(filename, obj_merge, import_text, import_light, export_acis, merge_lines, do_bbox, block_rep, recenter,
101 projDXF, projSCN, thicknessWidth, but_group_by_att, dxf_unit_scale)
103 errors = do.entities(os.path.basename(filename).replace(".dxf", ""), new_scene)
105 # display errors
106 for error in errors:
107 report({'ERROR', 'INFO'}, error)
109 # inform the user about the sat/sab files
110 if len(do.acis_files) > 0:
111 report({'INFO'}, "Exported %d NURBS objects to sat/sab files next to your DXF file" % len(do.acis_files))
114 def display_groups_in_outliner():
115 outliners = (a for a in bpy.context.screen.areas if a.type == "OUTLINER")
116 for outliner in outliners:
117 pass
118 #outliner.spaces[0].display_mode = "GROUPS"
121 # Update helpers (must be globals to be re-usable).
122 def _update_use_georeferencing_do(self, context):
123 if not self.create_new_scene:
124 scene = context.scene
125 # Try to get Scene SRID (ESPG) data from current scene.
126 srid = scene.get("SRID", None)
127 if srid is not None:
128 self.internal_using_scene_srid = True
129 srid = srid.upper()
130 if srid == 'TMERC':
131 self.proj_scene = 'TMERC'
132 self.merc_scene_lat = scene.get('latitude', 0)
133 self.merc_scene_lon = scene.get('longitude', 0)
134 else:
135 if srid in (p[0] for p in proj_epsg_items):
136 self.proj_scene = srid
137 else:
138 self.proj_scene = 'USER'
139 self.epsg_scene_user = srid
140 else:
141 self.internal_using_scene_srid = False
142 else:
143 self.internal_using_scene_srid = False
146 def _recenter_allowed(self):
147 scene = bpy.context.scene
148 conditional_requirement = self.proj_scene == 'TMERC' if PYPROJ else self.dxf_indi == "SPHERICAL"
149 return not (
150 self.use_georeferencing and
152 conditional_requirement or
153 (not self.create_new_scene and is_ref_scene(scene))
158 def _set_recenter(self, value):
159 self.recenter = value if _recenter_allowed(self) else False
162 def _update_proj_scene_do(self, context):
163 # make sure scene EPSG is not None if DXF EPSG is not None
164 if self.proj_scene == 'NONE' and self.proj_dxf != 'NONE':
165 self.proj_scene = self.proj_dxf
168 def _update_import_atts_do(self, context):
169 mo = merge_map[self.merge_options]
170 if mo == BY_CLOSED_NO_BULGE_POLY or mo == BY_BLOCKS:
171 self.import_atts = False
172 self.represent_thickness_and_width = False
173 elif self.represent_thickness_and_width and self.merge:
174 self.import_atts = True
175 elif not self.merge:
176 self.import_atts = False
179 class IMPORT_OT_dxf(bpy.types.Operator):
180 """Import from DXF file format (.dxf)"""
181 bl_idname = "import_scene.dxf"
182 bl_description = 'Import from DXF file format (.dxf)'
183 bl_label = "Import DXf v." + __version__
184 bl_space_type = 'PROPERTIES'
185 bl_region_type = 'WINDOW'
186 bl_options = {'UNDO'}
188 filepath: StringProperty(
189 name="input file",
190 subtype='FILE_PATH'
193 filename_ext = ".dxf"
195 filter_glob: StringProperty(
196 default="*.dxf",
197 options={'HIDDEN'},
200 def _update_merge(self, context):
201 _update_import_atts_do(self, context)
202 merge: BoolProperty(
203 name="Merged Objects",
204 description="Merge DXF entities to Blender objects",
205 default=T_Merge,
206 update=_update_merge
209 def _update_merge_options(self, context):
210 _update_import_atts_do(self, context)
212 merge_options: EnumProperty(
213 name="Merge",
214 description="Merge multiple DXF entities into one Blender object",
215 items=[('BY_LAYER', "By Layer", "Merge DXF entities of a layer to an object"),
216 ('BY_TYPE', "By Layer AND DXF-Type", "Merge DXF entities by type AND layer"),
217 ('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."),
218 ('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'.")],
219 default='BY_LAYER',
220 update=_update_merge_options
223 merge_lines: BoolProperty(
224 name="Combine LINE entities to polygons",
225 description="Checks if lines are connect on start or end and merges them to a polygon",
226 default=T_MergeLines
229 import_text: BoolProperty(
230 name="Import Text",
231 description="Import DXF Text Entities MTEXT and TEXT",
232 default=T_ImportText,
235 import_light: BoolProperty(
236 name="Import Lights",
237 description="Import DXF Text Entity LIGHT",
238 default=T_ImportLight
241 export_acis: BoolProperty(
242 name="Export ACIS Entities",
243 description="Export Entities consisting of ACIS code to ACIS .sat/.sab files",
244 default=T_ExportAcis
247 outliner_groups: BoolProperty(
248 name="Display Groups in Outliner(s)",
249 description="Make all outliners in current screen layout show groups",
250 default=T_OutlinerGroups
253 do_bbox: BoolProperty(
254 name="Parent Blocks to Bounding Boxes",
255 description="Create a bounding box for blocks with more than one object (faster without)",
256 default=T_Bbox
261 block_options: EnumProperty(
262 name="Blocks As",
263 description="Select the representation of DXF blocks: linked objects or group instances",
264 items=[('LINKED_OBJECTS', "Linked Objects", "Block objects get imported as linked objects"),
265 ('GROUP_INSTANCES', "Group Instances", "Block objects get imported as group instances")],
266 default='LINKED_OBJECTS',
270 def _update_create_new_scene(self, context):
271 _update_use_georeferencing_do(self, context)
272 _set_recenter(self, self.recenter)
273 create_new_scene: BoolProperty(
274 name="Import DXF to new scene",
275 description="Creates a new scene with the name of the imported file",
276 default=T_CreateNewScene,
277 update=_update_create_new_scene,
280 recenter: BoolProperty(
281 name="Center geometry to scene",
282 description="Moves geometry to the center of the scene",
283 default=T_Recenter,
286 def _update_thickness_width(self, context):
287 _update_import_atts_do(self, context)
288 represent_thickness_and_width: BoolProperty(
289 name="Represent line thickness/width",
290 description="Map thickness and width of lines to Bevel objects and extrusion attribute",
291 default=T_ThicknessBevel,
292 update=_update_thickness_width
295 import_atts: BoolProperty(
296 name="Merge by attributes",
297 description="If 'Merge objects' is on but thickness and width are not chosen to be represented, with this "
298 "option object still can be merged by thickness, with, subd and extrusion attributes "
299 "(extrusion = transformation matrix of DXF objects)",
300 default=T_import_atts
303 # geo referencing
305 def _update_use_georeferencing(self, context):
306 _update_use_georeferencing_do(self, context)
307 _set_recenter(self, self.recenter)
308 use_georeferencing: BoolProperty(
309 name="Geo Referencing",
310 description="Project coordinates to a given coordinate system or reference point",
311 default=True,
312 update=_update_use_georeferencing,
315 def _update_dxf_indi(self, context):
316 _set_recenter(self, self.recenter)
317 dxf_indi: EnumProperty(
318 name="DXF coordinate type",
319 description="Indication for spherical or euclidean coordinates",
320 items=[('EUCLIDEAN', "Euclidean", "Coordinates in x/y"),
321 ('SPHERICAL', "Spherical", "Coordinates in lat/lon")],
322 default='EUCLIDEAN',
323 update=_update_dxf_indi,
326 # Note: FloatProperty is not precise enough, e.g. 1.0 becomes 0.999999999. Python is more precise here (it uses
327 # doubles internally), so we store it as string here and convert to number with py's float() func.
328 dxf_scale: StringProperty(
329 name="Unit Scale",
330 description="Coordinates are assumed to be in meters; deviation must be indicated here",
331 default="1.0"
334 def _update_proj(self, context):
335 _update_proj_scene_do(self, context)
336 _set_recenter(self, self.recenter)
337 if PYPROJ:
338 pitems = proj_none_items + proj_user_items + proj_epsg_items
339 proj_dxf: EnumProperty(
340 name="DXF SRID",
341 description="The coordinate system for the DXF file (check http://epsg.io)",
342 items=pitems,
343 default='NONE',
344 update=_update_proj,
347 epsg_dxf_user: StringProperty(name="EPSG-Code", default="EPSG")
348 merc_dxf_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
349 merc_dxf_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)
351 pitems = proj_none_items + ((proj_user_items + proj_tmerc_items + proj_epsg_items) if PYPROJ else proj_tmerc_items)
352 proj_scene: EnumProperty(
353 name="Scn SRID",
354 description="The coordinate system for the Scene (check http://epsg.io)",
355 items=pitems,
356 default='NONE',
357 update=_update_proj,
360 epsg_scene_user: StringProperty(name="EPSG-Code", default="EPSG")
361 merc_scene_lat: FloatProperty(name="Geo-Reference Latitude", default=0.0)
362 merc_scene_lon: FloatProperty(name="Geo-Reference Longitude", default=0.0)
364 # internal use only!
365 internal_using_scene_srid: BoolProperty(default=False, options={'HIDDEN'})
367 def draw(self, context):
368 layout = self.layout
369 scene = context.scene
371 # merge options
372 layout.label(text="Merge Options:")
373 box = layout.box()
374 sub = box.row()
375 #sub.enabled = merge_map[self.merge_options] != BY_BLOCKS
376 sub.prop(self, "block_options")
377 box.prop(self, "do_bbox")
378 box.prop(self, "merge")
379 sub = box.row()
380 sub.enabled = self.merge
381 sub.prop(self, "merge_options")
382 box.prop(self, "merge_lines")
384 # general options
385 layout.label(text="Line thickness and width:")
386 box = layout.box()
387 box.enabled = not merge_map[self.merge_options] == BY_CLOSED_NO_BULGE_POLY
388 box.prop(self, "represent_thickness_and_width")
389 sub = box.row()
390 sub.enabled = (not self.represent_thickness_and_width and self.merge)
391 sub.prop(self, "import_atts")
393 # optional objects
394 layout.label(text="Optional Objects:")
395 box = layout.box()
396 box.prop(self, "import_text")
397 box.prop(self, "import_light")
398 box.prop(self, "export_acis")
400 # view options
401 layout.label(text="View Options:")
402 box = layout.box()
403 box.prop(self, "outliner_groups")
404 box.prop(self, "create_new_scene")
405 sub = box.row()
406 sub.enabled = _recenter_allowed(self)
407 sub.prop(self, "recenter")
409 # geo referencing
410 layout.prop(self, "use_georeferencing", text="Geo Referencing:")
411 box = layout.box()
412 box.enabled = self.use_georeferencing
413 self.draw_pyproj(box, context.scene) if PYPROJ else self.draw_merc(box)
415 def draw_merc(self, box):
416 box.label(text="DXF File:")
417 box.prop(self, "dxf_indi")
418 box.prop(self, "dxf_scale")
420 sub = box.column()
421 sub.enabled = not _recenter_allowed(self)
422 sub.label(text="Geo Reference:")
423 sub = box.column()
424 sub.enabled = not _recenter_allowed(self)
425 if is_ref_scene(bpy.context.scene):
426 sub.enabled = False
427 sub.prop(self, "merc_scene_lat", text="Lat")
428 sub.prop(self, "merc_scene_lon", text="Lon")
430 def draw_pyproj(self, box, scene):
431 valid_dxf_srid = True
433 # DXF SCALE
434 box.prop(self, "dxf_scale")
436 # EPSG DXF
437 box.alert = (self.proj_scene != 'NONE' and (not valid_dxf_srid or self.proj_dxf == 'NONE'))
438 box.prop(self, "proj_dxf")
439 box.alert = False
440 if self.proj_dxf == 'USER':
441 try:
442 Proj(init=self.epsg_dxf_user)
443 except:
444 box.alert = True
445 valid_dxf_srid = False
446 box.prop(self, "epsg_dxf_user")
447 box.alert = False
449 box.separator()
451 # EPSG SCENE
452 col = box.column()
453 # Only info in case of pre-defined EPSG from current scene.
454 if self.internal_using_scene_srid:
455 col.enabled = False
457 col.prop(self, "proj_scene")
459 if self.proj_scene == 'USER':
460 try:
461 Proj(init=self.epsg_scene_user)
462 except Exception as e:
463 col.alert = True
464 col.prop(self, "epsg_scene_user")
465 col.alert = False
466 col.label(text="") # Placeholder.
467 elif self.proj_scene == 'TMERC':
468 col.prop(self, "merc_scene_lat", text="Lat")
469 col.prop(self, "merc_scene_lon", text="Lon")
470 else:
471 col.label(text="") # Placeholder.
472 col.label(text="") # Placeholder.
474 # user info
475 if self.proj_scene != 'NONE':
476 if not valid_dxf_srid:
477 box.label(text="DXF SRID not valid", icon="ERROR")
478 if self.proj_dxf == 'NONE':
479 box.label(text="", icon='ERROR')
480 box.label(text="DXF SRID must be set, otherwise")
481 if self.proj_scene == 'USER':
482 code = self.epsg_scene_user
483 else:
484 code = self.proj_scene
485 box.label(text='Scene SRID %r is ignored!' % code)
487 def execute(self, context):
488 block_map = {"LINKED_OBJECTS": LINKED_OBJECTS, "GROUP_INSTANCES": GROUP_INSTANCES}
489 merge_options = SEPARATED
490 if self.merge:
491 merge_options = merge_map[self.merge_options]
492 scene = bpy.context.scene
493 if self.create_new_scene:
494 scene = bpy.data.scenes.new(os.path.basename(self.filepath).replace(".dxf", ""))
496 proj_dxf = None
497 proj_scn = None
498 dxf_unit_scale = 1.0
499 if self.use_georeferencing:
500 dxf_unit_scale = float(self.dxf_scale.replace(",", "."))
501 if PYPROJ:
502 if self.proj_dxf != 'NONE':
503 if self.proj_dxf == 'USER':
504 proj_dxf = Proj(init=self.epsg_dxf_user)
505 else:
506 proj_dxf = Proj(init=self.proj_dxf)
507 if self.proj_scene != 'NONE':
508 if self.proj_scene == 'USER':
509 proj_scn = Proj(init=self.epsg_scene_user)
510 elif self.proj_scene == 'TMERC':
511 proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
512 else:
513 proj_scn = Proj(init=self.proj_scene)
514 else:
515 proj_dxf = Indicator(self.dxf_indi)
516 proj_scn = TransverseMercator(lat=self.merc_scene_lat, lon=self.merc_scene_lon)
518 if RELEASE_TEST:
519 # for release testing
520 from . import test
521 test.test()
522 else:
523 read(self.report, self.filepath, merge_options, self.import_text, self.import_light, self.export_acis,
524 self.merge_lines, self.do_bbox, block_map[self.block_options], scene, self.recenter,
525 proj_dxf, proj_scn, self.represent_thickness_and_width, self.import_atts, dxf_unit_scale)
527 if self.outliner_groups:
528 display_groups_in_outliner()
530 return {'FINISHED'}
532 def invoke(self, context, event):
533 # Force first update...
534 self._update_use_georeferencing(context)
536 wm = context.window_manager
537 wm.fileselect_add(self)
538 return {'RUNNING_MODAL'}
541 def menu_func(self, context):
542 self.layout.operator(IMPORT_OT_dxf.bl_idname, text="AutoCAD DXF (.dxf)")
545 def register():
546 bpy.utils.register_class(IMPORT_OT_dxf)
547 bpy.types.TOPBAR_MT_file_import.append(menu_func)
550 def unregister():
551 bpy.utils.unregister_class(IMPORT_OT_dxf)
552 bpy.types.TOPBAR_MT_file_import.remove(menu_func)
555 if __name__ == "__main__":
556 register()