summaryrefslogtreecommitdiff
path: root/lib/extensions/fill_to_satin.py
blob: a762ae148804f81ed0655a60941ec7f9bd8a031c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# Authors: see git history
#
# Copyright (c) 2025 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.

from collections import defaultdict

from inkex import Boolean, Group, Path, PathElement
from shapely.geometry import LineString, MultiLineString, MultiPoint, Point
from shapely.ops import linemerge, snap, split, substring

from ..elements import FillStitch, Stroke
from ..gui.abort_message import AbortMessageApp
from ..i18n import _
from ..svg import get_correction_transform
from ..utils import ensure_multi_line_string, roll_linear_ring
from .base import InkstitchExtension


class FillToSatin(InkstitchExtension):
    def __init__(self, *args, **kwargs):
        InkstitchExtension.__init__(self, *args, **kwargs)
        self.arg_parser.add_argument("--notebook")
        self.arg_parser.add_argument("--skip_end_section", dest="skip_end_section", type=Boolean, default=False)
        self.arg_parser.add_argument("--pull_compensation_mm", dest="pull_compensation_mm", type=float, default=0)
        self.arg_parser.add_argument("--center", dest="center", type=Boolean, default=False)
        self.arg_parser.add_argument("--contour", dest="contour", type=Boolean, default=False)
        self.arg_parser.add_argument("--zigzag", dest="zigzag", type=Boolean, default=False)
        self.arg_parser.add_argument("--keep_originals", dest="keep_originals", type=Boolean, default=False)

        self.satin_index = 0

    def effect(self):
        if not self.svg.selected or not self.get_elements():
            self.print_error()
            return

        fill_elements, selected_rungs = self._get_shapes()
        if not fill_elements or not selected_rungs:
            self.print_error()
            return

        settings = {
            'skip_end_section': self.options.skip_end_section
        }

        for fill_element in fill_elements:
            fill_shape = fill_element.shape

            fill_linestrings = self._fill_to_linestrings(fill_shape)
            for linestrings in fill_linestrings:
                fill_to_satin = FillElementToSatin(self.svg, settings, fill_element, fill_shape, linestrings, selected_rungs)
                satins = fill_to_satin.convert_to_satin()
                self._insert_satins(fill_element, satins)

        self._remove_originals()

    def _get_shapes(self):
        '''Filter selected elements. Take rungs and fills.'''
        fill_elements = []
        selected_rungs = []
        nodes = []
        fill_and_stroke_elements = []
        for element in self.elements:
            if element.node in nodes:
                fill_and_stroke_elements.append(element)
            if isinstance(element, FillStitch) and element.shape.area > 0.1:
                fill_elements.append(element)
            elif isinstance(element, Stroke):
                selected_rungs.extend(list(element.as_multi_line_string().geoms))
            else:
                continue
            nodes.append(element.node)

        if fill_and_stroke_elements:
            elements = [f'{element.node.label} ({element.node.get_id()})' for element in fill_and_stroke_elements]
            if len(elements) > 15:
                elements = elements[:14]
                elements.append('...')
            self.print_error(
                    (_("The selection contains elements with both, a fill and a stroke.\n\n"
                     "Rungs only have a stroke color and fill elements a fill color.") +
                     "\n\n- " + '\n- '.join(elements))
                )

        return fill_elements, selected_rungs

    def _fill_to_linestrings(self, fill_shape):
        '''Takes a fill shape (Multipolygon) and returns the shape as a list of linestrings'''
        fill_linestrings = []
        for polygon in fill_shape.geoms:
            linestrings = ensure_multi_line_string(polygon.boundary, 1)
            fill_linestrings.append(list(linestrings.geoms))
        return fill_linestrings

    def _insert_satins(self, fill_element, satins):
        '''Insert satin elements into the document'''
        if not satins:
            return
        group = fill_element.node.getparent()
        index = group.index(fill_element.node) + 1
        transform = get_correction_transform(fill_element.node)
        style = f'stroke: {fill_element.color}; fill: none; stroke-width: {1 / fill_element.stroke_scale};'
        if len(satins) > 1:
            new_group = Group()
            group.insert(index, new_group)
            group = new_group
            group.label = _("Satin Group")
            index = 0
        for i, satin in enumerate(satins):
            node = PathElement()
            d = ""
            for segment in satin:
                for geom in segment.geoms:
                    d += str(Path(list(geom.coords)))
            node.set('d', d)
            node.set('style', style)
            node.set('inkstitch:satin_column', True)
            if self.options.center:
                node.set('inkstitch:center_walk_underlay', True)
            if self.options.contour:
                node.set('inkstitch:contour_underlay', True)
            if self.options.zigzag:
                node.set('inkstitch:zigzag_underlay', True)
            if self.options.pull_compensation_mm != 0:
                node.set('inkstitch:pull_compensation_mm', str(self.options.pull_compensation_mm))
            node.transform = transform
            node.apply_transform()
            node.label = _("Satin") + f" {self.satin_index}"
            group.insert(index, node)
            self.satin_index += 1

    def _remove_originals(self):
        '''Remove original elements - if requested'''
        for element in self.elements:
            if not self.options.keep_originals or element.name == "Stroke":
                try:
                    element.node.delete()
                except AttributeError:
                    pass

    def print_error(self, message=_("Please select a fill object and rungs.")):
        '''We did not receive the rigth elements, inform user'''
        app = AbortMessageApp(
            message,
            _("https://inkstitch.org/docs/satin-tools/#fill-to-satin")
        )
        app.MainLoop()


class FillElementToSatin:
    def __init__(self, svg, settings, fill_element, fill_shape, linestrings, selected_rungs):
        self.svg = svg
        self.settings = settings
        self.fill_element = fill_element
        self.fill_shape = fill_shape
        self.linestrings = linestrings
        self.selected_rungs = selected_rungs

        self.rungs = []  # rung geometries
        self.half_rungs = []  # index half rungs in self.rungs
        self.line_sections = []  # sections of the outline. LineStrings between the rungs
        self.rung_segments = {}  # assembled satin segments
        self.rung_sections = defaultdict(list)  # rung_index: section indices
        self.section_rungs = defaultdict(list)  # section index: rung indices
        self.bridged_rungs = defaultdict(list)  # bridge index: rung indices

    def convert_to_satin(self):
        intersection_points, bridges = self._validate_rungs()

        self._generate_line_sections()
        self._define_relations(bridges)

        if len(self.line_sections) == 2 and self.line_sections[0].distance(self.line_sections[1]) > 0 and len(self.rungs):
            # there is only one segment, add it directly
            rails = [MultiLineString([self.line_sections[0], self.line_sections[1]])]
            rungs = [ensure_multi_line_string(self.rungs[0])]
            return ([rails + rungs])
        else:
            rung_segments, satin_segments = self._get_segments(intersection_points)

        if len(self.rung_sections) == 2 and self.rung_sections[0] == self.rung_sections[1]:
            combined_satins = self._get_two_rung_circle_geoms(rung_segments, satin_segments)
        else:
            combined_satins = self._get_satin_geoms(rung_segments, satin_segments)

        return combined_satins

    def _get_two_rung_circle_geoms(self, rung_segments, satin_segments):
        '''Imagine a donut with two rungs: this is a special case where all segments connect to the very same two rungs'''
        combined = defaultdict(list)
        combined_rungs = defaultdict(list)

        combined[0] = [0, 1]
        combined_rungs[0] = [0, 1]

        return self._combined_segments_to_satin_geoms(combined, combined_rungs, satin_segments)

    def _get_satin_geoms(self, rung_segments, satin_segments):
        '''Combine segments and return satin geometries'''
        self.rung_segments = {rung: segments for rung, segments in rung_segments.items() if len(segments) == 2}
        finished_rungs = []
        finished_segments = []
        combined_rails = defaultdict(list)
        combined_rungs = defaultdict(list)

        for rung, segments in self.rung_segments.items():
            self._find_connected(rung, segments, rung, finished_rungs, finished_segments, combined_rails, combined_rungs)

        unfinished = {i for i, segment in enumerate(satin_segments) if i not in finished_segments}
        segment_count = len(satin_segments)
        for i, segment in enumerate(unfinished):
            index = segment_count + i + 1
            combined_rails[index] = [segment]

        return self._combined_segments_to_satin_geoms(combined_rails, combined_rungs, satin_segments)

    def _combined_segments_to_satin_geoms(self, combined_rails, combined_rungs, satin_segments):
        combined_satins = []
        for i, segments in combined_rails.items():
            segment_geoms = []
            for segment_index in set(segments):
                segment_geoms.extend(list(satin_segments[segment_index].geoms))
            satin_rails = ensure_multi_line_string(linemerge(segment_geoms))
            if len(satin_rails.geoms) == 1:
                satin_rails = self._fix_single_rail_issue(satin_rails, segments, satin_segments, set(combined_rungs[i]))
                if satin_rails is None:
                    continue
            elif len(satin_rails.geoms) != 2:
                continue

            # adjust rail direction and starting points
            satin_rails = [self._adjust_rail_direction(satin_rails)]
            satin_rails = [self._adjust_closed_path_starting_point(satin_rails, self.rungs[combined_rungs[i][0]])]

            segment_geoms = []
            for rung_index in set(combined_rungs[i]):
                rung = self.rungs[rung_index]
                # satin behaves bad if a rung is positioned directly at the beginning/end section
                start = Point(satin_rails[0].geoms[0].coords[0])
                end = Point(satin_rails[0].geoms[1].coords[-1])
                if rung.distance(start) > 1 and rung.distance(end) > 1:
                    segment_geoms.append(ensure_multi_line_string(rung))

            combined_satins.append(satin_rails + segment_geoms)
        return combined_satins

    def _adjust_closed_path_starting_point(self, rails, rung):
        # closed paths may need adjustments of the starting point
        rail1 = rails[0].geoms[0]
        rail2 = rails[0].geoms[1]
        if rail1.coords[0] == rail1.coords[-1]:
            rail1 = self._adjust_rail_starting_point(rail1, rail1.intersection(rung))
            rail2 = self._adjust_rail_starting_point(rail2, rail2.intersection(rung))
        return MultiLineString([rail1, rail2])

    def _adjust_rail_starting_point(self, rail, point):
        if point.geom_type == "Point":
            position = rail.project(point)
            return LineString(roll_linear_ring(rail, position))
        return rail

    def _fix_single_rail_issue(self, satin_rails, segments, satin_segments, combined_rungs):
        # This is a special case where the two satin rails have been combined into one.
        # It can happen if we try to convert for example a B with a single satin column
        # (it starts and ends at the center).
        #      ---
        #     |   \
        #     |   /
        #     |==x
        #     |   \
        #     |   /
        #     ---
        # We can face two situations:
        # 1. the rung at the intersection is within the combined_rungs, in this case we need to watch out for a rung which is bridged twice
        # 2. the rung isn't within the selection, adjacing bridged segments have only one connecting rung

        # Case 1: the rung is within the selection and is bridged twice
        intersection = self._fix_single_rail_issue_rung_included(satin_rails, combined_rungs)
        if intersection.is_empty:
            # Case 2: check for segments with only one adjacent rung
            intersection = self._fix_single_rail_issue_rung_excluded(satin_segments, segments, combined_rungs)
        if intersection.geom_type == 'MultiPoint':
            position = satin_rails.project(intersection.geoms[0])
            satin_rails = LineString(roll_linear_ring(satin_rails.geoms[0], position))
            return ensure_multi_line_string(split(satin_rails, intersection))
        return None

    def _fix_single_rail_issue_rung_included(self, satin_rails, combined_rungs):
        for rung in combined_rungs:
            rung_bridges = []
            for bridge, rungs in self.bridged_rungs.items():
                if rung in rungs:
                    rung_bridges.append(1)
            if len(rung_bridges) > 1:
                rung_geom = snap(self.rungs[rung], satin_rails, 0.001)
                return satin_rails.intersection(rung_geom)
        return Point()

    def _fix_single_rail_issue_rung_excluded(self, satin_segments, segments, combined_rungs):
        single_rung_segments = []
        for segment_index in set(segments):
            geom = satin_segments[segment_index]
            segment_rungs = []
            for rung_index in combined_rungs:
                if geom.distance(self.rungs[rung_index]) < 0.001:
                    segment_rungs.append(rung_index)
            if len(segment_rungs) == 1:
                single_rung_segments.append(geom)
        if len(single_rung_segments) == 2:
            points = []
            for seg in single_rung_segments:
                segment_end_points = []
                for g in seg.geoms:
                    segment_end_points.extend([g.coords[0], g.coords[-1]])
                points.append(segment_end_points)
            return MultiPoint(points[0]).intersection(MultiPoint(points[1]))
        return Point()

    def _get_segments(self, intersection_points):  # noqa: C901
        '''Combine line sections to satin segments (find the rails that belong together)'''
        line_section_multi = MultiLineString(self.line_sections)
        rung_segments = defaultdict(list)
        satin_segments = []
        used_bridges = []

        segment_index = 0
        finished_sections = []
        for i, section in enumerate(self.line_sections):
            if i in finished_sections:
                continue
            s_rungs = self.section_rungs[i]

            if len(s_rungs) == 1:
                # end section
                if self.settings['skip_end_section'] and len(self.rungs) > 1:
                    continue
                segment = self._get_end_segment(section)
                satin_segments.append(segment)
                finished_sections.append(i)
                for rung in s_rungs:
                    rung_segments[rung].append(segment_index)
                segment_index += 1

            elif len(s_rungs) == 2:
                connected_section = self._get_connected_section(i, s_rungs)
                if connected_section:
                    connect_index, segment = self._get_standard_segment(connected_section, s_rungs, section, finished_sections)
                    if segment is None:
                        continue
                    satin_segments.append(segment)
                    for rung in s_rungs:
                        rung_segments[rung].append(segment_index)
                    segment_index += 1
                    finished_sections.extend([i, connect_index])
                else:
                    for bridge, rung_list in self.bridged_rungs.items():
                        if len(rung_list) != 2:
                            continue
                        for rung in s_rungs:
                            if bridge in used_bridges:
                                continue
                            if rung in rung_list:
                                rung1 = rung_list[0]
                                rung2 = rung_list[1]
                                segment = self._get_bridged_segment(rung1, rung2, intersection_points, line_section_multi)
                                if not segment:
                                    continue
                                satin_segments.append(segment)
                                rung_segments[rung_list[0]].append(segment_index)
                                rung_segments[rung_list[1]].append(segment_index)
                                segment_index += 1
                                finished_sections.append(i)
                                used_bridges.append(bridge)
            else:
                # sections with multiple rungs, open ends, not bridged
                # IF users define their rungs well, they won't have a problem if we just ignore these sections
                # otherwise they will see some sort of gap, they can close it manually if they want
                pass

        # create segments for unused bridge segments
        unused_bridges = set(self.bridged_rungs.keys()) - set(used_bridges)
        if unused_bridges:
            for bridge in unused_bridges:
                rungs = self.bridged_rungs[bridge]
                if len(rungs) != 2:
                    continue
                segment = self._get_bridged_segment(rungs[0], rungs[1], intersection_points, line_section_multi)
                if not segment:
                    continue
                satin_segments.append(segment)
                rung_segments[rungs[0]].append(segment_index)
                rung_segments[rungs[1]].append(segment_index)
                segment_index += 1
        return rung_segments, satin_segments

    def _get_bridged_segment(self, rung1, rung2, intersection_points, line_section_multi):
        rung_sections1 = self.rung_sections[rung1]
        rung_sections2 = self.rung_sections[rung2]
        points1 = self._get_rung_points(rung1, intersection_points)
        points2 = self._get_rung_points(rung2, intersection_points)

        connected_section = list(set(rung_sections1) & set(rung_sections2))
        if len(connected_section) > 1:
            # this is an unnecessarily bridged section, we can savely skip it
            return
        if len(connected_section) == 1:
            # do not bridge a segment side if there is an actual section we could use
            segment1 = self.line_sections[connected_section[0]]

            points1 = sorted(points1, key=lambda point: segment1.distance(point), reverse=True)
            points2 = sorted(points2, key=lambda point: segment1.distance(point), reverse=True)
            segment2 = LineString([points1[0], points2[0]])

            segment1 = snap(segment1, line_section_multi, 0.0001)
            segment2 = snap(segment2, line_section_multi, 0.0001)

            segment = MultiLineString([segment1, segment2])
            return segment

        segment1 = LineString([points1[0], points2[0]])
        segment2 = LineString([points1[1], points2[1]])
        if segment1.intersects(segment2):
            segment1 = LineString([points1[0], points2[1]])
            segment2 = LineString([points1[1], points2[0]])
        segment1 = snap(segment1, line_section_multi, 0.0001)
        segment2 = snap(segment2, line_section_multi, 0.0001)
        segment = MultiLineString([segment1, segment2])
        return segment

    def _get_rung_points(self, rung, intersection_points):
        rung_geom = self.rungs[rung]
        intersections = intersection_points[rung]
        if not intersections:
            return [rung_geom.interpolate(0.3), rung_geom.interpolate(rung_geom.length - 0.3)]
        if intersections.geom_type == 'MultiPoint':
            return intersections.geoms
        if rung_geom.project(intersections, normalized=True) > 0.5:
            point1 = rung_geom.interpolate(0.3)
        else:  # Point
            point1 = rung_geom.interpolate(rung_geom.length - 0.3)
        return [intersections, point1]

    def _get_end_segment(self, section):
        section = section.simplify(0.5)
        rail1 = substring(section, 0, 0.40009, True).coords
        rail2 = substring(section, 0.50001, 1, True).coords
        if len(rail1) > 2:
            rail1 = rail1[:-1]
        if len(rail2) > 2:
            rail2 = rail2[1:]

        segment = MultiLineString([LineString(rail1), LineString(rail2)])
        return segment

    def _get_standard_segment(self, connected_section, s_rungs, section, finished_sections):
        section2 = None
        segment = None
        connect_index = None
        if len(connected_section) == 1:
            section2 = self.line_sections[connected_section[0]]
            connect_index = connected_section[0]
        else:
            for connect in connected_section:
                if connect in finished_sections:
                    continue
                offset_rung = self.rungs[s_rungs[0]].offset_curve(0.01)
                section_candidate = self.line_sections[connect]
                if offset_rung.intersects(section) == offset_rung.intersects(section_candidate):
                    section2 = section_candidate
                    connect_index = connect
                    break
        if section2 is not None:
            segment = MultiLineString([section, section2])
        return connect_index, segment

    def _get_connected_section(self, index, s_rungs):
        rung_section_list = []
        for rung in s_rungs:
            connections = self.rung_sections[rung]
            rung_section_list.append(connections)
        connected_section = list(set(rung_section_list[0]) & set(rung_section_list[1]))
        connected_section.remove(index)
        return connected_section

    def _adjust_rail_direction(self, satin_rails):
        # See also elements/satin_column.py (_get_rails_to_reverse)
        rails = list(satin_rails.geoms)
        lengths = []
        lengths_reverse = []

        for i in range(10):
            distance = i / 10
            point0 = rails[0].interpolate(distance, normalized=True)
            point1 = rails[1].interpolate(distance, normalized=True)
            point1_reverse = rails[1].interpolate(1 - distance, normalized=True)

            lengths.append(point0.distance(point1))
            lengths_reverse.append(point0.distance(point1_reverse))

        if sum(lengths) > sum(lengths_reverse):
            rails[0] = rails[0].reverse()

        return MultiLineString(rails)

    def _find_connected(self, rung, segments, first_rung, finished_rungs, finished_segments, combined_rails, combined_rungs):
        '''Group combinable segments'''
        if rung in finished_rungs:
            return
        finished_rungs.append(rung)
        combined_rails[first_rung].extend(segments)
        combined_rungs[first_rung].append(rung)
        finished_segments.extend(segments)
        for segment in segments:
            connected = self._get_combinable_segments(segment, segments)
            if not connected:
                continue
            for connected_rung, connected_segments in connected.items():
                self._find_connected(
                    connected_rung,
                    connected_segments,
                    first_rung, finished_rungs,
                    finished_segments,
                    combined_rails,
                    combined_rungs
                )

    def _get_combinable_segments(self, segment, segments_in):
        '''Finds the segments which are neighboring this segment'''
        return {rung: segments for rung, segments in self.rung_segments.items() if segment in segments and segments_in != segments}

    def _generate_line_sections(self):
        '''Splits the fill outline into sections. Splitter is a MultiLineString with all available rungs'''
        rungs = MultiLineString(self.rungs)

        for line in self.linestrings:
            sections = list(ensure_multi_line_string(split(line, rungs)).geoms)
            if len(sections) > 1:
                # merge end and start section
                sections[0] = linemerge(MultiLineString([sections[0], sections[-1]]))
                del sections[-1]
            self.line_sections.extend(sections)

    def _define_relations(self, bridges):
        ''' Defines information about the relations between line_sections and rungs
            rung_sections: dictionary with rung_index: neighboring sections
            section_rungs: dictionary with section_id: neighboring rungs
            bridged_rungs: lines which define which segments are to be bridged at intersection points
        '''
        for i, section in enumerate(self.line_sections):
            for j, rung in enumerate(self.rungs):
                if section.distance(rung) < 0.01:
                    self.section_rungs[i].append(j)
                    self.rung_sections[j].append(i)

        bridged_rungs = defaultdict(list)
        for i, bridge in enumerate(bridges):
            for j, rung in enumerate(self.rungs):
                if bridge.intersects(rung):
                    bridged_rungs[i].append(j)

        # for the case that they - for whatever reason -
        # drew a bridge over the same rungs twice, clean up duplicated bridges
        seen_bridged_rungs = []
        for bridge, rungs in bridged_rungs.items():
            if rungs not in seen_bridged_rungs:
                self.bridged_rungs[bridge] = rungs
            seen_bridged_rungs.append(rungs)

    def _validate_rungs(self):
        ''' Returns only valid rungs and bridge section markers'''
        multi_line_string = MultiLineString(self.linestrings)
        bridges = []
        intersection_points = []
        rungs = []
        half_rungs = []
        for rung in self.selected_rungs:
            intersection = multi_line_string.intersection(rung)
            if intersection.geom_type == 'MultiPoint' and len(intersection.geoms) == 2:
                rungs.append(rung)
                # intersection_points.append(intersection)
            elif intersection.is_empty and rung.within(self.fill_shape):
                # these rungs (possibly) connect two rungs
                bridges.append(rung)
            elif intersection.geom_type == 'Point':
                # half rungs can mark a bridge endpoint at an open end within the shape
                half_rungs.append(rung)
        # filter rungs when they are crossing other rungs. They could possibly produce bad line sections
        for i, rung in enumerate(rungs):
            multi_rung = MultiLineString([r for j, r in enumerate(rungs) if j != i])
            intersection = rung.intersection(multi_rung)
            if not rung.intersects(multi_rung) or not rung.intersection(multi_rung).intersects(self.fill_shape):
                self.rungs.append(rung)
                intersection_points.append(multi_line_string.intersection(rung))
        # filter half rungs if they are not bridged
        bridges_linestring = MultiLineString(bridges)
        for rung in half_rungs:
            if rung.intersects(bridges_linestring):
                self.half_rungs.append(len(self.rungs))
                self.rungs.append(rung)
                intersection_points.append(multi_line_string.intersection(rung))
        # filter bridges
        bridges = self._validate_bridges(bridges, intersection_points)
        return intersection_points, bridges

    def _validate_bridges(self, bridges, intersection_points):
        validated_bridges = []
        multi_rung = MultiLineString(self.rungs)
        # find elements marked as bridges, but don't intersect with any other rung.
        # they may be rungs drawn inside of a shape, so let's add them to the rungs and see if they are helpful
        for i, bridge in enumerate(bridges):
            rung_intersections = bridge.intersection(multi_rung)
            bridge_intersections = bridge.intersection(MultiLineString([b for j, b in enumerate(bridges) if j != i]))
            if rung_intersections.is_empty and not bridge_intersections.geom_type == "MultiPoint":
                # doesn't intersect with any rungs, so it is a rung itself (when bridged)
                self.half_rungs.append(len(self.rungs))
                self.rungs.append(bridge)
                intersection_points.append(Point())

        # now validate bridges and split them up if necessary
        multi_rung = MultiLineString(self.rungs)
        for bridge in bridges:
            rung_intersections = bridge.intersection(multi_rung)
            if rung_intersections.geom_type == "MultiPoint":
                if len(rung_intersections.geoms) == 2:
                    validated_bridges.append(bridge)
                elif len(rung_intersections.geoms) > 2:
                    # bridges multiple rungs
                    points = list(rung_intersections.geoms)
                    points = sorted(points, key=lambda point: bridge.project(point))
                    for point1, point2 in zip(points[:-1], points[1:]):
                        distance1 = bridge.project(point1) - 0.1
                        distance2 = bridge.project(point2) + 0.1
                        validated_bridges.append(substring(bridge, distance1, distance2))
            elif rung_intersections.geom_type == "Point":
                # bridges a rung within the shape
                validated_bridges.append(bridge)
        return validated_bridges