1090 lines
28 KiB
JavaScript
1090 lines
28 KiB
JavaScript
var component = {};
|
||
|
||
class LooseQuadtree {
|
||
constructor(boundary, capacity=4, looseness=1.25) {
|
||
this.boundary = boundary // {x, y, width, height}
|
||
this.capacity = capacity // max items before subdivision
|
||
this.looseness = looseness // factor by which nodes expand
|
||
this.sprites = [] // sprite objects stored here
|
||
this.divided = false
|
||
|
||
// "loose boundary": expand our node’s bounding box
|
||
// so items that cross the boundary lines can be placed deeper
|
||
this.looseBounds = this.getLooseBounds(boundary)
|
||
}
|
||
|
||
// Expand boundary by the looseness factor (looseness >= 1)
|
||
getLooseBounds(boundary) {
|
||
// For each dimension, we expand half on each side
|
||
let marginW = (this.looseness - 1) * boundary.width / 2
|
||
let marginH = (this.looseness - 1) * boundary.height / 2
|
||
|
||
return {
|
||
x: boundary.x - marginW,
|
||
y: boundary.y - marginH,
|
||
width: boundary.width + marginW * 2,
|
||
height: boundary.height + marginH * 2
|
||
}
|
||
}
|
||
|
||
subdivide() {
|
||
let x = this.boundary.x
|
||
let y = this.boundary.y
|
||
let w = this.boundary.width / 2
|
||
let h = this.boundary.height / 2
|
||
|
||
this.northwest = new LooseQuadtree({x, y, width: w, height: h}, this.capacity, this.looseness)
|
||
this.northeast = new LooseQuadtree({x: x + w, y, width: w, height: h}, this.capacity, this.looseness)
|
||
this.southwest = new LooseQuadtree({x, y: y + h, width: w, height: h}, this.capacity, this.looseness)
|
||
this.southeast = new LooseQuadtree({x: x + w, y: y + h, width: w, height: h}, this.capacity, this.looseness)
|
||
|
||
this.divided = true
|
||
}
|
||
|
||
insert(sprite) {
|
||
let rect = sprite.rect;
|
||
|
||
// If outside *loose* bounds, ignore
|
||
if (!geometry.rect_intersects(this.looseBounds, rect)) return false
|
||
|
||
// If we have room and no subdivision, store it here
|
||
if (this.sprites.length < this.capacity && !this.divided) {
|
||
this.sprites.push(sprite)
|
||
return true
|
||
}
|
||
|
||
// Otherwise, subdivide if not already
|
||
if (!this.divided) this.subdivide()
|
||
|
||
// Try placing into children
|
||
if (this.northwest.insert(sprite)) return true
|
||
if (this.northeast.insert(sprite)) return true
|
||
if (this.southwest.insert(sprite)) return true
|
||
if (this.southeast.insert(sprite)) return true
|
||
|
||
// If it doesn't cleanly fit (overlaps multiple child boundaries),
|
||
// store at this level
|
||
this.sprites.push(sprite)
|
||
return true
|
||
}
|
||
|
||
query(range, found=[]) {
|
||
// If query doesn't intersect our *loose* boundary, no need to check further
|
||
if (!geometry.rect_intersects(this.looseBounds, range)) return found
|
||
|
||
// Check sprites in this node
|
||
for (let s of this.sprites)
|
||
if (geometry.rect_intersects(s.rect, range)) found.push(s)
|
||
|
||
// If subdivided, recurse
|
||
if (this.divided) {
|
||
this.northwest.query(range, found)
|
||
this.northeast.query(range, found)
|
||
this.southwest.query(range, found)
|
||
this.southeast.query(range, found)
|
||
}
|
||
return found
|
||
}
|
||
}
|
||
|
||
class Quadtree {
|
||
constructor(boundary, capacity=4) {
|
||
this.boundary = boundary // {x, y, width, height} for this node
|
||
this.capacity = capacity // max sprites before subdividing
|
||
this.sprites = [] // sprite objects stored in this node
|
||
this.divided = false // has this node subdivided?
|
||
}
|
||
|
||
subdivide() {
|
||
let x = this.boundary.x
|
||
let y = this.boundary.y
|
||
let w = this.boundary.width / 2
|
||
let h = this.boundary.height / 2
|
||
|
||
this.northwest = new Quadtree({x, y, width: w, height: h}, this.capacity)
|
||
this.northeast = new Quadtree({x: x + w, y, width: w, height: h}, this.capacity)
|
||
this.southwest = new Quadtree({x, y: y + h, width: w, height: h}, this.capacity)
|
||
this.southeast = new Quadtree({x: x + w, y: y + h, width: w, height: h}, this.capacity)
|
||
|
||
this.divided = true
|
||
}
|
||
|
||
insert(sprite) {
|
||
// Get the sprite's bounding rect
|
||
let rect = sprite.rect;
|
||
|
||
// If it doesn't intersect this quadtree's boundary, ignore
|
||
if (!geometry.rect_intersects(this.boundary, rect)) return false
|
||
|
||
// If there's room here and no subdivision yet, just store it
|
||
if (this.sprites.length < this.capacity && !this.divided) {
|
||
this.sprites.push(sprite)
|
||
return true
|
||
}
|
||
|
||
// Otherwise, subdivide if not done yet
|
||
if (!this.divided) this.subdivide()
|
||
|
||
// Try inserting into children
|
||
if (this.northwest.insert(sprite)) return true
|
||
if (this.northeast.insert(sprite)) return true
|
||
if (this.southwest.insert(sprite)) return true
|
||
if (this.southeast.insert(sprite)) return true
|
||
|
||
// If it doesn't cleanly fit into a child (spans multiple quadrants), store here
|
||
this.sprites.push(sprite)
|
||
return true
|
||
}
|
||
|
||
// Query all sprites that intersect the given range (e.g. your camera rect)
|
||
query(range, found=[]) {
|
||
// If there's no overlap, nothing to do
|
||
if (!geometry.rect_intersects(range, this.boundary)) return found
|
||
|
||
// Check sprites in this node
|
||
for (let s of this.sprites)
|
||
if (geometry.rect_intersects(range, s.rect)) found.push(s)
|
||
|
||
// Recursively query children if subdivided
|
||
if (this.divided) {
|
||
this.northwest.query(range, found)
|
||
this.northeast.query(range, found)
|
||
this.southwest.query(range, found)
|
||
this.southeast.query(range, found)
|
||
}
|
||
return found
|
||
}
|
||
}
|
||
|
||
class RNode {
|
||
constructor() {
|
||
this.children = []
|
||
this.bbox = null
|
||
this.leaf = true
|
||
this.height = 1
|
||
}
|
||
}
|
||
|
||
class RTree {
|
||
constructor(maxEntries = 4) {
|
||
this.maxEntries = maxEntries
|
||
this.minEntries = Math.max(2, Math.floor(maxEntries * 0.4))
|
||
this.root = new RNode()
|
||
}
|
||
|
||
_getBBox(child) {
|
||
return child instanceof RNode ? child.bbox : child.rect
|
||
}
|
||
|
||
_unionBBox(a, b) {
|
||
if (!a) return {...b}
|
||
if (!b) return {...a}
|
||
const minX = Math.min(a.x, b.x)
|
||
const minY = Math.min(a.y, b.y)
|
||
const maxX = Math.max(a.x + a.width, b.x + b.width)
|
||
const maxY = Math.max(a.y + a.height, b.y + b.height)
|
||
return {x: minX, y: minY, width: maxX - minX, height: maxY - minY}
|
||
}
|
||
|
||
insert(item) {
|
||
if (!item) return
|
||
|
||
if (this.root.leaf && this.root.children.length === 0) {
|
||
this.root.children.push(item)
|
||
this.root.bbox = {...item.rect}
|
||
return
|
||
}
|
||
|
||
let node = this._chooseSubtree(this.root, item)
|
||
node.children.push(item)
|
||
this._extend(node, item)
|
||
|
||
while (node && node.children.length > this.maxEntries) {
|
||
this._split(node)
|
||
node = this._findParent(this.root, node)
|
||
}
|
||
}
|
||
|
||
_chooseSubtree(node, item) {
|
||
while (!node.leaf) {
|
||
let minEnlargement = Infinity
|
||
let bestChild = null
|
||
|
||
for (const child of node.children) {
|
||
const enlargement = this._enlargement(child.bbox, item)
|
||
if (enlargement < minEnlargement) {
|
||
minEnlargement = enlargement
|
||
bestChild = child
|
||
}
|
||
}
|
||
node = bestChild || node.children[0]
|
||
}
|
||
return node
|
||
}
|
||
|
||
_enlargement(bbox, item) {
|
||
if (!bbox) return Number.MAX_VALUE
|
||
const itemBBox = this._getBBox(item)
|
||
const union = this._unionBBox(bbox, itemBBox)
|
||
return (union.width * union.height) - (bbox.width * bbox.height)
|
||
}
|
||
|
||
_split(node) {
|
||
const items = node.children
|
||
|
||
items.sort((a, b) => {
|
||
const aBBox = this._getBBox(a)
|
||
const bBBox = this._getBBox(b)
|
||
return aBBox.x - bBBox.x
|
||
})
|
||
|
||
const splitIndex = Math.ceil(items.length / 2)
|
||
const group1 = items.slice(0, splitIndex)
|
||
const group2 = items.slice(splitIndex)
|
||
|
||
if (node === this.root) {
|
||
this.root = new RNode()
|
||
this.root.leaf = false
|
||
node.children = group1
|
||
node.bbox = this._getBBoxes(group1)
|
||
|
||
const sibling = new RNode()
|
||
sibling.leaf = node.leaf
|
||
sibling.children = group2
|
||
sibling.bbox = this._getBBoxes(group2)
|
||
|
||
this.root.children = [node, sibling]
|
||
this.root.bbox = this._unionBBox(node.bbox, sibling.bbox)
|
||
} else {
|
||
node.children = group1
|
||
node.bbox = this._getBBoxes(group1)
|
||
|
||
const sibling = new RNode()
|
||
sibling.leaf = node.leaf
|
||
sibling.children = group2
|
||
sibling.bbox = this._getBBoxes(group2)
|
||
|
||
const parent = this._findParent(this.root, node)
|
||
parent.children.push(sibling)
|
||
parent.bbox = this._unionBBox(parent.bbox, sibling.bbox)
|
||
}
|
||
}
|
||
|
||
_getBBoxes(items) {
|
||
let bbox = null
|
||
for (const item of items) {
|
||
bbox = this._unionBBox(bbox, this._getBBox(item))
|
||
}
|
||
return bbox
|
||
}
|
||
|
||
_extend(node, item) {
|
||
const itemBBox = this._getBBox(item)
|
||
node.bbox = node.bbox ? this._unionBBox(node.bbox, itemBBox) : {...itemBBox}
|
||
}
|
||
|
||
_findParent(node, target, parent = null) {
|
||
if (!node || node === target) return parent
|
||
if (!node.leaf) {
|
||
for (const child of node.children) {
|
||
if (child instanceof RNode) {
|
||
const result = this._findParent(child, target, node)
|
||
if (result) return result
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
query(range, results = []) {
|
||
const stack = [this.root]
|
||
while (stack.length > 0) {
|
||
const node = stack.pop()
|
||
if (!node.bbox || !geometry.rect_intersects(node.bbox, range)) continue
|
||
|
||
if (node.leaf) {
|
||
for (const item of node.children) {
|
||
if (geometry.rect_intersects(item.rect, range)) {
|
||
results.push(item)
|
||
}
|
||
}
|
||
} else {
|
||
stack.push(...node.children)
|
||
}
|
||
}
|
||
return results
|
||
}
|
||
}
|
||
|
||
function make_point_obj(o, p) {
|
||
return {
|
||
pos: p,
|
||
move(d) {
|
||
d = o.gameobject.dir_world2this(d);
|
||
p.x += d.x;
|
||
p.y += d.y;
|
||
},
|
||
sync: o.sync.bind(o),
|
||
};
|
||
};
|
||
|
||
/* an anim is simply an array of images */
|
||
/* an anim set is like this
|
||
frog = {
|
||
walk: [],
|
||
hop: [],
|
||
...etc
|
||
}
|
||
*/
|
||
|
||
var world = {x:-1000,y:-1000, width:3000, height:3000};
|
||
//globalThis.sprite_qt = new Quadtree({x:-1000,y:-1000, width:3000, height:3000}, 10);
|
||
//globalThis.sprite_qt = new LooseQuadtree(world, 10, 1.2);
|
||
//globalThis.sprite_qt = new RTree(10)
|
||
//globalThis.sprite_qt = os.make_quadtree(world, 4);
|
||
//globalThis.sprite_qt = os.make_qtree(world);
|
||
globalThis.sprite_qt = os.make_rtree();
|
||
|
||
var spritetree = os.make_quadtree([0,0], [10000,10000]);
|
||
globalThis.spritetree = spritetree;
|
||
|
||
var sprite = {
|
||
image: undefined,
|
||
get diffuse() { return this.image; },
|
||
set diffuse(x) {},
|
||
anim_speed: 1,
|
||
play(str, loop = true, reverse = false, fn) {
|
||
if (!this.animset) {
|
||
// console.warn(`Sprite has no animset when trying to play ${str}`);
|
||
fn?.();
|
||
return;
|
||
// return parseq.imm();
|
||
}
|
||
|
||
if (typeof str === 'string') {
|
||
if (!this.animset[str]) {
|
||
fn?.();
|
||
return;
|
||
}
|
||
this.anim = this.animset[str];
|
||
}
|
||
|
||
var playing = this.anim;
|
||
|
||
var self = this;
|
||
var stop;
|
||
|
||
this.del_anim?.();
|
||
self.del_anim = function () {
|
||
self.del_anim = undefined;
|
||
self = undefined;
|
||
advance = undefined;
|
||
stop?.();
|
||
};
|
||
|
||
var f = 0;
|
||
if (reverse) f = playing.frames.length - 1;
|
||
|
||
function advance(time) {
|
||
if (!self) return;
|
||
if (!self.gameobject) return;
|
||
|
||
var done = false;
|
||
if (reverse) {
|
||
f = (((f - 1) % playing.frames.length) + playing.frames.length) % playing.frames.length;
|
||
if (f === playing.frames.length - 1) done = true;
|
||
} else {
|
||
f = (f + 1) % playing.frames.length;
|
||
if (f === 0) done = true;
|
||
}
|
||
|
||
self.image = playing.frames[f];
|
||
|
||
if (done) {
|
||
// notify requestor
|
||
fn?.();
|
||
if (!loop) {
|
||
self?.stop();
|
||
return;
|
||
}
|
||
}
|
||
|
||
return playing.frames[f].time/self.anim_speed;
|
||
}
|
||
stop = self.gameobject.delay(advance, playing.frames[f].time/self.anim_speed);
|
||
advance();
|
||
},
|
||
tex_sync() {
|
||
if (this.anim) this.stop();
|
||
this.sync();
|
||
this.play();
|
||
this.transform.scale = [this.image.texture.width, this.image.texture.height];
|
||
// spritetree.remove(this);
|
||
|
||
},
|
||
stop() {
|
||
this.del_anim?.();
|
||
},
|
||
set path(p) {
|
||
var image = game.texture(p);
|
||
if (!image) {
|
||
console.warn(`Could not find image ${p}.`);
|
||
return;
|
||
}
|
||
|
||
this._p = p;
|
||
|
||
this.del_anim?.();
|
||
if (image.texture)
|
||
this.image = image;
|
||
else if (image.frames) {
|
||
// It's an animation
|
||
this.anim = image;
|
||
this.image = image.frames[0];
|
||
this.animset = [this.anim]
|
||
} else {
|
||
// Maybe an animset; try to grab the first one
|
||
for (var anim in image) {
|
||
if (image[anim].frames) {
|
||
this.anim = image[anim];
|
||
this.image = image[anim].frames[0];
|
||
this.animset = image;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
this.tex_sync();
|
||
},
|
||
get path() {
|
||
return this._p;
|
||
},
|
||
kill: function kill() {
|
||
this.del_anim?.();
|
||
this.anim = undefined;
|
||
this.gameobject = undefined;
|
||
allsprites.remove(this);
|
||
},
|
||
anchor: [0, 0],
|
||
sync: function sync() {
|
||
this.layer = this.gameobject.drawlayer;
|
||
},
|
||
pick() {
|
||
return this;
|
||
},
|
||
boundingbox() {
|
||
var dim = this.dimensions();
|
||
dim = dim.scale(this.gameobject.scale);
|
||
var realpos = dim.scale(0.5).add(this.pos);
|
||
return bbox.fromcwh(realpos, dim);
|
||
},
|
||
};
|
||
globalThis.allsprites = [];
|
||
var sprite_buckets = {};
|
||
|
||
component.sprite_buckets = function () {
|
||
return sprite_buckets;
|
||
};
|
||
|
||
component.dynamic_sprites = [];
|
||
|
||
sprite.doc = {
|
||
path: "Path to the texture.",
|
||
color: "Color to mix with the sprite.",
|
||
pos: "The offset position of the sprite, relative to its entity.",
|
||
};
|
||
|
||
sprite.setanchor = function (anch) {
|
||
var off = [0, 0];
|
||
switch (anch) {
|
||
case "ll":
|
||
break;
|
||
case "lm":
|
||
off = [-0.5, 0];
|
||
break;
|
||
case "lr":
|
||
off = [-1, 0];
|
||
break;
|
||
case "ml":
|
||
off = [0, -0.5];
|
||
break;
|
||
case "mm":
|
||
off = [-0.5, -0.5];
|
||
break;
|
||
case "mr":
|
||
off = [-1, -0.5];
|
||
break;
|
||
case "ul":
|
||
off = [0, -1];
|
||
break;
|
||
case "um":
|
||
off = [-0.5, -1];
|
||
break;
|
||
case "ur":
|
||
off = [-1, -1];
|
||
break;
|
||
}
|
||
this.anchor = off;
|
||
this.pos = this.dimensions().scale(off);
|
||
};
|
||
|
||
sprite.inputs = {};
|
||
sprite.inputs.kp9 = function () {
|
||
this.setanchor("ll");
|
||
};
|
||
sprite.inputs.kp8 = function () {
|
||
this.setanchor("lm");
|
||
};
|
||
sprite.inputs.kp7 = function () {
|
||
this.setanchor("lr");
|
||
};
|
||
sprite.inputs.kp6 = function () {
|
||
this.setanchor("ml");
|
||
};
|
||
sprite.inputs.kp5 = function () {
|
||
this.setanchor("mm");
|
||
};
|
||
sprite.inputs.kp4 = function () {
|
||
this.setanchor("mr");
|
||
};
|
||
sprite.inputs.kp3 = function () {
|
||
this.setanchor("ur");
|
||
};
|
||
sprite.inputs.kp2 = function () {
|
||
this.setanchor("um");
|
||
};
|
||
sprite.inputs.kp1 = function () {
|
||
this.setanchor("ul");
|
||
};
|
||
|
||
component.sprite = function (obj) {
|
||
var sp = Object.create(sprite);
|
||
sp.gameobject = obj;
|
||
sp.transform = os.make_transform();
|
||
sp.transform.parent = obj.transform;
|
||
sp.guid = prosperon.guid();
|
||
allsprites.push(sp);
|
||
return sp;
|
||
};
|
||
|
||
sprite.shade = [1, 1, 1, 1];
|
||
|
||
return {component};
|
||
|
||
Object.mixin(os.make_seg2d(), {
|
||
sync() {
|
||
this.set_endpoints(this.points[0], this.points[1]);
|
||
},
|
||
});
|
||
|
||
var collider2d = {};
|
||
collider2d.inputs = {};
|
||
collider2d.inputs["M-s"] = function () {
|
||
this.sensor = !this.sensor;
|
||
};
|
||
collider2d.inputs["M-s"].doc = "Toggle if this collider is a sensor.";
|
||
|
||
collider2d.inputs["M-t"] = function () {
|
||
this.enabled = !this.enabled;
|
||
};
|
||
collider2d.inputs["M-t"].doc = "Toggle if this collider is enabled.";
|
||
|
||
Object.mix(os.make_poly2d(), {
|
||
boundingbox() {
|
||
return bbox.frompoints(this.spoints());
|
||
},
|
||
|
||
/* EDITOR */
|
||
spoints() {
|
||
var spoints = this.points.slice();
|
||
|
||
if (this.flipx) {
|
||
spoints.forEach(function (x) {
|
||
var newpoint = x.slice();
|
||
newpoint.x = -newpoint.x;
|
||
spoints.push(newpoint);
|
||
});
|
||
}
|
||
|
||
if (this.flipy) {
|
||
spoints.forEach(function (x) {
|
||
var newpoint = x.slice();
|
||
newpoint.y = -newpoint.y;
|
||
spoints.push(newpoint);
|
||
});
|
||
}
|
||
return spoints;
|
||
},
|
||
|
||
gizmo() {
|
||
this.spoints().forEach(x => render.point(this.gameobject.this2screen(x), 3, Color.green));
|
||
this.points.forEach((x, i) => render.coordinate(this.gameobject.this2screen(x)));
|
||
},
|
||
|
||
pick(pos) {
|
||
if (!Object.hasOwn(this, "points")) this.points = deep_copy(this.__proto__.points);
|
||
|
||
var i = Gizmos.pick_gameobject_points(pos, this.gameobject, this.points);
|
||
var p = this.points[i];
|
||
if (p) return make_point_obj(this, p);
|
||
|
||
return undefined;
|
||
},
|
||
});
|
||
|
||
function pointscaler(x) {
|
||
if (typeof x === "number") return;
|
||
this.points = this.points.map(p => p.mult(x));
|
||
}
|
||
|
||
Object.mixin(os.make_poly2d(), {
|
||
sync() {
|
||
this.setverts(this.points);
|
||
},
|
||
grow: pointscaler,
|
||
});
|
||
|
||
var polyinputs = Object.create(collider2d.inputs);
|
||
os.make_poly2d().inputs = polyinputs;
|
||
|
||
polyinputs = {};
|
||
polyinputs.f10 = function () {
|
||
this.points = Math.sortpointsccw(this.points);
|
||
};
|
||
polyinputs.f10.doc = "Sort all points to be CCW order.";
|
||
|
||
polyinputs["C-lm"] = function () {
|
||
this.points.push(this.gameobject.world2this(input.mouse.worldpos()));
|
||
};
|
||
polyinputs["C-lm"].doc = "Add a point to location of mouse.";
|
||
polyinputs.lm = function () {};
|
||
polyinputs.lm.released = function () {};
|
||
|
||
polyinputs["C-M-lm"] = function () {
|
||
var idx = Math.grab_from_points(
|
||
input.mouse.worldpos(),
|
||
this.points.map(p => this.gameobject.this2world(p)),
|
||
25,
|
||
);
|
||
if (idx === -1) return;
|
||
this.points.splice(idx, 1);
|
||
};
|
||
polyinputs["C-M-lm"].doc = "Remove point under mouse.";
|
||
|
||
polyinputs["C-b"] = function () {
|
||
this.points = this.spoints;
|
||
this.flipx = false;
|
||
this.flipy = false;
|
||
};
|
||
polyinputs["C-b"].doc = "Freeze mirroring in place.";
|
||
|
||
var edge2d = {
|
||
dimensions: 2,
|
||
thickness: 1,
|
||
/* if type === -1, point to point */
|
||
type: Spline.type.catmull,
|
||
C: 1 /* when in bezier, continuity required. 0, 1 or 2. */,
|
||
looped: false,
|
||
angle: 0.5 /* smaller for smoother bezier */,
|
||
elasticity: 0,
|
||
friction: 0,
|
||
sync() {
|
||
var ppp = this.sample();
|
||
this.segs ??= [];
|
||
var count = ppp.length - 1;
|
||
this.segs.length = count;
|
||
for (var i = 0; i < count; i++) {
|
||
this.segs[i] ??= os.make_seg2d(this.body);
|
||
this.segs[i].set_endpoints(ppp[i], ppp[i + 1]);
|
||
this.segs[i].set_neighbors(ppp[i], ppp[i + 1]);
|
||
this.segs[i].radius = this.thickness;
|
||
this.segs[i].elasticity = this.elasticity;
|
||
this.segs[i].friction = this.friction;
|
||
this.segs[i].collide = this.collide;
|
||
}
|
||
},
|
||
|
||
flipx: false,
|
||
flipy: false,
|
||
|
||
hollow: false,
|
||
hollowt: 0,
|
||
|
||
spoints() {
|
||
if (!this.points) return [];
|
||
var spoints = this.points.slice();
|
||
|
||
if (this.flipx) {
|
||
if (Spline.is_bezier(this.type)) spoints.push(Vector.reflect_point(spoints.at(-2), spoints.at(-1)));
|
||
|
||
for (var i = spoints.length - 1; i >= 0; i--) {
|
||
var newpoint = spoints[i].slice();
|
||
newpoint.x = -newpoint.x;
|
||
spoints.push(newpoint);
|
||
}
|
||
}
|
||
|
||
if (this.flipy) {
|
||
if (Spline.is_bezier(this.type)) spoints.push(Vector.reflect(point(spoints.at(-2), spoints.at(-1))));
|
||
|
||
for (var i = spoints.length - 1; i >= 0; i--) {
|
||
var newpoint = spoints[i].slice();
|
||
newpoint.y = -newpoint.y;
|
||
spoints.push(newpoint);
|
||
}
|
||
}
|
||
|
||
if (this.hollow) {
|
||
var hpoints = vector.inflate(spoints, this.hollowt);
|
||
if (hpoints.length === spoints.length) return spoints;
|
||
var arr1 = hpoints.filter(function (x, i) {
|
||
return i % 2 === 0;
|
||
});
|
||
var arr2 = hpoints.filter(function (x, i) {
|
||
return i % 2 !== 0;
|
||
});
|
||
return arr1.concat(arr2.reverse());
|
||
}
|
||
|
||
if (this.looped) spoints = spoints.wrapped(1);
|
||
|
||
return spoints;
|
||
},
|
||
|
||
post() {
|
||
this.points = [];
|
||
},
|
||
|
||
sample() {
|
||
var spoints = this.spoints();
|
||
if (spoints.length === 0) return [];
|
||
|
||
if (this.type === -1) {
|
||
if (this.looped) spoints.push(spoints[0]);
|
||
return spoints;
|
||
}
|
||
|
||
if (this.type === Spline.type.catmull) {
|
||
if (this.looped) spoints = Spline.catmull_loop(spoints);
|
||
else spoints = Spline.catmull_caps(spoints);
|
||
|
||
return Spline.sample_angle(this.type, spoints, this.angle);
|
||
}
|
||
|
||
if (this.looped && Spline.is_bezier(this.type)) spoints = Spline.bezier_loop(spoints);
|
||
|
||
return Spline.sample_angle(this.type, spoints, this.angle);
|
||
},
|
||
|
||
boundingbox() {
|
||
return bbox.frompoints(this.points.map(x => x.scale(this.gameobject.scale)));
|
||
},
|
||
|
||
/* EDITOR */
|
||
gizmo() {
|
||
if (this.type === Spline.type.catmull || this.type === -1) {
|
||
this.spoints().forEach(x => render.point(this.gameobject.this2screen(x), 3, Color.teal));
|
||
this.points.forEach((x, i) => render.coordinate(this.gameobject.this2screen(x)));
|
||
} else {
|
||
for (var i = 0; i < this.points.length; i += 3) render.coordinate(this.gameobject.this2screen(this.points[i]), 1, Color.teal);
|
||
|
||
for (var i = 1; i < this.points.length; i += 3) {
|
||
render.coordinate(this.gameobject.this2screen(this.points[i]), 1, Color.green);
|
||
render.coordinate(this.gameobject.this2screen(this.points[i + 1]), 1, Color.green);
|
||
render.line([this.gameobject.this2screen(this.points[i - 1]), this.gameobject.this2screen(this.points[i])], Color.yellow);
|
||
render.line([this.gameobject.this2screen(this.points[i + 1]), this.gameobject.this2screen(this.points[i + 2])], Color.yellow);
|
||
}
|
||
}
|
||
},
|
||
|
||
finish_center(change) {
|
||
this.points = this.points.map(function (x) {
|
||
return x.sub(change);
|
||
});
|
||
},
|
||
|
||
pick(pos) {
|
||
var i = Gizmos.pick_gameobject_points(pos, this.gameobject, this.points);
|
||
var p = this.points[i];
|
||
if (!p) return undefined;
|
||
|
||
if (Spline.is_catmull(this.type) || this.type === -1) return make_point_obj(this, p);
|
||
|
||
var that = this.gameobject;
|
||
var me = this;
|
||
if (p) {
|
||
var o = {
|
||
pos: p,
|
||
sync: me.sync.bind(me),
|
||
};
|
||
if (Spline.bezier_is_handle(this.points, i))
|
||
o.move = function (d) {
|
||
d = that.dir_world2this(d);
|
||
p.x += d.x;
|
||
p.y += d.y;
|
||
Spline.bezier_cp_mirror(me.points, i);
|
||
};
|
||
else
|
||
o.move = function (d) {
|
||
d = that.dir_world2this(d);
|
||
p.x += d.x;
|
||
p.y += d.y;
|
||
var pp = Spline.bezier_point_handles(me.points, i);
|
||
pp.forEach(ph => (me.points[ph] = me.points[ph].add(d)));
|
||
};
|
||
return o;
|
||
}
|
||
},
|
||
|
||
rm_node(idx) {
|
||
if (idx < 0 || idx >= this.points.length) return;
|
||
if (Spline.is_catmull(this.type)) this.points.splice(idx, 1);
|
||
|
||
if (Spline.is_bezier(this.type)) {
|
||
assert(Spline.bezier_is_node(this.points, idx), "Attempted to delete a bezier handle.");
|
||
if (idx === 0) this.points.splice(idx, 2);
|
||
else if (idx === this.points.length - 1) this.points.splice(this.points.length - 2, 2);
|
||
else this.points.splice(idx - 1, 3);
|
||
}
|
||
},
|
||
|
||
add_node(pos) {
|
||
pos = this.gameobject.world2this(pos);
|
||
var idx = 0;
|
||
if (Spline.is_catmull(this.type) || this.type === -1) {
|
||
if (this.points.length >= 2) idx = physics.closest_point(pos, this.points, 400);
|
||
|
||
if (idx === this.points.length) this.points.push(pos);
|
||
else this.points.splice(idx, 0, pos);
|
||
}
|
||
|
||
if (Spline.is_bezier(this.type)) {
|
||
idx = physics.closest_point(pos, Spline.bezier_nodes(this.points), 400);
|
||
|
||
if (idx < 0) return;
|
||
|
||
if (idx === 0) {
|
||
this.points.unshift(pos.slice(), pos.add([-100, 0]), Vector.reflect_point(this.points[1], this.points[0]));
|
||
return;
|
||
}
|
||
if (idx === Spline.bezier_node_count(this.points)) {
|
||
this.points.push(Vector.reflect_point(this.points.at(-2), this.points.at(-1)), pos.add([-100, 0]), pos.slice());
|
||
return;
|
||
}
|
||
idx = 2 + (idx - 1) * 3;
|
||
var adds = [pos.add([100, 0]), pos.slice(), pos.add([-100, 0])];
|
||
this.points.splice(idx, 0, ...adds);
|
||
}
|
||
},
|
||
|
||
pick_all() {
|
||
var picks = [];
|
||
this.points.forEach(x => picks.push(make_point_obj(this, x)));
|
||
return picks;
|
||
},
|
||
};
|
||
|
||
component.edge2d = function (obj) {
|
||
// if (!obj.body) obj.rigidify();
|
||
var edge = Object.create(edge2d);
|
||
edge.body = obj.body;
|
||
return edge;
|
||
};
|
||
|
||
edge2d.spoints.doc = "Returns the controls points after modifiers are applied, such as it being hollow or mirrored on its axises.";
|
||
edge2d.inputs = {};
|
||
edge2d.inputs.h = function () {
|
||
this.hollow = !this.hollow;
|
||
};
|
||
edge2d.inputs.h.doc = "Toggle hollow.";
|
||
|
||
edge2d.inputs["C-g"] = function () {
|
||
if (this.hollowt > 0) this.hollowt--;
|
||
};
|
||
edge2d.inputs["C-g"].doc = "Thin the hollow thickness.";
|
||
edge2d.inputs["C-g"].rep = true;
|
||
|
||
edge2d.inputs["C-f"] = function () {
|
||
this.hollowt++;
|
||
};
|
||
edge2d.inputs["C-f"].doc = "Increase the hollow thickness.";
|
||
edge2d.inputs["C-f"].rep = true;
|
||
|
||
edge2d.inputs["M-v"] = function () {
|
||
if (this.thickness > 0) this.thickness--;
|
||
};
|
||
edge2d.inputs["M-v"].doc = "Decrease spline thickness.";
|
||
edge2d.inputs["M-v"].rep = true;
|
||
|
||
edge2d.inputs["C-y"] = function () {
|
||
this.points = this.spoints();
|
||
this.flipx = false;
|
||
this.flipy = false;
|
||
this.hollow = false;
|
||
};
|
||
edge2d.inputs["C-y"].doc = "Freeze mirroring,";
|
||
edge2d.inputs["M-b"] = function () {
|
||
this.thickness++;
|
||
};
|
||
edge2d.inputs["M-b"].doc = "Increase spline thickness.";
|
||
edge2d.inputs["M-b"].rep = true;
|
||
|
||
edge2d.inputs.plus = function () {
|
||
if (this.angle <= 1) {
|
||
this.angle = 1;
|
||
return;
|
||
}
|
||
this.angle *= 0.9;
|
||
};
|
||
edge2d.inputs.plus.doc = "Increase the number of samples of this spline.";
|
||
edge2d.inputs.plus.rep = true;
|
||
|
||
edge2d.inputs.minus = function () {
|
||
this.angle *= 1.1;
|
||
};
|
||
edge2d.inputs.minus.doc = "Decrease the number of samples on this spline.";
|
||
edge2d.inputs.minus.rep = true;
|
||
|
||
edge2d.inputs["C-r"] = function () {
|
||
this.points = this.points.reverse();
|
||
};
|
||
edge2d.inputs["C-r"].doc = "Reverse the order of the spline's points.";
|
||
|
||
edge2d.inputs["C-l"] = function () {
|
||
this.looped = !this.looped;
|
||
};
|
||
edge2d.inputs["C-l"].doc = "Toggle spline being looped.";
|
||
|
||
edge2d.inputs["C-c"] = function () {
|
||
switch (this.type) {
|
||
case Spline.type.bezier:
|
||
this.points = Spline.bezier2catmull(this.points);
|
||
break;
|
||
}
|
||
this.type = Spline.type.catmull;
|
||
};
|
||
|
||
edge2d.inputs["C-c"].doc = "Set type of spline to catmull-rom.";
|
||
|
||
edge2d.inputs["C-b"] = function () {
|
||
switch (this.type) {
|
||
case Spline.type.catmull:
|
||
this.points = Spline.catmull2bezier(Spline.catmull_caps(this.points));
|
||
break;
|
||
}
|
||
this.type = Spline.type.bezier;
|
||
};
|
||
|
||
edge2d.inputs["C-o"] = function () {
|
||
this.type = -1;
|
||
};
|
||
edge2d.inputs["C-o"].doc = "Set spline to linear.";
|
||
|
||
edge2d.inputs["C-M-lm"] = function () {
|
||
if (Spline.is_catmull(this.type)) {
|
||
var idx = Math.grab_from_points(
|
||
input.mouse.worldpos(),
|
||
this.points.map(p => this.gameobject.this2world(p)),
|
||
25,
|
||
);
|
||
if (idx === -1) return;
|
||
} else {
|
||
}
|
||
|
||
this.points = this.points.newfirst(idx);
|
||
};
|
||
edge2d.inputs["C-M-lm"].doc = "Select the given point as the '0' of this spline.";
|
||
|
||
edge2d.inputs["C-lm"] = function () {
|
||
this.add_node(input.mouse.worldpos());
|
||
};
|
||
edge2d.inputs["C-lm"].doc = "Add a point to the spline at the mouse position.";
|
||
|
||
edge2d.inputs["C-M-lm"] = function () {
|
||
var idx = -1;
|
||
if (Spline.is_catmull(this.type))
|
||
idx = Math.grab_from_points(
|
||
input.mouse.worldpos(),
|
||
this.points.map(p => this.gameobject.this2world(p)),
|
||
25,
|
||
);
|
||
else {
|
||
var nodes = Spline.bezier_nodes(this.points);
|
||
idx = Math.grab_from_points(
|
||
input.mouse.worldpos(),
|
||
nodes.map(p => this.gameobject.this2world(p)),
|
||
25,
|
||
);
|
||
idx *= 3;
|
||
}
|
||
|
||
this.rm_node(idx);
|
||
};
|
||
edge2d.inputs["C-M-lm"].doc = "Remove point from the spline.";
|
||
|
||
edge2d.inputs.lm = function () {};
|
||
edge2d.inputs.lm.released = function () {};
|
||
|
||
edge2d.inputs.lb = function () {
|
||
var np = [];
|
||
|
||
this.points.forEach(function (c) {
|
||
np.push(Vector.rotate(c, Math.deg2rad(-1)));
|
||
});
|
||
|
||
this.points = np;
|
||
};
|
||
edge2d.inputs.lb.doc = "Rotate the points CCW.";
|
||
edge2d.inputs.lb.rep = true;
|
||
|
||
edge2d.inputs.rb = function () {
|
||
var np = [];
|
||
|
||
this.points.forEach(function (c) {
|
||
np.push(Vector.rotate(c, Math.deg2rad(1)));
|
||
});
|
||
|
||
this.points = np;
|
||
};
|
||
edge2d.inputs.rb.doc = "Rotate the points CW.";
|
||
edge2d.inputs.rb.rep = true;
|
||
|
||
/* CIRCLE */
|
||
|
||
function shape_maker(maker) {
|
||
return function (obj) {
|
||
// if (!obj.body) obj.rigidify();
|
||
return maker(obj.body);
|
||
};
|
||
}
|
||
component.circle2d = shape_maker(os.make_circle2d);
|
||
component.poly2d = shape_maker(os.make_poly2d);
|
||
component.seg2d = shape_maker(os.make_seg2d);
|
||
|
||
Object.mix(os.make_circle2d(), {
|
||
boundingbox() {
|
||
return bbox.fromcwh(this.offset, [this.radius, this.radius]);
|
||
},
|
||
|
||
set scale(x) {
|
||
this.radius = x;
|
||
},
|
||
get scale() {
|
||
return this.radius;
|
||
},
|
||
|
||
get pos() {
|
||
return this.offset;
|
||
},
|
||
set pos(x) {
|
||
this.offset = x;
|
||
},
|
||
|
||
grow(x) {
|
||
if (typeof x === "number") this.scale *= x;
|
||
else if (typeof x === "object") this.scale *= x[0];
|
||
},
|
||
});
|
||
|
||
return { component };
|