279 lines
8.8 KiB
TypeScript
279 lines
8.8 KiB
TypeScript
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();
|
|
}
|
|
}
|