Files
geMoldInsight/venv/lib/python3.11/site-packages/trimesh/path/exchange/dxf.py
T

965 lines
32 KiB
Python
Raw Normal View History

2026-02-12 23:22:11 +08:00
from collections import defaultdict
import numpy as np
from ... import grouping, resources, util
from ... import transformations as tf
from ...constants import log
from ...constants import tol_path as tol
from ...util import multi_dict
from ..arc import to_threepoint
from ..entities import Arc, BSpline, Line, Text
# unit codes
_DXF_UNITS = {
1: "inches",
2: "feet",
3: "miles",
4: "millimeters",
5: "centimeters",
6: "meters",
7: "kilometers",
8: "microinches",
9: "mils",
10: "yards",
11: "angstroms",
12: "nanometers",
13: "microns",
14: "decimeters",
15: "decameters",
16: "hectometers",
17: "gigameters",
18: "AU",
19: "light years",
20: "parsecs",
}
# backwards, for reference
_UNITS_TO_DXF = {v: k for k, v in _DXF_UNITS.items()}
# a string which we will replace spaces with temporarily
_SAFESPACE = "|<^>|"
# save metadata to a DXF Xrecord starting here
# Valid values are 1-369 (except 5 and 105)
XRECORD_METADATA = 134
# the sentinel string for trimesh metadata
# this should be seen at XRECORD_METADATA
XRECORD_SENTINEL = "TRIMESH_METADATA:"
# the maximum line length before we split lines
XRECORD_MAX_LINE = 200
# the maximum index of XRECORDS
XRECORD_MAX_INDEX = 368
def load_dxf(file_obj, **kwargs):
"""
Load a DXF file to a dictionary containing vertices and
entities.
Parameters
----------
file_obj: file or file- like object (has object.read method)
Returns
----------
result: dict, keys are entities, vertices and metadata
"""
# in a DXF file, lines come in pairs,
# a group code then the next line is the value
# we are removing all whitespace then splitting with the
# splitlines function which uses the universal newline method
raw = file_obj.read()
# if we've been passed bytes
if hasattr(raw, "decode"):
# search for the sentinel string indicating binary DXF
# do it by encoding sentinel to bytes and subset searching
if raw[:22].find(b"AutoCAD Binary DXF") != -1:
# no converter to ASCII DXF available
raise NotImplementedError("Binary DXF is not supported!")
else:
# we've been passed bytes that don't have the
# header for binary DXF so try decoding as UTF-8
raw = raw.decode("utf-8", errors="ignore")
# remove trailing whitespace
raw = str(raw).strip()
# without any spaces and in upper case
cleaned = raw.replace(" ", "").strip().upper()
# blob with spaces and original case
blob_raw = np.array(str.splitlines(raw)).reshape((-1, 2))
# if this reshape fails, it means the DXF is malformed
blob = np.array(str.splitlines(cleaned)).reshape((-1, 2))
# get the section which contains the header in the DXF file
endsec = np.nonzero(blob[:, 1] == "ENDSEC")[0]
# store metadata
metadata = {}
# try reading the header, which may be malformed
header_start = np.nonzero(blob[:, 1] == "HEADER")[0]
if len(header_start) > 0:
header_end = endsec[np.searchsorted(endsec, header_start[0])]
header_blob = blob[header_start[0] : header_end]
# store some properties from the DXF header
metadata["DXF_HEADER"] = {}
for key, group in [
("$ACADVER", "1"),
("$DIMSCALE", "40"),
("$DIMALT", "70"),
("$DIMALTF", "40"),
("$DIMUNIT", "70"),
("$INSUNITS", "70"),
("$LUNITS", "70"),
]:
value = get_key(header_blob, key, group)
if value is not None:
metadata["DXF_HEADER"][key] = value
# store unit data pulled from the header of the DXF
# prefer LUNITS over INSUNITS
# I couldn't find a table for LUNITS values but they
# look like they are 0- indexed versions of
# the INSUNITS keys, so for now offset the key value
for offset, key in [(-1, "$LUNITS"), (0, "$INSUNITS")]:
# get the key from the header blob
units = get_key(header_blob, key, "70")
# if it exists add the offset
if units is None:
continue
metadata[key] = units
units += offset
# if the key is in our list of units store it
if units in _DXF_UNITS:
metadata["units"] = _DXF_UNITS[units]
# warn on drawings with no units
if "units" not in metadata:
log.debug("DXF doesn't have units specified!")
# get the section which contains entities in the DXF file
entity_start = np.nonzero(blob[:, 1] == "ENTITIES")[0][0]
entity_end = endsec[np.searchsorted(endsec, entity_start)]
blocks = None
check_entity = blob[entity_start:entity_end][:, 1]
# only load blocks if an entity references them via an INSERT
if "INSERT" in check_entity or "BLOCK" in check_entity:
try:
# which part of the raw file contains blocks
block_start = np.nonzero(blob[:, 1] == "BLOCKS")[0][0]
block_end = endsec[np.searchsorted(endsec, block_start)]
blob_block = blob[block_start:block_end]
blob_block_raw = blob_raw[block_start:block_end]
block_infl = np.nonzero((blob_block == ["0", "BLOCK"]).all(axis=1))[0]
# collect blocks by name
blocks = {}
for index in np.array_split(np.arange(len(blob_block)), block_infl):
try:
v, e, name = convert_entities(
blob_block[index], blob_block_raw[index], return_name=True
)
if len(e) > 0:
blocks[name] = (v, e)
except BaseException:
pass
except BaseException:
log.error("failed to parse blocks!", exc_info=True)
# actually load referenced entities
vertices, entities = convert_entities(
blob[entity_start:entity_end], blob_raw[entity_start:entity_end], blocks=blocks
)
# return result as kwargs for trimesh.path.Path2D constructor
result = {"vertices": vertices, "entities": entities, "metadata": metadata}
return result
def convert_entities(blob, blob_raw=None, blocks=None, return_name=False):
"""
Convert a chunk of entities into trimesh entities.
Parameters
------------
blob : (n, 2) str
Blob of entities uppercased
blob_raw : (n, 2) str
Blob of entities not uppercased
blocks : None or dict
Blocks referenced by INSERT entities
return_name : bool
If True return the first '2' value
Returns
----------
"""
if blob_raw is None:
blob_raw = blob
def info(e):
"""
Pull metadata based on group code, and return as a dict.
"""
# which keys should we extract from the entity data
# DXF group code : our metadata key
get = {"8": "layer", "2": "name"}
# replace group codes with names and only
# take info from the entity dict if it is in cand
renamed = {get[k]: util.make_sequence(v)[0] for k, v in e.items() if k in get}
return renamed
def convert_line(e):
"""
Convert DXF LINE entities into trimesh Line entities.
"""
# create a single Line entity
entities.append(Line(points=len(vertices) + np.arange(2), **info(e)))
# add the vertices to our collection
vertices.extend(
np.array([[e["10"], e["20"]], [e["11"], e["21"]]], dtype=np.float64)
)
def convert_circle(e):
"""
Convert DXF CIRCLE entities into trimesh Circle entities
"""
R = float(e["40"])
C = np.array([e["10"], e["20"]]).astype(np.float64)
points = to_threepoint(center=C[:2], radius=R)
entities.append(
Arc(points=(len(vertices) + np.arange(3)), closed=True, **info(e))
)
vertices.extend(points)
def convert_arc(e):
"""
Convert DXF ARC entities into into trimesh Arc entities.
"""
# the radius of the circle
R = float(e["40"])
# the center point of the circle
C = np.array([e["10"], e["20"]], dtype=np.float64)
# the start and end angle of the arc, in degrees
# this may depend on an AUNITS header data
A = np.radians(np.array([e["50"], e["51"]], dtype=np.float64))
# convert center/radius/angle representation
# to three points on the arc representation
points = to_threepoint(center=C[:2], radius=R, angles=A)
# add a single Arc entity
entities.append(Arc(points=len(vertices) + np.arange(3), closed=False, **info(e)))
# add the three vertices
vertices.extend(points)
def convert_polyline(e):
"""
Convert DXF LWPOLYLINE entities into trimesh Line entities.
"""
# load the points in the line
lines = np.column_stack((e["10"], e["20"])).astype(np.float64)
# save entity info so we don't have to recompute
polyinfo = info(e)
# 70 is the closed flag for polylines
# if the closed flag is set make sure to close
is_closed = "70" in e and int(e["70"][0]) & 1
if is_closed:
lines = np.vstack((lines, lines[:1]))
# 42 is the vertex bulge flag for LWPOLYLINE entities
# "bulge" is autocad for "add a stupid arc using flags
# in my otherwise normal polygon", it's like SVG arc
# flags but somehow even more annoying
if "42" in e:
# get the actual bulge float values
bulge = np.array(e["42"], dtype=np.float64)
# what position were vertices stored at
vid = np.nonzero(chunk[:, 0] == "10")[0]
# what position were bulges stored at in the chunk
bid = np.nonzero(chunk[:, 0] == "42")[0]
# filter out endpoint bulge if we're not closed
if not is_closed:
bid_ok = bid < vid.max()
bid = bid[bid_ok]
bulge = bulge[bid_ok]
# which vertex index is bulge value associated with
bulge_idx = np.searchsorted(vid, bid)
# convert stupid bulge to Line/Arc entities
v, e = bulge_to_arcs(
lines=lines, bulge=bulge, bulge_idx=bulge_idx, is_closed=is_closed
)
for i in e:
# offset added entities by current vertices length
i.points += len(vertices)
vertices.extend(v)
entities.extend(e)
# done with this polyline
return
# we have a normal polyline so just add it
# as single line entity and vertices
entities.append(Line(points=np.arange(len(lines)) + len(vertices), **polyinfo))
vertices.extend(lines)
def convert_bspline(e):
"""
Convert DXF Spline entities into trimesh BSpline entities.
"""
# in the DXF there are n points and n ordered fields
# with the same group code
points = np.column_stack((e["10"], e["20"])).astype(np.float64)
knots = np.array(e["40"]).astype(np.float64)
# if there are only two points, save it as a line
if len(points) == 2:
# create a single Line entity
entities.append(Line(points=len(vertices) + np.arange(2), **info(e)))
# add the vertices to our collection
vertices.extend(points)
return
# check bit coded flag for closed
# closed = bool(int(e['70'][0]) & 1)
# check euclidean distance to see if closed
closed = np.linalg.norm(points[0] - points[-1]) < tol.merge
# create a BSpline entity
entities.append(
BSpline(
points=np.arange(len(points)) + len(vertices),
knots=knots,
closed=closed,
**info(e),
)
)
# add the vertices
vertices.extend(points)
def convert_text(e):
"""
Convert a DXF TEXT entity into a native text entity.
"""
# text with leading and trailing whitespace removed
text = e["1"].strip()
# try getting optional height of text
try:
height = float(e["40"])
except BaseException:
height = None
try:
# rotation angle converted to radians
angle = np.radians(float(e["50"]))
except BaseException:
# otherwise no rotation
angle = 0.0
# origin point
origin = np.array([e["10"], e["20"]], dtype=np.float64)
# an origin-relative point (so transforms work)
vector = origin + [np.cos(angle), np.sin(angle)]
# try to extract a (horizontal, vertical) text alignment
align = ["center", "center"]
try:
align[0] = ["left", "center", "right"][int(e["72"])]
except BaseException:
pass
# append the entity
entities.append(
Text(
origin=len(vertices),
vector=len(vertices) + 1,
height=height,
text=text,
align=align,
)
)
# append the text origin and direction
vertices.append(origin)
vertices.append(vector)
def convert_insert(e):
"""
Convert an INSERT entity, which inserts a named group of
entities (i.e. a "BLOCK") at a specific location.
"""
if blocks is None:
return
# name of block to insert
name = e["2"]
# if we haven't loaded the block skip
if name not in blocks:
return
# angle to rotate the block by
angle = float(e.get("50", 0.0))
# the insertion point of the block
offset = np.array([e.get("10", 0.0), e.get("20", 0.0)], dtype=np.float64)
# what to scale the block by
scale = np.array([e.get("41", 1.0), e.get("42", 1.0)], dtype=np.float64)
# the current entities and vertices of the referenced block.
cv, ce = blocks[name]
for i in ce:
# copy the referenced entity as it may be included multiple times
entities.append(i.copy())
# offset its vertices to the current index
entities[-1].points += len(vertices)
# transform the block's vertices based on the entity settings
vertices.extend(
tf.transform_points(
cv, tf.planar_matrix(offset=offset, theta=np.radians(angle), scale=scale)
)
)
# find the start points of entities
# DXF object to trimesh object converters
loaders = {
"LINE": (dict, convert_line),
"LWPOLYLINE": (multi_dict, convert_polyline),
"ARC": (dict, convert_arc),
"CIRCLE": (dict, convert_circle),
"SPLINE": (multi_dict, convert_bspline),
"INSERT": (dict, convert_insert),
"BLOCK": (dict, convert_insert),
}
# store loaded vertices
vertices = []
# store loaded entities
entities = []
# an old-style polyline entity strings its data across
# multiple vertex entities like a real asshole
polyline = None
# chunks of entities are divided by group-code-0
inflection = np.nonzero(blob[:, 0] == "0")[0]
unsupported = defaultdict(lambda: 0)
# loop through chunks of entity information
for index in np.array_split(np.arange(len(blob)), inflection):
# if there is only a header continue
if len(index) < 1:
continue
# chunk will be an (n, 2) array of (group code, data) pairs
chunk = blob[index]
# the string representing entity type
entity_type = chunk[0][1]
# if we are referencing a block or insert by name make
# sure the name key is in the original case vs upper-case
if entity_type in ("BLOCK", "INSERT"):
try:
index_name = next(i for i, v in enumerate(chunk) if v[0] == "2")
chunk[index_name][1] = blob_raw[index][index_name][1]
except StopIteration:
pass
# special case old- style polyline entities
if entity_type == "POLYLINE":
polyline = [dict(chunk)]
# if we are collecting vertex entities
elif polyline is not None and entity_type == "VERTEX":
polyline.append(dict(chunk))
# the end of a polyline
elif polyline is not None and entity_type == "SEQEND":
# pull the geometry information for the entity
lines = np.array([[i["10"], i["20"]] for i in polyline[1:]], dtype=np.float64)
is_closed = False
# check for a closed flag on the polyline
if "70" in polyline[0]:
# flag is bit- coded integer
flag = int(polyline[0]["70"])
# first bit represents closed
is_closed = bool(flag & 1)
if is_closed:
lines = np.vstack((lines, lines[:1]))
# get the index of each bulged vertices
bulge_idx = np.array(
[i for i, e in enumerate(polyline) if "42" in e], dtype=np.int64
)
# get the actual bulge value
bulge = np.array(
[float(e["42"]) for i, e in enumerate(polyline) if "42" in e],
dtype=np.float64,
)
# convert bulge to new entities
cv, ce = bulge_to_arcs(
lines=lines, bulge=bulge, bulge_idx=bulge_idx, is_closed=is_closed
)
for i in ce:
# offset entities by existing vertices
i.points += len(vertices)
vertices.extend(cv)
entities.extend(ce)
# we no longer have an active polyline
polyline = None
elif entity_type == "TEXT":
# text entities need spaces preserved so take
# group codes from clean representation (0- column)
# and data from the raw representation (1- column)
chunk_raw = blob_raw[index]
# if we didn't use clean group codes we wouldn't
# be able to access them by key as whitespace
# is random and crazy, like: ' 1 '
chunk_raw[:, 0] = blob[index][:, 0]
try:
convert_text(dict(chunk_raw))
except BaseException:
log.debug("failed to load text entity!", exc_info=True)
# if the entity contains all relevant data we can
# cleanly load it from inside a single function
elif entity_type in loaders:
# the chunker converts an (n,2) list into a dict
chunker, loader = loaders[entity_type]
# convert data to dict
entity_data = chunker(chunk)
# append data to the lists we're collecting
loader(entity_data)
elif entity_type != "ENTITIES":
unsupported[entity_type] += 1
if len(unsupported) > 0:
log.debug(
"skipping dxf entities: {}".format(
", ".join(f"{k}: {v}" for k, v in unsupported.items())
)
)
# stack vertices into single array
vertices = util.vstack_empty(vertices).astype(np.float64)
if return_name:
name = blob_raw[blob[:, 0] == "2"][0][1]
return vertices, entities, name
return vertices, entities
def export_dxf(path, only_layers=None):
"""
Export a 2D path object to a DXF file.
Parameters
----------
path : trimesh.path.path.Path2D
Input geometry to export
only_layers : None or set
If passed only export the layers specified
Returns
----------
export : str
Path formatted as a DXF file
"""
# get the template for exporting DXF files
template = resources.get_json("templates/dxf.json")
def format_points(points, as_2D=False, increment=True):
"""
Format points into DXF- style point string.
Parameters
-----------
points : (n,2) or (n,3) float
Points in space
as_2D : bool
If True only output 2 points per vertex
increment : bool
If True increment group code per point
Example:
[[X0, Y0, Z0], [X1, Y1, Z1]]
Result, new lines replaced with spaces:
True -> 10 X0 20 Y0 30 Z0 11 X1 21 Y1 31 Z1
False -> 10 X0 20 Y0 30 Z0 10 X1 20 Y1 30 Z1
Returns
-----------
packed : str
Points formatted with group code
"""
points = np.asanyarray(points, dtype=np.float64)
# get points in 3D
three = util.stack_3D(points)
if increment:
group = np.tile(
np.arange(len(three), dtype=np.int64).reshape((-1, 1)), (1, 3)
)
else:
group = np.zeros((len(three), 3), dtype=np.int64)
group += [10, 20, 30]
if as_2D:
group = group[:, :2]
three = three[:, :2]
# join into result string
packed = "\n".join(
f"{g:d}\n{v:.12g}" for g, v in zip(group.reshape(-1), three.reshape(-1))
)
return packed
def entity_info(entity):
"""
Pull layer, color, and name information about an entity
Parameters
-----------
entity : entity object
Source entity to pull metadata
Returns
----------
subs : dict
Has keys 'COLOR', 'LAYER', 'NAME'
"""
# TODO : convert RGBA entity.color to index
subs = {
"COLOR": 255, # default is ByLayer
"LAYER": 0,
"NAME": str(id(entity))[:16],
}
if hasattr(entity, "layer"):
# make sure layer name is forced into ASCII
subs["LAYER"] = util.to_ascii(entity.layer)
return subs
def convert_line(line, vertices):
"""
Convert an entity to a discrete polyline
Parameters
-------------
line : entity
Entity which has 'e.discrete' method
vertices : (n, 2) float
Vertices in space
Returns
-----------
as_dxf : str
Entity exported as a DXF
"""
# get a discrete representation of entity
points = line.discrete(vertices)
# if one or fewer points return nothing
if len(points) <= 1:
return ""
# generate a substitution dictionary for template
subs = entity_info(line)
subs["POINTS"] = format_points(points, as_2D=True, increment=False)
subs["TYPE"] = "LWPOLYLINE"
subs["VCOUNT"] = len(points)
# 1 is closed
# 0 is default (open)
subs["FLAG"] = int(bool(line.closed))
result = template["line"].format(**subs)
return result
def convert_arc(arc, vertices):
# get the center of arc and include span angles
info = arc.center(vertices, return_angle=True, return_normal=False)
subs = entity_info(arc)
center = info.center
if len(center) == 2:
center = np.append(center, 0.0)
data = "10\n{:.12g}\n20\n{:.12g}\n30\n{:.12g}".format(*center)
data += f"\n40\n{info.radius:.12g}"
if arc.closed:
subs["TYPE"] = "CIRCLE"
else:
subs["TYPE"] = "ARC"
# an arc is the same as a circle, with an added start
# and end angle field
data += "\n100\nAcDbArc"
data += "\n50\n{:.12g}\n51\n{:.12g}".format(*np.degrees(info.angles))
subs["DATA"] = data
result = template["arc"].format(**subs)
return result
def convert_bspline(spline, vertices):
# points formatted with group code
points = format_points(vertices[spline.points], increment=False)
# (n,) float knots, formatted with group code
knots = ("40\n{:.12g}\n" * len(spline.knots)).format(*spline.knots)[:-1]
# bit coded
flags = {"closed": 1, "periodic": 2, "rational": 4, "planar": 8, "linear": 16}
flag = flags["planar"]
if spline.closed:
flag = flag | flags["closed"]
normal = [0.0, 0.0, 1.0]
n_code = [210, 220, 230]
n_str = "\n".join(f"{i:d}\n{j:.12g}" for i, j in zip(n_code, normal))
subs = entity_info(spline)
subs.update(
{
"TYPE": "SPLINE",
"POINTS": points,
"KNOTS": knots,
"NORMAL": n_str,
"DEGREE": 3,
"FLAG": flag,
"FCOUNT": 0,
"KCOUNT": len(spline.knots),
"PCOUNT": len(spline.points),
}
)
# format into string template
result = template["bspline"].format(**subs)
return result
def convert_text(txt, vertices):
"""
Convert a Text entity to DXF string.
"""
# start with layer info
sub = entity_info(txt)
# get the origin point of the text
sub["ORIGIN"] = format_points(vertices[[txt.origin]], increment=False)
# rotation angle in degrees
sub["ANGLE"] = np.degrees(txt.angle(vertices))
# actual string of text with spaces escaped
# force into ASCII to avoid weird encoding issues
sub["TEXT"] = (
txt.text.replace(" ", _SAFESPACE)
.encode("ascii", errors="ignore")
.decode("ascii")
)
# height of text
sub["HEIGHT"] = txt.height
result = template["text"].format(**sub)
return result
def convert_generic(entity, vertices):
"""
For entities we don't know how to handle, return their
discrete form as a polyline
"""
return convert_line(entity, vertices)
# make sure we're not losing a ton of
# precision in the string conversion
np.set_printoptions(precision=12)
# trimesh entity to DXF entity converters
conversions = {
"Line": convert_line,
"Text": convert_text,
"Arc": convert_arc,
"Bezier": convert_generic,
"BSpline": convert_bspline,
}
collected = []
for e, layer in zip(path.entities, path.layers):
name = type(e).__name__
# only export specified layers
if only_layers is not None and layer not in only_layers:
continue
if name in conversions:
converted = conversions[name](e, path.vertices).strip()
if len(converted) > 0:
# only save if we converted something
collected.append(converted)
else:
log.debug("Entity type %s not exported!", name)
# join all entities into one string
entities_str = "\n".join(collected)
# add in the extents of the document as explicit XYZ lines
hsub = {f"EXTMIN_{k}": v for k, v in zip("XYZ", np.append(path.bounds[0], 0.0))}
hsub.update({f"EXTMAX_{k}": v for k, v in zip("XYZ", np.append(path.bounds[1], 0.0))})
# apply a units flag defaulting to `1`
hsub["LUNITS"] = _UNITS_TO_DXF.get(path.units, 1)
# run the format for the header
sections = [template["header"].format(**hsub).strip()]
# do the same for entities
sections.append(template["entities"].format(ENTITIES=entities_str).strip())
# and the footer
sections.append(template["footer"].strip())
# filter out empty sections
# random whitespace causes AutoCAD to fail to load
# although Draftsight, LibreCAD, and Inkscape don't care
# what a giant legacy piece of shit
# create the joined string blob
blob = "\n".join(sections).replace(_SAFESPACE, " ")
# run additional self- checks
if tol.strict:
# check that every line pair is (group code, value)
lines = str.splitlines(str(blob))
# should be even number of lines
assert (len(lines) % 2) == 0
# group codes should all be convertible to int and positive
assert all(int(i) >= 0 for i in lines[::2])
# make sure we didn't slip any unicode in there
blob.encode("ascii")
return blob
def bulge_to_arcs(lines, bulge, bulge_idx, is_closed=False, metadata=None):
"""
Polylines can have "vertex bulge" which means the polyline
has an arc tangent to segments, rather than meeting at a
vertex.
From Autodesk reference:
The bulge is the tangent of one fourth the included
angle for an arc segment, made negative if the arc
goes clockwise from the start point to the endpoint.
A bulge of 0 indicates a straight segment, and a
bulge of 1 is a semicircle.
Parameters
----------------
lines : (n, 2) float
Polyline vertices in order
bulge : (m,) float
Vertex bulge value
bulge_idx : (m,) float
Which index of lines is bulge associated with
is_closed : bool
Is segment closed
metadata : None, or dict
Entity metadata to add
Returns
---------------
vertices : (a, 2) float
New vertices for poly-arc
entities : (b,) entities.Entity
New entities, either line or arc
"""
# make sure lines are 2D array
lines = np.asanyarray(lines, dtype=np.float64)
# make sure inputs are numpy arrays
bulge = np.asanyarray(bulge, dtype=np.float64)
bulge_idx = np.asanyarray(bulge_idx, dtype=np.int64)
# filter out zero- bulged polylines
ok = np.abs(bulge) > 1e-5
bulge = bulge[ok]
bulge_idx = bulge_idx[ok]
# metadata to apply to new entities
if metadata is None:
metadata = {}
# if there's no bulge, just return the input curve
if len(bulge) == 0:
index = np.arange(len(lines))
# add a single line entity and vertices
entities = [Line(index, **metadata)]
return lines, entities
# use bulge to calculate included angle of the arc
angle = np.arctan(bulge) * 4.0
# the indexes making up a bulged segment
tid = np.column_stack((bulge_idx, bulge_idx - 1))
# if it's a closed segment modulus to start vertex
if is_closed:
tid %= len(lines)
# the vector connecting the two ends of the arc
vector = lines[tid[:, 0]] - lines[tid[:, 1]]
# the length of the connector segment
length = np.linalg.norm(vector, axis=1)
# perpendicular vectors by crossing vector with Z
perp = np.cross(
np.column_stack((vector, np.zeros(len(vector)))),
np.ones((len(vector), 3)) * [0, 0, 1],
)
# strip the zero Z
perp = util.unitize(perp[:, :2])
# midpoint of each line
midpoint = lines[tid].mean(axis=1)
# calculate the signed radius of each arc segment
radius = (length / 2.0) / np.sin(angle / 2.0)
# offset magnitude to point on arc
offset = radius - np.cos(angle / 2) * radius
# convert each arc to three points:
# start, any point on arc, end
three = np.column_stack(
(lines[tid[:, 0]], midpoint + perp * offset.reshape((-1, 1)), lines[tid[:, 1]])
).reshape((-1, 3, 2))
# if we're in strict mode make sure our arcs
# have the same magnitude as the input data
if tol.strict:
from ..arc import arc_center
check_angle = [arc_center(i).span for i in three]
assert np.allclose(np.abs(angle), np.abs(check_angle))
check_radii = [arc_center(i).radius for i in three]
assert np.allclose(check_radii, np.abs(radius))
# collect new entities and vertices
entities, vertices = [], []
# add the entities for each new arc
for arc_points in three:
entities.append(Arc(points=np.arange(3) + len(vertices), **metadata))
vertices.extend(arc_points)
# if there are unconsumed line
# segments add them to drawing
if (len(lines) - 1) > len(bulge):
# indexes of line segments
existing = util.stack_lines(np.arange(len(lines)))
# remove line segments replaced with arcs
for line_idx in grouping.boolean_rows(
existing, np.sort(tid, axis=1), np.setdiff1d
):
# add a single line entity and vertices
entities.append(Line(points=np.arange(2) + len(vertices), **metadata))
vertices.extend(lines[line_idx].copy())
# make sure vertices are clean numpy array
vertices = np.array(vertices, dtype=np.float64)
return vertices, entities
def get_key(blob, field, code):
"""
Given a loaded (n, 2) blob and a field name
get a value by code.
"""
try:
line = blob[np.nonzero(blob[:, 1] == field)[0][0] + 1]
except IndexError:
return None
if line[0] == code:
try:
return int(line[1])
except ValueError:
return line[1]
else:
return None
# store the loaders we have available
_dxf_loaders = {"dxf": load_dxf}