Proof of concept for IFC construction documentation

This commit is contained in:
Dion Moult
2019-10-28 21:01:32 +11:00
parent cf454c6306
commit d6ef64aef4
6 changed files with 3234 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
import xml.etree.ElementTree as ET
import pystache
import ntpath
import os
sheet_path = 'sheets/sheet1.svg'
sheet_filename = ntpath.basename(sheet_path)
sheet_name = sheet_filename[0:-4]
# hardcoded for testing
data = [
{'number': 'ASDF', 'revision': 'A'},
{},
{'no': '01', 'name': 'HOUSE', 'scale': '1:100'},
{},
{'no': '02', 'name': 'ELEVATION', 'scale': '1:100'},
{},
{'no': '03', 'name': 'PLAN', 'scale': '1:100'},
{},
{'no': '04', 'name': 'SECTION', 'scale': '1:100'},
]
tree = ET.parse(sheet_path)
root = tree.getroot()
embedded_svgs = root.findall('{http://www.w3.org/2000/svg}svg')
n = 0
for svg in embedded_svgs:
use = svg.findall('{http://www.w3.org/2000/svg}use')[0]
source = use.attrib.get('{http://www.w3.org/1999/xlink}href')
source_path = source.split('#')[0]
source_filename = ntpath.basename(source_path)
source_anchor = source.split('#')[1]
dest_filename = '{}-{}.svg'.format(source_filename[0:-4], n)
print(dest_filename)
os.makedirs('build/refs/{}/'.format(sheet_name), exist_ok=True)
with open('build/refs/{}/{}'.format(sheet_name, dest_filename), 'w') as out:
with open('sheets/{}'.format(source_path), 'r') as template:
out.write(pystache.render(template.read(), data[n]))
use.set('{http://www.w3.org/1999/xlink}href',
'refs/{}/{}#{}'.format(sheet_name, dest_filename, source_anchor))
n += 1
with open('build/{}'.format(sheet_filename), 'wb') as output:
tree.write(output)
@@ -0,0 +1,527 @@
import os
import math
import svgwrite
import time
from pathlib import Path
import OCC.gp
import OCC.Geom
import OCC.Bnd
import OCC.BRepBndLib
import OCC.BRep
import OCC.BRepPrimAPI
import OCC.BRepAlgoAPI
import OCC.BRepBuilderAPI
import OCC.BRepAlgo
import OCC.TopOpeBRepTool
import OCC.TopOpeBRepBuild
import OCC.ShapeExtend
import OCC.GProp
import OCC.BRepGProp
import OCC.GC
import OCC.ShapeAnalysis
import OCC.TopTools
import OCC.TopExp
import OCC.HLRAlgo
import OCC.HLRBRep
import OCC.TopLoc
import OCC.Bnd
import OCC.BRepBndLib
import OCC.BRepTools
import OCC.TopoDS
import OCC.GeomLProp
from OCC.TopoDS import topods
import ifcopenshell
import ifcopenshell.geom
class IfcCutter:
def __init__(self):
self.product_shapes = []
self.background_elements = []
self.cut_polygons = []
self.ifc_files = []
self.unit = None
self.section_box = {
'top_left_corner': (-3., -2.45, 4.75),
'projection': (0., 1., 0.),
'x_axis': (1., 0., 0.),
'x': 15.,
'y': 7.,
'z': 3.5,
'shape': None,
'face': None
}
self.section_box = {
'top_left_corner': (-3.097275733947754, 0.42036718130111694, 11.297039031982422),
'projection': (0.0, 1.0000001192092896, 0.0),
'x_axis': (1.0000001192092896, 0.0, 0.0),
'x': 16.4,
'y': 16.4,
'z': 16.5,
'shape': None,
'face': None
}
self.section_box = {
'projection': (0.0, 1.0000001192092896, 0.0),
#'top_left_corner': (11.5675630569458, -4.225902557373047, 7.368159770965576),
'top_left_corner': (11.5675630569458, 0.1, 7.368159770965576),
'x_axis': (1.0000001192092896, 0.0, 0.0),
'x': 17.4,
'y': 8.37,
'z': 5.23,
'shape': None,
'face': None
}
self.section_box = {
'projection': (0.0, 3.2584136988589307e-07, -1.0),
'top_left_corner': (12.898375511169434, 31.655664443969727, 2.339174270629883),
'x_axis': (1.0, 0.0, 0.0),
'x': 17.4,
'y': 34.4,
'z': 5.23,
'shape': None,
'face': None
}
def cut(self):
start_time = time.time()
print('# Load files')
self.load_ifc_files()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get units')
self.get_units()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get product shapes')
self.get_product_shapes()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Create section box')
self.create_section_box()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get cut polygons')
self.get_cut_polygons()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Get background elements')
self.get_background_elements()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
start_time = time.time()
print('# Sort background elements')
self.sort_background_elements()
print('# Timer logged at {:.2f} seconds'.format(time.time() - start_time))
def load_ifc_files(self):
for filename in Path('ifc/').glob('*.ifc'):
print('Loading file {} ...'.format(filename))
self.ifc_files.append(ifcopenshell.open(filename))
def get_units(self):
unit_assignment = self.ifc_files[0].by_type('IfcUnitAssignment')[0]
for unit in unit_assignment.Units:
if unit.UnitType == 'LENGTHUNIT':
self.unit = unit
break
def get_product_shapes(self):
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_PYTHON_OPENCASCADE, True)
products = []
for ifc_file in self.ifc_files:
products.extend(ifc_file.by_type('IfcProduct'))
total_products = len(products)
for i, product in enumerate(products):
print('{}/{} geometry processed ...'.format(i, total_products), end='\r', flush=True)
if product.is_a('IfcOpeningElement') or product.is_a('IfcSite'):
continue
if product.Representation is not None:
shape = ifcopenshell.geom.create_shape(settings, product).geometry
self.product_shapes.append((product, shape))
def sort_background_elements(self):
new_list = sorted(self.background_elements, key=lambda k: k['z'])
self.background_elements = new_list
def create_section_box(self):
top_left_corner = OCC.gp.gp_Pnt(
self.section_box['top_left_corner'][0],
self.section_box['top_left_corner'][1],
self.section_box['top_left_corner'][2])
axis = OCC.gp.gp_Ax2(
top_left_corner,
OCC.gp.gp_Dir(
self.section_box['projection'][0],
self.section_box['projection'][1],
self.section_box['projection'][2]),
OCC.gp.gp_Dir(
self.section_box['x_axis'][0],
self.section_box['x_axis'][1],
self.section_box['x_axis'][2])
)
section_box = OCC.BRepPrimAPI.BRepPrimAPI_MakeBox(
axis, self.section_box['x'], self.section_box['y'], self.section_box['z']
)
self.section_box['shape'] = section_box.Shape()
self.section_box['face'] = section_box.BottomFace()
source = OCC.gp.gp_Ax3(axis)
destination = OCC.gp.gp_Ax3(
OCC.gp.gp_Pnt(0, 0, 0),
OCC.gp.gp_Dir(0, 0, -1),
OCC.gp.gp_Dir(1, 0, 0))
self.transformation = OCC.gp.gp_Trsf()
self.transformation.SetDisplacement(source, destination)
def get_background_elements(self):
total_product_shapes = len(self.product_shapes)
n = 0
for product, shape in self.product_shapes:
print('{}/{} background elements processed ...'.format(n, total_product_shapes), end='\r', flush=True)
#print('Processing product {} '.format(product.Name))
n += 1
intersection = OCC.BRepAlgoAPI.BRepAlgoAPI_Common(self.section_box['shape'], shape).Shape()
intersection_edges = self.get_booleaned_edges(intersection)
if len(intersection_edges) <= 0:
continue
transformed_intersection = OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(
intersection, self.transformation)
intersection = transformed_intersection.Shape()
edge_face_map = OCC.TopTools.TopTools_IndexedDataMapOfShapeListOfShape()
OCC.TopExp.topexp.MapShapesAndAncestors(
intersection, OCC.TopAbs.TopAbs_EDGE,
OCC.TopAbs.TopAbs_FACE, edge_face_map)
exp = OCC.TopExp.TopExp_Explorer(intersection, OCC.TopAbs.TopAbs_FACE)
while exp.More():
face = topods.Face(exp.Current())
zpos, zmax = self.calculate_face_zpos(face)
self.build_new_face(face, zpos, product)
self.get_split_edges(edge_face_map, face, zmax, product)
exp.Next()
def calculate_face_zpos(self, face):
bbox = OCC.Bnd.Bnd_Box()
OCC.BRepBndLib.brepbndlib_Add(face, bbox)
xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get()
zpos = zmin + ((zmax - zmin)/2)
return zpos, zmax
def get_split_edges(self, edge_face_map, face, zmax, product):
exp2 = OCC.TopExp.TopExp_Explorer(face, OCC.TopAbs.TopAbs_EDGE)
while exp2.More():
edge = topods.Edge(exp2.Current())
adjface = OCC.TopoDS.TopoDS_Face()
getadj = OCC.TopOpeBRepBuild.TopOpeBRepBuild_Tools.GetAdjacentFace(face, edge, edge_face_map, adjface)
if getadj:
try:
edge_angle = math.degrees(self.get_angle_between_faces(face, adjface))
except:
# TODO: Figure out when a math domain error might occur,
# because it does, sometimes.
edge_angle = 0
if edge_angle > 30 and edge_angle < 160:
newedge = self.build_new_edge(edge, zmax+0.01)
if newedge:
self.background_elements.append({
'raw': product,
'geometry': newedge,
'type': 'line',
'z': zmax+0.01
})
exp2.Next()
def get_angle_between_faces(self, f1, f2):
return self.convert_dot_product_to_angle(
self.get_dot_product_of_normals(
self.get_normal(f1), self.get_normal(f2)))
def get_normal(self, face):
surface = OCC.Geom.Handle_Geom_Surface(OCC.BRep.BRep_Tool.Surface(face))
props = OCC.GeomLProp.GeomLProp_SLProps(surface, 0, 0, 1, .001)
return props.Normal()
def get_dot_product_of_normals(self, n1, n2):
return n1.X() * n2.X() + n1.Y() * n2.Y() + n1.Z() * n2.Z()
def convert_dot_product_to_angle(self, dp):
return math.acos(dp)
def is_same_point(self, p1, p2):
return p1.X() == p2.X() \
and p1.Y() == p2.Y() \
and p1.Z() == p2.Z()
def build_new_edge(self, edge, zpos):
exp = OCC.TopExp.TopExp_Explorer(edge, OCC.TopAbs.TopAbs_VERTEX)
new_vertices = []
while exp.More():
current_vertex = topods.Vertex(exp.Current())
current_point = OCC.BRep.BRep_Tool.Pnt(current_vertex)
current_point.SetZ(zpos)
new_vertices.append(OCC.BRepBuilderAPI.BRepBuilderAPI_MakeVertex(current_point).Vertex())
exp.Next()
try:
return OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(
new_vertices[0], new_vertices[1]
).Edge()
except:
return None
def build_new_face(self, face, zpos, product):
exp = OCC.TopExp.TopExp_Explorer(face, OCC.TopAbs.TopAbs_WIRE)
while exp.More():
wireexp = OCC.BRepTools.BRepTools_WireExplorer(topods.Wire(exp.Current()))
new_wire_builder = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeWire()
first_vertex = None
previous_vertex = None
while wireexp.More():
current_vertex = wireexp.CurrentVertex()
current_point = OCC.BRep.BRep_Tool.Pnt(current_vertex)
# Dodgy technique to squash in Z axis
current_point.SetZ(zpos)
current_vertex = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeVertex(current_point).Vertex()
if not first_vertex:
first_vertex = current_vertex
if not previous_vertex:
previous_vertex = current_vertex
else:
try:
new_wire_builder.Add(topods.Edge(
OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(
previous_vertex, current_vertex
).Edge()))
previous_vertex = current_vertex
except:
pass
wireexp.Next()
# make last edge
if not wireexp.More():
try:
new_wire_builder.Add(topods.Edge(
OCC.BRepBuilderAPI.BRepBuilderAPI_MakeEdge(
current_vertex, first_vertex
).Edge()))
except:
pass
try:
new_wire = new_wire_builder.Wire()
new_face = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeFace(new_wire).Face()
area = self.get_area(new_face)
if area == 0:
self.background_elements.append({
'raw': product,
'geometry': new_wire,
'geometry_face': new_face,
'type': 'polyline',
'z': zpos
})
else:
self.background_elements.append({
'raw': product,
'geometry': new_wire,
'geometry_face': new_face,
'type': 'polygon',
'z': zpos
})
except:
#print('Could not build face')
pass
exp.Next()
def get_area(self, shape):
gprops = OCC.GProp.GProp_GProps()
OCC.BRepGProp.brepgprop.SurfaceProperties(shape, gprops)
return gprops.Mass()
def get_booleaned_edges(self, shape):
edges = []
exp = OCC.TopExp.TopExp_Explorer(shape, OCC.TopAbs.TopAbs_EDGE)
while exp.More():
edges.append(topods.Edge(exp.Current()))
exp.Next()
return edges
def get_cut_polygons(self):
total_product_shapes = len(self.product_shapes)
n = 0
for product, shape in self.product_shapes:
print('{}/{} cut elements processed ...'.format(n, total_product_shapes), end='\r', flush=True)
n += 1
section = OCC.BRepAlgoAPI.BRepAlgoAPI_Section(self.section_box['face'], shape).Shape()
section_edges = self.get_booleaned_edges(section)
if len(section_edges) <= 0:
continue
wires = self.connect_edges_into_wires(section_edges)
for i in range(wires.Length()):
wire_shape = wires.Value(i+1)
transformed_wire = OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(
wire_shape, self.transformation)
wire_shape = transformed_wire.Shape()
wire = topods.Wire(wire_shape)
self.cut_polygons.append({
'raw': product,
'geometry': wire
})
def connect_edges_into_wires(self, unconnected_edges):
edges = OCC.TopTools.TopTools_HSequenceOfShape()
edges_handle = OCC.TopTools.Handle_TopTools_HSequenceOfShape(edges)
wires = OCC.TopTools.TopTools_HSequenceOfShape()
wires_handle = OCC.TopTools.Handle_TopTools_HSequenceOfShape(wires)
for edge in unconnected_edges:
edges.Append(edge)
OCC.ShapeAnalysis.ShapeAnalysis_FreeBounds.ConnectEdgesToWires(edges_handle, 1e-5, True, wires_handle)
return wires_handle.GetObject()
class IfcCutterDebug(IfcCutter):
def cut(self):
self.occ_display = ifcopenshell.geom.utils.initialize_display()
super().cut()
def create_section_box(self):
super().create_section_box()
self.display_everything_with_section_plane()
def get_cut_polygons(self):
super().get_cut_polygons()
self.display_cut_polygons()
def get_background_elements(self):
super().get_background_elements()
self.display_background_elements()
def display_everything_with_section_plane(self):
section_face_display = ifcopenshell.geom.utils.display_shape(self.section_box['face'])
ifcopenshell.geom.utils.set_shape_transparency(section_face_display, 0.8)
section_box_display = ifcopenshell.geom.utils.display_shape(self.section_box['shape'])
ifcopenshell.geom.utils.set_shape_transparency(section_box_display, 0.5)
transformed_box = OCC.BRepBuilderAPI.BRepBuilderAPI_Transform(
self.section_box['shape'], self.transformation)
box_display = ifcopenshell.geom.utils.display_shape(transformed_box.Shape())
ifcopenshell.geom.utils.set_shape_transparency(box_display, 0.2)
for shape in self.product_shapes:
ifcopenshell.geom.utils.display_shape(shape[1])
input('Debug: showing everything with section plane.')
def display_cut_polygons(self):
self.occ_display.EraseAll()
for polygon in self.cut_polygons:
ifcopenshell.geom.utils.display_shape(polygon['geometry'], clr='BLACK')
face = OCC.BRepBuilderAPI.BRepBuilderAPI_MakeFace(polygon['geometry']).Face()
face_display = ifcopenshell.geom.utils.display_shape(face)
ifcopenshell.geom.utils.set_shape_transparency(face_display, 0.5)
input('Debug: showing cut polygons.')
def display_background_elements(self):
self.occ_display.EraseAll()
for element in self.background_elements:
if element['type'] == 'line':
ifcopenshell.geom.utils.display_shape(element['geometry'], clr='PURPLE')
elif element['type'] == 'polyline':
ifcopenshell.geom.utils.display_shape(element['geometry_face'], clr='RED')
elif element['type'] == 'polygon':
ifcopenshell.geom.utils.display_shape(element['geometry_face'])
input('Debug: showing background elements.')
class SvgWriter():
def __init__(self, ifc_cutter):
self.ifc_cutter = ifc_cutter
self.scale = 1 / 100 # 1:100
def write(self):
self.calculate_scale()
self.svg = svgwrite.Drawing('output.svg',
debug=False,
size=('{}mm'.format(self.ifc_cutter.section_box['x'] * self.scale),
'{}mm'.format(self.ifc_cutter.section_box['y'] * self.scale)),
viewBox=('0 0 {} {}'.format(
self.ifc_cutter.section_box['x'] * self.scale,
self.ifc_cutter.section_box['y'] * self.scale)))
self.add_stylesheet()
self.draw_background_elements()
self.draw_cut_polygons()
self.svg.save(pretty=True)
def calculate_scale(self):
# TODO: properly handle units
if self.ifc_cutter.unit.Name == 'METRE':
self.scale *= 1000
def add_stylesheet(self):
with open('styles/default.css', 'r') as stylesheet:
self.svg.defs.add(self.svg.style(stylesheet.read()))
def draw_background_elements(self):
for element in self.ifc_cutter.background_elements:
if element['type'] == 'polygon':
self.draw_polygon(element, 'background')
elif element['type'] == 'polyline':
self.draw_polyline(element, 'background')
elif element['type'] == 'line':
self.draw_line(element, 'background')
def draw_cut_polygons(self):
for polygon in self.ifc_cutter.cut_polygons:
self.draw_polygon(polygon, 'cut')
def draw_polyline(self, element, position):
classes = self.get_classes(element['raw'], position)
exp = OCC.BRepTools.BRepTools_WireExplorer(element['geometry'])
points = []
while exp.More():
point = OCC.BRep.BRep_Tool.Pnt(exp.CurrentVertex())
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.polyline(points=points, class_=' '.join(classes)))
def draw_line(self, element, position):
classes = self.get_classes(element['raw'], position)
exp = OCC.TopExp.TopExp_Explorer(element['geometry'], OCC.TopAbs.TopAbs_VERTEX)
points = []
while exp.More():
point = OCC.BRep.BRep_Tool.Pnt(topods.Vertex(exp.Current()))
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.line(start=points[0], end=points[1], class_=' '.join(classes)))
def draw_polygon(self, polygon, position):
classes = self.get_classes(polygon['raw'], position)
exp = OCC.BRepTools.BRepTools_WireExplorer(polygon['geometry'])
points = []
while exp.More():
point = OCC.BRep.BRep_Tool.Pnt(exp.CurrentVertex())
points.append((point.X() * self.scale, -point.Y() * self.scale))
exp.Next()
self.svg.add(self.svg.polygon(points=points, class_=' '.join(classes)))
def get_classes(self, element, position):
classes = [position, element.is_a()]
for association in element.HasAssociations:
if association.is_a('IfcRelAssociatesMaterial'):
classes.append('material-{}'.format(association.RelatingMaterial.Name))
classes.append('globalid-{}'.format(element.GlobalId))
return classes
print('# Starting process')
ifc_cutter = IfcCutter()
#ifc_cutter = IfcCutterDebug()
svg_writer = SvgWriter(ifc_cutter)
ifc_cutter.cut()
start_time = time.time()
svg_writer.write()
print('# SVG writing finished in {:.2f} seconds'.format(time.time() - start_time))
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" height="594mm" width="841mm">
<defs />
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="../templates/titleblock.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="150mm" y="50mm">
<use xlink:href="../views/house.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="150mm" y="50mm">
<use xlink:href="../templates/view-title.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="550mm" y="50mm">
<use xlink:href="../views/elevation.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="150mm" y="50mm">
<use xlink:href="../templates/view-title.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="550mm" y="50mm">
<use xlink:href="../views/plan.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="150mm" y="50mm">
<use xlink:href="../templates/view-title.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="550mm" y="50mm">
<use xlink:href="../views/section.svg#root" />
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="150mm" y="50mm">
<use xlink:href="../templates/view-title.svg#root" />
</svg>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,2 @@
.cut {fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3px; }
.background { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.05px; }
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 108 KiB

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="root"
version="1.1"
viewBox="0 0 50.222462 10"
height="10mm"
width="50.222462mm">
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-27.82024,-96.649986)"
id="layer1">
<circle
r="4.8499999"
cy="101.64999"
cx="32.82024"
id="path853"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.30000001;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path855"
d="m 37.67024,101.64999 40.37246,-1e-5"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<text
id="text859"
y="102.19775"
x="32.820755"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:2.11666656px;text-align:center;text-anchor:middle;stroke-width:0.26458332px"
y="102.19775"
x="32.820755"
id="tspan857">{{no}}</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="39.368858"
y="99.525047"
id="text863"><tspan
id="tspan861"
x="39.368858"
y="99.525047"
style="font-size:2.11666656px;text-align:start;text-anchor:start;stroke-width:0.26458332px">{{name}}</tspan></text>
<text
id="text867"
y="104.66998"
x="39.368858"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.58333302px;line-height:125%;font-family:Arial;-inkscape-font-specification:Arial;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:2.11666656px;text-align:start;text-anchor:start;stroke-width:0.26458332px"
y="104.66998"
x="39.368858"
id="tspan865">{{scale}}</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB