Files
fps_project_1/addons/proton_scatter/src/common/domain.gd

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)