313 lines
8.4 KiB
GDScript
313 lines
8.4 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
|
|
# A domain is the complete area where transforms can (and can't) be placed.
|
|
# A Scatter node has one single domain, a domain has one or more shape nodes.
|
|
#
|
|
# It's the combination of every shape defined under a Scatter node, grouped in
|
|
# a single class that exposes utility functions (check if a point is inside, or
|
|
# along the surface etc).
|
|
#
|
|
# An instance of this class is passed to the modifiers during a rebuild.
|
|
|
|
|
|
const ProtonScatter := preload("../scatter.gd")
|
|
const ProtonScatterShape := preload("../scatter_shape.gd")
|
|
const BaseShape := preload("../shapes/base_shape.gd")
|
|
const Bounds := preload("../common/bounds.gd")
|
|
|
|
|
|
class DomainShapeInfo:
|
|
var node: Node3D
|
|
var shape: BaseShape
|
|
|
|
func is_point_inside(point: Vector3, local: bool) -> bool:
|
|
var t: Transform3D
|
|
if is_instance_valid(node):
|
|
t = node.get_transform() if local else node.get_global_transform()
|
|
return shape.is_point_inside(point, t)
|
|
else:
|
|
return false
|
|
|
|
func get_corners_global() -> Array:
|
|
return shape.get_corners_global(node.get_global_transform())
|
|
|
|
# A polygon made of one outer boundary and one or multiple holes (inner polygons)
|
|
class ComplexPolygon:
|
|
var inner: Array[PackedVector2Array] = []
|
|
var outer: PackedVector2Array
|
|
|
|
func add(polygon: PackedVector2Array) -> void:
|
|
if polygon.is_empty(): return
|
|
if Geometry2D.is_polygon_clockwise(polygon):
|
|
inner.push_back(polygon)
|
|
else:
|
|
if not outer.is_empty():
|
|
print_debug("ProtonScatter error: Replacing polygon's existing outer boundary. This should not happen, please report.")
|
|
outer = polygon
|
|
|
|
func add_array(array: Array, reverse := false) -> void:
|
|
for p in array:
|
|
if reverse:
|
|
p.reverse()
|
|
add(p)
|
|
|
|
func get_all() -> Array[PackedVector2Array]:
|
|
var res = inner.duplicate()
|
|
res.push_back(outer)
|
|
return res
|
|
|
|
func _to_string() -> String:
|
|
var res = "o: " + var_to_str(outer.size()) + ", i: ["
|
|
for i in inner:
|
|
res += var_to_str(i.size()) + ", "
|
|
res += "]"
|
|
return res
|
|
|
|
|
|
var root: ProtonScatter
|
|
var positive_shapes: Array[DomainShapeInfo]
|
|
var negative_shapes: Array[DomainShapeInfo]
|
|
var bounds_global: Bounds = Bounds.new()
|
|
var bounds_local: Bounds = Bounds.new()
|
|
var edges: Array[Curve3D] = []
|
|
|
|
|
|
func is_empty() -> bool:
|
|
return positive_shapes.is_empty()
|
|
|
|
|
|
# If a point is in an exclusion shape, returns false
|
|
# If a point is in an inclusion shape (but not in an exclusion one), returns true
|
|
# If a point is in neither, returns false
|
|
func is_point_inside(point: Vector3, local := true) -> bool:
|
|
for s in negative_shapes:
|
|
if s.is_point_inside(point, local):
|
|
return false
|
|
|
|
for s in positive_shapes:
|
|
if s.is_point_inside(point, local):
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
# If a point is inside an exclusion shape, returns true
|
|
# Returns false in every other case
|
|
func is_point_excluded(point: Vector3, local := true) -> bool:
|
|
for s in negative_shapes:
|
|
if s.is_point_inside(point, local):
|
|
return true
|
|
|
|
return false
|
|
|
|
|
|
# Recursively find all ScatterShape nodes under the provided root. In case of
|
|
# nested Scatter nodes, shapes under these other Scatter nodes will be ignored
|
|
func discover_shapes(root_node: Node3D) -> void:
|
|
root = root_node
|
|
positive_shapes.clear()
|
|
negative_shapes.clear()
|
|
|
|
if not is_instance_valid(root):
|
|
return
|
|
|
|
for c in root.get_children():
|
|
_discover_shapes_recursive(c)
|
|
compute_bounds()
|
|
compute_edges()
|
|
|
|
|
|
func compute_bounds() -> void:
|
|
bounds_global.clear()
|
|
bounds_local.clear()
|
|
|
|
if not is_instance_valid(root):
|
|
return
|
|
|
|
var gt: Transform3D = root.get_global_transform().affine_inverse()
|
|
|
|
for info in positive_shapes:
|
|
for point in info.get_corners_global():
|
|
bounds_global.feed(point)
|
|
bounds_local.feed(gt * point)
|
|
|
|
bounds_global.compute_bounds()
|
|
bounds_local.compute_bounds()
|
|
|
|
|
|
func compute_edges() -> void:
|
|
edges.clear()
|
|
|
|
if not is_instance_valid(root):
|
|
return
|
|
|
|
var source_polygons: Array[ComplexPolygon] = []
|
|
|
|
## Retrieve all polygons
|
|
for info in positive_shapes:
|
|
# Store all closed polygons in a specific array
|
|
var polygon := ComplexPolygon.new()
|
|
polygon.add_array(info.shape.get_closed_edges(info.node.transform))
|
|
|
|
# Polygons with holes must be merged together first
|
|
if not polygon.inner.is_empty():
|
|
source_polygons.push_back(polygon)
|
|
else:
|
|
source_polygons.push_front(polygon)
|
|
|
|
# Store open edges directly since they are already Curve3D and we
|
|
# don't apply boolean operations to them.
|
|
var open_edges = info.shape.get_open_edges(info.node.transform)
|
|
edges.append_array(open_edges)
|
|
|
|
if source_polygons.is_empty():
|
|
return
|
|
|
|
## Merge all closed polygons together
|
|
var merged_polygons: Array[ComplexPolygon] = []
|
|
|
|
while not source_polygons.is_empty():
|
|
var merged := false
|
|
var p1: ComplexPolygon = source_polygons.pop_back()
|
|
var max_steps: int = source_polygons.size()
|
|
var i = 0
|
|
|
|
# Test p1 against every other polygon from source_polygon until a
|
|
# successful merge. If no merge happened, put it in the final array.
|
|
while i < max_steps and not merged:
|
|
i += 1
|
|
|
|
# Get the next polygon in the list
|
|
var p2: ComplexPolygon = source_polygons.pop_back()
|
|
|
|
# If the outer boundary of any of the two polygons is completely
|
|
# enclosed in one of the other polygon's hole, we don't try to
|
|
# merge them and go the next iteration.
|
|
var full_overlap = false
|
|
for ip1 in p1.inner:
|
|
var res = Geometry2D.clip_polygons(p2.outer, ip1)
|
|
if res.is_empty():
|
|
full_overlap = true
|
|
break
|
|
|
|
for ip2 in p2.inner:
|
|
var res = Geometry2D.clip_polygons(p1.outer, ip2)
|
|
if res.is_empty():
|
|
full_overlap = true
|
|
break
|
|
|
|
if full_overlap:
|
|
source_polygons.push_front(p2)
|
|
continue
|
|
|
|
# Try to merge the two polygons p1 and p2
|
|
var res = Geometry2D.merge_polygons(p1.outer, p2.outer)
|
|
var outer_polygons := 0
|
|
for p in res:
|
|
if not Geometry2D.is_polygon_clockwise(p):
|
|
outer_polygons += 1
|
|
|
|
# If the merge generated a new polygon, process the holes data from
|
|
# the two original polygons and store in the new_polygon
|
|
# P1 and P2 are then discarded and replaced by the new polygon.
|
|
if outer_polygons == 1:
|
|
var new_polygon = ComplexPolygon.new()
|
|
new_polygon.add_array(res)
|
|
|
|
# Process the holes data from p1 and p2
|
|
for ip1 in p1.inner:
|
|
for ip2 in p2.inner:
|
|
new_polygon.add_array(Geometry2D.intersect_polygons(ip1, ip2), true)
|
|
new_polygon.add_array(Geometry2D.clip_polygons(ip2, p1.outer), true)
|
|
|
|
new_polygon.add_array(Geometry2D.clip_polygons(ip1, p2.outer), true)
|
|
|
|
source_polygons.push_back(new_polygon)
|
|
merged = true
|
|
|
|
# If the polygons don't overlap, return it to the pool to be tested
|
|
# against other polygons
|
|
else:
|
|
source_polygons.push_front(p2)
|
|
|
|
# If p1 is not overlapping any other polygon, add it to the final list
|
|
if not merged:
|
|
merged_polygons.push_back(p1)
|
|
|
|
## For each polygons from the previous step, create a corresponding Curve3D
|
|
for cp in merged_polygons:
|
|
for polygon in cp.get_all():
|
|
if polygon.size() < 2: # Ignore polygons too small to form a loop
|
|
continue
|
|
|
|
var curve := Curve3D.new()
|
|
for point in polygon:
|
|
curve.add_point(Vector3(point.x, 0.0, point.y))
|
|
|
|
# Close the look if the last vertex is missing (Randomly happens)
|
|
var first_point := polygon[0]
|
|
var last_point := polygon[-1]
|
|
if first_point != last_point:
|
|
curve.add_point(Vector3(first_point.x, 0.0, first_point.y))
|
|
|
|
edges.push_back(curve)
|
|
|
|
|
|
func get_root() -> ProtonScatter:
|
|
return root
|
|
|
|
|
|
func get_global_transform() -> Transform3D:
|
|
return root.get_global_transform()
|
|
|
|
|
|
func get_local_transform() -> Transform3D:
|
|
return root.get_transform()
|
|
|
|
|
|
func get_edges() -> Array[Curve3D]:
|
|
if edges.is_empty():
|
|
compute_edges()
|
|
return edges
|
|
|
|
|
|
func get_copy():
|
|
var copy = get_script().new()
|
|
|
|
copy.root = root
|
|
copy.bounds_global = bounds_global
|
|
copy.bounds_local = bounds_local
|
|
|
|
for s in positive_shapes:
|
|
var s_copy = DomainShapeInfo.new()
|
|
s_copy.node = s.node
|
|
s_copy.shape = s.shape.get_copy()
|
|
copy.positive_shapes.push_back(s_copy)
|
|
|
|
for s in negative_shapes:
|
|
var s_copy = DomainShapeInfo.new()
|
|
s_copy.node = s.node
|
|
s_copy.shape = s.shape.get_copy()
|
|
copy.negative_shapes.push_back(s_copy)
|
|
|
|
return copy
|
|
|
|
|
|
func _discover_shapes_recursive(node: Node) -> void:
|
|
if node is ProtonScatter: # Ignore shapes under nested Scatter nodes
|
|
return
|
|
|
|
if node is ProtonScatterShape and node.shape != null:
|
|
var info := DomainShapeInfo.new()
|
|
info.node = node
|
|
info.shape = node.shape
|
|
|
|
if node.negative:
|
|
negative_shapes.push_back(info)
|
|
else:
|
|
positive_shapes.push_back(info)
|
|
|
|
for c in node.get_children():
|
|
_discover_shapes_recursive(c)
|