dxf_input_modified/dxf_input.py
2024-01-14 18:52:37 +01:00

1563 lines
52 KiB
Python

#! /usr/bin/python3 -sP
# coding=utf-8
#
# Copyright (C) 2008-2009 Alvin Penner, penner@vaxxine.com
# 2009, Christian Mayer, inkscape@christianmayer.de
# 2020, MartinOwens, doctormo@geek-2.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
"""
Input a DXF file >= (AutoCAD Release 13 == AC1012)
"""
import os
import re
import sys
import math
from collections import defaultdict
from urllib.parse import quote
from lxml import etree
import inkex
from inkex.localization import inkex_gettext as _
global defs
global block # 2021.6
global layer
global svg
global scale
global xmin
global ymin
global height
global style_font3
global style_direction
COLORS = [
"PAD",
"#FF0000",
"#FFFF00",
"#00FF00",
"#00FFFF",
"#0000FF",
"#FF00FF",
"#000000",
"#808080",
"#C0C0C0",
"#FF0000",
"#FF7F7F",
"#CC0000",
"#CC6666",
"#990000",
"#994C4C",
"#7F0000",
"#7F3F3F",
"#4C0000",
"#4C2626",
"#FF3F00",
"#FF9F7F",
"#CC3300",
"#CC7F66",
"#992600",
"#995F4C",
"#7F1F00",
"#7F4F3F",
"#4C1300",
"#4C2F26",
"#FF7F00",
"#FFBF7F",
"#CC6600",
"#CC9966",
"#994C00",
"#99724C",
"#7F3F00",
"#7F5F3F",
"#4C2600",
"#4C3926",
"#FFBF00",
"#FFDF7F",
"#CC9900",
"#CCB266",
"#997200",
"#99854C",
"#7F5F00",
"#7F6F3F",
"#4C3900",
"#4C4226",
"#FFFF00",
"#FFFF7F",
"#CCCC00",
"#CCCC66",
"#989800",
"#98984C",
"#7F7F00",
"#7F7F3F",
"#4C4C00",
"#4C4C26",
"#BFFF00",
"#DFFF7F",
"#99CC00",
"#B2CC66",
"#729800",
"#85984C",
"#5F7F00",
"#6F7F3F",
"#394C00",
"#424C26",
"#7FFF00",
"#BFFF7F",
"#66CC00",
"#99CC66",
"#4C9800",
"#72984C",
"#3F7F00",
"#5F7F3F",
"#264C00",
"#394C26",
"#3FFF00",
"#9FFF7F",
"#33CC00",
"#7FCC66",
"#269800",
"#5F984C",
"#1F7F00",
"#4F7F3F",
"#134C00",
"#2F4C26",
"#00FF00",
"#7FFF7F",
"#00CC00",
"#66CC66",
"#009800",
"#4C984C",
"#007F00",
"#3F7F3F",
"#004C00",
"#264C26",
"#00FF3F",
"#7FFF9F",
"#00CC33",
"#66CC7F",
"#009826",
"#4C985F",
"#007F1F",
"#3F7F4F",
"#004C13",
"#264C2F",
"#00FF7F",
"#7FFFBF",
"#00CC66",
"#66CC99",
"#00984C",
"#4C9872",
"#007F3F",
"#3F7F5F",
"#004C26",
"#264C39",
"#00FFBF",
"#7FFFDF",
"#00CC99",
"#66CCB2",
"#009872",
"#4C9885",
"#007F5F",
"#3F7F6F",
"#004C39",
"#264C42",
"#00FFFF",
"#7FFFFF",
"#00CCCC",
"#66CCCC",
"#009898",
"#4C9898",
"#007F7F",
"#3F7F7F",
"#004C4C",
"#264C4C",
"#00BFFF",
"#7FDFFF",
"#0099CC",
"#66B2CC",
"#007298",
"#4C8598",
"#005F7F",
"#3F6F7F",
"#00394C",
"#26424C",
"#007FFF",
"#7FBFFF",
"#0066CC",
"#6699CC",
"#004C98",
"#4C7298",
"#003F7F",
"#3F5F7F",
"#00264C",
"#26394C",
"#003FFF",
"#7F9FFF",
"#0033CC",
"#667FCC",
"#002698",
"#4C5F98",
"#001F7F",
"#3F4F7F",
"#00134C",
"#262F4C",
"#0000FF",
"#7F7FFF",
"#0000CC",
"#6666CC",
"#000098",
"#4C4C98",
"#00007F",
"#3F3F7F",
"#00004C",
"#26264C",
"#3F00FF",
"#9F7FFF",
"#3300CC",
"#7F66CC",
"#260098",
"#5F4C98",
"#1F007F",
"#4F3F7F",
"#13004C",
"#2F264C",
"#7F00FF",
"#BF7FFF",
"#6600CC",
"#9966CC",
"#4C0098",
"#724C98",
"#3F007F",
"#5F3F7F",
"#26004C",
"#39264C",
"#BF00FF",
"#DF7FFF",
"#9900CC",
"#B266CC",
"#720098",
"#854C98",
"#5F007F",
"#6F3F7F",
"#39004C",
"#42264C",
"#FF00FF",
"#FF7FFF",
"#CC00CC",
"#CC66CC",
"#980098",
"#984C98",
"#7F007F",
"#7F3F7F",
"#4C004C",
"#4C264C",
"#FF00BF",
"#FF7FDF",
"#CC0099",
"#CC66B2",
"#980072",
"#984C85",
"#7F005F",
"#7F3F6F",
"#4C0039",
"#4C2642",
"#FF007F",
"#FF7FBF",
"#CC0066",
"#CC6699",
"#98004C",
"#984C72",
"#7F003F",
"#7F3F5F",
"#4C0026",
"#4C2639",
"#FF003F",
"#FF7F9F",
"#CC0033",
"#CC667F",
"#980026",
"#984C5F",
"#7F001F",
"#7F3F4F",
"#4C0013",
"#4C262F",
"#333333",
"#5B5B5B",
"#848484",
"#ADADAD",
"#D6D6D6",
"#FFFFFF",
]
def get_rgbcolor(dxfcolor, parent_color="#000000"):
"""Returns hex color code corresponding to a color value
dxfcolor -- dxf code to convert to hex color code
0 (BYBLOCK) and 256 (BYLAYER) use parent_color
No more differentiation is currently done
Negative values are ignored (specification
allows layer to be hidden here)
Negative values also use parent_color
parent_color -- hex color code from parent layer.
Use default color '#000000' if
parent layer color undefined.
"""
rgbcolor = None
if dxfcolor in range(1, len(COLORS)):
rgbcolor = COLORS[dxfcolor]
if not rgbcolor:
rgbcolor = parent_color
return rgbcolor
class ValueConstruct(defaultdict):
"""Store values from the DXF and provide them as named attributes"""
values = {
"1": ("text", "default"),
"2": ("tag", "block_name"),
"3": ("mtext",),
"6": ("line_type",),
"7": ("text_style",),
"8": ("layer_name",),
"10": ("x1",),
"11": ("x2",),
"13": ("x3",),
"14": ("x4",),
"20": ("y1",),
"21": ("y2",),
"23": ("y3",),
"24": ("y4",),
"40": ("scale", "knots", "radius", "width_ratio"),
"41": ("ellipse_a1", "insert_scale_x", "mtext_width"),
"42": ("ellipse_a2", "bulge", "insert_scale_y"),
"50": ("angle",),
"51": ("angle2",),
"62": ("color",),
"70": ("fill", "flags"),
"71": ("attach_pos",),
"72": ("edge_type",),
"73": ("sweep",), # ccw
"92": ("path_type",),
"93": ("num_edges",),
"230": ("extrude",),
"370": ("line_weight",),
}
attrs = dict([(name, a) for a, b in values.items() for name in b])
def __init__(self):
super().__init__(list)
@classmethod
def is_valid(cls, key):
return key in cls.values
def __getattr__(self, attr):
is_list = attr.endswith("_list")
key = attr[:-5] if is_list else attr
if key in self.attrs:
ret = self[self.attrs[key]]
if not attr.endswith("_list"):
return ret[0]
return ret
if attr.startswith("has_"):
key = attr[4:]
if key in self.attrs:
return self.attrs[key] in self
raise AttributeError(f"Can't find dxf attribute '{key}' {attr}")
def __setattr__(self, attr, value):
if not attr in self.attrs:
raise AttributeError(f"Can't set bad dxf attribute '{attr}'")
if not isinstance(value, list):
value = [value]
self[self.attrs[attr]] = value
def adjust_coords(self, xmin, ymin, scale, extrude, height):
"""Adjust the x,y coordinates to fit on the page"""
for xgrp in set(["10", "11", "13", "14"]) & set(self): # scale/reflect x values
for i in range(len(self[xgrp])):
self[xgrp][i] = scale * (extrude * self[xgrp][i] - xmin)
for ygrp in set(["20", "21", "23", "24"]) & set(self): # scale y values
for i in range(len(self[ygrp])):
self[ygrp][i] = height - scale * (self[ygrp][i] - ymin)
export_viewport = False
export_endsec = False
def re_hex2unichar(m):
# return unichr(int(m.group(1), 16))
return chr(int(m.group(1), 16))
def formatStyle(style):
return str(inkex.Style(style))
def export_text(vals):
# mandatory group codes : (11, 12, 72, 73) (fit_x, fit_y, horizon, vertical)
# TODO: position to display at by (x2,y2) according to 72(horizon),73(vertical)
# groupcode 72:0(left),1(center),2(right),3(both side),4(middle),5(fit)
# grouocode 73:0(standard),1(floor),2(center),3(ceiling)
vals["71"].append(1) # attach=pos left in mtext
vals["70"].append(1) # text: flags=1
return export_mtext(vals)
def export_mtext(vals):
# mandatory group codes : (1 or 3, 10, 20) (text, x, y)
# TODO: text-format: \Font; \W; \Q; \L..\l etc
if (vals.has_text or vals.has_mtext) and vals.has_x1 and vals.has_y1:
x = vals.x1
y = vals.y1
# optional group codes : (21, 40, 50) (direction, text height mm, text angle)
# optional group codes : 2: char style is defined at TABLES Section
size = 12 # default fontsize in px
if vals.has_scale:
size = scale * textscale * vals.scale
dx = dy = 0
if not vals.has_flags: # as mtext, putting in the box
dy = size
anchor = "start"
if vals.has_attach_pos:
if vals.attach_pos in (2, 5, 8):
anchor = "middle"
elif vals.attach_pos in (3, 6, 9):
anchor = "end"
if vals.attach_pos in (4, 5, 6):
dy = size / 2
# if vals.has_text_style and vals.text_style in style_font3 :
# attribs = {'x': '%f' % x, 'y': '%f' % y, 'style': 'font-size: %.3fpx; fill: %s; font-family: %s' \
# % (size, color, style_font3[vals.text_style])}
# else :
# attribs = {'x': '%f' % x, 'y': '%f' % y, 'style': 'font-size: %.3fpx; fill: %s; font-family: %s' % (size, color, options.font)}
attribs = {
"x": "%f" % x,
"y": "%f" % y,
"style": "font-size: %.3fpx; fill: %s; font-family: %s; text-anchor: %s"
% (size, color, options.font, anchor),
}
angle = 0 # default angle in degrees
bVertical = False
if vals.has_angle: # TEXT only
if vals.angle != 0:
angle = vals.angle
# attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)})
elif vals.has_y2 and vals.has_x2:
# MTEXT
# recover original data
# (x,y)=(scale*(x-xmin), height-scale*(y-ymin)
orgx = vals.x2 / scale + xmin
orgy = -(vals.y2 - height) / scale + ymin
unit = math.sqrt(orgy * orgy + orgx * orgx)
if (unit < 1.01) and (unit > 0.99):
ang1 = math.atan2(orgy, orgx)
angle = 180 * ang1 / math.pi
# attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)})
if vals.has_text_style and vals.text_style in style_direction:
if style_direction[vals.text_style] & 4:
# angle = -90
# attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)})
bVertical = True
angle = 0
dx = size
attribs = {
"x": "%f" % x,
"y": "%f" % y,
"style": "font-size: %.3fpx; fill: %s; font-family: %s; text-anchor: %s; writing-mode: tb"
% (size, color, options.font, anchor),
}
if angle != 0:
attribs.update({"transform": "rotate (%f %f %f)" % (-angle, x, y)})
node = layer.add(inkex.TextElement(**attribs))
node.set("sodipodi:linespacing", "125%")
text = ""
if vals.has_mtext:
text = "".join(vals.mtext_list)
if vals.has_text:
text += vals.text
# folding long text
if vals.has_mtext_width and vals.has_scale:
if vals.mtext_width > 10.0 and vals.scale > 1.0:
charsperline = vals.mtext_width * 2 / vals.scale # or 1.6?
nochars = int(charsperline)
if len(text) > charsperline:
# plain text only no \P,{} better divide by space
if (text.find(r"\P") < 0) and (text.find(r"{") < 0):
pos = 0
while len(text) > pos:
text = text[:pos] + r"\P" + text[pos:]
pos += nochars + 2
text = mtext_normalize(text)
lines = 0
found = text.find(r"\P") # new line
while found > -1:
tspan = node.add(inkex.Tspan())
if bVertical:
tspan.set("y", "%f" % y)
if lines > 0:
tspan.set("dx", "%f" % size)
else:
tspan.set("sodipodi:role", "line")
if lines > 0:
tspan.set("x", x + dx)
else:
tspan.set("dx", "%f" % dx)
tspan.set("dy", "%f" % dy)
# tspan.text = text[:found]
text1 = text[:found]
mtext_separate(node, tspan, text1)
text = text[(found + 2) :]
found = text.find(r"\P")
lines += 1
tspan = node.add(inkex.Tspan())
if bVertical:
tspan.set("y", "%f" % y)
if lines > 0:
tspan.set("dx", "%f" % dx)
else:
tspan.set("sodipodi:role", "line")
if lines > 0:
tspan.set("x", x + dx)
else:
tspan.set("dx", "%f" % dx)
tspan.set("dy", "%f" % dy)
# tspan.text = text
text1 = text
mtext_separate(node, tspan, text1)
def mtext_normalize(text):
# { \P } -> { }\P{ }
found = text.find(r"\P")
while found > -1:
nest = False
posL = text.rfind(r"{", 0, found)
posR = text.rfind(r"}", 0, found)
if posL > -1:
if posR == -1:
nest = True
else:
if posL > posR:
nest = True
if nest:
# paste }\P{
control = ""
if text[posL + 1] == "\\":
posC = text.find(r";", posL)
if posC != -1 and (posC - posL) < 20:
control = text[posL + 1 : posC + 1]
text = text[:found] + r"}\P{" + control + text[found + 2 :]
found = text.find(r"\P", found + 2)
return text
def mtext_separate(node, tspan, text):
# sparate aaa{bbb}(ccc) -> aaa,bbb.ccc
tspanAdd = True
found = text.find(r"{")
while found > -1:
if found == 0:
found1 = text.find(r"}")
if found1 < 1:
break
text1 = text[:found1] # tspan
text1 = text1[found + 1 :]
if tspanAdd == False:
tspan = node.add(inkex.Tspan())
mtext_ctrl(tspan, text1)
# tspan.text = text1 +'+1'
tspanAdd = False
text = text[found1 + 1 :]
found = text.find(r"{")
else:
text1 = text[:found] # tspan
if tspanAdd == False:
tspan = node.add(inkex.Tspan())
mtext_ctrl(tspan, text1)
# tspan.text = text1 +'+2'
tspanAdd = False
text = text[found:]
found = 0
if len(text) > 0:
text1 = text
if tspanAdd == False:
tspan = node.add(inkex.Tspan())
mtext_ctrl(tspan, text1)
# tspan.text = text1 +'+3'
tspanAdd = False
def mtext_ctrl(tspan, phrase):
if len(phrase) == 0:
return
if phrase[0] != "\\":
tspan.text = phrase
return
# if you'll add the function, you should remove the auto re.sub at setting group code:1
if phrase[1].upper() in ("C", "H", "T", "Q", "W", "A"):
# get the value
found = phrase.find(r";")
if found > 2:
cvalue = phrase[:found]
cvalue = cvalue[2:]
try:
value = float(cvalue)
except ValueError:
done = False
else:
done = True
if phrase[1].upper() == "C":
i = int(value)
color = get_rgbcolor(i)
tspan.set("style", "stroke: %s" % color)
elif phrase[1].upper() == "H":
value *= scale
tspan.set("style", "font-size: %.3fpx;" % value)
elif phrase[1].upper() == "T":
tspan.set("style", "letter-spacing: %f;" % value)
elif phrase[1].upper() == "A":
if value == 0:
tspan.set("dominant-baseline", "text-bottom")
elif value == 1:
tspan.set("dominant-baseline", "central")
elif value == 2:
tspan.set("dominant-baseline", "text-top")
tspan.text = phrase[found + 1 :]
else:
tspan.text = phrase
else:
if phrase[1].upper() == "F":
# get the value font-name & style & cut from text 2022.March
# \FArial|b0|i0|c0|p0; b:bold,i:italic,c:charset,ppitch
found = phrase.find(r";")
if found > 2:
cvalue = phrase[:found]
tspan.text = phrase[found + 1 :]
else:
tspan.text = phrase
def export_point(vals, w):
# mandatory group codes : (10, 20) (x, y)
if vals.has_x1 and vals.has_y1:
if options.gcodetoolspoints:
generate_gcodetools_point(vals.x1, vals.y1)
else:
generate_ellipse(vals.x1, vals.y1, w / 2, 0.0, 1.0, 0.0, 0.0)
def export_line(vals):
"""Draw a straight line from the dxf"""
# mandatory group codes : (10, 11, 20, 21) (x1, x2, y1, y2)
if vals.has_x1 and vals.has_x2 and vals.has_y1 and vals.has_y2:
path = inkex.PathElement()
path.style = style
path.path = "M %f,%f %f,%f" % (vals.x1, vals.y1, vals.x2, vals.y2)
layer.add(path)
def export_solid(vals):
# arrows of dimension
# mandatory group codes : (10, 11, 12, 20, 21, 22) (x1, x2, x3, y1, y2, y3)
# TODO: 4th point
if (
vals.has_x1
and vals.has_x2
and vals.has_x3
and vals.has_y1
and vals.has_y2
and vals.has_y3
):
path = inkex.PathElement()
path.style = style
path.path = "M %f,%f %f,%f %f,%f z" % (
vals.x1,
vals.y1,
vals.x2,
vals.y2,
vals.x3,
vals.y3,
)
layer.add(path)
def export_spline(vals):
# see : http://www.mactech.com/articles/develop/issue_25/schneider.html
# mandatory group codes : (10, 20, 40, 70) (x[], y[], knots[], flags)
if (
vals.has_flags
and vals.has_knots
and vals.x1_list
and len(vals.x1_list) == len(vals.y1_list)
):
knots = vals.knots_list
ctrls = len(vals.x1_list)
if ctrls > 3 and len(knots) == ctrls + 4: # cubic
if ctrls > 4:
for i in range(len(knots) - 5, 3, -1):
if knots[i] != knots[i - 1] and knots[i] != knots[i + 1]:
a0 = (knots[i] - knots[i - 2]) / (knots[i + 1] - knots[i - 2])
a1 = (knots[i] - knots[i - 1]) / (knots[i + 2] - knots[i - 1])
vals.x1_list.insert(
i - 1,
(1.0 - a1) * vals.x1_list[i - 2] + a1 * vals.x1_list[i - 1],
)
vals.y1_list.insert(
i - 1,
(1.0 - a1) * vals.y1_list[i - 2] + a1 * vals.y1_list[i - 1],
)
vals.x1_list[i - 2] = (1.0 - a0) * vals.x1_list[
i - 3
] + a0 * vals.x1_list[i - 2]
vals.y1_list[i - 2] = (1.0 - a0) * vals.y1_list[
i - 3
] + a0 * vals.y1_list[i - 2]
knots.insert(i, knots[i])
for i in range(len(knots) - 6, 3, -2):
if (
knots[i] != knots[i + 2]
and knots[i - 1] != knots[i + 1]
and knots[i - 2] != knots[i]
):
a1 = (knots[i] - knots[i - 1]) / (knots[i + 2] - knots[i - 1])
vals.x1_list.insert(
i - 1,
(1.0 - a1) * vals.x1_list[i - 2] + a1 * vals.x1_list[i - 1],
)
vals.y1_list.insert(
i - 1,
(1.0 - a1) * vals.y1_list[i - 2] + a1 * vals.y1_list[i - 1],
)
ctrls = len(vals.x1_list)
path = "M %f,%f" % (vals.x1, vals.y1)
for i in range(0, (ctrls - 1) // 3):
path += " C %f,%f %f,%f %f,%f" % (
vals.x1_list[3 * i + 1],
vals.y1_list[3 * i + 1],
vals.x1_list[3 * i + 2],
vals.y1_list[3 * i + 2],
vals.x1_list[3 * i + 3],
vals.y1_list[3 * i + 3],
)
if vals.flags & 1: # closed path
path += " z"
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
if ctrls == 3 and len(knots) == 6: # quadratic
path = "M %f,%f Q %f,%f %f,%f" % (
vals.x1,
vals.y1,
vals.x1_list[1],
vals.y1_list[1],
vals.x1_list[2],
vals.y1_list[2],
)
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
if ctrls == 5 and len(knots) == 8: # spliced quadratic
path = "M %f,%f Q %f,%f %f,%f Q %f,%f %f,%f" % (
vals.x1,
vals.y1,
vals.x1_list[1],
vals.y1_list[1],
vals.x1_list[2],
vals.y1_list[2],
vals.x1_list[3],
vals.y1_list[3],
vals.x1_list[4],
vals.y1_list[4],
)
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
def export_circle(vals):
# mandatory group codes : (10, 20, 40) (x, y, radius)
if vals.has_x1 and vals.has_y1 and vals.has_radius:
generate_ellipse(vals.x1, vals.y1, scale * vals.radius, 0.0, 1.0, 0.0, 0.0)
def export_arc(vals):
# mandatory group codes : (10, 20, 40, 50, 51) (x, y, radius, angle1, angle2)
if (
vals.has_x1
and vals.has_y1
and vals.has_radius
and vals.has_angle
and vals.has_angle2
):
generate_ellipse(
vals.x1,
vals.y1,
scale * vals.radius,
0.0,
1.0,
vals.angle * math.pi / 180.0,
vals.angle2 * math.pi / 180.0,
)
def export_ellipse(vals):
# mandatory group codes : (10, 11, 20, 21, 40, 41, 42) (xc, xm, yc, ym, width ratio, angle1, angle2)
if (
vals.has_x1
and vals.has_x2
and vals.has_y1
and vals.has_y2
and vals.has_width_ratio
and vals.has_ellipse_a1
and vals.has_ellipse_a2
):
# generate_ellipse(vals.x1, vals.y1, scale*vals.x2, scale*vals.y2, vals.width_ratio, vals.ellipse_a1, vals.ellipse_a2)
# vals are through adjust_coords : recover proper value
# (x,y)=(scale*x-xmin, height-scale*y-ymin)
x2 = vals.x2 + xmin
y2 = -vals.y2 + ymin + height
generate_ellipse(
vals.x1, vals.y1, x2, y2, vals.width_ratio, vals.ellipse_a1, vals.ellipse_a2
)
def export_leader(vals):
# mandatory group codes : (10, 20) (x, y)
if vals.has_x1 and vals.has_y1:
if len(vals.x1_list) > 1 and len(vals.y1_list) == len(vals.x1_list):
path = "M %f,%f" % (vals.x1, vals.y1)
for i in range(1, len(vals.x1_list)):
path += " %f,%f" % (vals.x1_list[i], vals.y1_list[i])
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
def export_polyline(vals):
return export_lwpolyline(vals)
def export_lwpolyline(vals):
# mandatory group codes : (10, 20, 70) (x, y, flags)
if vals.has_x1 and vals.has_y1 and vals.has_flags:
if len(vals.x1_list) > 1 and len(vals.y1_list) == len(vals.x1_list):
# optional group codes : (42) (bulge)
iseqs = 0
ibulge = 0
if vals.flags & 1: # closed path
seqs.append("20")
vals.x1_list.append(vals.x1)
vals.y1_list.append(vals.y1)
while seqs[iseqs] != "20":
iseqs += 1
path = "M %f,%f" % (vals.x1, vals.y1)
xold = vals.x1
yold = vals.y1
for i in range(1, len(vals.x1_list)):
bulge = 0
iseqs += 1
while seqs[iseqs] != "20":
if seqs[iseqs] == "42":
bulge = vals.bulge_list[ibulge]
ibulge += 1
iseqs += 1
if bulge:
sweep = 0 # sweep CCW
if bulge < 0:
sweep = 1 # sweep CW
bulge = -bulge
large = 0 # large-arc-flag
if bulge > 1:
large = 1
r = math.sqrt(
(vals.x1_list[i] - xold) ** 2 + (vals.y1_list[i] - yold) ** 2
)
r = 0.25 * r * (bulge + 1.0 / bulge)
path += " A %f,%f 0.0 %d %d %f,%f" % (
r,
r,
large,
sweep,
vals.x1_list[i],
vals.y1_list[i],
)
else:
path += " L %f,%f" % (vals.x1_list[i], vals.y1_list[i])
xold = vals.x1_list[i]
yold = vals.y1_list[i]
if vals.flags & 1: # closed path
path += " z"
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
def export_hatch(vals):
# mandatory group codes : (10, 20, 70, 72, 92, 93) (x, y, fill, Edge Type, Path Type, Number of edges)
# TODO: Hatching Pattern
if (
vals.has_x1
and vals.has_y1
and vals.has_fill
and vals.has_edge_type
and vals.has_path_type
and vals.has_num_edges
):
if len(vals.x1_list) > 1 and len(vals.y1_list) == len(vals.x1_list):
# optional group codes : (11, 21, 40, 50, 51, 73) (x, y, r, angle1, angle2, CCW)
i10 = 1 # count start points
i11 = 0 # count line end points
i40 = 0 # count circles
i72 = 0 # count edge type flags
path = ""
for i in range(0, len(vals.num_edges_list)):
xc = vals.x1_list[i10]
yc = vals.y1_list[i10]
if vals.edge_type_list[i72] == 2: # arc
rm = scale * vals.radius_list[i40]
a1 = vals.angle_list[i40]
path += "M %f,%f " % (
xc + rm * math.cos(a1 * math.pi / 180.0),
yc + rm * math.sin(a1 * math.pi / 180.0),
)
else:
a1 = 0
path += "M %f,%f " % (xc, yc)
for j in range(0, vals.num_edges_list[i]):
if vals.path_type_list[i] & 2: # polyline
if j > 0:
path += "L %f,%f " % (vals.x1_list[i10], vals.y1_list[i10])
if j == vals.path_type_list[i] - 1:
i72 += 1
elif vals.edge_type_list[i72] == 2: # arc
xc = vals.x1_list[i10]
yc = vals.y1_list[i10]
rm = scale * vals.radius_list[i40]
a2 = vals.angle2_list[i40]
diff = (a2 - a1 + 360) % 360
sweep = 1 - vals.sweep_list[i40] # sweep CCW
large = 0 # large-arc-flag
if diff:
path += "A %f,%f 0.0 %d %d %f,%f " % (
rm,
rm,
large,
sweep,
xc + rm * math.cos(a2 * math.pi / 180.0),
yc + rm * math.sin(a2 * math.pi / 180.0),
)
else:
path += "A %f,%f 0.0 %d %d %f,%f " % (
rm,
rm,
large,
sweep,
xc + rm * math.cos((a1 + 180.0) * math.pi / 180.0),
yc + rm * math.sin((a1 + 180.0) * math.pi / 180.0),
)
path += "A %f,%f 0.0 %d %d %f,%f " % (
rm,
rm,
large,
sweep,
xc + rm * math.cos(a1 * math.pi / 180.0),
yc + rm * math.sin(a1 * math.pi / 180.0),
)
i40 += 1
i72 += 1
elif vals.edge_type_list[i72] == 1: # line
path += "L %f,%f " % (vals.x2_list[i11], vals.y2_list[i11])
i11 += 1
i72 += 1
i10 += 1
path += "z "
if vals.has_fill:
style = formatStyle({"fill": "%s" % color})
else:
style = formatStyle({"fill": "url(#Hatch)", "fill-opacity": "1.0"})
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
def export_dimension(vals):
# mandatory group codes : (10, 11, 13, 14, 20, 21, 23, 24) (x1..4, y1..4)
# block_name: dimension definition for 10mm
if vals.has_x1 and vals.has_x2 and vals.has_y1 and vals.has_y2:
if vals.has_block_name:
attribs = {
inkex.addNS("href", "xlink"): "#%s" % (vals.block_name)
} # not use quote because name *D2 etc. changed to %2AD2
tform = "translate(0 0)"
# if vals.has_angle :
# tform += ' rotate(%f,%f,%f)' % (vals.angle,vals.x4,vals.y4)
attribs.update({"transform": tform})
etree.SubElement(layer, "use", attribs)
else:
# TODO: improve logic when INSERT in BLOCK
dx = abs(vals.x1 - vals.x3)
dy = abs(vals.y1 - vals.y3)
if (vals.x1 == vals.x4) and dx > 0.00001:
d = dx / scale
dy = 0
path = "M %f,%f %f,%f" % (vals.x1, vals.y1, vals.x3, vals.y1)
elif (vals.y1 == vals.y4) and dy > 0.00001:
d = dy / scale
dx = 0
path = "M %f,%f %f,%f" % (vals.x1, vals.y1, vals.x1, vals.y3)
else:
return
attribs = {
"d": path,
"style": style
+ "; marker-start: url(#DistanceX); marker-end: url(#DistanceX); stroke-width: 0.25px",
}
etree.SubElement(layer, "path", attribs)
x = vals.x2
y = vals.y2
size = 12 # default fontsize in px
if vals.has_mtext:
if vals.mtext in DIMTXT:
size = scale * textscale * DIMTXT[vals.mtext]
if size < 2:
size = 2
attribs = {
"x": "%f" % x,
"y": "%f" % y,
"style": "font-size: %.3fpx; fill: %s; font-family: %s; text-anchor: middle; text-align: center"
% (size, color, options.font),
}
if dx == 0:
attribs.update({"transform": "rotate (%f %f %f)" % (-90, x, y)})
node = etree.SubElement(layer, "text", attribs)
tspan = node.add(inkex.Tspan())
tspan.set("sodipodi:role", "line")
tspan.text = str(float("%.2f" % d))
def export_insert(vals):
# mandatory group codes : (2, 10, 20) (block name, x, y)
# TODO: repeat by row and column
# (times,interval)= row(70,44), column(71,45)
if vals.has_block_name and vals.has_x1 and vals.has_y1:
# vals are through adjust_coords except block
# block (x,y)=(0,0) : (scale*x-xmin, height-scale*y-ymin)
# translate(move x units,move y units)
# 2021.6 translate..ok scale..x rotate X
# as scale, the line is wider ->same width -> you should fix
global height
cx = scale * xmin # transorm-origin:
cy = scale * ymin + height # center of rotation
x = vals.x1 + scale * xmin
y = vals.y1 - scale * ymin - height
ixscale = iyscale = 1
if vals.has_insert_scale_y:
ixscale = vals.insert_scale_x
if vals.has_insert_scale_y:
iyscale = vals.insert_scale_y
x += cx * (iyscale - 1)
y -= cy * (iyscale - 1)
elem = layer.add(inkex.Use())
elem.set(
inkex.addNS("href", "xlink"),
"#" + quote(vals.block_name.replace(" ", "_").encode("utf-8")),
)
# add style stroke-width=1px for reducing thick line
fwide = abs(0.5 / ixscale) # better to use w/ixscale
elem.style["stroke-width"] = "%.3fpx" % fwide
elem.transform.add_translate(x, y)
if vals.has_insert_scale_x and vals.has_insert_scale_y:
elem.transform.add_scale(ixscale, iyscale)
if vals.has_angle:
rotated_angle = vals.angle
if ixscale * iyscale > 0:
rotated_angle = 360 - rotated_angle
elem.transform.add_rotate(rotated_angle, -cx, cy)
def export_block(vals):
# mandatory group codes : (2) (block name)
if vals.has_block_name:
global block
def export_endblk(vals):
global block
block = defs # initiallize with dummy
def export_attdef(vals):
# mandatory group codes : (1, 2) (default, tag)
if vals.has_default and vals.has_tag:
vals.text_list.append(vals.tag)
export_mtext(vals)
def generate_ellipse(xc, yc, xm, ym, w, a1, a2):
rm = math.sqrt(xm * xm + ym * ym)
a = math.atan2(ym, xm) # x-axis-rotation
diff = (a2 - a1 + 2 * math.pi) % (2 * math.pi)
if abs(diff) > 0.0000001 and abs(diff - 2 * math.pi) > 0.0000001: # open arc
large = 0 # large-arc-flag
if diff > math.pi:
large = 1
xt = rm * math.cos(a1)
yt = w * rm * math.sin(a1)
x1 = xt * math.cos(a) - yt * math.sin(a)
y1 = xt * math.sin(a) + yt * math.cos(a)
xt = rm * math.cos(a2)
yt = w * rm * math.sin(a2)
x2 = xt * math.cos(a) - yt * math.sin(a)
y2 = xt * math.sin(a) + yt * math.cos(a)
path = "M %f,%f A %f,%f %f %d 0 %f,%f" % (
xc + x1,
yc - y1,
rm,
w * rm,
-180.0 * a / math.pi,
large,
xc + x2,
yc - y2,
)
else: # closed arc
path = "M %f,%f A %f,%f %f 0, 0 %f,%f A %f,%f %f 0, 0 %f,%f z" % (
xc + xm,
yc - ym,
rm,
w * rm,
-180.0 * a / math.pi,
xc - xm,
yc + ym,
rm,
w * rm,
-180.0 * a / math.pi,
xc + xm,
yc - ym,
)
attribs = {"d": path, "style": style}
etree.SubElement(layer, "path", attribs)
def generate_gcodetools_point(xc, yc):
elem = layer.add(inkex.PathElement())
elem.style = "stroke:none;fill:#ff0000"
elem.set("inkscape:dxfpoint", "1")
elem.path = (
"m %s,%s 2.9375,-6.34375 0.8125,1.90625 6.84375,-6.84375 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.8125 z"
% (xc, yc)
)
# define DXF Entities and specify which Group Codes to monitor
class DxfInput(inkex.InputExtension):
def add_arguments(self, pars):
pars.add_argument("--tab", default="options")
pars.add_argument("--scalemethod", default="manual")
pars.add_argument("--scale", default="1.0")
pars.add_argument("--textscale", default="1.0")
pars.add_argument("--xmin", default="0.0")
pars.add_argument("--ymin", default="0.0")
pars.add_argument("--gcodetoolspoints", default=False, type=inkex.Boolean)
pars.add_argument("--encoding", dest="input_encode", default="latin_1")
pars.add_argument("--font", default="Arial")
def load(self, stream):
return stream
def effect(self):
global options
global defs
global entity
global seqs
global style
global layer
global scale
global textscale
global color
global extrude
global xmin
global ymin
global height
global DIMTXT
global block
global svg
global style_font3
global style_direction
global be_extrude
options = self.options
doc = self.get_template(width=210 * 96 / 25.4, height=297 * 96 / 25.4)
svg = doc.getroot()
defs = svg.defs
def _get_line():
return self.document.readline().strip().decode(options.input_encode)
def get_line():
return _get_line(), _get_line()
def get_group(group):
line = get_line()
if line[0] == group:
return float(line[1])
return 0.0
xmax = xmin = ymin = 0.0
ltscale = 1.0 # $LTSCALE:global scale of line-style
height = 297.0 * 96.0 / 25.4 # default A4 height in pixels
measurement = 0 # default inches
flag = 0 # (0, 1, 2, 3, 4) = (none, LAYER, LTYPE, DIMTXT, STYLE)
layer_colors = {} # store colors by layer
layer_nodes = {} # store nodes by layer
linetypes = {} # store linetypes by name
DIMTXT = {} # store DIMENSION text sizes
# style_name = {} # style name
style_font3 = {} # style font 1byte
style_font4 = {} # style font 2byte
style_direction = {} # style display direction
be_extrude = False
line = get_line()
if line[0] == "AutoCAD Binary DXF":
inkex.errormsg(
_(
"Inkscape cannot read binary DXF files. \n"
"Please convert to ASCII format first."
)
+ str(len(line[0]))
+ " "
+ str(len(line[1]))
)
self.document = doc
return
inENTITIES = False
style_name = "*"
layername = None
linename = None
stylename = None
style_name = None
errno = 0
pdmode_err = False
while line[0] and line[1] != "BLOCKS":
line = get_line()
if line[1] == "ENTITIES": # no BLOCK SECTION
inENTITIES = True
break
if line[1] == "$PDMODE" and not pdmode_err:
pdmode_err = True
if options.scalemethod == "file":
if line[1] == "$MEASUREMENT":
measurement = get_group("70")
elif options.scalemethod == "auto":
if line[1] == "$EXTMIN":
xmin = get_group("10")
ymin = get_group("20")
if line[1] == "$EXTMAX":
xmax = get_group("10")
if line[1] == "$LTSCALE":
ltscale = get_group("40")
if flag == 1 and line[0] == "2":
layername = line[1]
layer_nodes[layername] = svg.add(inkex.Layer.new(layername))
if flag == 2 and line[0] == "2":
linename = line[1]
linetypes[linename] = []
if flag == 3 and line[0] == "2":
stylename = line[1]
if flag == 4 and line[0] == "2":
style_name = line[1]
style_font3[style_name] = []
style_font4[style_name] = []
style_direction[style_name] = []
if line[0] == "2" and line[1] == "LAYER":
flag = 1
if line[0] == "2" and line[1] == "LTYPE":
flag = 2
if line[0] == "2" and line[1] == "DIMSTYLE":
flag = 3
if line[0] == "2" and line[1] == "STYLE":
flag = 4
if flag == 1 and line[0] == "62":
if layername is None:
errno = 1
break
layer_colors[layername] = int(line[1])
if flag == 2 and line[0] == "49":
if linename is None:
errno = 2
break
linetypes[linename].append(float(line[1]))
if flag == 3 and line[0] == "140":
if stylename is None:
errno = 3
break
DIMTXT[stylename] = float(line[1])
if flag == 4 and line[0] == "3":
if style_name is None:
errno = 4
break
style_font3[style_name].append(line[1])
if flag == 4 and line[0] == "4":
if style_name is None:
errno = 4
break
style_font4[style_name].append(line[1])
if flag == 4 and line[0] == "70": # not no of STYLE
if style_name != "*":
style_direction[style_name] = int(line[1])
if line[0] == "0" and line[1] == "ENDTAB":
flag = 0
if errno != 0:
if errno == 1:
errMsg = "LAYER"
elif errno == 2:
errMsg = "LTYPE"
elif errno == 3:
errMsg = "DIMSTYLE"
else: # errno == 4
errMsg = "STYLE"
inkex.errormsg(
"Import stopped. DXF is incorrect.\ngroup code 2 ("
+ errMsg
+ ") is missing"
)
self.document = doc
return
if options.scalemethod == "file":
scale = 25.4 # default inches
if measurement == 1.0:
scale = 1.0 # use mm
elif options.scalemethod == "auto":
scale = 1.0
if xmax > xmin:
scale = 210.0 / (xmax - xmin) # scale to A4 width
else:
scale = float(options.scale) # manual scale factor
xmin = float(options.xmin)
ymin = float(options.ymin)
scale *= 96.0 / 25.4 # convert from mm to pixels
textscale = float(options.textscale)
if "0" not in layer_nodes:
layer_nodes["0"] = svg.add(inkex.Layer.new("0"))
layer_colors["0"] = 7
for linename in linetypes.keys(): # scale the dashed lines
linetype = ""
for length in linetypes[linename]:
if length == 0: # test for dot
linetype += " 0.5,"
else:
linetype += "%.4f," % math.fabs(length * scale * ltscale)
if linetype == "":
linetypes[linename] = "stroke-linecap: round"
else:
linetypes[linename] = "stroke-dasharray:" + linetype
entity = ""
block = defs # initiallize with dummy
while line[0] and (line[1] != "ENDSEC" or not inENTITIES):
line = get_line()
if line[1] == "ENTITIES":
inENTITIES = True
if entity and vals.is_valid(line[0]):
seqs.append(line[0]) # list of group codes
if line[0] in ("1", "2", "3", "6", "7", "8"): # text value
# TODO: if add funs of export_mtext, delete the line
val = line[1].replace(r"\~", " ")
# val = re.sub(r"\\A.*;", "", val)
# val = re.sub(r'\\H.*;', '', val)
val = re.sub(r"\^I", "", val)
val = re.sub(r"{\\L", "", val)
# val = re.sub(r'}', '', val) {\\C; '}' in mtext
val = re.sub(r"\\S.*;", "", val)
val = re.sub(r"\\W.*;", "", val)
val = val
val = re.sub(r"\\U\+([0-9A-Fa-f]{4})", re_hex2unichar, val)
elif line[0] in ("62", "70", "92", "93"):
val = int(line[1])
else: # unscaled float value
val = float(line[1])
vals[line[0]].append(val)
elif has_export(line[1]):
if has_export(entity):
if block != defs: # in a BLOCK
layer = block
elif vals.has_layer_name: # use Common Layer Name
if not vals.layer_name:
vals.layer_name = "0" # use default name
if vals.layer_name not in layer_nodes:
# attribs = {inkex.addNS('groupmode','inkscape') :
# 'layer', inkex.addNS('label','inkscape') : '%s' % vals.layer_name}
# layer_nodes[vals.layer_name] = etree.SubElement(doc.getroot(), 'g', attribs)
layer_nodes[vals.layer_name] = svg.add(
inkex.Layer.new(vals.layer_name)
)
layer = layer_nodes[vals.layer_name]
color = "#000000" # default color
if vals.has_layer_name:
if vals.layer_name in layer_colors:
color = get_rgbcolor(layer_colors[vals.layer_name], color)
if vals.has_color: # Common Color Number
color = get_rgbcolor(vals.color, color)
style = formatStyle({"stroke": "%s" % color, "fill": "none"})
w = 0.5 # default lineweight for POINT
if vals.has_line_weight: # Common Lineweight
if vals.line_weight > 0:
w = 96.0 / 25.4 * vals.line_weight / 100.0
w *= scale # real wide : line_weight /144 inch
if w < 0.5:
w = 0.5
if (
block == defs
): # not in a BLOCK for INSERT except stroke-width 2021.july
style = formatStyle(
{
"stroke": "%s" % color,
"fill": "none",
"stroke-width": "%.3f" % w,
}
)
if vals.has_line_type: # Common Linetype
if vals.line_type in linetypes:
style += ";" + linetypes[vals.line_type]
extrude = 1.0
if vals.has_extrude:
if (entity != "LINE") and (entity != "POINT"):
extrude = float(vals.extrude)
if extrude < 1.0:
be_extrude = True
vals.adjust_coords(xmin, ymin, scale, extrude, height)
if extrude == -1.0: # reflect angles
if vals.has_angle and vals.has_angle2:
vals.angle2, vals.angle = (
180.0 - vals.angle,
180.0 - vals.angle2,
)
exporter = get_export(entity)
if exporter:
if entity == "POINT":
exporter(vals, w)
else:
exporter(vals)
if line[1] == "POLYLINE":
inVertexs = False
entity = "LWPOLYLINE"
vals = ValueConstruct()
seqs = []
flag70 = 0 # default closed-line or not
val8 = "0" # default layer name
val10 = 0 # x
val20 = 0 # y
val42 = 0 # bulge
valid = True
while line[0] and (line[1] != "SEQEND"):
line = get_line()
if line[1] == "VERTEX":
if inVertexs == True:
if valid:
seqs.append("10")
vals["10"].append(val10)
seqs.append("20")
vals["20"].append(val20)
seqs.append("42")
vals["42"].append(val42)
val42 = 0
inVertexs = True
valid = True
if inVertexs == False:
if line[0] == "6": # 6:line style
seqs.append(line[0])
vals[line[0]].append(line[1])
if line[0] == "8": # 8:layer
val8 = line[1]
if line[0] == "70": # flag
flag70 = int(line[1])
else:
if line[0] == "70":
if int(line[1]) == 16: # control point
valid = False
if line[0] in ("10", "20", "42"): # vertexs
val = float(line[1])
if line[0] == "10":
val10 = val
elif line[0] == "20":
val20 = val
else:
val42 = val
if valid:
seqs.append("8") # layer_name
vals["8"].append(val8)
seqs.append("10")
vals["10"].append(val10)
seqs.append("20")
vals["20"].append(val20)
seqs.append("42") # bulge
vals["42"].append(val42)
seqs.append("70") # closed line?
vals["70"].append(flag70)
continue
entity = line[1]
vals = ValueConstruct()
seqs = []
# for debug
# tree = etree.ElementTree(svg)
# tree.write('c:\Python\svgCH2.xml')
if be_extrude:
inkex.errormsg(
_(
"An object that has the extrude parameter set was detected. "
"The imported figure may be displayed incorrectly."
)
)
self.document = doc
def get_export(opt):
return globals().get("export_" + opt.lower(), None)
def has_export(opt):
return get_export(opt) is not None
if __name__ == "__main__":
DxfInput().run()