add cad class
This commit is contained in:
287
src/fvr/cad.py
Normal file
287
src/fvr/cad.py
Normal file
@@ -0,0 +1,287 @@
|
||||
import ezdxf
|
||||
import ezdxf.addons.drawing
|
||||
import ezdxf.addons.drawing.svg
|
||||
import ezdxf.addons.drawing.properties
|
||||
|
||||
# dxfattribs={'color': 1}
|
||||
# color
|
||||
# 0: white
|
||||
# 1: red
|
||||
# 2: yellow
|
||||
# 3: green
|
||||
# 4: cyan
|
||||
# 5: blue
|
||||
# 6: magenta
|
||||
# 7: white (as 0 - on dark background, black on light background)
|
||||
# 8: gray
|
||||
# 9: light-gray
|
||||
#
|
||||
# 10 - 19 shades of red (10 red same as 1)
|
||||
# ...
|
||||
# 240 - 249 shades of red (slightly blue shift)
|
||||
# 250 - 255 shades of gray
|
||||
# see https://ezdxf.mozman.at/docs/concepts/aci.html#aci
|
||||
|
||||
class doc:
|
||||
"""
|
||||
contour scale and dimension scale
|
||||
|
||||
to create a line in dimension scale = (x, y)/sc*sd
|
||||
"""
|
||||
|
||||
# scale
|
||||
_sd = 1/1 # scale dimension
|
||||
_sc = 1/1 # scale countour
|
||||
|
||||
def __init__(self, setup=True, scale=1, sc=1, sd=1):
|
||||
"""
|
||||
Args:
|
||||
scale: scale line and font size of contour and dimensions
|
||||
sc: scale contour
|
||||
sd: scale dimension position
|
||||
"""
|
||||
self.doc = ezdxf.new(dxfversion='AC1032', setup=setup)
|
||||
self.msp = self.doc.modelspace()
|
||||
layercon = self.doc.layers.add("con")
|
||||
layercon.color = 0
|
||||
layercon.dxf.lineweight = 75
|
||||
layerdim = self.doc.layers.add("dim")
|
||||
layerdim.color = 6
|
||||
layerdim.dxf.lineweight = 35
|
||||
# dimstyle = self.doc.dimstyles.add("dim")
|
||||
# dimstyle.set_text_align(valign='center', vshift=1)
|
||||
# dimstyle.set_dimline_format(color=6)
|
||||
# dimstyle.set_extline_format(color=6)
|
||||
self.sc = self._sc*sc*scale
|
||||
self.sd = self._sd*sd*scale
|
||||
#self.sd = self._sd*sd*scale/sc
|
||||
#self.sd = sc/(self._sd*sd)*scale
|
||||
self.dims = []
|
||||
self._dimstyle()
|
||||
|
||||
def _dimstyle(self):
|
||||
self.dimstyle = {
|
||||
'dimtxsty': 'Standard',
|
||||
# text
|
||||
'dimtxt': 0.35, # 0.35mm line weight
|
||||
'dimclrt': 6, # text color ## set via layer
|
||||
# arrow
|
||||
'dimtsz': 0, # set tick size to 0 to enable arrow usage
|
||||
'dimasz': 0.35, # arrow size in drawing units
|
||||
'dimblk': ezdxf.ARROWS.closed_filled, # arrows
|
||||
# dimension line
|
||||
'dimclrd': 6, # color ## set via layer
|
||||
'dimlwd': 35, # 0.35mm line weight
|
||||
# extension line
|
||||
'dimclre': 6, # color ## set via layer
|
||||
'dimlwe': 35, # 0.35mm line weight
|
||||
'dimexe': 0.15, # length above dimension line
|
||||
'dimex0': 0.10, # offset from measurement point
|
||||
'dimlfac': 1/self.sc, # scale factor for dimension measurements.
|
||||
#'dimlfac': 100*self._sdc*self.sd,
|
||||
}
|
||||
|
||||
def line(
|
||||
self, p1, p2, color=None, lineweight=None, linetype='CONTINUOUS',
|
||||
layer=None, dxfattribs=None, shift=(0, 0)):
|
||||
"""
|
||||
Args:
|
||||
p1: (x, y)
|
||||
p2: (x, y)
|
||||
color: defaults to 0
|
||||
lineweight: defaults to 75, corresponds to a 0.75 mm linewidth (conversion 1/100).
|
||||
linetype: 'DASHED2'
|
||||
leyer: e.g. 'con' for contour, 'dim' dimension
|
||||
"""
|
||||
_dxfattribs={'layer': 'con', 'linetype': linetype}
|
||||
if layer is not None: _dxfattribs.update({'layer': layer})
|
||||
if color is not None: _dxfattribs.update({'color': color})
|
||||
if lineweight is not None: _dxfattribs.update({'lineweight': lineweight})
|
||||
if dxfattribs is not None:
|
||||
_dxfattribs = dxfattribs
|
||||
p1 = ((p1[0]+shift[0])*self.sc, (p1[1]+shift[1])*self.sc)
|
||||
p2 = ((p2[0]+shift[0])*self.sc, (p2[1]+shift[1])*self.sc)
|
||||
self.msp.add_line(p1, p2, dxfattribs=_dxfattribs)
|
||||
|
||||
def polyline(
|
||||
self, pts, close=False, color=None, lineweight=None, linetype='CONTINUOUS',
|
||||
layer=None, dxfattribs=None, shift=(0, 0)):
|
||||
"""
|
||||
Args:
|
||||
pts: [(x1, y1), (x2, y2), ...]
|
||||
"""
|
||||
_dxfattribs={'layer': 'con', 'linetype': linetype}
|
||||
if layer is not None: _dxfattribs.update({'layer': layer})
|
||||
if color is not None: _dxfattribs.update({'color': color})
|
||||
if lineweight is not None: _dxfattribs.update({'lineweight': lineweight})
|
||||
if dxfattribs is not None:
|
||||
_dxfattribs = dxfattribs
|
||||
pts = [((x+shift[0])*self.sc, (y+shift[1])*self.sc) for x, y in pts]
|
||||
self.msp.add_lwpolyline(pts, close=close, dxfattribs=_dxfattribs)
|
||||
|
||||
def circle(
|
||||
self, pt, radius, color=None, lineweight=None, linetype='CONTINUOUS',
|
||||
layer=None, dxfattribs=None):
|
||||
"""
|
||||
Args:
|
||||
pt: (x, y)
|
||||
radius:
|
||||
"""
|
||||
_dxfattribs={'layer': 'con', 'linetype': linetype}
|
||||
if layer is not None: _dxfattribs.update({'layer': layer})
|
||||
if color is not None: _dxfattribs.update({'color': color})
|
||||
if lineweight is not None: _dxfattribs.update({'lineweight': lineweight})
|
||||
if dxfattribs is not None:
|
||||
_dxfattribs = dxfattribs
|
||||
pt = [i*self.sc for i in pt]
|
||||
radius *= self.sc
|
||||
self.msp.add_circle(pt, radius=radius, dxfattribs=_dxfattribs)
|
||||
|
||||
def linear_dim(self, base, p1, p2, text, dim_shift=None):
|
||||
"""
|
||||
Args:
|
||||
base: (x, y) location of the dimension line in contour scale.
|
||||
Use dim_shift to be able to scale contour only and not the dimension.
|
||||
p1: (x, y) 1st measurement point
|
||||
p2: (x, y) 2nd measurement point
|
||||
dim_shift: shift (dx, dy), in dimension scale not contour scale
|
||||
"""
|
||||
_dxfattribs={'layer': 'dim'}
|
||||
base = [i*self.sc for i in base]
|
||||
p1, p2 = [i*self.sc for i in p1], [i*self.sc for i in p2]
|
||||
if dim_shift is not None:
|
||||
dim_shift = [i*self.sd for i in dim_shift]
|
||||
base = ((base[0]+dim_shift[0]), (base[1]+dim_shift[1]))
|
||||
dim = self.msp.add_linear_dim(base=base, p1=p1, p2=p2, text=text,
|
||||
# dimstyle="EZDXF", # default dimension style
|
||||
# dimstyle="dim",
|
||||
override=self.dimstyle,
|
||||
dxfattribs=_dxfattribs,
|
||||
)
|
||||
self.dims.append(dim)
|
||||
# dim.render() below when fetching the rendering
|
||||
|
||||
def aligned_dim(
|
||||
self, p1, p2, distance, text, text_rotation=None, text_shifth=0,
|
||||
text_shiftv=0, arrow1=None, arrow2=None):
|
||||
"""
|
||||
Args:
|
||||
p1: (x, y) 1st measurement point
|
||||
p2: (x, y) 2nd measurement point
|
||||
distance: NOTE! in dimension scale not contour scale
|
||||
text:
|
||||
text_rotation: global rotation in in degree
|
||||
text_shifth: shift parallel to the dimension line
|
||||
text_shiftv: shift perpendicular to the dimension line
|
||||
arrow1: 'DOTSMALL'
|
||||
arrow2: 'DOTSMALL'
|
||||
"""
|
||||
_dxfattribs = {'layer': 'dim'}
|
||||
if text_rotation is not None: _dxfattribs.update({'text_rotation': text_rotation})
|
||||
p1, p2 = [i*self.sc for i in p1], [i*self.sc for i in p2]
|
||||
# distance # not distance, this is in dimension scale
|
||||
distance *= self.sd
|
||||
text_shifth *= self.sd
|
||||
text_shiftv *= self.sd
|
||||
dim = self.msp.add_aligned_dim(
|
||||
p1=p1, p2=p2, distance=distance, text=text, override=self.dimstyle,
|
||||
dxfattribs=_dxfattribs)
|
||||
dim.shift_text(dh=text_shifth, dv=text_shiftv)
|
||||
if arrow1 is not None:
|
||||
dim.set_arrows(blk1=arrow1)
|
||||
if arrow2 is not None:
|
||||
dim.set_arrows(blk2=arrow2)
|
||||
self.dims.append(dim)
|
||||
# dim.render() below when fetching the rendering
|
||||
|
||||
def leader(
|
||||
self, pts=(0, 0), layer=None):
|
||||
"""
|
||||
Args:
|
||||
pts: [(x1, y1), (x2, y2), ...]
|
||||
distance: NOTE! in dimension scale not contour scale
|
||||
text:
|
||||
text_rotation: global rotation in in degree
|
||||
text_shifth: shift parallel to the dimension line
|
||||
text_shiftv: shift perpendicular to the dimension line
|
||||
arrow1: 'DOTSMALL'
|
||||
arrow2: 'DOTSMALL'
|
||||
"""
|
||||
_dxfattribs = {'layer': 'dim'}
|
||||
pts = [((x)*self.sc, (y)*self.sc) for x, y in pts]
|
||||
dim = self.msp.add_leader(
|
||||
vertices=pts, override=self.dimstyle,
|
||||
dxfattribs=_dxfattribs)
|
||||
|
||||
def text(
|
||||
self, text, pt=(0, 0), height=0.35, rotation=0, layer=None):
|
||||
"""
|
||||
Args:
|
||||
p1: (x, y) 1st measurement point
|
||||
p2: (x, y) 2nd measurement point
|
||||
distance: NOTE! in dimension scale not contour scale
|
||||
text:
|
||||
text_rotation: global rotation in in degree
|
||||
text_shifth: shift parallel to the dimension line
|
||||
text_shiftv: shift perpendicular to the dimension line
|
||||
arrow1: 'DOTSMALL'
|
||||
arrow2: 'DOTSMALL'
|
||||
"""
|
||||
_dxfattribs = {'layer': ''}
|
||||
if layer is not None: _dxfattribs.update({'layer': layer})
|
||||
pt = [i*self.sc for i in pt]
|
||||
dim = self.msp.add_text(
|
||||
text=text, height=height, rotation=rotation, dxfattribs=_dxfattribs)
|
||||
# set position and alignment
|
||||
dim.set_placement(
|
||||
pt,
|
||||
# align=ezdxf.enums.TextEntityAlignment.MIDDLE_CENTER,
|
||||
# align=ezdxf.enums.TextEntityAlignment.MIDDLE_LEFT,
|
||||
align=ezdxf.enums.TextEntityAlignment.LEFT,
|
||||
)
|
||||
|
||||
def render_dims(self):
|
||||
# Necessary second step to create the BLOCK entity with the dimension geometry.
|
||||
# Additional processing of the DIMENSION entity could happen between adding
|
||||
# the entity and the rendering call.
|
||||
[dim.render() for dim in self.dims]
|
||||
|
||||
def save_dxf(self, filename="output.dxf"):
|
||||
self.render_dims()
|
||||
self.doc.saveas(filename)
|
||||
|
||||
def export_svg_string(self, background="#21283000"):
|
||||
self.render_dims()
|
||||
doc = self.doc
|
||||
msp = self.msp
|
||||
context = ezdxf.addons.drawing.RenderContext(doc)
|
||||
backend = ezdxf.addons.drawing.svg.SVGBackend()
|
||||
layoutprops = ezdxf.addons.drawing.properties.LayoutProperties.from_layout(msp)
|
||||
layoutprops.set_colors(background) # set background, transparent "#21283000", default "#212830"
|
||||
frontend = ezdxf.addons.drawing.Frontend(context, backend)
|
||||
frontend.draw_layout(msp, layout_properties=layoutprops)
|
||||
# page size
|
||||
# page = ezdxf.addons.drawing.layout.Page(
|
||||
# 210, 297, units=ezdxf.addons.drawing.layout.Units.mm,
|
||||
# margins=ezdxf.addons.drawing.layout.Margins.all(20))
|
||||
# svg_string = backend.get_string(page)
|
||||
# autodetect page size
|
||||
page = ezdxf.addons.drawing.layout.Page(
|
||||
0, 0, units=ezdxf.addons.drawing.layout.Units.mm,
|
||||
margins=ezdxf.addons.drawing.layout.Margins.all(2))
|
||||
svg_string = backend.get_string(
|
||||
page, settings=ezdxf.addons.drawing.layout.Settings(scale=20/1, fit_page=False))
|
||||
return svg_string
|
||||
|
||||
def export_svg(self, filename="output.svg"):
|
||||
doc = self.doc
|
||||
svg_string = export_svg_string(doc)
|
||||
with open(filename, "wt", encoding="utf8") as fh:
|
||||
fh.write(svg_string)
|
||||
|
||||
if __name__ == '__main__':
|
||||
dd = doc()
|
||||
# detects the bounding box for any Iterable[DXFEntity]
|
||||
bounding_box = ezdxf.bbox.extents(dd.modelspace())
|
||||
print(bounding_box)
|
||||
print('\n'.join([f"{i}" for i in dir(dd.doc)]))
|
||||
Reference in New Issue
Block a user