@orangeworks/orangetree 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/LICENSE +8 -0
- package/README.md +55 -0
- package/dist/bin/orangetree.js +89 -0
- package/dist/public/api.js +33 -0
- package/dist/public/app.js +650 -0
- package/dist/public/canvas.js +362 -0
- package/dist/public/chat.js +890 -0
- package/dist/public/connection.js +102 -0
- package/dist/public/dirpicker.js +93 -0
- package/dist/public/explorer.js +299 -0
- package/dist/public/guidelines.js +35 -0
- package/dist/public/i18n.js +959 -0
- package/dist/public/index.html +75 -0
- package/dist/public/insight.js +40 -0
- package/dist/public/logo.svg +5 -0
- package/dist/public/md.js +103 -0
- package/dist/public/pairing.js +114 -0
- package/dist/public/projectSettings.js +150 -0
- package/dist/public/slash.js +95 -0
- package/dist/public/style.css +649 -0
- package/dist/public/ui.js +372 -0
- package/dist/public/viewer.html +156 -0
- package/dist/server.js +18573 -0
- package/package.json +45 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { t } from "./i18n.js";
|
|
2
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
3
|
+
const COL_W = 170;
|
|
4
|
+
const ROW_H = 120;
|
|
5
|
+
const R = 24;
|
|
6
|
+
const DEFAULT_FIT_SCALE = 0.7;
|
|
7
|
+
const FIT_PAD = 72;
|
|
8
|
+
const SCALE_KEY = "otree-fit-scale";
|
|
9
|
+
const STATUS_VIEW = {
|
|
10
|
+
preparing: { fill: "#eeeeee", stroke: "#cccccc" },
|
|
11
|
+
in_progress: { fill: "#e8f1ea", stroke: "#2F6B3D" },
|
|
12
|
+
blocked: { fill: "#eef1f4", stroke: "#64748b" },
|
|
13
|
+
waiting_handoff: { fill: "#eaf2ee", stroke: "#2F6B3D" },
|
|
14
|
+
review: { fill: "#fbf0e4", stroke: "#B45309" },
|
|
15
|
+
done: { fill: "#fbe9dc", stroke: "#E0612A" }
|
|
16
|
+
};
|
|
17
|
+
function svgEl(tag, attrs) {
|
|
18
|
+
const e = document.createElementNS(SVG_NS, tag);
|
|
19
|
+
for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, String(v));
|
|
20
|
+
return e;
|
|
21
|
+
}
|
|
22
|
+
const GLYPHS = {
|
|
23
|
+
preparing: [
|
|
24
|
+
["circle", { r: 7, fill: "none", stroke: "#bbb", "stroke-width": 2, "stroke-dasharray": "4 4" }]
|
|
25
|
+
],
|
|
26
|
+
blocked: [
|
|
27
|
+
["ellipse", { cx: 0, cy: 1, rx: 7, ry: 10, fill: "#64748b" }],
|
|
28
|
+
["path", { d: "M 0 -9 Q 4 -1 0 7 Q -4 -1 0 -9 Z", fill: "#94a3b8" }]
|
|
29
|
+
],
|
|
30
|
+
in_progress: [
|
|
31
|
+
["path", { d: "M 0 9 Q -16 2 -11 -13 Q 5 -7 0 9 Z", fill: "#2F6B3D" }],
|
|
32
|
+
["path", { d: "M 2 10 Q 15 0 10 -14 Q -5 -5 2 10 Z", fill: "#4C9A5E" }],
|
|
33
|
+
["path", { d: "M 2 10 Q 5 -2 10 -14", stroke: "#2F6B3D", "stroke-width": 1.2, fill: "none" }]
|
|
34
|
+
],
|
|
35
|
+
waiting_handoff: [
|
|
36
|
+
["line", { x1: 0, y1: 14, x2: 0, y2: 3, stroke: "#2F6B3D", "stroke-width": 1.8 }],
|
|
37
|
+
["path", { d: "M 0 3 Q -3 -12 0 -17 Q 3 -12 0 3 Z", fill: "#C2710C" }],
|
|
38
|
+
["path", { d: "M 0 6 Q -9 1 -7 -9 Q 0 -5 0 6 Z", fill: "#3f8a52" }],
|
|
39
|
+
["path", { d: "M 0 6 Q 9 1 7 -9 Q 0 -5 0 6 Z", fill: "#4C9A5E" }]
|
|
40
|
+
],
|
|
41
|
+
review: [
|
|
42
|
+
["ellipse", { cx: 0, cy: -9, rx: 4.5, ry: 7.5, fill: "#C2710C" }],
|
|
43
|
+
["ellipse", { cx: 9, cy: -3, rx: 4.5, ry: 7.5, fill: "#C2710C", transform: "rotate(72 9 -3)" }],
|
|
44
|
+
["ellipse", { cx: 5.5, cy: 8, rx: 4.5, ry: 7.5, fill: "#C2710C", transform: "rotate(144 5.5 8)" }],
|
|
45
|
+
["ellipse", { cx: -5.5, cy: 8, rx: 4.5, ry: 7.5, fill: "#C2710C", transform: "rotate(216 -5.5 8)" }],
|
|
46
|
+
["ellipse", { cx: -9, cy: -3, rx: 4.5, ry: 7.5, fill: "#C2710C", transform: "rotate(288 -9 -3)" }],
|
|
47
|
+
["circle", { r: 4, fill: "#7C3A06" }]
|
|
48
|
+
],
|
|
49
|
+
done: [
|
|
50
|
+
["circle", { cx: 0, cy: 3, r: 11, fill: "#E0612A" }],
|
|
51
|
+
["circle", { cx: -3.5, cy: -0.5, r: 3, fill: "#EE8A5C" }],
|
|
52
|
+
["line", { x1: 0, y1: -8, x2: 0, y2: -13, stroke: "#2F6B3D", "stroke-width": 1.8 }],
|
|
53
|
+
["path", { d: "M 0 -11 Q 9 -16 12 -9 Q 3 -6 0 -11 Z", fill: "#4C9A5E" }]
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
function makeGlyph(status) {
|
|
57
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
58
|
+
g.setAttribute("class", "glyph");
|
|
59
|
+
for (const [tag, attrs] of GLYPHS[status] ?? GLYPHS.in_progress) g.append(svgEl(tag, attrs));
|
|
60
|
+
return g;
|
|
61
|
+
}
|
|
62
|
+
class Canvas {
|
|
63
|
+
svg;
|
|
64
|
+
onSelect;
|
|
65
|
+
onMove;
|
|
66
|
+
onContextMenu;
|
|
67
|
+
view;
|
|
68
|
+
fitScale;
|
|
69
|
+
nodes = [];
|
|
70
|
+
sig = "";
|
|
71
|
+
constructor(svg, { onSelect, onMove, onContextMenu }) {
|
|
72
|
+
this.svg = svg;
|
|
73
|
+
this.onSelect = onSelect;
|
|
74
|
+
this.onMove = onMove;
|
|
75
|
+
this.onContextMenu = onContextMenu;
|
|
76
|
+
this.view = { x: -80, y: -60, w: 1200, h: 800 };
|
|
77
|
+
const saved = parseFloat(localStorage.getItem(SCALE_KEY) ?? "");
|
|
78
|
+
this.fitScale = saved > 0 ? saved : DEFAULT_FIT_SCALE;
|
|
79
|
+
this.applyView();
|
|
80
|
+
this.bindPanZoom();
|
|
81
|
+
}
|
|
82
|
+
applyView() {
|
|
83
|
+
const { x, y, w, h } = this.view;
|
|
84
|
+
this.svg.setAttribute("viewBox", `${x} ${y} ${w} ${h}`);
|
|
85
|
+
}
|
|
86
|
+
// Remember a user-chosen zoom as the fit cap (so the next auto-fit lands the same size).
|
|
87
|
+
rememberScale() {
|
|
88
|
+
const cw = this.svg.clientWidth || 800;
|
|
89
|
+
const scale = Math.max(0.1, Math.min(3, cw / this.view.w));
|
|
90
|
+
this.fitScale = scale;
|
|
91
|
+
localStorage.setItem(SCALE_KEY, String(scale));
|
|
92
|
+
}
|
|
93
|
+
// Zoom about the view center (factor>1 = out, <1 = in).
|
|
94
|
+
zoomBy(factor) {
|
|
95
|
+
const cx = this.view.x + this.view.w / 2;
|
|
96
|
+
const cy = this.view.y + this.view.h / 2;
|
|
97
|
+
this.view.w *= factor;
|
|
98
|
+
this.view.h *= factor;
|
|
99
|
+
this.view.x = cx - this.view.w / 2;
|
|
100
|
+
this.view.y = cy - this.view.h / 2;
|
|
101
|
+
this.applyView();
|
|
102
|
+
this.rememberScale();
|
|
103
|
+
}
|
|
104
|
+
// Fit the whole tree to the top-center of the canvas (capped so nodes don't blow up).
|
|
105
|
+
fitView() {
|
|
106
|
+
const nodes = this.nodes;
|
|
107
|
+
if (!nodes.length) return;
|
|
108
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
109
|
+
for (const n of nodes) {
|
|
110
|
+
minX = Math.min(minX, n.px);
|
|
111
|
+
maxX = Math.max(maxX, n.px);
|
|
112
|
+
minY = Math.min(minY, n.py);
|
|
113
|
+
maxY = Math.max(maxY, n.py);
|
|
114
|
+
}
|
|
115
|
+
const cw = this.svg.clientWidth || 800;
|
|
116
|
+
const ch = this.svg.clientHeight || 600;
|
|
117
|
+
const aspect = cw / ch;
|
|
118
|
+
const contentW = maxX - minX + R * 2;
|
|
119
|
+
const contentH = maxY - minY + R * 2;
|
|
120
|
+
let vw = Math.max(contentW + FIT_PAD * 2, (contentH + FIT_PAD * 2) * aspect);
|
|
121
|
+
const minVw = cw / this.fitScale;
|
|
122
|
+
if (vw < minVw) vw = minVw;
|
|
123
|
+
const vh = vw / aspect;
|
|
124
|
+
this.view.w = vw;
|
|
125
|
+
this.view.h = vh;
|
|
126
|
+
this.view.x = (minX + maxX) / 2 - vw / 2;
|
|
127
|
+
this.view.y = minY - R - FIT_PAD;
|
|
128
|
+
this.applyView();
|
|
129
|
+
}
|
|
130
|
+
bindPanZoom() {
|
|
131
|
+
let panning = null;
|
|
132
|
+
this.svg.addEventListener("pointerdown", (e) => {
|
|
133
|
+
if (e.target?.closest(".node")) return;
|
|
134
|
+
panning = { px: e.clientX, py: e.clientY, vx: this.view.x, vy: this.view.y };
|
|
135
|
+
this.svg.classList.add("panning");
|
|
136
|
+
this.svg.setPointerCapture(e.pointerId);
|
|
137
|
+
});
|
|
138
|
+
this.svg.addEventListener("pointermove", (e) => {
|
|
139
|
+
if (!panning) return;
|
|
140
|
+
const scale = this.view.w / this.svg.clientWidth;
|
|
141
|
+
this.view.x = panning.vx - (e.clientX - panning.px) * scale;
|
|
142
|
+
this.view.y = panning.vy - (e.clientY - panning.py) * scale;
|
|
143
|
+
this.applyView();
|
|
144
|
+
});
|
|
145
|
+
this.svg.addEventListener("pointerup", () => {
|
|
146
|
+
panning = null;
|
|
147
|
+
this.svg.classList.remove("panning");
|
|
148
|
+
});
|
|
149
|
+
this.svg.addEventListener("wheel", (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
const factor = e.deltaY > 0 ? 1.12 : 1 / 1.12;
|
|
152
|
+
const rect = this.svg.getBoundingClientRect();
|
|
153
|
+
const cx = this.view.x + (e.clientX - rect.left) / rect.width * this.view.w;
|
|
154
|
+
const cy = this.view.y + (e.clientY - rect.top) / rect.height * this.view.h;
|
|
155
|
+
this.view.w *= factor;
|
|
156
|
+
this.view.h *= factor;
|
|
157
|
+
this.view.x = cx - (cx - this.view.x) * factor;
|
|
158
|
+
this.view.y = cy - (cy - this.view.y) * factor;
|
|
159
|
+
this.applyView();
|
|
160
|
+
this.rememberScale();
|
|
161
|
+
}, { passive: false });
|
|
162
|
+
}
|
|
163
|
+
// Tree layout: assign x-slots from leaves up, parent centered over children.
|
|
164
|
+
layout(nodes) {
|
|
165
|
+
const byId = {};
|
|
166
|
+
for (const n of nodes) byId[n.id] = { ...n };
|
|
167
|
+
const children = {};
|
|
168
|
+
const roots = [];
|
|
169
|
+
for (const n of Object.values(byId)) {
|
|
170
|
+
if (n.parentId && byId[n.parentId]) (children[n.parentId] ??= []).push(n);
|
|
171
|
+
else roots.push(n);
|
|
172
|
+
}
|
|
173
|
+
const order = (a, b) => a.createdAt - b.createdAt;
|
|
174
|
+
let slot = 0;
|
|
175
|
+
const place = (n, depth) => {
|
|
176
|
+
const kids = (children[n.id] ?? []).sort(order);
|
|
177
|
+
if (!kids.length) {
|
|
178
|
+
n.gx = slot++;
|
|
179
|
+
} else {
|
|
180
|
+
for (const k of kids) place(k, depth + 1);
|
|
181
|
+
n.gx = (kids[0].gx + kids[kids.length - 1].gx) / 2;
|
|
182
|
+
}
|
|
183
|
+
n.gy = depth;
|
|
184
|
+
};
|
|
185
|
+
for (const r of roots.sort(order)) {
|
|
186
|
+
place(r, 0);
|
|
187
|
+
slot++;
|
|
188
|
+
}
|
|
189
|
+
for (const n of Object.values(byId)) {
|
|
190
|
+
if (n.pos && typeof n.pos.x === "number") {
|
|
191
|
+
n.px = n.pos.x;
|
|
192
|
+
n.py = n.pos.y;
|
|
193
|
+
} else {
|
|
194
|
+
n.px = n.gx * COL_W;
|
|
195
|
+
n.py = n.gy * ROW_H;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { byId };
|
|
199
|
+
}
|
|
200
|
+
// tree.nodes = full layout set (whole project). visibleIds = ids actually shown (null = all).
|
|
201
|
+
// dimmedIds = subset of shown ids drawn faded (archived nodes under '아카이브 표시'). S7.
|
|
202
|
+
render(tree, selectedId, candidates = /* @__PURE__ */ new Set(), busy = /* @__PURE__ */ new Set(), visibleIds = null, dimmedIds = /* @__PURE__ */ new Set()) {
|
|
203
|
+
const nodes = Object.values(tree.nodes ?? {});
|
|
204
|
+
this.svg.replaceChildren();
|
|
205
|
+
this.nodes = [];
|
|
206
|
+
if (!nodes.length) {
|
|
207
|
+
this.sig = "";
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const { byId } = this.layout(nodes);
|
|
211
|
+
const isVisible = (id) => !visibleIds || visibleIds.has(id);
|
|
212
|
+
const shown = Object.values(byId).filter((n) => isVisible(n.id));
|
|
213
|
+
this.nodes = shown;
|
|
214
|
+
const edges = document.createElementNS(SVG_NS, "g");
|
|
215
|
+
const nodeLayer = document.createElementNS(SVG_NS, "g");
|
|
216
|
+
this.svg.append(edges, nodeLayer);
|
|
217
|
+
for (const n of shown) {
|
|
218
|
+
if (n.parentId && byId[n.parentId] && isVisible(n.parentId)) {
|
|
219
|
+
const p = byId[n.parentId];
|
|
220
|
+
edges.append(this.edge(p.px, p.py + R, n.px, n.py - R, false));
|
|
221
|
+
}
|
|
222
|
+
for (const dep of n.dependsOn ?? []) {
|
|
223
|
+
const d = byId[dep];
|
|
224
|
+
if (d && isVisible(dep)) edges.append(this.edge(d.px, d.py, n.px, n.py, true));
|
|
225
|
+
}
|
|
226
|
+
nodeLayer.append(this.node(n, n.id === selectedId, candidates.has(n.id), busy.has(n.id), dimmedIds.has(n.id)));
|
|
227
|
+
}
|
|
228
|
+
const sig = this.nodes.map((n) => n.id).sort().join(",");
|
|
229
|
+
if (sig !== this.sig) {
|
|
230
|
+
this.sig = sig;
|
|
231
|
+
this.fitView();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Called during drag: redraw only the edge layer from current px/py.
|
|
235
|
+
redrawEdges() {
|
|
236
|
+
const edgeLayer = this.svg.querySelector("g");
|
|
237
|
+
if (!edgeLayer || !this.nodes.length) return;
|
|
238
|
+
const byId = {};
|
|
239
|
+
for (const n of this.nodes) byId[n.id] = n;
|
|
240
|
+
edgeLayer.replaceChildren();
|
|
241
|
+
for (const n of this.nodes) {
|
|
242
|
+
if (n.parentId && byId[n.parentId]) {
|
|
243
|
+
const p = byId[n.parentId];
|
|
244
|
+
edgeLayer.append(this.edge(p.px, p.py + R, n.px, n.py - R, false));
|
|
245
|
+
}
|
|
246
|
+
for (const dep of n.dependsOn ?? []) {
|
|
247
|
+
const d = byId[dep];
|
|
248
|
+
if (d) edgeLayer.append(this.edge(d.px, d.py, n.px, n.py, true));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
edge(x1, y1, x2, y2, isDep) {
|
|
253
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
254
|
+
const my = (y1 + y2) / 2;
|
|
255
|
+
path.setAttribute("d", `M ${x1} ${y1} C ${x1} ${my}, ${x2} ${my}, ${x2} ${y2}`);
|
|
256
|
+
path.setAttribute("class", isDep ? "edge dep" : "edge");
|
|
257
|
+
return path;
|
|
258
|
+
}
|
|
259
|
+
rimBadge(symbol, fill, size = 16) {
|
|
260
|
+
const t2 = document.createElementNS(SVG_NS, "text");
|
|
261
|
+
t2.setAttribute("x", "18");
|
|
262
|
+
t2.setAttribute("y", "-18");
|
|
263
|
+
t2.setAttribute("text-anchor", "middle");
|
|
264
|
+
t2.setAttribute("dominant-baseline", "central");
|
|
265
|
+
t2.style.fill = fill;
|
|
266
|
+
t2.style.fontSize = `${size}px`;
|
|
267
|
+
t2.style.fontWeight = "800";
|
|
268
|
+
t2.style.pointerEvents = "none";
|
|
269
|
+
t2.textContent = symbol;
|
|
270
|
+
return t2;
|
|
271
|
+
}
|
|
272
|
+
node(n, selected, candidate, busy, dimmed = false) {
|
|
273
|
+
const view = STATUS_VIEW[n.status] ?? STATUS_VIEW.in_progress;
|
|
274
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
275
|
+
g.setAttribute("class", `node${selected ? " selected" : ""}${n.status === "preparing" ? " preparing" : ""}${dimmed ? " dimmed" : ""}`);
|
|
276
|
+
g.setAttribute("transform", `translate(${n.px}, ${n.py})`);
|
|
277
|
+
g.dataset.id = n.id;
|
|
278
|
+
const body = document.createElementNS(SVG_NS, "circle");
|
|
279
|
+
body.setAttribute("class", "body");
|
|
280
|
+
body.setAttribute("r", String(R));
|
|
281
|
+
body.setAttribute("fill", view.fill);
|
|
282
|
+
body.setAttribute("stroke", view.stroke);
|
|
283
|
+
if (busy) {
|
|
284
|
+
for (const delay of ["0s", "0.8s"]) {
|
|
285
|
+
const ring = document.createElementNS(SVG_NS, "circle");
|
|
286
|
+
ring.setAttribute("class", "busy-ring");
|
|
287
|
+
ring.setAttribute("r", String(R));
|
|
288
|
+
ring.style.animationDelay = delay;
|
|
289
|
+
g.append(ring);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const icon = makeGlyph(n.status);
|
|
293
|
+
const otid = document.createElementNS(SVG_NS, "text");
|
|
294
|
+
otid.setAttribute("class", "otid");
|
|
295
|
+
otid.setAttribute("y", String(-R - 8));
|
|
296
|
+
otid.textContent = n.id;
|
|
297
|
+
const label = document.createElementNS(SVG_NS, "text");
|
|
298
|
+
label.setAttribute("class", "label");
|
|
299
|
+
label.setAttribute("y", String(R + 16));
|
|
300
|
+
label.textContent = n.title.length > 18 ? `${n.title.slice(0, 17)}\u2026` : n.title;
|
|
301
|
+
if (n.mergeState === "conflict") {
|
|
302
|
+
g.append(this.rimBadge("!", "#dc2626"));
|
|
303
|
+
} else if (n.warn) {
|
|
304
|
+
const w = this.rimBadge("!", "#dc2626");
|
|
305
|
+
const t2 = document.createElementNS(SVG_NS, "title");
|
|
306
|
+
t2.textContent = n.warn;
|
|
307
|
+
w.append(t2);
|
|
308
|
+
w.style.pointerEvents = "auto";
|
|
309
|
+
g.append(w);
|
|
310
|
+
} else if ((n.notices ?? []).some((x) => x.kind === "code-change" && !x.delivered && !x.ack)) {
|
|
311
|
+
g.append(this.rimBadge("\u27F3", "#e8740c", 15));
|
|
312
|
+
}
|
|
313
|
+
if (candidate) {
|
|
314
|
+
const ring = document.createElementNS(SVG_NS, "circle");
|
|
315
|
+
ring.setAttribute("r", String(R + 6));
|
|
316
|
+
ring.setAttribute("class", "candidate-ring");
|
|
317
|
+
g.prepend(ring);
|
|
318
|
+
const hint = document.createElementNS(SVG_NS, "text");
|
|
319
|
+
hint.setAttribute("class", "otid");
|
|
320
|
+
hint.setAttribute("y", String(-R - 20));
|
|
321
|
+
hint.textContent = t("canvas.candidate.hint");
|
|
322
|
+
g.append(hint);
|
|
323
|
+
}
|
|
324
|
+
g.append(body, icon, otid, label);
|
|
325
|
+
g.addEventListener("pointerdown", (e) => {
|
|
326
|
+
e.stopPropagation();
|
|
327
|
+
const scale = this.view.w / this.svg.clientWidth;
|
|
328
|
+
const start = { px: e.clientX, py: e.clientY, x: n.px, y: n.py, moved: false };
|
|
329
|
+
const onMove = (ev) => {
|
|
330
|
+
if (!start.moved && Math.hypot(ev.clientX - start.px, ev.clientY - start.py) < 4) return;
|
|
331
|
+
start.moved = true;
|
|
332
|
+
const dx = (ev.clientX - start.px) * scale;
|
|
333
|
+
const dy = (ev.clientY - start.py) * scale;
|
|
334
|
+
n.px = start.x + dx;
|
|
335
|
+
n.py = start.y + dy;
|
|
336
|
+
g.setAttribute("transform", `translate(${n.px}, ${n.py})`);
|
|
337
|
+
this.redrawEdges();
|
|
338
|
+
};
|
|
339
|
+
const onUp = () => {
|
|
340
|
+
window.removeEventListener("pointermove", onMove);
|
|
341
|
+
window.removeEventListener("pointerup", onUp);
|
|
342
|
+
if (start.moved) this.onMove(n.id, { x: Math.round(n.px), y: Math.round(n.py) });
|
|
343
|
+
else this.onSelect(n.id);
|
|
344
|
+
};
|
|
345
|
+
window.addEventListener("pointermove", onMove);
|
|
346
|
+
window.addEventListener("pointerup", onUp);
|
|
347
|
+
});
|
|
348
|
+
g.addEventListener("dblclick", (e) => {
|
|
349
|
+
e.stopPropagation();
|
|
350
|
+
this.onMove(n.id, null);
|
|
351
|
+
});
|
|
352
|
+
g.addEventListener("contextmenu", (e) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
e.stopPropagation();
|
|
355
|
+
this.onContextMenu?.(n.id, e.clientX, e.clientY);
|
|
356
|
+
});
|
|
357
|
+
return g;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
export {
|
|
361
|
+
Canvas
|
|
362
|
+
};
|