Add shared-scene 2D authoring, sprite animation and Rapier2D physics
CI / validate (push) Canceled after 0s
CI / validate (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import * as B from "@babylonjs/core";
|
||||
import type { FormaRuntime } from "./runtime.ts";
|
||||
import { clone, type Vec3 } from "./schema.ts";
|
||||
import { paintTiles, fillTiles } from "./two-d.ts";
|
||||
export class View2D {
|
||||
enabled = false;
|
||||
width = 20;
|
||||
snap = 0;
|
||||
brush: { id: string; frame: number | null; fill?: boolean } | null = null;
|
||||
private orbit: any;
|
||||
private drag: any;
|
||||
private cleanup: (() => void)[] = [];
|
||||
constructor(
|
||||
private runtime: FormaRuntime,
|
||||
canvas: HTMLCanvasElement,
|
||||
) {
|
||||
if (runtime.options.headless) return;
|
||||
const down = (e: PointerEvent) => {
|
||||
const r = this.runtime;
|
||||
if (!this.enabled || r.playing) return;
|
||||
if (e.button === 1 || e.button === 2 || e.pointerType === "touch") {
|
||||
this.drag = {
|
||||
type: "pan",
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
target: r.camera.target.clone(),
|
||||
};
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.button !== 0) return;
|
||||
if (this.brush) {
|
||||
const node = r.state.find((n) => n.id === this.brush!.id);
|
||||
if (node?.components.tilemap) {
|
||||
this.drag = {
|
||||
type: "paint",
|
||||
id: node.id,
|
||||
original: clone(node.components.tilemap.cells),
|
||||
last: null,
|
||||
};
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
paint(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const bounds = canvas.getBoundingClientRect(),
|
||||
pick = r.scene.pick(
|
||||
((e.clientX - bounds.left) * r.engine.getRenderWidth()) /
|
||||
bounds.width,
|
||||
((e.clientY - bounds.top) * r.engine.getRenderHeight()) /
|
||||
bounds.height,
|
||||
(m) => !!m.metadata?.entityId,
|
||||
);
|
||||
const id = pick?.pickedMesh?.metadata?.entityId;
|
||||
if (id) {
|
||||
r.callbacks.select?.(id);
|
||||
const root = r.nodes.get(id)!;
|
||||
if (r.tool === "move") {
|
||||
this.drag = {
|
||||
type: "move",
|
||||
id,
|
||||
point: this.point(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
root.getAbsolutePosition().z,
|
||||
),
|
||||
original: root.position.clone(),
|
||||
};
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
}
|
||||
}
|
||||
};
|
||||
const paint = (e: PointerEvent) => {
|
||||
const r = this.runtime,
|
||||
node = r.state.find((n) => n.id === this.drag?.id),
|
||||
root = node && r.nodes.get(node.id);
|
||||
if (!node || !root || !this.brush) return;
|
||||
const point = this.point(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
root.getAbsolutePosition().z,
|
||||
),
|
||||
local = B.Vector3.TransformCoordinates(
|
||||
point,
|
||||
B.Matrix.Invert(root.getWorldMatrix()),
|
||||
),
|
||||
map = node.components.tilemap,
|
||||
x = Math.floor(local.x / map.tileSize[0]),
|
||||
y = Math.floor(local.y / map.tileSize[1]);
|
||||
if (this.drag.last?.x === x && this.drag.last?.y === y) return;
|
||||
const points = [];
|
||||
if (this.brush.fill) {
|
||||
if (this.drag.last) return;
|
||||
map.cells = fillTiles(
|
||||
map.cells,
|
||||
x,
|
||||
y,
|
||||
this.brush.frame,
|
||||
map.width,
|
||||
map.height,
|
||||
);
|
||||
} else {
|
||||
const prev = this.drag.last || { x, y },
|
||||
count = Math.max(Math.abs(x - prev.x), Math.abs(y - prev.y));
|
||||
for (let i = 0; i <= count; i++)
|
||||
points.push({
|
||||
x: Math.round(prev.x + ((x - prev.x) * i) / Math.max(1, count)),
|
||||
y: Math.round(prev.y + ((y - prev.y) * i) / Math.max(1, count)),
|
||||
});
|
||||
map.cells = paintTiles(
|
||||
map.cells,
|
||||
points,
|
||||
this.brush.frame,
|
||||
map.width,
|
||||
map.height,
|
||||
);
|
||||
}
|
||||
this.drag.last = { x, y };
|
||||
r.graphics2d.update(node, true);
|
||||
};
|
||||
const move = (e: PointerEvent) => {
|
||||
const r = this.runtime,
|
||||
d = this.drag;
|
||||
if (!d || r.playing) return;
|
||||
if (d.type === "pan") {
|
||||
const rect = canvas.getBoundingClientRect(),
|
||||
scale = this.width / rect.width;
|
||||
r.camera.setTarget(
|
||||
d.target.add(
|
||||
new B.Vector3(
|
||||
(d.x - e.clientX) * scale,
|
||||
(e.clientY - d.y) * scale,
|
||||
0,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (d.type === "paint") paint(e);
|
||||
else {
|
||||
const root = r.nodes.get(d.id)!;
|
||||
let delta = this.point(
|
||||
e.clientX,
|
||||
e.clientY,
|
||||
root.getAbsolutePosition().z,
|
||||
).subtract(d.point);
|
||||
if (root.parent)
|
||||
delta = B.Vector3.TransformNormal(
|
||||
delta,
|
||||
B.Matrix.Invert(root.parent.getWorldMatrix()),
|
||||
);
|
||||
const p = d.original.add(delta);
|
||||
if (this.snap) {
|
||||
p.x = Math.round(p.x / this.snap) * this.snap;
|
||||
p.y = Math.round(p.y / this.snap) * this.snap;
|
||||
}
|
||||
root.position.copyFrom(p);
|
||||
root.computeWorldMatrix(true);
|
||||
}
|
||||
};
|
||||
const end = (e: PointerEvent) => {
|
||||
const r = this.runtime,
|
||||
d = this.drag;
|
||||
if (!d) return;
|
||||
this.drag = null;
|
||||
if (d.type === "move") {
|
||||
const root = r.nodes.get(d.id)!,
|
||||
node = r.state.find((n) => n.id === d.id)!;
|
||||
if (e.type === "pointercancel") root.position.copyFrom(d.original);
|
||||
else if (!root.position.equals(d.original))
|
||||
r.callbacks.transform?.(d.id, {
|
||||
...clone(node.transform),
|
||||
position: root.position.asArray() as Vec3,
|
||||
});
|
||||
}
|
||||
if (d.type === "paint") {
|
||||
const node = r.state.find((n) => n.id === d.id);
|
||||
if (!node) return;
|
||||
const cells = clone(node.components.tilemap.cells);
|
||||
node.components.tilemap.cells = d.original;
|
||||
r.graphics2d.update(node, true);
|
||||
if (
|
||||
e.type !== "pointercancel" &&
|
||||
JSON.stringify(cells) !== JSON.stringify(d.original)
|
||||
)
|
||||
r.callbacks.paintTiles?.(d.id, cells);
|
||||
}
|
||||
};
|
||||
const wheel = (e: WheelEvent) => {
|
||||
if (this.enabled && !this.runtime.playing) {
|
||||
e.preventDefault();
|
||||
this.width = Math.max(
|
||||
0.25,
|
||||
Math.min(2000, this.width * Math.exp(e.deltaY * 0.001)),
|
||||
);
|
||||
this.update();
|
||||
}
|
||||
};
|
||||
canvas.addEventListener("pointerdown", down);
|
||||
canvas.addEventListener("pointermove", move);
|
||||
canvas.addEventListener("pointerup", end);
|
||||
canvas.addEventListener("pointercancel", end);
|
||||
canvas.addEventListener("wheel", wheel, { passive: false });
|
||||
this.cleanup.push(() => {
|
||||
canvas.removeEventListener("pointerdown", down);
|
||||
canvas.removeEventListener("pointermove", move);
|
||||
canvas.removeEventListener("pointerup", end);
|
||||
canvas.removeEventListener("pointercancel", end);
|
||||
canvas.removeEventListener("wheel", wheel);
|
||||
});
|
||||
}
|
||||
point(x: number, y: number, z = 0) {
|
||||
const r = this.runtime,
|
||||
rect = r.canvas.getBoundingClientRect(),
|
||||
ray = r.scene.createPickingRay(
|
||||
((x - rect.left) * r.engine.getRenderWidth()) / rect.width,
|
||||
((y - rect.top) * r.engine.getRenderHeight()) / rect.height,
|
||||
B.Matrix.Identity(),
|
||||
r.camera,
|
||||
);
|
||||
const t = (z - ray.origin.z) / ray.direction.z;
|
||||
return ray.origin.add(ray.direction.scale(t));
|
||||
}
|
||||
set(enabled: boolean) {
|
||||
const r = this.runtime;
|
||||
this.enabled = enabled;
|
||||
if (!r.camera) return;
|
||||
if (enabled) {
|
||||
if (!this.orbit)
|
||||
this.orbit = {
|
||||
alpha: r.camera.alpha,
|
||||
beta: r.camera.beta,
|
||||
radius: r.camera.radius,
|
||||
target: r.camera.target.clone(),
|
||||
};
|
||||
r.camera.detachControl();
|
||||
r.camera.alpha = Math.PI / 2;
|
||||
r.camera.beta = Math.PI / 2;
|
||||
r.camera.radius = 100;
|
||||
r.camera.mode = B.Camera.ORTHOGRAPHIC_CAMERA;
|
||||
r.grid.rotation.x = Math.PI / 2;
|
||||
r.grid.color = B.Color3.FromHexString("#334655");
|
||||
r.grid.position.z = -0.05;
|
||||
this.update();
|
||||
} else {
|
||||
r.camera.mode = B.Camera.PERSPECTIVE_CAMERA;
|
||||
if (this.orbit) {
|
||||
Object.assign(r.camera, {
|
||||
alpha: this.orbit.alpha,
|
||||
beta: this.orbit.beta,
|
||||
radius: this.orbit.radius,
|
||||
});
|
||||
r.camera.setTarget(this.orbit.target);
|
||||
this.orbit = null;
|
||||
}
|
||||
r.grid.rotation.x = 0;
|
||||
r.grid.position.z = 0;
|
||||
r.grid.color = B.Color3.FromHexString("#989b91");
|
||||
if (!r.options.headless && !r.playing)
|
||||
r.camera.attachControl(r.canvas, true);
|
||||
this.brush = null;
|
||||
}
|
||||
r.setTool(r.tool);
|
||||
}
|
||||
update() {
|
||||
if (!this.enabled || !this.runtime.camera) return;
|
||||
const c = this.runtime.camera,
|
||||
aspect =
|
||||
this.runtime.engine.getRenderWidth() /
|
||||
Math.max(1, this.runtime.engine.getRenderHeight());
|
||||
c.orthoLeft = -this.width / 2;
|
||||
c.orthoRight = this.width / 2;
|
||||
c.orthoTop = this.width / aspect / 2;
|
||||
c.orthoBottom = -this.width / aspect / 2;
|
||||
}
|
||||
dispose() {
|
||||
for (const f of this.cleanup) f();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user