254 lines
7.4 KiB
GDScript
254 lines
7.4 KiB
GDScript
# MIT License
|
|
#
|
|
# Copyright (c) 2023 Mark McKay
|
|
# https://github.com/blackears/cyclopsLevelBuilder
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
@tool
|
|
extends RefCounted
|
|
class_name FacePacker
|
|
|
|
class SpawnResult extends RefCounted:
|
|
var point:Vector2
|
|
var flip:bool
|
|
|
|
func _init(point:Vector2, flip:bool):
|
|
self.point = point
|
|
self.flip = flip
|
|
|
|
class FaceTree extends RefCounted:
|
|
# var root:FaceTreeNode
|
|
var size:Vector2
|
|
var spawn_points:PackedVector2Array = [Vector2.ZERO]
|
|
var face_list:Array[FaceTracker]
|
|
var bounds:Rect2
|
|
|
|
func _to_string()->String:
|
|
var res:String = ""
|
|
for face in face_list:
|
|
res += "%s,\n" % str(face)
|
|
return res
|
|
|
|
func is_collision(rect:Rect2)->bool:
|
|
for face in face_list:
|
|
if face.bounds.intersects(rect):
|
|
return true
|
|
return false
|
|
|
|
func max_vec_dim(v:Vector2):
|
|
return max(v.x, v.y)
|
|
|
|
func get_best_spawn_point(face:FaceTracker)->SpawnResult:
|
|
var started:bool = false
|
|
var best_spawn_point:Vector2 = Vector2.INF
|
|
var best_bounds:Rect2
|
|
var best_flip:bool
|
|
|
|
for s_idx in spawn_points.size():
|
|
var spawn_point:Vector2 = spawn_points[s_idx]
|
|
|
|
var placed_bounds:Rect2 = face.bounds
|
|
placed_bounds.position += spawn_point
|
|
|
|
if !is_collision(placed_bounds):
|
|
var new_bounds:Rect2 = bounds.merge(placed_bounds)
|
|
|
|
if new_bounds.is_equal_approx(bounds):
|
|
return SpawnResult.new(spawn_point, false)
|
|
else:
|
|
if !started || max_vec_dim(best_bounds.size) > max_vec_dim(new_bounds.size):
|
|
best_bounds = new_bounds
|
|
best_flip = false
|
|
best_spawn_point = spawn_point
|
|
started = true
|
|
|
|
var placed_bounds_flipped:Rect2 = face.bounds
|
|
placed_bounds_flipped.size = Vector2(placed_bounds_flipped.size.y, placed_bounds_flipped.size.x)
|
|
placed_bounds_flipped.position += spawn_point
|
|
|
|
if !is_collision(placed_bounds_flipped):
|
|
var new_bounds_flipped:Rect2 = bounds.merge(placed_bounds_flipped)
|
|
|
|
if new_bounds_flipped.is_equal_approx(bounds):
|
|
return SpawnResult.new(spawn_point, true)
|
|
else:
|
|
if !started || max_vec_dim(best_bounds.size) > max_vec_dim(new_bounds_flipped.size):
|
|
best_bounds = new_bounds_flipped
|
|
best_flip = true
|
|
best_spawn_point = spawn_point
|
|
started = true
|
|
|
|
return SpawnResult.new(best_spawn_point, best_flip)
|
|
|
|
func add_face(face:FaceTracker):
|
|
var spawn:SpawnResult = get_best_spawn_point(face)
|
|
|
|
var idx = spawn_points.find(spawn.point)
|
|
spawn_points.remove_at(idx)
|
|
|
|
if spawn.flip:
|
|
face.reflect_diagonal()
|
|
|
|
face.translate(spawn.point)
|
|
face_list.append(face)
|
|
bounds = bounds.merge(face.bounds)
|
|
|
|
var sp_0:Vector2 = face.bounds.position + Vector2(face.bounds.size.x, 0)
|
|
var sp_1:Vector2 = face.bounds.position + Vector2(0, face.bounds.size.y)
|
|
if !spawn_points.has(sp_0):
|
|
spawn_points.append(sp_0)
|
|
if !spawn_points.has(sp_1):
|
|
spawn_points.append(sp_1)
|
|
|
|
|
|
|
|
class FaceTracker extends RefCounted:
|
|
var points:PackedVector2Array
|
|
var indices:PackedInt32Array
|
|
var bounds:Rect2
|
|
var face_index:int
|
|
|
|
func _to_string()->String:
|
|
var res:String = "["
|
|
for p in points:
|
|
res += "%s, " % str(p)
|
|
res += "]"
|
|
return res
|
|
|
|
func max_dim()->float:
|
|
return max(bounds.size.x, bounds.size.y)
|
|
|
|
func reflect_diagonal():
|
|
for p_idx in points.size():
|
|
var p:Vector2 = points[p_idx]
|
|
points[p_idx] = Vector2(p.y, p.x)
|
|
bounds.size = Vector2(bounds.size.y, bounds.size.x)
|
|
|
|
func translate(offset:Vector2):
|
|
for p_idx in points.size():
|
|
points[p_idx] += offset
|
|
bounds.position += offset
|
|
|
|
func fit_initial_rect():
|
|
bounds = Rect2(points[0], Vector2.ZERO)
|
|
for i in range(1, points.size()):
|
|
bounds = bounds.expand(points[i])
|
|
|
|
#Move so corner of bounds is at (0, 0)
|
|
for i in points.size():
|
|
points[i] -= bounds.position
|
|
bounds.position = Vector2.ZERO
|
|
|
|
func get_best_base_index()->int:
|
|
var best_index:int = -1
|
|
var best_height:float = INF
|
|
|
|
for i0 in points.size():
|
|
var i1:int = wrap(i0 + 1, 0, points.size())
|
|
|
|
var base_dir:Vector2 = points[i1] - points[i0]
|
|
var base_origin:Vector2 = points[i0]
|
|
var base_dir_perp:Vector2 = Vector2(-base_dir.y, base_dir.x)
|
|
|
|
var max_height:float = 0
|
|
|
|
for j in range(2, points.size()):
|
|
var p_idx:int = wrap(j + i0, 0, points.size())
|
|
var p:Vector2 = points[p_idx]
|
|
var offset:Vector2 = p - base_origin
|
|
var offset_proj:Vector2 = offset.project(base_dir_perp)
|
|
|
|
max_height = max(max_height, offset_proj.length_squared())
|
|
|
|
if max_height < best_height:
|
|
best_height = max_height
|
|
best_index = i0
|
|
|
|
return best_index
|
|
|
|
func rotate_to_best_fit():
|
|
var i0:int = get_best_base_index()
|
|
var i1:int = wrap(i0 + 1, 0, points.size())
|
|
|
|
var base_dir:Vector2 = (points[i1] - points[i0]).normalized()
|
|
var base_dir_perp:Vector2 = Vector2(-base_dir.y, base_dir.x)
|
|
|
|
var xform:Transform2D = Transform2D(base_dir, base_dir_perp, Vector2.ZERO)
|
|
var xform_inv:Transform2D = xform.affine_inverse()
|
|
|
|
for p_idx in points.size():
|
|
var p:Vector2 = xform_inv * points[p_idx]
|
|
points[p_idx] = p
|
|
|
|
|
|
func pack_faces(faces:Array[FaceTracker])->FaceTree:
|
|
faces.sort_custom(func (a:FaceTracker, b:FaceTracker): return a.max_dim() > b.max_dim())
|
|
|
|
var tree:FaceTree = FaceTree.new()
|
|
for f in faces:
|
|
tree.add_face(f)
|
|
|
|
#print(tree)
|
|
return tree
|
|
|
|
func build_faces(vol:ConvexVolume, margin:float)->FaceTree:
|
|
var faces:Array[FaceTracker]
|
|
|
|
for f_idx in vol.faces.size():
|
|
var face:ConvexVolume.FaceInfo = vol.faces[f_idx]
|
|
var axis:MathUtil.Axis = MathUtil.get_longest_axis(face.normal)
|
|
|
|
var cross_vec:Vector3
|
|
if axis == MathUtil.Axis.Y:
|
|
cross_vec = Vector3.FORWARD
|
|
else:
|
|
cross_vec = Vector3.UP
|
|
|
|
var u_axis:Vector3 = face.normal.cross(cross_vec)
|
|
var v_axis:Vector3 = u_axis.cross(face.normal)
|
|
var basis:Basis = Basis(u_axis, face.normal, v_axis)
|
|
|
|
var xform:Transform3D = Transform3D(basis, face.get_centroid())
|
|
var xz_xform:Transform3D = xform.affine_inverse()
|
|
|
|
var tracker:FaceTracker = FaceTracker.new()
|
|
tracker.face_index = f_idx
|
|
faces.append(tracker)
|
|
|
|
for v_idx in face.vertex_indices:
|
|
var v:ConvexVolume.VertexInfo = vol.vertices[v_idx]
|
|
var proj:Vector3 = xz_xform * v.point
|
|
tracker.points.append(Vector2(proj.x, proj.z))
|
|
tracker.indices.append(v_idx)
|
|
|
|
#print("face init points %s" % tracker.points)
|
|
|
|
tracker.rotate_to_best_fit()
|
|
#print("after rot %s" % tracker.points)
|
|
tracker.fit_initial_rect()
|
|
#print("after fit %s" % tracker.points)
|
|
for p_idx in tracker.points.size():
|
|
tracker.points[p_idx] += Vector2(margin, margin)
|
|
tracker.bounds.size += Vector2(margin, margin) * 2
|
|
|
|
return pack_faces(faces)
|
|
|
|
|