Last active
February 9, 2020 10:37
-
-
Save nadult/d9a01d4841ad9bc389fd7fb68c54e8db to your computer and use it in GitHub Desktop.
CSG based on exact integer arithmetic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include "gen_base.h" | |
#include "geom/immutable_graph.h" | |
#include "geom_base.h" | |
namespace geom { | |
// TODO: flagi (neutral jest niezależne od intersection & subtraction, addition niepotrzebne) | |
DEFINE_ENUM(CSGEdgeType, addition, intersection, subtraction, neutral); | |
template <class T> struct CSG { | |
using Vec = T; | |
using Segment = fwk::Segment<T>; | |
using PRVec = MakeRat<Promote<T, 1>, 2>; | |
using LinePt = Rational<Promote<Scalar<T>>>; | |
using EType = CSGEdgeType; | |
static constexpr int invalid_rvalue = INT_MIN; | |
struct EdgeInfo { | |
int lregion = -1, rregion = -1; | |
int count = 0, label = 0; | |
EType type = EType::addition; | |
Segment orig_segment; | |
LinePt from_t, to_t; | |
}; | |
CSG(CSpan<Vec> points, CSpan<Pair<NodeId>> edges, CSpan<int> labels, CSpan<EType> types = {}); | |
void makeIntersectionGraph(CSpan<Vec>, CSpan<Pair<NodeId>>, CSpan<int>, CSpan<EType>); | |
void computeRegions(); | |
void performSubtraction(); | |
void findOuterRegions(); | |
Expected<void> computeRegionValues(); | |
void computeFinalGraph(); | |
void investigate() const; | |
int probeRegion(NodeId start_node, Vec) const; | |
bool finalEdge(EdgeId) const; | |
// Intersction graph | |
ImmutableGraph graph; | |
vector<PRVec> gpoints; | |
vector<EdgeInfo> einfos; | |
// Region & sub-graph information | |
vector<EdgeId> subgroup_reps; | |
vector<vector<EdgeId>> regions; | |
vector<vector<EdgeId>> subgraphs; | |
vector<vector<int>> twin_regions; | |
vector<int> edge_subgraphs; | |
vector<bool> removed_regions; | |
vector<int> outer_regions; | |
vector<int> rvalues; | |
// Final result | |
vector<PRVec> out_points; | |
vector<Pair<NodeId>> out_edges; | |
vector<bool> out_neutral_edges; | |
vector<int> out_labels; | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "geom/csg.h" | |
#include "geom/planar_loops.h" | |
#include "geom/plane_graph_builder.h" | |
#include <fwk/math/box_iter.h> | |
#include <fwk/math/line.h> | |
#include <fwk/sys/expected.h> | |
#include "investigate.h" | |
#include "perf_base.h" | |
#include "vis/visualizer2.h" | |
#include "geom/regular_grid.h" | |
namespace geom { | |
template <class T> | |
CSG<T>::CSG(CSpan<Vec> input_points, CSpan<Pair<NodeId>> input_edges, CSpan<int> input_labels, | |
CSpan<EType> input_types) { | |
PERF_SCOPE("CSG"); | |
if(input_types) | |
DASSERT_EQ(input_types.size(), input_edges.size()); | |
DASSERT_EQ(input_labels.size(), input_edges.size()); | |
if(!input_edges) | |
return; | |
makeIntersectionGraph(input_points, input_edges, input_labels, input_types); | |
computeRegions(); | |
findOuterRegions(); | |
performSubtraction(); | |
auto result = computeRegionValues(); | |
if(!result) { | |
result.error().print(); | |
investigate(); | |
FATAL(""); | |
} | |
computeFinalGraph(); | |
//investigate(); | |
} | |
template <class T> | |
void CSG<T>::makeIntersectionGraph(CSpan<Vec> input_points, CSpan<Pair<NodeId>> input_edges, | |
CSpan<int> input_labels, CSpan<EType> input_types) { | |
PERF_SCOPE(); | |
PERF_CHILD_SCOPE("initialization"); | |
DASSERT(distinct(input_points)); | |
vector<LinePt> mid_points; | |
vector<Segment> input_segs; | |
input_segs.reserve(einfos.size()); | |
Box<Vec> input_box{input_points[0], input_points[0]}; | |
for(auto [n1, n2] : input_edges) { | |
Segment seg{input_points[n1], input_points[n2]}; | |
input_box = enclose(input_box, enclose(seg)); | |
input_segs.emplace_back(seg); | |
} | |
int num_buckets = clamp((int)sqrt(input_edges.size()) * 4, 1, 64); | |
PERF_SIBLING_SCOPE("filling buckets"); | |
// TODO: sweep line ? | |
// TODO: this may cause inaccuracy problems; But how can we instantiate RegularGrid for Ext24? | |
// TODO: simply enlarge search box for bit T's ? | |
double2 enlargement = double2(input_box.size()) * 0.001; | |
RegularGrid<double2> grid(DRect(input_box).enlarge(enlargement), | |
double2(input_box.size()) / num_buckets); | |
vector<vector<EdgeId>> buckets(grid.width() * grid.height()); | |
vector<IRect> edge_cells(input_edges.size()); | |
auto cell_idx = [=](int2 cell) { return cell.x + cell.y * grid.width(); }; | |
for(auto eid : indexRange<EdgeId>(input_edges)) { | |
auto [n1, n2] = input_edges[eid]; | |
Segment seg{input_points[n1], input_points[n2]}; | |
auto crect = grid.toCellRect(DRect(enclose(seg)).enlarge(enlargement)); | |
edge_cells[eid] = crect; | |
for(auto cell : cells(crect)) | |
buckets[cell_idx(cell)].emplace_back(eid); | |
} | |
vector<int> tested(input_edges.size(), -1); | |
PERF_SIBLING_SCOPE("computing intersections"); | |
PlaneGraphBuilder<PRVec> builder; | |
// Przecięcia liczymy tylko pomiędzy źródłowymi segmentami | |
// Dla każdego segmentu wyznaczamy listę punktów | |
// TODO: wrzucić wszstkie midpointy do wektora (pary mid-point, indeks segmentu) | |
// posortować; powinno być szybsze niż std::mapa | |
for(int e1 : intRange(input_edges)) { | |
auto &edge1 = input_edges[e1]; | |
auto &seg1 = input_segs[e1]; | |
mid_points = {{0}}; | |
tested[e1] = e1; | |
PERF_SCOPE("finding mid-points"); | |
for(auto cell : cells(edge_cells[e1])) { | |
for(int e2 : buckets[cell_idx(cell)]) { | |
if(tested[e2] == e1) | |
continue; | |
tested[e2] = e1; | |
auto edge2 = input_edges[e2]; | |
auto &seg2 = input_segs[e2]; | |
if(IsectParam ip = seg1.isectParam(seg2)) { | |
if(ip.closest() != 0) { | |
if(ip.closest() != 1) | |
mid_points.emplace_back(ip.closest()); | |
} | |
if(ip.isInterval()) | |
mid_points.emplace_back(ip.farthest()); | |
} | |
} | |
} | |
PERF_CLOSE_SCOPE(); | |
makeSortedUnique(mid_points); | |
if(mid_points.back() != 1) | |
mid_points.emplace_back(1); | |
EType type = input_types ? input_types[e1] : EType::addition; | |
bool isect_type = isOneOf(type, EType::intersection, EType::subtraction); | |
for(int i = 0; i < mid_points.size() - 1; i++) { | |
int j = i + 1; | |
PRVec p1(seg1.at(mid_points[i])); // TODO: downcast | |
PRVec p2(seg1.at(mid_points[j])); | |
// TODO: co jeśli różne typy sie pokrywają? | |
// TODO: co jeśli typ neutralny wyląduje pod intersekcją ? jest to nawet możliwe | |
if(auto rid = builder.find(p2, p1)) { | |
if(isect_type) { | |
DASSERT(einfos[*rid].type != EType::neutral); | |
einfos[*rid].type = | |
type == EType::subtraction ? EType::intersection : EType::subtraction; | |
} else | |
einfos[*rid].count--; | |
} else { | |
auto eid = builder(p1, p2); | |
if(einfos.size() <= eid) | |
einfos.resize(eid + 1); | |
if(!isect_type) | |
einfos[eid].count++; | |
einfos[eid].label = input_labels[e1]; // TODO: multiplicity... | |
einfos[eid].type = type; | |
einfos[eid].orig_segment = seg1; | |
einfos[eid].from_t = mid_points[i]; | |
einfos[eid].to_t = mid_points[j]; | |
} | |
} | |
} | |
vector<Pair<NodeId>> gedges = builder.extractEdges(); | |
PERF_SIBLING_SCOPE("removing empty edges"); | |
{ // Removing unnecessary edges | |
for(int n : intRange(einfos)) | |
if(einfos[n].count == 0 && einfos[n].type == EType::addition) | |
gedges[n] = pair(NodeId(0), NodeId(0)); | |
auto end1 = std::remove_if(begin(einfos), end(einfos), [](auto &info) { | |
return info.count == 0 && info.type == EType::addition; | |
}); | |
auto end2 = std::remove_if(begin(gedges), end(gedges), | |
[](auto &pair) { return pair.first == pair.second; }); | |
einfos.erase(end1, end(einfos)); | |
gedges.erase(end2, end(gedges)); | |
} | |
PERF_SIBLING_SCOPE("constructing graph"); | |
graph = ImmutableGraph(gedges); | |
gpoints = builder.extractPoints(); | |
ASSERT_EQ(sortedUnique(gpoints).size(), gpoints.size()); | |
orderEdges<PRVec>(graph, gpoints); | |
graph.computeExtendedInfo(); | |
} | |
template <class T> void CSG<T>::computeRegions() { | |
PERF_SCOPE(); | |
PERF_CHILD_SCOPE("computing edge-region mapping"); // ----------------------------------------- | |
regions = planarLoops<PRVec>(graph, gpoints); | |
edge_subgraphs.clear(); | |
edge_subgraphs.resize(einfos.size(), -1); | |
twin_regions.resize(regions.size()); | |
removed_regions.resize(regions.size(), false); | |
vector<bool> flips; | |
for(int r : intRange(regions)) { | |
auto &loop = regions[r]; | |
flips.clear(); | |
PRVec prev_point; | |
{ | |
auto ecur = graph.ref(loop.front()), eprev = graph.ref(loop.back()); | |
auto first_node = | |
isOneOf(ecur.from(), eprev.from(), eprev.to()) ? ecur.from() : ecur.to(); | |
auto prev_node = eprev.other(first_node); | |
prev_point = gpoints[first_node]; | |
} | |
for(auto i : intRange(loop)) { | |
auto eid = loop[i]; | |
auto p1 = gpoints[graph.from(eid)], p2 = gpoints[graph.to(eid)]; | |
bool flip = p1 != prev_point; | |
flips.emplace_back(flip); | |
prev_point = flip ? p1 : p2; | |
} | |
for(int i : intRange(loop)) { | |
auto eid = loop[i]; | |
auto &rid = flips[i] ? einfos[eid].rregion : einfos[eid].lregion; | |
DASSERT(rid == -1); | |
rid = r; | |
} | |
} | |
PERF_SIBLING_SCOPE("computing sub-graphs"); // ------------------------------------------------ | |
vector<int> stack; | |
for(int e : intRange(einfos)) { | |
if(edge_subgraphs[e] != -1) | |
continue; | |
int sgid = subgraphs.size(); | |
auto &sg = subgraphs.emplace_back(); | |
stack.clear(); | |
stack.emplace_back(e); | |
while(!stack.empty()) { | |
auto eid = stack.back(); | |
stack.pop_back(); | |
if(edge_subgraphs[eid] != -1) | |
continue; | |
sg.emplace_back(eid); | |
edge_subgraphs[eid] = sgid; | |
for(int nid : regions[einfos[eid].lregion]) | |
if(edge_subgraphs[nid] == -1) | |
stack.emplace_back(nid); | |
for(int nid : regions[einfos[eid].rregion]) | |
if(edge_subgraphs[nid] == -1) | |
stack.emplace_back(nid); | |
} | |
} | |
PERF_SIBLING_SCOPE("Finding twin-regions"); // ------------------------------------------------ | |
for(int sg : intRange(subgraphs)) { | |
auto &sub_graph = subgraphs[sg]; | |
Maybe<EdgeId> best_edge; | |
using PPRT = MakeRat<Promote<T, 2>, 0>; | |
PPRT best_pos = inf; | |
for(auto eid : sub_graph) { | |
auto &info = einfos[eid]; | |
auto seg = info.orig_segment; | |
auto n1 = graph.from(eid), n2 = graph.to(eid); | |
PPRT pos = min(gpoints[n1].x(), gpoints[n2].x()); | |
// TODO: handle cases where there are no such segments | |
if((pos < best_pos || !best_edge) && seg.dir().x < 0) { | |
best_edge = eid; | |
best_pos = pos; | |
} | |
} | |
if(!best_edge) | |
continue; // TODO: handle that | |
subgroup_reps.emplace_back(*best_edge); | |
//print("[%] Best seg: [%] %\n", sg, *best_edge, best_seg); | |
auto &info = einfos[*best_edge]; | |
auto best_seg = info.orig_segment; | |
auto start_pt = info.to_t; | |
Line line{best_seg.from, best_seg.dir()}; | |
int best_region = probeRegion(graph.to(*best_edge), line.dir); | |
Pair<LinePt, int> min_pt = {inf, {}}; | |
for(auto e : intRange(einfos)) { | |
auto &seg = einfos[e].orig_segment; | |
Line rline{seg.from, seg.dir()}; | |
auto iparam1 = line.isectParam(rline); | |
auto iparam2 = rline.isectParam(line); | |
if(iparam1.isPoint() && iparam1.closest() > start_pt && | |
iparam2.closest() >= einfos[e].from_t && iparam2.closest() <= einfos[e].to_t) | |
min_pt = min(min_pt, {iparam1.closest(), e}); | |
} | |
if(min_pt.first != inf) { | |
auto eref = graph.ref(EdgeId(min_pt.second)); | |
auto hit_pos = best_seg.at(min_pt.first); | |
Maybe<NodeId> node; | |
if(hit_pos == gpoints[eref.from()]) | |
node = eref.from(); | |
else if(hit_pos == gpoints[eref.to()]) | |
node = eref.to(); | |
int hit_region; | |
if(node) { | |
hit_region = probeRegion(*node, -line.dir); | |
//print("Hit node: % (region: %)\n", hit_pos, hit_region); | |
} else { | |
auto hit_seg = einfos[eref].orig_segment; | |
bool hit_left_side = ccwSide(hit_seg.dir(), -best_seg.dir()); | |
hit_region = hit_left_side ? einfos[eref].lregion : einfos[eref].rregion; | |
} | |
//print("Joining sub_graphs: % - % (regions: % - %)\n", sg, edge_subgraphs[min_pt.second], | |
// hit_region, best_region); | |
if(hit_region != best_region) { | |
twin_regions[best_region].emplace_back(hit_region); | |
twin_regions[hit_region].emplace_back(best_region); | |
} | |
} | |
} | |
for(auto &twins : twin_regions) | |
makeSortedUnique(twins); | |
} | |
template <class T> void CSG<T>::findOuterRegions() { | |
outer_regions.clear(); | |
vector<NodeId> ordered_nodes; | |
ordered_nodes.reserve(graph.numNodes()); | |
for(auto nid : graph.nodeIds()) | |
if(graph.numEdges(nid) >= 1) | |
ordered_nodes.emplace_back(nid); | |
std::sort(begin(ordered_nodes), end(ordered_nodes), | |
[&](int n1, int n2) { return gpoints[n1][0] < gpoints[n2][0]; }); | |
vector<bool> visited(regions.size(), false); | |
vector<int> stack; | |
for(auto node : ordered_nodes) { | |
int r = probeRegion(node, Vec(-1, 0)); | |
if(visited[r]) | |
continue; | |
stack = {r}; | |
outer_regions.emplace_back(r); | |
for(auto twin : twin_regions[r]) | |
outer_regions.emplace_back(twin); | |
while(stack) { | |
int r = stack.back(); | |
stack.pop_back(); | |
if(visited[r]) | |
continue; | |
visited[r] = true; | |
for(auto twin : twin_regions[r]) | |
if(!visited[twin]) | |
stack.emplace_back(twin); | |
for(auto e : regions[r]) { | |
auto &info = einfos[e]; | |
int other = info.lregion == r ? info.rregion : info.lregion; | |
if(!visited[other]) | |
stack.emplace_back(other); | |
} | |
} | |
} | |
makeSorted(outer_regions); | |
for(auto r1 : outer_regions) | |
for(auto r2 : outer_regions) | |
if(r1 != r2) | |
twin_regions[r1].emplace_back(r2); | |
for(auto &twins : twin_regions) | |
makeSortedUnique(twins); | |
} | |
template <class T> void CSG<T>::performSubtraction() { | |
vector<int> stack; | |
for(auto estart : graph.edgeRefs()) { | |
auto &info = einfos[estart]; | |
if(isOneOf(info.type, EType::intersection, EType::subtraction)) | |
stack.emplace_back(info.type == EType::intersection ? info.rregion : info.lregion); | |
} | |
vector<bool> visited(regions.size(), false); | |
while(stack) { | |
int r = stack.back(); | |
stack.pop_back(); | |
if(visited[r]) | |
continue; | |
visited[r] = true; | |
removed_regions[r] = true; | |
//print("Remove: %\n", r); | |
for(auto twin : twin_regions[r]) | |
if(!visited[twin]) | |
stack.emplace_back(twin); | |
for(auto edge : regions[r]) { | |
auto &info = einfos[edge]; | |
if(!isOneOf(info.type, EType::intersection, EType::subtraction)) { | |
int other = info.lregion == r ? info.rregion : info.lregion; | |
if(!visited[other]) | |
stack.emplace_back(other); | |
} | |
} | |
} | |
} | |
template <class T> Expected<void> CSG<T>::computeRegionValues() { | |
PERF_SCOPE(); | |
rvalues.clear(); | |
rvalues.resize(regions.size(), invalid_rvalue); | |
Error out_error; | |
bool rvalue_fail = false; | |
vector<Pair<int>> stack; | |
for(auto rstart : outer_regions) { | |
if(rvalues[rstart] != invalid_rvalue) | |
continue; | |
stack.emplace_back(rstart, 0); | |
for(auto twin : twin_regions[rstart]) | |
stack.emplace_back(twin, 0); | |
while(stack) { | |
auto [rid, value] = stack.back(); | |
stack.pop_back(); | |
if(!isOneOf(rvalues[rid], invalid_rvalue, value) && !rvalue_fail) { | |
out_error += format("Inconsistent rvalues: region:%: value:% -> %\n", rid, | |
rvalues[rid], value); | |
rvalue_fail = true; | |
} | |
if(rvalues[rid] != invalid_rvalue || rvalues[rid] == value) | |
continue; | |
rvalues[rid] = value; | |
//print("[%] = %\n", rid, value); | |
for(auto eid : regions[rid]) { | |
auto &edge = einfos[eid]; | |
auto [nrid, nvalue] = edge.lregion == rid ? pair(edge.rregion, value + edge.count) | |
: pair(edge.lregion, value - edge.count); | |
if(!isOneOf(rvalues[nrid], invalid_rvalue, nvalue) && !rvalue_fail) { | |
out_error += format("Inconsistent rvalues: region:% value:% -> % from:%\n", | |
nrid, rvalues[nrid], nvalue, rid); | |
rvalue_fail = true; | |
} | |
if(rvalues[nrid] == invalid_rvalue) { | |
stack.emplace_back(nrid, nvalue); | |
for(auto twin : twin_regions[nrid]) | |
stack.emplace_back(twin, nvalue); | |
} | |
} | |
} | |
} | |
for(auto r : intRange(regions)) | |
if(removed_regions[r]) | |
rvalues[r] = 0; | |
if(rvalue_fail) | |
return out_error; | |
return {}; | |
} | |
template <class T> bool CSG<T>::finalEdge(EdgeId eid) const { | |
auto &info = einfos[eid]; | |
auto lvalue = rvalues[info.lregion], rvalue = rvalues[info.rregion]; | |
return (lvalue != 0) != (rvalue != 0) || (info.type == EType::neutral && lvalue != 0); | |
} | |
template <class T> void CSG<T>::investigate() const { | |
using namespace vis; | |
auto vis_func = [&](Visualizer2 &vis, double2 mouse_pos) -> string { | |
auto pts = transform<double2>(gpoints); | |
vis(pts); | |
for(auto eref : graph.edgeRefs()) { | |
bool is_representative = isOneOf(eref.id(), subgroup_reps); | |
auto col = is_representative ? ColorId::yellow | |
: finalEdge(eref) ? ColorId::white : ColorId::gray; | |
vis(Segment2D{pts[eref.from()], pts[eref.to()]}, {col, VisOpt::arrow}); | |
} | |
TextFormatter fmt; | |
if(1) { // Showing subgraphs | |
pair<double, int> closest = {inf, 0}; | |
for(int n : intRange(subgraphs)) { | |
double2 center; | |
for(auto eid : subgraphs[n]) | |
center += pts[graph.from(eid)]; | |
center /= subgraphs[n].size(); | |
closest = min(closest, pair(distance(center, mouse_pos), n)); | |
} | |
for(auto eid : subgraphs[closest.second]) { | |
auto col = finalEdge(eid) ? ColorId::red : IColor(200, 100, 100); | |
vis(Segment2D{pts[graph.from(eid)], pts[graph.to(eid)]}, {col, VisOpt::arrow}); | |
} | |
fmt("Sub-graph: %\n", closest.second); | |
} | |
if(1) { // Showing edges | |
pair<double, int> closest = {inf, 0}; | |
for(auto eref : graph.edgeRefs()) { | |
fwk::Segment seg{pts[eref.from()], pts[eref.to()]}; | |
closest = min(closest, {seg.distanceSq(mouse_pos), eref.idx()}); | |
} | |
EdgeId eid(closest.second); | |
vis(Segment2D{pts[graph.from(eid)], pts[graph.to(eid)]}, | |
{ColorId::cyan, VisOpt::arrow}); | |
auto lr = einfos[eid].lregion, rr = einfos[eid].rregion; | |
fmt("%: Left:%[%%] Right:%[%%] count:% type:%\nFrom:% To:% (Orig: % % - %)", eid, lr, | |
rvalues[lr], removed_regions[lr] ? " removed" : "", rr, rvalues[rr], | |
removed_regions[rr] ? " removed" : "", einfos[eid].count, einfos[eid].type, | |
FormatMode::structured, gpoints[graph.from(eid)], gpoints[graph.to(eid)], | |
einfos[eid].orig_segment, einfos[eid].from_t, einfos[eid].to_t); | |
} | |
return fmt.text(); | |
}; | |
::investigate(vis_func, none, none); | |
} | |
template <class T> void CSG<T>::computeFinalGraph() { | |
PERF_SCOPE(); | |
PlaneGraphBuilder<PRVec> builder; | |
out_labels.resize(einfos.size() * 2, -1); | |
out_neutral_edges.resize(einfos.size() * 2, false); | |
// TODO: describe why neutral edges are handled differently... | |
for(auto eref : graph.edgeRefs()) { | |
if(finalEdge(eref)) { | |
auto &info = einfos[eref]; | |
auto lvalue = rvalues[info.lregion], rvalue = rvalues[info.rregion]; | |
auto p1 = gpoints[eref.from()], p2 = gpoints[eref.to()]; | |
if(info.type == EType::neutral) { | |
auto eid1 = builder(p1, p2); | |
auto eid2 = builder(p2, p1); | |
out_labels[eid1] = info.label; | |
out_labels[eid2] = info.label; | |
out_neutral_edges[eid1] = out_neutral_edges[eid2] = true; | |
} else { | |
auto eid = lvalue != 0 ? builder(p2, p1) : builder(p1, p2); | |
out_labels[eid] = info.label; | |
} | |
} | |
} | |
out_points = builder.extractPoints(); | |
out_edges = builder.extractEdges(); | |
out_labels.resize(out_edges.size()); | |
out_neutral_edges.resize(out_edges.size()); | |
ASSERT_EQ(sortedUnique(transform<float2>(out_points)).size(), out_points.size()); | |
ASSERT_EQ(sortedUnique(out_edges).size(), out_edges.size()); | |
} | |
template <class T> int CSG<T>::probeRegion(NodeId node_id, Vec dir) const { | |
auto node = graph.ref(node_id); | |
CSpan<EdgeId> edges = graph.edges(node); | |
int ccw_edge_idx = ccwNext<Vec>(dir, edges.size(), [&](int idx) { | |
auto eref = graph.ref(edges[idx]); | |
auto dir = einfos[eref].orig_segment.dir(); | |
return eref.from() == node ? dir : -dir; | |
}); | |
auto eref = graph.ref(edges[ccw_edge_idx]); | |
return eref.from() == node ? einfos[eref].rregion : einfos[eref].lregion; | |
} | |
template struct CSG<vec2<int>>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment