1 # SPDX-FileCopyrightText: 2014-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
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
13 from pyproj
import Proj
, transform
19 "name": "Import AutoCAD DXF Format (.dxf)",
20 "author": "Lukas Treyer, Manfred Moitzi (support + dxfgrabber library), Vladimir Elistratov, Bastien Montagne, Remigiusz Fiedler (AKA migius)",
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",
31 ('NONE', "None", "No Coordinate System is available / will be set"),
34 ('USER', "User Defined", "Define the EPSG code"),
37 ('TMERC', "Transverse Mercator", "Mercator Projection using a lat/lon coordinate as its geo-reference"),
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']])
66 BY_CLOSED_NO_BULGE_POLY
= 2
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
}
80 T_OutlinerGroups
= True
82 T_CreateNewScene
= False
84 T_ThicknessBevel
= True
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
)
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
:
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)
129 self
.internal_using_scene_srid
= True
132 self
.proj_scene
= 'TMERC'
133 self
.merc_scene_lat
= scene
.get('latitude', 0)
134 self
.merc_scene_lon
= scene
.get('longitude', 0)
136 if srid
in (p
[0] for p
in proj_epsg_items
):
137 self
.proj_scene
= srid
139 self
.proj_scene
= 'USER'
140 self
.epsg_scene_user
= srid
142 self
.internal_using_scene_srid
= False
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"
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
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(
194 filename_ext
= ".dxf"
196 files
: CollectionProperty(
197 type=bpy
.types
.OperatorFileListElement
,
198 options
={'HIDDEN', 'SKIP_SAVE'}
201 directory
: StringProperty(
205 filter_glob
: StringProperty(
210 def _update_merge(self
, context
):
211 _update_import_atts_do(self
, context
)
213 name
="Merged Objects",
214 description
="Merge DXF entities to Blender objects",
219 def _update_merge_options(self
, context
):
220 _update_import_atts_do(self
, context
)
222 merge_options
: EnumProperty(
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'.")],
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",
239 import_text
: BoolProperty(
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",
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)",
269 scene_options
: EnumProperty(
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(
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(
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",
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
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",
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")],
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(
356 description
="Coordinates are assumed to be in meters; deviation must be indicated here",
360 def _update_proj(self
, context
):
361 _update_proj_scene_do(self
, context
)
362 _set_recenter(self
, self
.recenter
)
364 pitems
= proj_none_items
+ proj_user_items
+ proj_epsg_items
365 proj_dxf
: EnumProperty(
367 description
="The coordinate system for the DXF file (check http://epsg.io)",
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(
380 description
="The coordinate system for the Scene (check http://epsg.io)",
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)
391 internal_using_scene_srid
: BoolProperty(default
=False, options
={'HIDDEN'})
393 def draw(self
, context
):
395 scene
= context
.scene
398 layout
.label(text
="Import Options:")
400 box
.prop(self
, "scene_options")
401 box
.prop(self
, "collection_options")
404 layout
.label(text
="Merge Options:")
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")
412 sub
.enabled
= self
.merge
413 sub
.prop(self
, "merge_options")
414 box
.prop(self
, "merge_lines")
417 layout
.label(text
="Line thickness and width:")
419 box
.enabled
= not merge_map
[self
.merge_options
] == BY_CLOSED_NO_BULGE_POLY
420 box
.prop(self
, "represent_thickness_and_width")
422 sub
.enabled
= (not self
.represent_thickness_and_width
and self
.merge
)
423 sub
.prop(self
, "import_atts")
426 layout
.label(text
="Optional Objects:")
428 box
.prop(self
, "import_text")
429 box
.prop(self
, "import_light")
430 box
.prop(self
, "export_acis")
433 layout
.label(text
="View Options:")
435 box
.prop(self
, "outliner_groups")
437 sub
.enabled
= _recenter_allowed(self
)
438 sub
.prop(self
, "recenter")
441 layout
.prop(self
, "use_georeferencing", text
="Geo Referencing:")
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")
452 sub
.enabled
= not _recenter_allowed(self
)
453 sub
.label(text
="Geo Reference:")
455 sub
.enabled
= not _recenter_allowed(self
)
456 if is_ref_scene(bpy
.context
.scene
):
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
465 box
.prop(self
, "dxf_scale")
468 box
.alert
= (self
.proj_scene
!= 'NONE' and (not valid_dxf_srid
or self
.proj_dxf
== 'NONE'))
469 box
.prop(self
, "proj_dxf")
471 if self
.proj_dxf
== 'USER':
473 Proj(init
=self
.epsg_dxf_user
)
476 valid_dxf_srid
= False
477 box
.prop(self
, "epsg_dxf_user")
484 # Only info in case of pre-defined EPSG from current scene.
485 if self
.internal_using_scene_srid
:
488 col
.prop(self
, "proj_scene")
490 if self
.proj_scene
== 'USER':
492 Proj(init
=self
.epsg_scene_user
)
493 except Exception as e
:
495 col
.prop(self
, "epsg_scene_user")
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")
502 col
.label(text
="") # Placeholder.
503 col
.label(text
="") # Placeholder.
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
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
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", ""))
530 if self
.use_georeferencing
:
531 dxf_unit_scale
= float(self
.dxf_scale
.replace(",", "."))
533 if self
.proj_dxf
!= 'NONE':
534 if self
.proj_dxf
== 'USER':
535 proj_dxf
= Proj(init
=self
.epsg_dxf_user
)
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
)
544 proj_scn
= Proj(init
=self
.proj_scene
)
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
:
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
)
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
572 collection
= bpy
.context
.collection
573 if collection
!= scene
.collection
and collection
.name
not in scene
.collection
.children
: scene
.collection
.children
.link(collection
)
576 # for release testing
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()
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)")
603 bpy
.utils
.register_class(IMPORT_OT_dxf
)
604 bpy
.types
.TOPBAR_MT_file_import
.append(menu_func
)
608 bpy
.utils
.unregister_class(IMPORT_OT_dxf
)
609 bpy
.types
.TOPBAR_MT_file_import
.remove(menu_func
)
612 if __name__
== "__main__":