@@ -485,7 +485,7 @@ def generate_linework(self, context):
485485 self .serialiser .finalize ()
486486 results = self .svg_buffer .get_value ()
487487
488- if self .camera .data .BIMCameraProperties .calculate_surfaces :
488+ if self .camera .data .BIMCameraProperties .calculate_shapely_surfaces :
489489 # shapely variant
490490 root = etree .fromstring (results )
491491 group = root .findall ('.//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}name]' )[0 ]
@@ -532,6 +532,160 @@ def generate_linework(self, context):
532532 path .set ("class" , " " .join (list (classes )))
533533
534534 results = etree .tostring (root )
535+
536+ if self .camera .data .BIMCameraProperties .calculate_svgfill_surfaces :
537+ svg_data_1 = results
538+ from xml .dom .minidom import parseString
539+
540+ def yield_groups (n ):
541+ if n .nodeType == n .ELEMENT_NODE and n .tagName == "g" :
542+ yield n
543+ for c in n .childNodes :
544+ yield from yield_groups (c )
545+
546+ dom1 = parseString (svg_data_1 )
547+ svg1 = dom1 .childNodes [0 ]
548+ groups1 = [g for g in yield_groups (svg1 ) if g .getAttribute ("class" ) == "projection" ]
549+
550+ ls_groups = ifcopenshell .ifcopenshell_wrapper .svg_to_line_segments (results , "projection" )
551+
552+ for i , (ls , g1 ) in enumerate (zip (ls_groups , groups1 )):
553+ projection , g1 = g1 , g1 .parentNode
554+
555+ svgfill_context = ifcopenshell .ifcopenshell_wrapper .context (
556+ ifcopenshell .ifcopenshell_wrapper .EXACT_CONSTRUCTIONS , 1.0e-3
557+ )
558+
559+ # EXACT_CONSTRUCTIONS is significantly faster than FILTERED_CARTESIAN_QUOTIENT
560+ # remove duplicates (without tolerance)
561+ ls = [l for l in map (tuple , set (map (frozenset , ls ))) if len (l ) == 2 and l [0 ] != l [1 ]]
562+ svgfill_context .add (ls )
563+
564+ num_passes = 0
565+
566+ for iteration in range (num_passes + 1 ):
567+ # initialize empty group, note that in the current approach only one
568+ # group is stored
569+ ps = ifcopenshell .ifcopenshell_wrapper .svg_groups_of_polygons ()
570+ if iteration != 0 or svgfill_context .build ():
571+ svgfill_context .write (ps )
572+
573+ if iteration != num_passes :
574+ pairs = svgfill_context .get_face_pairs ()
575+ semantics = [None ] * (max (pairs ) + 1 )
576+
577+ # Reserialize cells into an SVG string
578+ svg_data_2 = ifcopenshell .ifcopenshell_wrapper .polygons_to_svg (ps , True )
579+
580+ # We parse both SVG files to create on document with the combination of sections from
581+ # the output directly from the serializer and the cells found from the hidden line
582+ # rendering
583+ dom2 = parseString (svg_data_2 )
584+ svg2 = dom2 .childNodes [0 ]
585+ # file 2 only has the groups we are interested in.
586+ # in fact in the approach, it's only a single group
587+
588+ g2 = list (yield_groups (svg2 ))[0 ]
589+
590+ # These are attributes on the original group that we can use to reconstruct
591+ # a 4x4 matrix of the projection used in the SVG generation process
592+ nm = g1 .getAttribute ("ifc:name" )
593+ m4 = np .array (json .loads (g1 .getAttribute ("ifc:plane" )))
594+ m3 = np .array (json .loads (g1 .getAttribute ("ifc:matrix3" )))
595+ m44 = np .eye (4 )
596+ m44 [0 ][0 :2 ] = m3 [0 ][0 :2 ]
597+ m44 [1 ][0 :2 ] = m3 [1 ][0 :2 ]
598+ m44 [0 ][3 ] = m3 [0 ][2 ]
599+ m44 [1 ][3 ] = m3 [1 ][2 ]
600+ m44 = np .linalg .inv (m44 )
601+
602+ # Loop over the cell paths
603+ for pi , p in enumerate (g2 .getElementsByTagName ("path" )):
604+
605+ d = p .getAttribute ("d" )
606+ coords = [co [1 :].split ("," ) for co in d .split () if co [1 :]]
607+ polygon = shapely .Polygon (coords )
608+ # 1mm2 polygons aren't worth styling in paperspace. Raycasting is expensive!
609+ if polygon .area < 1 :
610+ continue
611+ # point inside is an attribute that comes from line_segments_to_polygons() polygons_to_svg?
612+ # it is an arbitrary point guaranteed to be inside the polygon and outside
613+ # of any potential inner bounds. We can use this to construct a ray to find
614+ # the face of the IFC element that the cell belongs to.
615+ assert p .hasAttribute ("ifc:pointInside" )
616+
617+ xy = list (map (float , p .getAttribute ("ifc:pointInside" ).split ("," )))
618+
619+ a , b = self .drawing_to_model_co (m44 , m4 , xy , 0.0 ), self .drawing_to_model_co (m44 , m4 , xy , - 100.0 )
620+
621+ inside_elements = [e for e in tree .select (self .pythonize (a )) if not e .is_a ("IfcAnnotation" )]
622+ if inside_elements :
623+ elements = None
624+ if iteration != num_passes :
625+ semantics [pi ] = (inside_elements [0 ], - 1 )
626+ else :
627+ elements = [e for e in tree .select_ray (self .pythonize (a ), self .pythonize (b - a )) if not e .instance .is_a ("IfcAnnotation" )]
628+
629+ if elements :
630+ classes = self .get_svg_classes (ifc .by_id (elements [0 ].instance .id ()))
631+ classes .append ("projection" )
632+
633+ if iteration != num_passes :
634+ semantics [pi ] = elements [0 ]
635+ else :
636+ classes = ["projection" ]
637+
638+ p .setAttribute ("style" , "" )
639+ p .setAttribute ("class" , " " .join (classes ))
640+
641+ if iteration != num_passes :
642+ to_remove = []
643+
644+ for he_idx in range (0 , len (pairs ), 2 ):
645+ # @todo instead of ray_distance, better do (x.point - y.point).dot(x.normal)
646+ # to see if they're coplanar, because ray-distance will be different in case
647+ # of element surfaces non-orthogonal to the view direction
648+
649+ def format (x ):
650+ if x is None :
651+ return None
652+ elif isinstance (x , tuple ):
653+ # found to be inside element using tree.select() no face or style info
654+ return x
655+ else :
656+ return (x .instance .is_a (), x .ray_distance , tuple (x .position ))
657+
658+ pp = pairs [he_idx : he_idx + 2 ]
659+ if pp == (- 1 , - 1 ):
660+ continue
661+ data = list (map (format , map (semantics .__getitem__ , pp )))
662+ if None not in data and data [0 ][0 ] == data [1 ][0 ] and abs (data [0 ][1 ] - data [1 ][1 ]) < 1.0e-5 :
663+ to_remove .append (he_idx // 2 )
664+ # Print edge index and semantic data
665+ # print(he_idx // 2, *data)
666+
667+ svgfill_context .merge (to_remove )
668+
669+ # Swap the XML nodes from the files
670+ # Remove the original hidden line node we still have in the serializer output
671+ g1 .removeChild (projection )
672+ g2 .setAttribute ("class" , "projection" )
673+ # Find the children of the projection node parent
674+ children = [x for x in g1 .childNodes if x .nodeType == x .ELEMENT_NODE ]
675+ if children :
676+ # Insert the new semantically enriched cell-based projection node
677+ # *before* the node with sections from the serializer. SVG derives
678+ # draw order from node order in the DOM so sections are draw over
679+ # the projections.
680+ g1 .insertBefore (g2 , children [0 ])
681+ else :
682+ # This generally shouldn't happen
683+ g1 .appendChild (g2 )
684+
685+ data = dom1 .toxml ()
686+ data = data .encode ("ascii" , "xmlcharrefreplace" )
687+
688+ results = data
535689 with profile ("Post processing" ):
536690 root = etree .fromstring (results )
537691 self .move_projection_to_bottom (root )
@@ -664,6 +818,9 @@ def pythonize(self, arr):
664818 def move_projection_to_bottom (self , root ):
665819 # https://stackoverflow.com/questions/36018627/sorting-child-elements-with-lxml-based-on-attribute-value
666820 group = root .find ("{http://www.w3.org/2000/svg}g" )
821+ if self .camera .data .BIMCameraProperties .calculate_svgfill_surfaces :
822+ # SVGFill already places the projection in the right spot.
823+ return
667824 # group[:] = sorted(group, key=lambda e : "projection" in e.get("class"))
668825 if group is not None :
669826 group [:] = reversed (group )
0 commit comments