mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-13 15:24:25 +00:00
497 lines
18 KiB
JavaScript
497 lines
18 KiB
JavaScript
// Pointer input: select, move, resize, connect, create, draw, pan and zoom.
|
|
import { isShape, snap, clamp, newId, idPrefix, normalizeElement, DEFAULT_SIZES, SHAPE_TYPES, MAX_SIDE, MAX_PEN_POINTS } from "/lib/model.mjs";
|
|
import {
|
|
hitTest, shapeAt, pointInShape, elementBounds, unionBounds, boxContains, arrowGeometry, lineHeight, fontSizeOf, fitStroke,
|
|
} from "/lib/geometry.mjs";
|
|
import { translateElement } from "./editor.js";
|
|
|
|
const DRAG = 3;
|
|
const DOUBLE_MS = 400;
|
|
const r2 = (v) => Math.round(v * 100) / 100;
|
|
|
|
function snapAxis(pos, size, cands, th) {
|
|
let best = null;
|
|
for (const off of [0, size / 2, size]) {
|
|
for (const c of cands) {
|
|
const d = c.v - (pos + off);
|
|
if (Math.abs(d) <= th && (!best || Math.abs(d) < Math.abs(best.d))) best = { d, v: c.v };
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function guideLine(axis, v, box, cands) {
|
|
const all = unionBounds([box, ...cands.filter((c) => Math.abs(c.v - v) < 0.5).map((c) => c.b)]);
|
|
return axis === "x"
|
|
? { x1: v, y1: all.y - 8, x2: v, y2: all.y + all.h + 8 }
|
|
: { x1: all.x - 8, y1: v, x2: all.x + all.w + 8, y2: v };
|
|
}
|
|
|
|
export function attachPointer(ed) {
|
|
const svg = ed.svg;
|
|
let it = null;
|
|
let lastClick = null;
|
|
let cursor = "";
|
|
|
|
const local = (e) => {
|
|
const r = ed.stage.getBoundingClientRect();
|
|
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
};
|
|
const tol = () => 4 / ed.view.zoom;
|
|
const grid = (v, e) => (e.altKey ? r2(v) : snap(v));
|
|
|
|
function setCursor(c) {
|
|
if (c !== cursor) svg.style.cursor = cursor = c;
|
|
}
|
|
|
|
function cursorFor(h, hit) {
|
|
if (it) {
|
|
if (it.kind === "pan") return "grabbing";
|
|
if (it.kind === "move") return "move";
|
|
if (it.kind === "resize") return it.cursor;
|
|
if (it.kind === "marquee" || it.kind === "dbl") return "default";
|
|
return it.kind === "text" ? "text" : "crosshair";
|
|
}
|
|
if (ed.spaceDown || ed.tool === "hand") return "grab";
|
|
if (ed.tool === "text") return "text";
|
|
if (ed.tool !== "select") return "crosshair";
|
|
if (h) {
|
|
if (h.kind === "resize") return h.which === "nw" || h.which === "se" ? "nwse-resize" : "nesw-resize";
|
|
return h.kind === "connect" ? "pointer" : "grab";
|
|
}
|
|
return hit ? "move" : "default";
|
|
}
|
|
|
|
function hoverAt(s, p) {
|
|
if (ed.tool !== "select" || ed.editing || ed.spaceDown) {
|
|
setHover(null, null);
|
|
setCursor(cursorFor(null, null));
|
|
return;
|
|
}
|
|
const h = ed.handleAt(s.x, s.y);
|
|
let hover = null;
|
|
let hit = null;
|
|
if (h) {
|
|
hover = h.kind === "connect" ? h.id : ed.hover;
|
|
} else {
|
|
hit = hitTest(ed.elements, p.x, p.y, tol(), ed.measure);
|
|
if (hit && isShape(hit)) hover = hit.id;
|
|
else if (!hit && ed.hover) {
|
|
const cur = ed.byId.get(ed.hover);
|
|
if (cur && pointInShape(cur, p.x, p.y, 34 / ed.view.zoom)) hover = cur.id;
|
|
}
|
|
}
|
|
setHover(hover, h ? `${h.kind}:${h.id}:${h.which}` : null);
|
|
setCursor(cursorFor(h, hit));
|
|
}
|
|
|
|
function setHover(id, handle) {
|
|
if (id === ed.hover && handle === ed.hoverHandle) return;
|
|
ed.hover = id;
|
|
ed.hoverHandle = handle;
|
|
ed.requestRender();
|
|
}
|
|
|
|
ed.refreshCursor = () => setCursor(cursorFor(null, null));
|
|
|
|
function begin(next, e) {
|
|
it = next;
|
|
try {
|
|
svg.setPointerCapture(e.pointerId);
|
|
} catch {
|
|
// The pointer may already be gone.
|
|
}
|
|
setHover(it.kind === "connect" ? ed.hover : null, null);
|
|
ed.beginInteraction(it);
|
|
setCursor(cursorFor(null, null));
|
|
}
|
|
|
|
function down(e) {
|
|
if (it || (e.button !== 0 && e.button !== 1)) return;
|
|
e.preventDefault();
|
|
if (ed.editing) ed.labels.commit({ refocus: false });
|
|
ed.focusCanvas();
|
|
const s = local(e);
|
|
const p = ed.toWorld(e.clientX, e.clientY);
|
|
ed.lastPointer = p;
|
|
const base = { pointerId: e.pointerId, sx: s.x, sy: s.y, start: p, moved: false };
|
|
if (e.button === 1 || ed.spaceDown || ed.tool === "hand") return begin({ ...base, kind: "pan", view: { ...ed.view } }, e);
|
|
|
|
const now = performance.now();
|
|
const dbl = !!lastClick && now - lastClick.t < DOUBLE_MS && Math.hypot(s.x - lastClick.x, s.y - lastClick.y) < 6;
|
|
lastClick = dbl ? null : { t: now, x: s.x, y: s.y };
|
|
const tool = ed.tool;
|
|
|
|
if (tool === "select") {
|
|
const h = ed.handleAt(s.x, s.y);
|
|
if (h) {
|
|
lastClick = null;
|
|
return beginHandle(h, base, e);
|
|
}
|
|
const hit = hitTest(ed.elements, p.x, p.y, tol(), ed.measure);
|
|
if (dbl) return begin({ ...base, kind: "dbl", hit: hit ? hit.id : null }, e);
|
|
if (hit) {
|
|
if (e.shiftKey) {
|
|
ed.select([hit.id], { toggle: true });
|
|
if (!ed.selection.has(hit.id)) return;
|
|
} else if (!ed.selection.has(hit.id)) {
|
|
ed.select([hit.id]);
|
|
}
|
|
return begin({ ...base, kind: "move", hit: hit.id, shift: e.shiftKey }, e);
|
|
}
|
|
if (!e.shiftKey) ed.select([]);
|
|
return begin({ ...base, kind: "marquee", baseSel: e.shiftKey ? [...ed.selection] : [] }, e);
|
|
}
|
|
if (tool === "text") return begin({ ...base, kind: "text" }, e);
|
|
if (tool === "pen") {
|
|
begin({ ...base, kind: "pen", points: [[r2(p.x), r2(p.y)]] }, e);
|
|
ed.preview = { points: it.points, color: ed.styles.pen.color, width: ed.styles.pen.width };
|
|
return;
|
|
}
|
|
if (tool === "arrow") {
|
|
const from = e.altKey ? null : shapeAt(ed.elements, p.x, p.y, tol());
|
|
return begin({ ...base, kind: "arrow", from: from ? from.id : null, x1: grid(p.x, e), y1: grid(p.y, e) }, e);
|
|
}
|
|
if (SHAPE_TYPES.includes(tool)) return begin({ ...base, kind: "create", type: tool }, e);
|
|
}
|
|
|
|
function beginHandle(h, base, e) {
|
|
const el = ed.byId.get(h.id);
|
|
if (!el) return;
|
|
if (h.kind === "resize") {
|
|
const cursorName = h.which === "nw" || h.which === "se" ? "nwse-resize" : "nesw-resize";
|
|
return begin({ ...base, kind: "resize", which: h.which, orig: el, cursor: cursorName }, e);
|
|
}
|
|
if (h.kind === "end") return begin({ ...base, kind: "endpoint", which: h.which, orig: el }, e);
|
|
return begin({ ...base, kind: "connect", src: el.id, dir: h.which, arrowId: null }, e);
|
|
}
|
|
|
|
function move(e) {
|
|
const s = local(e);
|
|
const p = ed.toWorld(e.clientX, e.clientY);
|
|
ed.lastPointer = p;
|
|
if (!it) return hoverAt(s, p);
|
|
if (e.pointerId !== it.pointerId) return;
|
|
if (it.kind === "pen") return drawPen(e);
|
|
if (!it.moved && Math.hypot(s.x - it.sx, s.y - it.sy) < DRAG) return;
|
|
it.moved = true;
|
|
if (it.kind === "pan") {
|
|
const z = it.view.zoom;
|
|
ed.setView({ zoom: z, x: it.view.x - (s.x - it.sx) / z, y: it.view.y - (s.y - it.sy) / z });
|
|
return;
|
|
}
|
|
const fn = { move: dragMove, marquee: dragMarquee, resize: dragResize, endpoint: dragEndpoint, connect: dragConnect, arrow: dragArrow, create: dragCreate }[it.kind];
|
|
if (fn) fn(p, e);
|
|
ed.requestRender();
|
|
}
|
|
|
|
// How far a pen stroke may stray from what was drawn when it is simplified.
|
|
const penTolerance = () => 0.6 / ed.view.zoom;
|
|
|
|
function drawPen(e) {
|
|
const events = e.getCoalescedEvents ? e.getCoalescedEvents() : [];
|
|
const min = 0.75 / ed.view.zoom;
|
|
for (const ev of events.length ? events : [e]) {
|
|
const q = ed.toWorld(ev.clientX, ev.clientY);
|
|
const last = it.points[it.points.length - 1];
|
|
if (Math.hypot(q.x - last[0], q.y - last[1]) >= min) it.points.push([r2(q.x), r2(q.y)]);
|
|
}
|
|
// A long stroke is simplified as it grows, so it never holds much more than a saved one can.
|
|
if (it.points.length >= 2 * MAX_PEN_POINTS) {
|
|
it.points = fitStroke(it.points, penTolerance());
|
|
if (ed.preview) ed.preview.points = it.points;
|
|
}
|
|
it.moved = true;
|
|
ed.requestRender();
|
|
}
|
|
|
|
function dragMove(p, e) {
|
|
if (!it.orig) {
|
|
it.orig = ed.elements;
|
|
it.ids = new Set(ed.selection);
|
|
const moving = it.orig.filter((el) => it.ids.has(el.id));
|
|
const boxes = (list) => list.map((el) => elementBounds(el, ed.byId, ed.measure));
|
|
it.box = unionBounds(boxes(moving.filter((el) => el.type !== "arrow"))) || unionBounds(boxes(moving));
|
|
it.xs = [];
|
|
it.ys = [];
|
|
for (const el of it.orig) {
|
|
if (it.ids.has(el.id) || !(isShape(el) || el.type === "text")) continue;
|
|
const b = elementBounds(el, ed.byId, ed.measure);
|
|
for (const v of [b.x, b.x + b.w / 2, b.x + b.w]) it.xs.push({ v, b });
|
|
for (const v of [b.y, b.y + b.h / 2, b.y + b.h]) it.ys.push({ v, b });
|
|
}
|
|
}
|
|
let dx = p.x - it.start.x;
|
|
let dy = p.y - it.start.y;
|
|
let sx = null;
|
|
let sy = null;
|
|
const b = it.box;
|
|
if (b && !e.altKey) {
|
|
const th = 6 / ed.view.zoom;
|
|
sx = snapAxis(b.x + dx, b.w, it.xs, th);
|
|
sy = snapAxis(b.y + dy, b.h, it.ys, th);
|
|
dx = sx ? dx + sx.d : snap(b.x + dx) - b.x;
|
|
dy = sy ? dy + sy.d : snap(b.y + dy) - b.y;
|
|
}
|
|
if (e.shiftKey) {
|
|
if (Math.abs(p.x - it.start.x) >= Math.abs(p.y - it.start.y)) { dy = 0; sy = null; } else { dx = 0; sx = null; }
|
|
}
|
|
ed.guides = [];
|
|
if (b) {
|
|
const nb = { x: b.x + dx, y: b.y + dy, w: b.w, h: b.h };
|
|
if (sx) ed.guides.push(guideLine("x", sx.v, nb, it.xs));
|
|
if (sy) ed.guides.push(guideLine("y", sy.v, nb, it.ys));
|
|
}
|
|
ed.setElements(it.orig.map((el) => (it.ids.has(el.id) ? translateElement(el, dx, dy) : el)));
|
|
}
|
|
|
|
function dragMarquee(p) {
|
|
const m = {
|
|
x: Math.min(it.start.x, p.x), y: Math.min(it.start.y, p.y),
|
|
w: Math.abs(p.x - it.start.x), h: Math.abs(p.y - it.start.y),
|
|
};
|
|
ed.marquee = m;
|
|
const inside = ed.elements.filter((el) => {
|
|
const b = elementBounds(el, ed.byId, ed.measure);
|
|
return b && boxContains(m, b);
|
|
});
|
|
ed.select([...it.baseSel, ...inside.map((el) => el.id)]);
|
|
}
|
|
|
|
function dragResize(p, e) {
|
|
const s = it.orig;
|
|
const west = it.which.includes("w");
|
|
const north = it.which.includes("n");
|
|
const fx = west ? s.x + s.w : s.x;
|
|
const fy = north ? s.y + s.h : s.y;
|
|
const px = grid(p.x, e);
|
|
const py = grid(p.y, e);
|
|
let w = clamp(west ? fx - px : px - fx, 20, MAX_SIDE);
|
|
let h = clamp(north ? fy - py : py - fy, 20, MAX_SIDE);
|
|
if (e.shiftKey) {
|
|
// Keeps the proportions, up to the size limit.
|
|
const k = Math.min(Math.max(w / s.w, h / s.h), MAX_SIDE / Math.max(s.w, s.h));
|
|
w = Math.max(20, s.w * k);
|
|
h = Math.max(20, s.h * k);
|
|
}
|
|
ed.replaceElement({ ...s, x: r2(west ? fx - w : fx), y: r2(north ? fy - h : fy), w: r2(w), h: r2(h) });
|
|
}
|
|
|
|
function dragEndpoint(p, e) {
|
|
const a = it.orig;
|
|
const isStart = it.which === "start";
|
|
const other = isStart ? a.to : a.from;
|
|
const target = e.altKey ? null : shapeAt(ed.elements, p.x, p.y, tol(), other);
|
|
ed.target = target ? target.id : null;
|
|
const x = target ? r2(p.x) : grid(p.x, e);
|
|
const y = target ? r2(p.y) : grid(p.y, e);
|
|
const id = target ? target.id : null;
|
|
ed.replaceElement(isStart ? { ...a, from: id, x1: x, y1: y } : { ...a, to: id, x2: x, y2: y });
|
|
}
|
|
|
|
// Shared by the connect handle and the arrow tool: a live arrow from `from` to the pointer.
|
|
function liveArrow(p, e, from, extra = {}) {
|
|
const src = from ? ed.byId.get(from) : null;
|
|
const inside = src && pointInShape(src, p.x, p.y, 2 / ed.view.zoom);
|
|
const target = inside || e.altKey ? null : shapeAt(ed.elements, p.x, p.y, tol(), from);
|
|
ed.target = target ? target.id : null;
|
|
if (inside) {
|
|
if (it.arrowId && ed.byId.has(it.arrowId)) ed.setElements(ed.elements.filter((el) => el.id !== it.arrowId));
|
|
return;
|
|
}
|
|
if (!it.arrowId) it.arrowId = newId("a");
|
|
const x2 = target ? r2(p.x) : grid(p.x, e);
|
|
const y2 = target ? r2(p.y) : grid(p.y, e);
|
|
ed.upsertElement(ed.newArrow({ id: it.arrowId, from, to: target ? target.id : null, x2, y2, ...extra }));
|
|
}
|
|
|
|
function dragConnect(p, e) {
|
|
liveArrow(p, e, it.src);
|
|
}
|
|
|
|
function dragArrow(p, e) {
|
|
liveArrow(p, e, it.from, { x1: it.x1, y1: it.y1 });
|
|
}
|
|
|
|
function dragCreate(p, e) {
|
|
const x0 = grid(it.start.x, e);
|
|
const y0 = grid(it.start.y, e);
|
|
const x1 = grid(p.x, e);
|
|
const y1 = grid(p.y, e);
|
|
let w = Math.abs(x1 - x0);
|
|
let h = Math.abs(y1 - y0);
|
|
if (e.shiftKey) w = h = Math.max(w, h);
|
|
if (!it.id) it.id = newId(idPrefix(it.type));
|
|
ed.upsertElement(ed.newShape(it.type, {
|
|
id: it.id, x: x1 < x0 ? x0 - w : x0, y: y1 < y0 ? y0 - h : y0, w: Math.max(10, w), h: Math.max(10, h),
|
|
}));
|
|
}
|
|
|
|
function arrowLength(id) {
|
|
const a = ed.byId.get(id);
|
|
const g = a && arrowGeometry(a, ed.byId);
|
|
return g ? Math.hypot(g.end.x - g.start.x, g.end.y - g.start.y) : 0;
|
|
}
|
|
|
|
function createText(p) {
|
|
const before = ed.elements;
|
|
const lh = lineHeight(fontSizeOf(ed.styles.text));
|
|
const t = ed.newText({ x: snap(p.x), y: snap(p.y - lh / 2) });
|
|
ed.setElements([...ed.elements, t], { record: false });
|
|
ed.select([t.id]);
|
|
ed.setTool("select");
|
|
ed.labels.open(t.id, { before });
|
|
}
|
|
|
|
function editOrCreateText(p) {
|
|
const hit = hitTest(ed.elements, p.x, p.y, tol(), ed.measure);
|
|
if (hit && hit.type !== "pen") {
|
|
ed.setTool("select");
|
|
ed.select([hit.id]);
|
|
ed.labels.open(hit.id);
|
|
} else {
|
|
createText(p);
|
|
}
|
|
}
|
|
|
|
function up(e) {
|
|
if (it && e.pointerId === it.pointerId) finish(e, false);
|
|
}
|
|
|
|
function finish(e, cancelled) {
|
|
const cur = it;
|
|
it = null;
|
|
try {
|
|
svg.releasePointerCapture(cur.pointerId);
|
|
} catch {
|
|
// Already released.
|
|
}
|
|
if (cancelled) {
|
|
ed.endInteraction({ revert: cur.kind !== "pan" && cur.kind !== "marquee" });
|
|
ed.refreshCursor();
|
|
return;
|
|
}
|
|
const start = ed.changeStart;
|
|
const p = cur.moved ? ed.lastPointer || cur.start : cur.start;
|
|
let after = null;
|
|
|
|
if (cur.kind === "move") {
|
|
if (!cur.moved && !cur.shift && ed.selection.size > 1) ed.select([cur.hit]);
|
|
} else if (cur.kind === "endpoint") {
|
|
if (arrowLength(cur.orig.id) < 4) ed.replaceElement(cur.orig);
|
|
} else if (cur.kind === "dbl") {
|
|
const hit = cur.hit ? ed.byId.get(cur.hit) : null;
|
|
after = () => {
|
|
if (!hit) return createText(cur.start);
|
|
ed.select([hit.id]);
|
|
if (hit.type !== "pen") ed.labels.open(hit.id);
|
|
};
|
|
} else if (cur.kind === "text") {
|
|
after = () => editOrCreateText(cur.start);
|
|
} else if (cur.kind === "pen") {
|
|
const el = normalizeElement({ id: newId("p"), type: "pen", points: fitStroke(cur.points, penTolerance()), color: ed.styles.pen.color, width: ed.styles.pen.width });
|
|
if (el) ed.setElements([...ed.elements, el]);
|
|
} else if (cur.kind === "connect") {
|
|
if (!cur.moved) {
|
|
after = () => ed.addConnected(cur.src, cur.dir);
|
|
} else if (cur.arrowId && ed.byId.has(cur.arrowId)) {
|
|
const arrow = ed.byId.get(cur.arrowId);
|
|
const src = ed.byId.get(cur.src);
|
|
const ddx = p.x - (src.x + src.w / 2);
|
|
const ddy = p.y - (src.y + src.h / 2);
|
|
ed.lastDirection = Math.abs(ddx) >= Math.abs(ddy) ? (ddx >= 0 ? "right" : "left") : (ddy >= 0 ? "down" : "up");
|
|
if (arrow.to) {
|
|
after = () => ed.select([arrow.id]);
|
|
} else {
|
|
const shape = ed.newShape(src.type, {
|
|
...src, id: newId(idPrefix(src.type)), text: "", x: snap(p.x - src.w / 2), y: snap(p.y - src.h / 2),
|
|
});
|
|
ed.setElements([...ed.elements.map((el) => (el.id === arrow.id ? { ...el, to: shape.id } : el)), shape]);
|
|
ed.endInteraction({ record: false });
|
|
ed.select([shape.id]);
|
|
ed.labels.open(shape.id, { fresh: true, before: start });
|
|
ed.refreshCursor();
|
|
return;
|
|
}
|
|
}
|
|
} else if (cur.kind === "arrow") {
|
|
if (!cur.arrowId || !ed.byId.has(cur.arrowId)) {
|
|
if (!cur.moved) ed.emit("hint", "Drag to draw an arrow. Start on a shape to connect it.");
|
|
} else if (arrowLength(cur.arrowId) < 8) {
|
|
ed.setElements(ed.elements.filter((el) => el.id !== cur.arrowId));
|
|
} else {
|
|
after = () => {
|
|
ed.select([cur.arrowId]);
|
|
ed.setTool("select");
|
|
};
|
|
}
|
|
} else if (cur.kind === "create") {
|
|
let el = cur.id ? ed.byId.get(cur.id) : null;
|
|
const fresh = !el || el.w < 20 || el.h < 20;
|
|
if (fresh) {
|
|
const [w, h] = DEFAULT_SIZES[cur.type];
|
|
el = ed.newShape(cur.type, { id: cur.id || newId(idPrefix(cur.type)), x: snap(cur.start.x - w / 2), y: snap(cur.start.y - h / 2), w, h });
|
|
ed.upsertElement(el);
|
|
}
|
|
ed.endInteraction({ record: false });
|
|
ed.select([el.id]);
|
|
ed.setTool("select");
|
|
ed.labels.open(el.id, { fresh, before: start });
|
|
ed.refreshCursor();
|
|
return;
|
|
}
|
|
ed.endInteraction();
|
|
if (after) after();
|
|
ed.refreshCursor();
|
|
}
|
|
|
|
function cancel(e) {
|
|
if (it && e.pointerId === it.pointerId) finish(e, true);
|
|
}
|
|
|
|
function wheel(e) {
|
|
e.preventDefault();
|
|
const s = local(e);
|
|
if (e.ctrlKey || e.metaKey) {
|
|
const dy = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY;
|
|
ed.zoomAt(Math.exp(-dy * 0.002), s.x, s.y);
|
|
return;
|
|
}
|
|
let dx = e.deltaMode === 1 ? e.deltaX * 16 : e.deltaX;
|
|
let dy = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY;
|
|
if (e.shiftKey && !dx) {
|
|
dx = dy;
|
|
dy = 0;
|
|
}
|
|
ed.panBy(dx, dy);
|
|
}
|
|
|
|
svg.addEventListener("pointerdown", down);
|
|
svg.addEventListener("pointermove", move);
|
|
svg.addEventListener("pointerup", up);
|
|
svg.addEventListener("pointercancel", cancel);
|
|
svg.addEventListener("lostpointercapture", (e) => {
|
|
if (it && e.pointerId === it.pointerId) finish(e, false);
|
|
});
|
|
svg.addEventListener("pointerleave", () => {
|
|
if (!it) setHover(null, null);
|
|
});
|
|
svg.addEventListener("mousedown", (e) => e.preventDefault());
|
|
svg.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
svg.addEventListener("wheel", wheel, { passive: false });
|
|
|
|
ed.cancelPointer = () => {
|
|
if (!it) return;
|
|
const cur = it;
|
|
it = null;
|
|
try {
|
|
svg.releasePointerCapture(cur.pointerId);
|
|
} catch {
|
|
// Already released.
|
|
}
|
|
ed.endInteraction({ revert: cur.kind !== "pan" && cur.kind !== "marquee" });
|
|
ed.refreshCursor();
|
|
};
|
|
ed.pointerActive = () => !!it;
|
|
}
|