|
| 1 | +// Copyright 2025 The S2 Geometry Project Authors. All rights reserved. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package s2 |
| 16 | + |
| 17 | +// incidentEdgeKey is a tuple of (shape id, vertex) that compares by shape id. |
| 18 | +type incidentEdgeKey struct { |
| 19 | + shapeID int32 |
| 20 | + vertex Point |
| 21 | +} |
| 22 | + |
| 23 | +// We need a strict ordering to be a valid key for an ordered container, but |
| 24 | +// we don't actually care about the ordering of the vertices (as long as |
| 25 | +// they're grouped by shape id). Vertices are 3D points so they don't have a |
| 26 | +// natural ordering, so we'll just compare them lexicographically. |
| 27 | +func (i incidentEdgeKey) Cmp(o incidentEdgeKey) int { |
| 28 | + if i.shapeID < o.shapeID { |
| 29 | + return -1 |
| 30 | + } |
| 31 | + if i.shapeID > o.shapeID { |
| 32 | + return 1 |
| 33 | + } |
| 34 | + |
| 35 | + return i.vertex.Cmp(o.vertex.Vector) |
| 36 | +} |
| 37 | + |
| 38 | +// vertexEdge is a tuple of vertex and edgeID for processing incident edges. |
| 39 | +type vertexEdge struct { |
| 40 | + vertex Point |
| 41 | + edgeID int32 |
| 42 | +} |
| 43 | + |
| 44 | +// incidentEdgeTracker is a used for detecting and tracking shape edges that |
| 45 | +// are incident on the same vertex. Edges of multiple shapes may be tracked, |
| 46 | +// but lookup is by shape id and vertex: there is no facility to get all |
| 47 | +// edges of all shapes at a vertex. Edge vertices must compare exactly equal |
| 48 | +// to be considered the same vertex, no tolerance is applied as this isn't |
| 49 | +// intended for e.g.: snapping shapes together, which Builder does better |
| 50 | +// and more robustly. |
| 51 | +// |
| 52 | +// To use, instantiate and then add edges with one or more sequences of calls, |
| 53 | +// where each sequence begins with startShape(), followed by addEdge() calls to |
| 54 | +// add edges for that shape, and ends with finishShape(). Those sequences do |
| 55 | +// not need to visit shapes or edges in order. Then, call incidentEdges() to get |
| 56 | +// the resulting map from incidentEdgeKeys (which are shapeId, vertex pairs) to |
| 57 | +// a set of edgeIds of the shape that are incident to that vertex.. |
| 58 | +// |
| 59 | +// This works on a block of edges at a time, meaning that to detect incident |
| 60 | +// edges on a particular vertex, we must have at least three edges incident |
| 61 | +// at that vertex when finishShape() is called. We don't maintain partial |
| 62 | +// information between calls. However, subject to this constraint, a single |
| 63 | +// shape's edges may be defined with multiple sequences of startShape(), |
| 64 | +// addEdge()... , finishShape() calls. |
| 65 | +// |
| 66 | +// The reason for this is simple: most edges don't have more than two incident |
| 67 | +// edges (the incoming and outgoing edge). If we had to maintain information |
| 68 | +// between calls, we'd end up with a map that contains every vertex, to no |
| 69 | +// benefit. Instead, when finishShape() is called, we discard vertices that |
| 70 | +// contain two or fewer incident edges. |
| 71 | +// |
| 72 | +// In principle this isn't a real limitation because generally we process a |
| 73 | +// ShapeIndex cell at a time, and, if a vertex has multiple edges, we'll see |
| 74 | +// all the edges in the same cell as the vertex, and, in general, it's possible |
| 75 | +// to aggregate edges before calling. |
| 76 | +// |
| 77 | +// The tracker maintains incident edges until it's cleared. If you call it with |
| 78 | +// each cell from an ShapeIndex, then at the end you will have all the |
| 79 | +// incident edge information for the whole index. If only a subset is needed, |
| 80 | +// call reset() when you're done. |
| 81 | +type incidentEdgeTracker struct { |
| 82 | + currentShapeID int32 |
| 83 | + |
| 84 | + nursery []vertexEdge |
| 85 | + |
| 86 | + // We can and do encounter the same edges multiple times, so we need to |
| 87 | + // deduplicate edges as they're inserted. |
| 88 | + edgeMap map[incidentEdgeKey]map[int32]bool |
| 89 | +} |
| 90 | + |
| 91 | +// newIncidentEdgeTracker returns a new tracker. |
| 92 | +func newIncidentEdgeTracker() *incidentEdgeTracker { |
| 93 | + return &incidentEdgeTracker{ |
| 94 | + currentShapeID: -1, |
| 95 | + nursery: []vertexEdge{}, |
| 96 | + edgeMap: make(map[incidentEdgeKey]map[int32]bool), |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +// startShape is used to start adding edges to the edge tracker. After calling, |
| 101 | +// any vertices with multiple (> 2) incident edges will appear in the |
| 102 | +// incident edge map. |
| 103 | +func (t *incidentEdgeTracker) startShape(id int32) { |
| 104 | + t.currentShapeID = id |
| 105 | + t.nursery = t.nursery[:0] |
| 106 | +} |
| 107 | + |
| 108 | +// addEdge adds the given edges start to the nursery, and if not degenerate, |
| 109 | +// adds it second endpoint as well. |
| 110 | +func (t *incidentEdgeTracker) addEdge(edgeID int32, e Edge) { |
| 111 | + if t.currentShapeID < 0 { |
| 112 | + return |
| 113 | + } |
| 114 | + |
| 115 | + // Add non-degenerate edges to the nursery. |
| 116 | + t.nursery = append(t.nursery, vertexEdge{vertex: e.V0, edgeID: edgeID}) |
| 117 | + if !e.IsDegenerate() { |
| 118 | + t.nursery = append(t.nursery, vertexEdge{vertex: e.V1, edgeID: edgeID}) |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +func (t *incidentEdgeTracker) finishShape() { |
| 123 | + // We want to keep any vertices with more than two incident edges. We could |
| 124 | + // sort the array by vertex and remove any with fewer, but that would require |
| 125 | + // shifting the array and could turn quadratic quickly. |
| 126 | + // |
| 127 | + // Instead we'll scan forward from each vertex, swapping entries with the same |
| 128 | + // vertex into a contiguous range. Once we've done all the swapping we can |
| 129 | + // just make sure that we have at least three edges in the range. |
| 130 | + nurserySize := len(t.nursery) |
| 131 | + for start := 0; start < nurserySize; { |
| 132 | + end := start + 1 |
| 133 | + |
| 134 | + // Scan to the end of the array, swap entries so that entries with |
| 135 | + // the same vertex as the start are adjacent. |
| 136 | + next := start |
| 137 | + currVertex := t.nursery[start].vertex |
| 138 | + for next+1 < nurserySize { |
| 139 | + next++ |
| 140 | + if t.nursery[next].vertex == currVertex { |
| 141 | + t.nursery[next], t.nursery[end] = t.nursery[end], t.nursery[next] |
| 142 | + end++ |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // Most vertices will have two incident edges (the incoming edge and the |
| 147 | + // outgoing edge), which aren't interesting, skip them. |
| 148 | + numEdges := end - start |
| 149 | + if numEdges <= 2 { |
| 150 | + start = end |
| 151 | + continue |
| 152 | + } |
| 153 | + |
| 154 | + key := incidentEdgeKey{ |
| 155 | + shapeID: t.currentShapeID, |
| 156 | + vertex: t.nursery[start].vertex, |
| 157 | + } |
| 158 | + |
| 159 | + // If we don't have this key yet, create it manually. |
| 160 | + if _, ok := t.edgeMap[key]; !ok { |
| 161 | + t.edgeMap[key] = map[int32]bool{} |
| 162 | + } |
| 163 | + |
| 164 | + for ; start != end; start++ { |
| 165 | + t.edgeMap[key][t.nursery[start].edgeID] = true |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +// reset removes all incident edges from the tracker. |
| 171 | +func (t *incidentEdgeTracker) reset() { |
| 172 | + t.edgeMap = make(map[incidentEdgeKey]map[int32]bool) |
| 173 | +} |
0 commit comments