visualproof 0.2.0 → 0.3.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/README.md +40 -26
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
- package/dist/server/agent.js +84 -86
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/live.js +6 -0
- package/dist/server/main.js +136 -8
- package/dist/server/mcptools.js +4 -1
- package/dist/server/pipeline.js +15 -9
- package/dist/server/readonlygate.js +1 -1
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +10 -1
- package/dist/server/view.js +0 -7
- package/dist/server/views.js +24 -0
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +7 -2
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/pastgraph.js +6 -6
- package/dist/shared/review.js +62 -5
- package/dist/web/bundle/app.js +713 -164
- package/dist/web/bundle/edges.js +1 -1
- package/dist/web/src/agentmark.js +14 -0
- package/dist/web/src/artwork.js +1000 -0
- package/dist/web/src/bundles.js +151 -0
- package/dist/web/src/canvas.js +3694 -0
- package/dist/web/src/drag.js +61 -0
- package/dist/web/src/edgequeue.js +204 -0
- package/dist/web/src/gl/gl.js +123 -0
- package/dist/web/src/gl/renderer.js +451 -0
- package/dist/web/src/halo.js +95 -0
- package/dist/web/src/highlight.js +108 -0
- package/dist/web/src/overview.js +393 -0
- package/dist/web/src/reviewqueue.js +250 -0
- package/dist/web/src/selection.js +272 -0
- package/dist/web/src/status.js +80 -0
- package/dist/web/src/visitors.js +318 -0
- package/dist/web/src/visits.js +174 -0
- package/dist/web/style.css +7 -0
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/history.md +2 -1
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/package.json +3 -1
package/dist/web/bundle/app.js
CHANGED
|
@@ -71,8 +71,9 @@ function cleanFootSize(v2) {
|
|
|
71
71
|
function artNameId(kind, s2) {
|
|
72
72
|
return `${kind === "cluster" ? "c" : "d"}:${s2}`;
|
|
73
73
|
}
|
|
74
|
-
var ART_NAMES_BYTES =
|
|
74
|
+
var ART_NAMES_BYTES = 4e3;
|
|
75
75
|
var ART_TARGETS_BYTES = 2500;
|
|
76
|
+
var ART_RULED_BYTES = 1e3;
|
|
76
77
|
var jsonBytes = (x2) => new TextEncoder().encode(JSON.stringify(x2)).length + 1;
|
|
77
78
|
function cleanNames(v2, budget, ok = () => true) {
|
|
78
79
|
const out = [];
|
|
@@ -103,10 +104,11 @@ function cleanArtwork(v2) {
|
|
|
103
104
|
if (typeof o2.notes !== "boolean" || typeof o2.selection !== "boolean") return null;
|
|
104
105
|
const namesFrom = o2.namesFrom === "none" ? "none" : "board";
|
|
105
106
|
const hiddenNames = cleanNames(o2.hiddenNames, ART_NAMES_BYTES, isArtNameId), shownNames = cleanNames(o2.shownNames, ART_NAMES_BYTES, isArtNameId);
|
|
107
|
+
const ruledNames = cleanNames(o2.ruledNames, ART_RULED_BYTES, isArtNameId);
|
|
106
108
|
const shownTargets = cleanNames(o2.shownTargets, ART_TARGETS_BYTES);
|
|
107
109
|
const keys = typeof o2.keys === "boolean" ? o2.keys : targets !== "plain";
|
|
108
110
|
const style = one2(ART_STYLES, o2.style) ?? "board";
|
|
109
|
-
return { w: w2, h: h2, scheme, theme, background, labels, namesFrom, hiddenNames, shownNames, edges, notes: o2.notes, targets, shownTargets, keys, selection: o2.selection, source: o2.source === true, style, title: cleanTitle(o2.title), subtitle: cleanTitle(o2.subtitle), titleSize: cleanTitleSize(o2.titleSize), footSize: cleanFootSize(o2.footSize), border: o2.border === true, region: [...r2] };
|
|
111
|
+
return { w: w2, h: h2, scheme, theme, background, labels, namesFrom, hiddenNames, shownNames, ruledNames, edges, notes: o2.notes, targets, shownTargets, keys, selection: o2.selection, source: o2.source === true, style, title: cleanTitle(o2.title), subtitle: cleanTitle(o2.subtitle), titleSize: cleanTitleSize(o2.titleSize), footSize: cleanFootSize(o2.footSize), border: o2.border === true, region: [...r2] };
|
|
110
112
|
}
|
|
111
113
|
var SCHEMES = ["kind", "status"];
|
|
112
114
|
var ACTIVITY_KINDS = ["all", "review", "build", "change"];
|
|
@@ -1843,8 +1845,8 @@ function applyEvent(d2, e2) {
|
|
|
1843
1845
|
case "name":
|
|
1844
1846
|
return;
|
|
1845
1847
|
case "opinion": {
|
|
1846
|
-
const { op: _op, name, ...o2 } = e2;
|
|
1847
|
-
(Object.hasOwn(d2.opinions, name) ? d2.opinions[name] : d2.opinions[name] = []).push(o2);
|
|
1848
|
+
const { op: _op, name, for: forName, forPerson, ...o2 } = e2;
|
|
1849
|
+
(Object.hasOwn(d2.opinions, name) ? d2.opinions[name] : d2.opinions[name] = []).push(forName === void 0 ? o2 : recordedFor(d2, o2, forName, forPerson));
|
|
1848
1850
|
return;
|
|
1849
1851
|
}
|
|
1850
1852
|
case "target":
|
|
@@ -1878,12 +1880,23 @@ function named(d2, id, name) {
|
|
|
1878
1880
|
const p2 = Object.hasOwn(d2.people, id) ? d2.people[id] : null;
|
|
1879
1881
|
if (p2?.name === name) return;
|
|
1880
1882
|
if (!p2) {
|
|
1881
|
-
|
|
1883
|
+
const was = /* @__PURE__ */ new Set();
|
|
1884
|
+
for (const ops of Object.values(d2.opinions))
|
|
1885
|
+
for (const o2 of ops)
|
|
1886
|
+
if (o2.person === id && o2.by !== name) {
|
|
1887
|
+
was.add(o2.by);
|
|
1888
|
+
o2.by = name;
|
|
1889
|
+
}
|
|
1890
|
+
d2.people[id] = { name, was: [...was] };
|
|
1882
1891
|
return;
|
|
1883
1892
|
}
|
|
1884
1893
|
p2.was = [...p2.was.filter((n3) => n3 !== p2.name && n3 !== name), p2.name];
|
|
1885
1894
|
p2.name = name;
|
|
1886
|
-
for (const ops of Object.values(d2.opinions))
|
|
1895
|
+
for (const ops of Object.values(d2.opinions))
|
|
1896
|
+
for (const o2 of ops) {
|
|
1897
|
+
if (o2.person === id) o2.by = name;
|
|
1898
|
+
if (o2.recordedBy?.person === id) o2.recordedBy.by = name;
|
|
1899
|
+
}
|
|
1887
1900
|
for (const t2 of Object.values(d2.targets)) if (t2.person === id) t2.by = name;
|
|
1888
1901
|
for (const marks of Object.values(d2.keys)) for (const m3 of Object.values(marks)) if (m3.person === id) m3.by = name;
|
|
1889
1902
|
renamePerson(d2, id, name);
|
|
@@ -1918,6 +1931,14 @@ function readEvent(raw, agentLog, personLog = null) {
|
|
|
1918
1931
|
e2[k2] = o2[k2];
|
|
1919
1932
|
}
|
|
1920
1933
|
if (e2.text !== void 0 && e2.statement !== void 0) return "a text and a statement";
|
|
1934
|
+
if (o2.for !== void 0 || o2.forPerson !== void 0) {
|
|
1935
|
+
if (agentLog) return "an agent's opinion recorded for someone else";
|
|
1936
|
+
if (typeof o2.for !== "string" || !validName(o2.for)) return "for";
|
|
1937
|
+
if (o2.forPerson !== void 0 && !validPersonId(o2.forPerson)) return "forPerson";
|
|
1938
|
+
if (o2.for === by || o2.forPerson !== void 0 && o2.forPerson === personLog) return "an opinion recorded for its own writer";
|
|
1939
|
+
e2.for = o2.for;
|
|
1940
|
+
if (o2.forPerson !== void 0) e2.forPerson = o2.forPerson;
|
|
1941
|
+
}
|
|
1921
1942
|
if (agentLog) e2.agent = true;
|
|
1922
1943
|
return e2;
|
|
1923
1944
|
}
|
|
@@ -2001,6 +2022,11 @@ function problemsText(ps) {
|
|
|
2001
2022
|
function author(o2) {
|
|
2002
2023
|
return o2.person ? `link ${o2.person}` : (o2.agent ? "agent " : "person ") + o2.by;
|
|
2003
2024
|
}
|
|
2025
|
+
function recordedFor(d2, o2, forName, forPerson) {
|
|
2026
|
+
const { by, person, ...rest } = o2;
|
|
2027
|
+
const name = forPerson && Object.hasOwn(d2.people, forPerson) ? d2.people[forPerson].name : forName;
|
|
2028
|
+
return { ...rest, by: name, ...forPerson ? { person: forPerson } : {}, recordedBy: { by, ...person ? { person } : {} } };
|
|
2029
|
+
}
|
|
2004
2030
|
function latest(d2, name, agents) {
|
|
2005
2031
|
const out = /* @__PURE__ */ new Map();
|
|
2006
2032
|
for (const o2 of Object.hasOwn(d2.opinions, name) ? d2.opinions[name] : []) if (!!o2.agent === agents) out.set(author(o2), o2);
|
|
@@ -5580,17 +5606,50 @@ function rgba(css, alpha = 1) {
|
|
|
5580
5606
|
const key = css;
|
|
5581
5607
|
let c3 = colorCache.get(key);
|
|
5582
5608
|
if (!c3) {
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5609
|
+
if (typeof document === "undefined") c3 = parseColour(css);
|
|
5610
|
+
else {
|
|
5611
|
+
probe ??= document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
|
5612
|
+
probe.clearRect(0, 0, 1, 1);
|
|
5613
|
+
probe.fillStyle = "#000";
|
|
5614
|
+
probe.fillStyle = css || "transparent";
|
|
5615
|
+
probe.fillRect(0, 0, 1, 1);
|
|
5616
|
+
const d2 = probe.getImageData(0, 0, 1, 1).data;
|
|
5617
|
+
c3 = [d2[0] / 255, d2[1] / 255, d2[2] / 255, d2[3] / 255];
|
|
5618
|
+
}
|
|
5590
5619
|
colorCache.set(key, c3);
|
|
5591
5620
|
}
|
|
5592
5621
|
return [c3[0], c3[1], c3[2], c3[3] * alpha];
|
|
5593
5622
|
}
|
|
5623
|
+
var NAMED = { transparent: "#0000", black: "#000", white: "#fff", red: "#f00", green: "#008000", blue: "#00f", gray: "#808080", grey: "#808080", orange: "#ffa500", gold: "#ffd700" };
|
|
5624
|
+
function parseColour(css) {
|
|
5625
|
+
let s2 = (css || "").trim().toLowerCase();
|
|
5626
|
+
s2 = NAMED[s2] ?? s2;
|
|
5627
|
+
const byte = (v2) => Math.round(Math.min(255, Math.max(0, v2))) / 255;
|
|
5628
|
+
const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(s2);
|
|
5629
|
+
if (hex) {
|
|
5630
|
+
let h3 = hex[1];
|
|
5631
|
+
if (h3.length <= 4) h3 = [...h3].map((x2) => x2 + x2).join("");
|
|
5632
|
+
const n3 = (i2) => parseInt(h3.slice(i2, i2 + 2), 16);
|
|
5633
|
+
return [n3(0) / 255, n3(2) / 255, n3(4) / 255, h3.length === 8 ? n3(6) / 255 : 1];
|
|
5634
|
+
}
|
|
5635
|
+
const fn = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(s2);
|
|
5636
|
+
if (!fn) return [0, 0, 0, 0];
|
|
5637
|
+
const parts = fn[2].replace(/\//g, " / ").split(/[\s,]+/).filter((x2) => x2 && x2 !== "/");
|
|
5638
|
+
const num = (x2, scale) => x2 === void 0 ? NaN : x2.endsWith("%") ? parseFloat(x2) / 100 * scale : parseFloat(x2);
|
|
5639
|
+
const a3 = parts.length > 3 ? Math.min(1, Math.max(0, num(parts[3], 1))) : 1;
|
|
5640
|
+
if (fn[1].startsWith("rgb")) {
|
|
5641
|
+
const [r2, g2, b2] = [0, 1, 2].map((i2) => num(parts[i2], 255));
|
|
5642
|
+
return [r2, g2, b2].some(Number.isNaN) ? [0, 0, 0, 0] : [byte(r2), byte(g2), byte(b2), Number.isNaN(a3) ? 1 : Math.round(a3 * 255) / 255];
|
|
5643
|
+
}
|
|
5644
|
+
const h2 = (parseFloat(parts[0]) % 360 + 360) % 360 / 360, sat = num(parts[1], 1), l2 = num(parts[2], 1);
|
|
5645
|
+
if ([h2, sat, l2].some(Number.isNaN)) return [0, 0, 0, 0];
|
|
5646
|
+
const q2 = l2 < 0.5 ? l2 * (1 + sat) : l2 + sat - l2 * sat, p2 = 2 * l2 - q2;
|
|
5647
|
+
const t2 = (x2) => {
|
|
5648
|
+
x2 = (x2 + 1) % 1;
|
|
5649
|
+
return x2 < 1 / 6 ? p2 + (q2 - p2) * 6 * x2 : x2 < 1 / 2 ? q2 : x2 < 2 / 3 ? p2 + (q2 - p2) * (2 / 3 - x2) * 6 : p2;
|
|
5650
|
+
};
|
|
5651
|
+
return [byte(t2(h2 + 1 / 3) * 255), byte(t2(h2) * 255), byte(t2(h2 - 1 / 3) * 255), Number.isNaN(a3) ? 1 : Math.round(a3 * 255) / 255];
|
|
5652
|
+
}
|
|
5594
5653
|
function program(gl, vs, fs) {
|
|
5595
5654
|
const make = (type, src) => {
|
|
5596
5655
|
const s2 = gl.createShader(type);
|
|
@@ -5618,7 +5677,7 @@ var Instances = class {
|
|
|
5618
5677
|
this.gl = gl;
|
|
5619
5678
|
this.stride = stride;
|
|
5620
5679
|
this.data = new Float32Array(stride * capacity);
|
|
5621
|
-
this.buffer = gl.createBuffer();
|
|
5680
|
+
this.buffer = gl ? gl.createBuffer() : null;
|
|
5622
5681
|
}
|
|
5623
5682
|
clear() {
|
|
5624
5683
|
this.count = 0;
|
|
@@ -5640,6 +5699,7 @@ var Instances = class {
|
|
|
5640
5699
|
upload() {
|
|
5641
5700
|
if (this.uploaded === this.version) return;
|
|
5642
5701
|
const gl = this.gl;
|
|
5702
|
+
if (!gl) return;
|
|
5643
5703
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
5644
5704
|
gl.bufferData(gl.ARRAY_BUFFER, this.data.subarray(0, this.count * this.stride), gl.DYNAMIC_DRAW);
|
|
5645
5705
|
this.uploaded = this.version;
|
|
@@ -5867,6 +5927,7 @@ function bundlePath(L2, paths, off, len, beta, widthPx, c0, c1) {
|
|
|
5867
5927
|
d2[o2 + 18] = c1[2];
|
|
5868
5928
|
d2[o2 + 19] = c1[3];
|
|
5869
5929
|
}
|
|
5930
|
+
var NO_CARD_SIDE = 16384;
|
|
5870
5931
|
var Renderer = class {
|
|
5871
5932
|
gl;
|
|
5872
5933
|
circleProg;
|
|
@@ -5881,6 +5942,10 @@ var Renderer = class {
|
|
|
5881
5942
|
circleCorners;
|
|
5882
5943
|
lineCorners;
|
|
5883
5944
|
constructor(canvas) {
|
|
5945
|
+
if (!canvas) {
|
|
5946
|
+
this.gl = null;
|
|
5947
|
+
return;
|
|
5948
|
+
}
|
|
5884
5949
|
const gl = canvas.getContext("webgl2", { antialias: false, premultipliedAlpha: true, alpha: true });
|
|
5885
5950
|
if (!gl) throw new Error("WebGL2 is not available in this browser");
|
|
5886
5951
|
this.gl = gl;
|
|
@@ -5903,6 +5968,11 @@ var Renderer = class {
|
|
|
5903
5968
|
const gl = this.gl;
|
|
5904
5969
|
const stride = kind === "lines" ? LINE_STRIDE : kind === "bundles" ? BUNDLE_STRIDE : CIRCLE_STRIDE;
|
|
5905
5970
|
const inst = new Instances(gl, stride);
|
|
5971
|
+
if (!gl) {
|
|
5972
|
+
this.layers.set(name, { kind, inst, vao: null, visible: true });
|
|
5973
|
+
this.order.push(name);
|
|
5974
|
+
return inst;
|
|
5975
|
+
}
|
|
5906
5976
|
const vao = gl.createVertexArray();
|
|
5907
5977
|
gl.bindVertexArray(vao);
|
|
5908
5978
|
if (kind === "circles" || kind === "lines") {
|
|
@@ -5925,6 +5995,7 @@ var Renderer = class {
|
|
|
5925
5995
|
/** World positions the bundled edges read: (x, y) pairs, 1024 per texture row. */
|
|
5926
5996
|
setPositions(xy) {
|
|
5927
5997
|
const gl = this.gl;
|
|
5998
|
+
if (!gl) return;
|
|
5928
5999
|
const count2 = xy.length / 2;
|
|
5929
6000
|
const rows = Math.max(1, Math.ceil(count2 / 1024));
|
|
5930
6001
|
gl.bindTexture(gl.TEXTURE_2D, this.posTex);
|
|
@@ -5963,6 +6034,7 @@ var Renderer = class {
|
|
|
5963
6034
|
/** The most pixels a side of what this context draws at once can have. */
|
|
5964
6035
|
maxSide() {
|
|
5965
6036
|
const gl = this.gl;
|
|
6037
|
+
if (!gl) return NO_CARD_SIDE;
|
|
5966
6038
|
const dims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
|
|
5967
6039
|
return Math.min(dims[0], dims[1], gl.getParameter(gl.MAX_RENDERBUFFER_SIZE));
|
|
5968
6040
|
}
|
|
@@ -5971,6 +6043,7 @@ var Renderer = class {
|
|
|
5971
6043
|
* screen's (an artwork's PNG) keeps it as wide in pixels as the screen does. */
|
|
5972
6044
|
render(cam, w2, h2, dpr, dotPx, aa = 0.35) {
|
|
5973
6045
|
const gl = this.gl;
|
|
6046
|
+
if (!gl) return;
|
|
5974
6047
|
gl.viewport(0, 0, Math.round(w2 * dpr), Math.round(h2 * dpr));
|
|
5975
6048
|
gl.clearColor(0, 0, 0, 0);
|
|
5976
6049
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
@@ -6360,7 +6433,7 @@ var OverviewModel = class {
|
|
|
6360
6433
|
return looks;
|
|
6361
6434
|
}
|
|
6362
6435
|
};
|
|
6363
|
-
var DIM = 0.
|
|
6436
|
+
var DIM = 0.3;
|
|
6364
6437
|
function blendPoints(sets, put) {
|
|
6365
6438
|
const give = (L2, j2, beta, width, alpha) => {
|
|
6366
6439
|
if (alpha > 2e-3) put(L2, j2, beta, width, alpha);
|
|
@@ -7857,9 +7930,9 @@ function statusCodes(d2, g2, files) {
|
|
|
7857
7930
|
var PAST_GHOST = 1;
|
|
7858
7931
|
var PAST_CHANGED = 4;
|
|
7859
7932
|
var PAST_RENAMED = 8;
|
|
7860
|
-
function gitThen(
|
|
7933
|
+
function gitThen(commit) {
|
|
7861
7934
|
if (!commit || commit.guessed) return { git: void 0 };
|
|
7862
|
-
return { git: { source:
|
|
7935
|
+
return { git: { source: null, commit: commit.hash, branch: commit.branch, ...commit.dirty ? { dirty: true } : {} } };
|
|
7863
7936
|
}
|
|
7864
7937
|
function pastGraph(snap, edges, now, diff) {
|
|
7865
7938
|
const P2 = snap.projectCount;
|
|
@@ -7871,8 +7944,8 @@ function pastGraph(snap, edges, now, diff) {
|
|
|
7871
7944
|
const meta = {
|
|
7872
7945
|
schema: 1,
|
|
7873
7946
|
layoutVersion: now.meta.layoutVersion,
|
|
7874
|
-
// the commit of that build, where it is known
|
|
7875
|
-
project: { ...now.meta.project, extractedAt: snap.snapshot.at, ...gitThen(
|
|
7947
|
+
// the commit of that build, where it is known
|
|
7948
|
+
project: { ...now.meta.project, extractedAt: snap.snapshot.at, ...gitThen(snap.snapshot.commit) },
|
|
7876
7949
|
modules: snap.modules.map((m3) => ({ name: m3.name, file: null, imports: m3.imports })),
|
|
7877
7950
|
nodes,
|
|
7878
7951
|
projectCount: P2,
|
|
@@ -8314,7 +8387,7 @@ function openFor(plan, parent, chosen) {
|
|
|
8314
8387
|
function placeVisits(S2, L2, plan, vs, ok = () => true) {
|
|
8315
8388
|
const names2 = vs.map((v2) => v2.name);
|
|
8316
8389
|
const pack2 = packPlan(plan, names2);
|
|
8317
|
-
const
|
|
8390
|
+
const keyOf3 = (c3) => c3 < 0 ? "" : L2.clusters[c3].key;
|
|
8318
8391
|
const items = [];
|
|
8319
8392
|
const ofTemp = /* @__PURE__ */ new Map();
|
|
8320
8393
|
plan.temps.forEach((t2, k2) => {
|
|
@@ -8337,7 +8410,7 @@ function placeVisits(S2, L2, plan, vs, ok = () => true) {
|
|
|
8337
8410
|
const placed = /* @__PURE__ */ new Map();
|
|
8338
8411
|
for (const j2 of order) {
|
|
8339
8412
|
const it2 = items[j2];
|
|
8340
|
-
const kept = members[j2].map((k2) => vs[k2].place).find((p2) => p2 && p2.in ===
|
|
8413
|
+
const kept = members[j2].map((k2) => vs[k2].place).find((p2) => p2 && p2.in === keyOf3(it2.in));
|
|
8341
8414
|
if (kept) it2.at = [kept.at[0], kept.at[1]];
|
|
8342
8415
|
else {
|
|
8343
8416
|
const room2 = S2.roomOf(it2.in);
|
|
@@ -8362,7 +8435,7 @@ function placeVisits(S2, L2, plan, vs, ok = () => true) {
|
|
|
8362
8435
|
}
|
|
8363
8436
|
const held = /* @__PURE__ */ new Map();
|
|
8364
8437
|
for (const [c3, list2] of placed) held.set(c3, list2.map((o2) => ({ x: o2.at[0], y: o2.at[1], r: o2.r })));
|
|
8365
|
-
const places = item.map((j2) => ({ in:
|
|
8438
|
+
const places = item.map((j2) => ({ in: keyOf3(items[j2].in), at: [items[j2].at[0], items[j2].at[1]] }));
|
|
8366
8439
|
return { plan, pack: pack2, items, item, places, held };
|
|
8367
8440
|
}
|
|
8368
8441
|
function visitWorld(V2, S2) {
|
|
@@ -8704,7 +8777,9 @@ var DOUBLE_MS = 450;
|
|
|
8704
8777
|
var HOLD_MS = 500;
|
|
8705
8778
|
function readTheme() {
|
|
8706
8779
|
const cs = getComputedStyle(document.body);
|
|
8707
|
-
|
|
8780
|
+
return themeOf((name) => cs.getPropertyValue(name).trim());
|
|
8781
|
+
}
|
|
8782
|
+
function themeOf(v2) {
|
|
8708
8783
|
const kind = {};
|
|
8709
8784
|
for (const k2 of KINDS2) kind[k2] = rgba(v2(`--k-${k2}`));
|
|
8710
8785
|
return {
|
|
@@ -8870,6 +8945,9 @@ var GraphCanvas = class {
|
|
|
8870
8945
|
/** of its names shown by selection, the clusters' keys and the declarations' full names */
|
|
8871
8946
|
artClusters = /* @__PURE__ */ new Set();
|
|
8872
8947
|
artDecls = [];
|
|
8948
|
+
/** the names a recipe's rule chose (ExportSpec decision 20): clusters by key, declarations */
|
|
8949
|
+
artRuled = /* @__PURE__ */ new Set();
|
|
8950
|
+
artRuledDecls = [];
|
|
8873
8951
|
/** what the names the last frame wrote name (artNameId), on an artwork: what its list of names
|
|
8874
8952
|
* shows ticked */
|
|
8875
8953
|
namesWritten = /* @__PURE__ */ new Set();
|
|
@@ -8925,11 +9003,17 @@ var GraphCanvas = class {
|
|
|
8925
9003
|
* scene's and these */
|
|
8926
9004
|
openAround = /* @__PURE__ */ new Set();
|
|
8927
9005
|
tempOpen = /* @__PURE__ */ new Set();
|
|
8928
|
-
|
|
9006
|
+
/** drawn without a browser: no graphics card, no input, no frames but those asked for (`settle`) */
|
|
9007
|
+
headless;
|
|
9008
|
+
/** A board on the page's canvas; or, with `host` (and nulls for the elements), the same board drawn
|
|
9009
|
+
* without a browser — its renderer kept to its data, which an artwork's SVG reads (ExportSpec
|
|
9010
|
+
* decision 21). */
|
|
9011
|
+
constructor(canvas, labels, cardsEl, g2, ev, host) {
|
|
9012
|
+
this.headless = !!host;
|
|
8929
9013
|
this.canvas = canvas;
|
|
8930
|
-
this.gl = new Renderer(canvas);
|
|
9014
|
+
this.gl = new Renderer(host ? null : canvas);
|
|
8931
9015
|
this.labels = labels;
|
|
8932
|
-
this.ctx = labels.getContext("2d");
|
|
9016
|
+
this.ctx = host ? host.ctx : labels.getContext("2d");
|
|
8933
9017
|
for (const [name, kind] of [
|
|
8934
9018
|
["discs", "circles"],
|
|
8935
9019
|
["figures", "lines"],
|
|
@@ -8948,15 +9032,20 @@ var GraphCanvas = class {
|
|
|
8948
9032
|
this.g = g2;
|
|
8949
9033
|
this.ev = ev;
|
|
8950
9034
|
this.edgeQ = new EdgeQueue(
|
|
8951
|
-
() => typeof Worker === "undefined" ? null : new Worker("/edges.js", { type: "module" }),
|
|
9035
|
+
() => host || typeof Worker === "undefined" ? null : new Worker("/edges.js", { type: "module" }),
|
|
8952
9036
|
() => this.request()
|
|
8953
9037
|
);
|
|
9038
|
+
if (host) {
|
|
9039
|
+
this.theme = host.theme;
|
|
9040
|
+
[this.w, this.h, this.dpr] = [host.w, host.h, 1];
|
|
9041
|
+
return;
|
|
9042
|
+
}
|
|
8954
9043
|
this.theme = readTheme();
|
|
8955
9044
|
new MutationObserver(() => {
|
|
8956
9045
|
this.theme = readTheme();
|
|
8957
9046
|
this.invalidate();
|
|
8958
9047
|
}).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
8959
|
-
new ResizeObserver(() => this.resize()).observe(canvas);
|
|
9048
|
+
new ResizeObserver(() => this.resize()).observe(this.canvas);
|
|
8960
9049
|
this.resize();
|
|
8961
9050
|
this.bindInput();
|
|
8962
9051
|
let last2 = performance.now();
|
|
@@ -8982,6 +9071,21 @@ var GraphCanvas = class {
|
|
|
8982
9071
|
request() {
|
|
8983
9072
|
this.dirty = true;
|
|
8984
9073
|
}
|
|
9074
|
+
/** Without a browser (ExportSpec decision 21): draw until nothing more comes — the scene at rest,
|
|
9075
|
+
* the overview's points chosen and blended, nothing asking for another frame — at most `most`
|
|
9076
|
+
* frames; whether it came to rest. */
|
|
9077
|
+
settle(most = 60) {
|
|
9078
|
+
for (let n3 = 0; n3 < most; n3++) {
|
|
9079
|
+
if (this.scene?.moving) {
|
|
9080
|
+
this.scene.step(64);
|
|
9081
|
+
this.afterMotion();
|
|
9082
|
+
}
|
|
9083
|
+
this.dirty = false;
|
|
9084
|
+
this.draw();
|
|
9085
|
+
if (!this.dirty && !this.scene?.moving) return true;
|
|
9086
|
+
}
|
|
9087
|
+
return false;
|
|
9088
|
+
}
|
|
8985
9089
|
/** Called after every frame drawn: what lies over the canvas and follows it (the notes,
|
|
8986
9090
|
* web/src/sticky.ts) is placed again. */
|
|
8987
9091
|
onFrame = null;
|
|
@@ -9550,10 +9654,11 @@ var GraphCanvas = class {
|
|
|
9550
9654
|
this.focus = focus;
|
|
9551
9655
|
this.picked = clusters;
|
|
9552
9656
|
this.active = closure;
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9657
|
+
if (!this.headless)
|
|
9658
|
+
requestAnimationFrame(() => {
|
|
9659
|
+
this.theme = readTheme();
|
|
9660
|
+
this.invalidate();
|
|
9661
|
+
});
|
|
9557
9662
|
this.activeList = [];
|
|
9558
9663
|
if (closure) {
|
|
9559
9664
|
for (let i2 = 0; i2 < closure.length; i2++) if (closure[i2]) this.activeList.push(i2);
|
|
@@ -9691,7 +9796,7 @@ var GraphCanvas = class {
|
|
|
9691
9796
|
for (let c3 = 0; c3 < S2.nc; c3++) {
|
|
9692
9797
|
const spot = this.chosenSpot(c3);
|
|
9693
9798
|
const cl = this.layout.clusters[c3];
|
|
9694
|
-
if (!spot || !inside2(spot.x, spot.y) || !spot.board && !this.artClusters.has(cl.key)) continue;
|
|
9799
|
+
if (!spot || !inside2(spot.x, spot.y) || !spot.board && !this.artClusters.has(cl.key) && !this.artRuled.has(cl.key)) continue;
|
|
9695
9800
|
out.push({ id: artNameId("cluster", cl.key), label: cl.label, full: cl.key.replace("\u27E8external\u27E9", "External"), cluster: true });
|
|
9696
9801
|
}
|
|
9697
9802
|
const mx = rect.x + rect.w / 2, my = rect.y + rect.h / 2;
|
|
@@ -9749,10 +9854,16 @@ var GraphCanvas = class {
|
|
|
9749
9854
|
this.art = look;
|
|
9750
9855
|
this.artClusters = /* @__PURE__ */ new Set();
|
|
9751
9856
|
this.artDecls = [];
|
|
9857
|
+
this.artRuled = /* @__PURE__ */ new Set();
|
|
9858
|
+
this.artRuledDecls = [];
|
|
9752
9859
|
for (const id of look?.shown ?? []) {
|
|
9753
9860
|
if (id.startsWith("c:")) this.artClusters.add(id.slice(2));
|
|
9754
9861
|
else if (id.startsWith("d:")) this.artDecls.push(id.slice(2));
|
|
9755
9862
|
}
|
|
9863
|
+
for (const id of look?.ruled ?? []) {
|
|
9864
|
+
if (id.startsWith("c:")) this.artRuled.add(id.slice(2));
|
|
9865
|
+
else if (id.startsWith("d:")) this.artRuledDecls.push(id.slice(2));
|
|
9866
|
+
}
|
|
9756
9867
|
this.overviewBase = "";
|
|
9757
9868
|
this.setHover(null, 0, 0);
|
|
9758
9869
|
this.invalidate();
|
|
@@ -10867,7 +10978,8 @@ var GraphCanvas = class {
|
|
|
10867
10978
|
};
|
|
10868
10979
|
const targets = art ? this.markedTargets() : [];
|
|
10869
10980
|
const look = art ? artText(art.style, th) : null;
|
|
10870
|
-
const artCluster = (c3,
|
|
10981
|
+
const artCluster = (c3, how) => {
|
|
10982
|
+
const force = how === true, anywhere = how !== false;
|
|
10871
10983
|
const L2 = look;
|
|
10872
10984
|
const cl = this.layout.clusters[c3];
|
|
10873
10985
|
const id = artNameId("cluster", cl.key);
|
|
@@ -10876,12 +10988,12 @@ var GraphCanvas = class {
|
|
|
10876
10988
|
ctx.textAlign = "center";
|
|
10877
10989
|
if (S2.rep[c3] === c3) {
|
|
10878
10990
|
const r2 = Math.max(6, S2.drawnR[c3] * k2);
|
|
10879
|
-
if (!
|
|
10991
|
+
if (!anywhere && (r2 <= 10 || !onScreen(sx, sy, r2) || act && this.clusterHasActive(c3))) return false;
|
|
10880
10992
|
ctx.font = L2.sub(11);
|
|
10881
10993
|
return place2(cl.label, sx, sy + r2 + 13, 11, id, force);
|
|
10882
10994
|
}
|
|
10883
10995
|
const sr = S2.drawnR[c3] * k2;
|
|
10884
|
-
if (!
|
|
10996
|
+
if (!anywhere && (sr <= 38 || !onScreen(sx, sy, sr))) return false;
|
|
10885
10997
|
const top = cl.depth === 0;
|
|
10886
10998
|
const size = Math.max(10, Math.min(top ? 22 : 16, 9 + sr / 40));
|
|
10887
10999
|
const text2 = top ? cl.label.toUpperCase() : cl.label;
|
|
@@ -10932,9 +11044,27 @@ var GraphCanvas = class {
|
|
|
10932
11044
|
ctx.textAlign = "center";
|
|
10933
11045
|
ctx.fillStyle = th.label;
|
|
10934
11046
|
}
|
|
11047
|
+
if (chosen) {
|
|
11048
|
+
for (let c3 = 0; c3 < S2.nc; c3++) {
|
|
11049
|
+
const cl = this.layout.clusters[c3];
|
|
11050
|
+
if (!this.artRuled.has(cl.key) || this.artClusters.has(cl.key) || S2.rep[c3] >= 0 && S2.rep[c3] !== c3 || !this.clusterVisible(c3)) continue;
|
|
11051
|
+
const [sx, sy] = this.toScreen(S2.cwx[c3], S2.cwy[c3]);
|
|
11052
|
+
if (onScreen(sx, sy, 400)) artCluster(c3, "fit");
|
|
11053
|
+
}
|
|
11054
|
+
ctx.textAlign = "left";
|
|
11055
|
+
for (const name of this.artRuledDecls) {
|
|
11056
|
+
const i2 = this.g.byName.get(name);
|
|
11057
|
+
if (i2 === void 0 || !shown(i2)) continue;
|
|
11058
|
+
ctx.font = look.decl;
|
|
11059
|
+
ctx.fillStyle = look.declFill;
|
|
11060
|
+
nameAt(i2, this.g.short(i2), ctx.measureText(this.g.short(i2)).width);
|
|
11061
|
+
}
|
|
11062
|
+
ctx.textAlign = "center";
|
|
11063
|
+
ctx.fillStyle = th.label;
|
|
11064
|
+
}
|
|
10935
11065
|
for (let c3 = 0; c3 < S2.nc; c3++) {
|
|
10936
11066
|
if (art?.labels === "none" || chosen && !asShown) break;
|
|
10937
|
-
if (chosen && this.artClusters.has(this.layout.clusters[c3].key)) continue;
|
|
11067
|
+
if (chosen && (this.artClusters.has(this.layout.clusters[c3].key) || this.artRuled.has(this.layout.clusters[c3].key))) continue;
|
|
10938
11068
|
const rep = S2.rep[c3];
|
|
10939
11069
|
if (!this.clusterVisible(c3)) continue;
|
|
10940
11070
|
if (art) {
|
|
@@ -11574,6 +11704,7 @@ var GraphCanvas = class {
|
|
|
11574
11704
|
}
|
|
11575
11705
|
/** Place the cards for this frame; returns the nodes that got one. */
|
|
11576
11706
|
drawCards(activeDots) {
|
|
11707
|
+
if (this.headless) return /* @__PURE__ */ new Set();
|
|
11577
11708
|
const k2 = this.cam.k;
|
|
11578
11709
|
const S2 = this.scene;
|
|
11579
11710
|
const chosen = new Set(this.focus);
|
|
@@ -11765,7 +11896,8 @@ function menuItems(t2, ctx, title = "View") {
|
|
|
11765
11896
|
const out = [];
|
|
11766
11897
|
const note = ctx.arrange ? [{ act: "note", label: "Add a note here", sep: true }, { act: "note-view", label: "Add a note for this view" }] : [];
|
|
11767
11898
|
if (t2.kind === "note") {
|
|
11768
|
-
|
|
11899
|
+
const toggle = { act: "note-toggle", label: t2.open ? "Collapse" : "Expand", key: "click" };
|
|
11900
|
+
if (!ctx.arrange) return t2.own ? [toggle] : [];
|
|
11769
11901
|
const carries = t2.carries ?? [];
|
|
11770
11902
|
const attach = t2.attach ? [{ act: "note-attach", label: "Attach\u2026", sep: true }] : [];
|
|
11771
11903
|
if (carries.length) attach.push({ act: "note-detach", label: "Detach", ...attach.length ? {} : { sep: true }, sub: carries.map((label, k2) => ({ act: `detach:${k2}`, label })) });
|
|
@@ -11774,7 +11906,7 @@ function menuItems(t2, ctx, title = "View") {
|
|
|
11774
11906
|
if (t2.items?.remove) items.push({ act: "item-remove", label: "Remove from this view", ...items.length ? {} : { sep: true } });
|
|
11775
11907
|
return [
|
|
11776
11908
|
{ act: "note-edit", label: "Edit", key: "double-click" },
|
|
11777
|
-
|
|
11909
|
+
toggle,
|
|
11778
11910
|
...attach,
|
|
11779
11911
|
// the other kind, keeping its text, its author, what it carries and where it is on this board
|
|
11780
11912
|
{ act: "note-kind", label: t2.view ? "In every view" : "Only in this view", sep: true },
|
|
@@ -12188,7 +12320,8 @@ var Panel = class {
|
|
|
12188
12320
|
if (i2 === null) return this.hide();
|
|
12189
12321
|
const same2 = i2 === this.current && this.el.classList.contains("open");
|
|
12190
12322
|
const keepScroll = this.el.scrollTop;
|
|
12191
|
-
const
|
|
12323
|
+
const typing2 = document.activeElement?.matches?.("#panel [data-rv-comment], #panel [data-rv-for]") ? document.activeElement : null;
|
|
12324
|
+
const hadFocus = typing2 ? { sel: typing2.matches("[data-rv-for]") ? "[data-rv-for]" : "[data-rv-comment]", from: typing2.selectionStart, to: typing2.selectionEnd } : null;
|
|
12192
12325
|
this.current = i2;
|
|
12193
12326
|
const g2 = this.g;
|
|
12194
12327
|
const v2 = g2.nodes[i2];
|
|
@@ -12277,7 +12410,11 @@ var Panel = class {
|
|
|
12277
12410
|
this.el.classList.add("open");
|
|
12278
12411
|
if (!same2) this.el.scrollTop = 0;
|
|
12279
12412
|
else this.el.scrollTop = keepScroll;
|
|
12280
|
-
if (same2 && hadFocus)
|
|
12413
|
+
if (same2 && hadFocus) {
|
|
12414
|
+
const box = this.el.querySelector(hadFocus.sel);
|
|
12415
|
+
box?.focus();
|
|
12416
|
+
if (hadFocus.from !== null && hadFocus.to !== null) box?.setSelectionRange(hadFocus.from, hadFocus.to);
|
|
12417
|
+
}
|
|
12281
12418
|
this.box.reveal(this.el);
|
|
12282
12419
|
}
|
|
12283
12420
|
};
|
|
@@ -12954,6 +13091,7 @@ function snapshotViews(json) {
|
|
|
12954
13091
|
}
|
|
12955
13092
|
function snapshotSource(info2) {
|
|
12956
13093
|
let sources = null;
|
|
13094
|
+
let git = null;
|
|
12957
13095
|
const json = async (id) => JSON.parse(new TextDecoder().decode(await inflate(id)));
|
|
12958
13096
|
return {
|
|
12959
13097
|
views: snapshotViews(json),
|
|
@@ -12963,6 +13101,8 @@ function snapshotSource(info2) {
|
|
|
12963
13101
|
forget: () => {
|
|
12964
13102
|
},
|
|
12965
13103
|
meta: () => json("vp-meta"),
|
|
13104
|
+
// the commit as the snapshot was taken, in its graph: read once
|
|
13105
|
+
git: () => git ??= json("vp-meta").then((m3) => m3.project.git ?? null),
|
|
12966
13106
|
edges: () => inflate("vp-edges"),
|
|
12967
13107
|
// a snapshot shows the review as it was when taken
|
|
12968
13108
|
review: () => json("vp-review").then((data) => ({ epoch: "snapshot", rev: 0, data: reviewFromJson(data), problems: [], error: null })),
|
|
@@ -13034,6 +13174,7 @@ function serverSource() {
|
|
|
13034
13174
|
status: () => fetch("/api/status").then((r2) => r2.json()),
|
|
13035
13175
|
forget: () => cache.clear(),
|
|
13036
13176
|
meta: (v2) => fetch(`/api/meta${v2 ? `?v=${v2}` : ""}`).then((r2) => r2.ok ? r2.json() : Promise.reject(new Error(`meta ${r2.status}`))),
|
|
13177
|
+
git: () => fetch("/api/git").then((r2) => r2.ok ? r2.json() : null).then((b2) => b2?.git ?? null).catch(() => null),
|
|
13037
13178
|
// packed for the way (shared/edgecodec.ts): a quarter of the bytes
|
|
13038
13179
|
edges: (v2) => fetch(`/api/edges?pack=2${v2 ? `&v=${v2}` : ""}`).then((r2) => r2.ok ? r2.arrayBuffer() : Promise.reject(new Error(`edges ${r2.status}`))).then((b2) => isPackedEdges(new Uint8Array(b2)) ? unpackEdges(new Uint8Array(b2)).buffer : b2),
|
|
13039
13180
|
review: () => fetch("/api/review").then((r2) => r2.ok ? r2.json() : Promise.reject(new Error(`review ${r2.status}`))).then(reviewState),
|
|
@@ -13986,6 +14127,9 @@ function setPeople(list2) {
|
|
|
13986
14127
|
known.clear();
|
|
13987
14128
|
for (const p2 of Array.isArray(list2) ? list2 : []) if (p2 && typeof p2.id === "string" && typeof p2.name === "string") known.set(p2.id, p2);
|
|
13988
14129
|
}
|
|
14130
|
+
function invitedNames() {
|
|
14131
|
+
return [...known.values()].filter((p2) => !p2.revoked).map((p2) => p2.name);
|
|
14132
|
+
}
|
|
13989
14133
|
function nameNow(by, person) {
|
|
13990
14134
|
return person && known.get(person)?.name || by;
|
|
13991
14135
|
}
|
|
@@ -15440,12 +15584,22 @@ var ReviewBox = class {
|
|
|
15440
15584
|
* review is drawn, since when it flashes (0: not yet), and the panel and windows that scrolled to
|
|
15441
15585
|
* it already */
|
|
15442
15586
|
flashing = null;
|
|
15587
|
+
/** whom the opinions given on this page are for, where not the reader (ReviewSpec decision 8): it
|
|
15588
|
+
* stays, on every review box, until changed back; kept for the tab */
|
|
15589
|
+
speakingFor = readFor();
|
|
15443
15590
|
constructor(g2, hooks) {
|
|
15444
15591
|
this.g = g2;
|
|
15445
15592
|
this.hooks = hooks;
|
|
15446
15593
|
}
|
|
15447
|
-
/**
|
|
15594
|
+
/** Whom an opinion given now is for: another person's name, or null for the reader's own. */
|
|
15595
|
+
forName() {
|
|
15596
|
+
const f2 = this.speakingFor?.trim() || null;
|
|
15597
|
+
return f2 && f2 !== this.hooks.review().reviewer?.trim() ? f2 : null;
|
|
15598
|
+
}
|
|
15599
|
+
/** Keep what is typed into a comment box: the other boxes of the declaration show it too. Whom
|
|
15600
|
+
* one speaks for, typed into any box, is every box's. */
|
|
15448
15601
|
input(t2, i2) {
|
|
15602
|
+
if (t2.matches("[data-rv-for]")) return this.speakFor(t2.value, t2);
|
|
15449
15603
|
if (!t2.matches("[data-rv-comment]")) return;
|
|
15450
15604
|
const name = this.g.nodes[i2].name;
|
|
15451
15605
|
const value = t2.value;
|
|
@@ -15453,6 +15607,42 @@ var ReviewBox = class {
|
|
|
15453
15607
|
this.mirror(name, value, t2);
|
|
15454
15608
|
fitBox(t2);
|
|
15455
15609
|
}
|
|
15610
|
+
/** The opinions given from now on are `name`'s (none: the reader's own): every review box on the
|
|
15611
|
+
* page says so at once, without being drawn again (ReviewSpec decision 8). */
|
|
15612
|
+
speakFor(name, except) {
|
|
15613
|
+
this.speakingFor = name.slice(0, MAX_NAME) || null;
|
|
15614
|
+
try {
|
|
15615
|
+
if (this.speakingFor) sessionStorage.setItem(FOR_KEY, this.speakingFor);
|
|
15616
|
+
else sessionStorage.removeItem(FOR_KEY);
|
|
15617
|
+
} catch {
|
|
15618
|
+
}
|
|
15619
|
+
if (typeof document === "undefined") return;
|
|
15620
|
+
const f2 = this.forName();
|
|
15621
|
+
for (const b2 of document.querySelectorAll("input[data-rv-for]")) if (b2 !== except && b2.value !== name) b2.value = name;
|
|
15622
|
+
for (const on of document.querySelectorAll("[data-rv-for-on]")) {
|
|
15623
|
+
on.hidden = !f2;
|
|
15624
|
+
on.innerHTML = f2 ? `Recording for <b>${escapeHtml(f2)}</b> \u2014 their opinion, recorded by you` : "";
|
|
15625
|
+
}
|
|
15626
|
+
for (const w2 of document.querySelectorAll("[data-rv-for-who]")) w2.textContent = f2 ? ` for ${f2}` : "";
|
|
15627
|
+
}
|
|
15628
|
+
/** The names the "Speaking for" box offers: the people invited by a personal link and those the
|
|
15629
|
+
* logs name, agents left out, the reader not; kept in the page's one list of them. */
|
|
15630
|
+
people(d2) {
|
|
15631
|
+
if (typeof document === "undefined") return;
|
|
15632
|
+
const me = this.hooks.review().reviewer?.trim();
|
|
15633
|
+
const names2 = new Set(invitedNames());
|
|
15634
|
+
for (const ops of Object.values(d2.opinions)) for (const o2 of ops) if (!o2.agent) names2.add(o2.by);
|
|
15635
|
+
for (const t2 of Object.values(d2.targets)) if (!t2.agent) names2.add(t2.by);
|
|
15636
|
+
if (me) names2.delete(me);
|
|
15637
|
+
let list2 = document.getElementById("rv-people");
|
|
15638
|
+
if (!list2) {
|
|
15639
|
+
list2 = document.createElement("datalist");
|
|
15640
|
+
list2.id = "rv-people";
|
|
15641
|
+
document.body.append(list2);
|
|
15642
|
+
}
|
|
15643
|
+
const html = [...names2].sort((a3, b2) => a3.localeCompare(b2)).map((n3) => `<option value="${escapeHtml(n3)}"></option>`).join("");
|
|
15644
|
+
if (list2.innerHTML !== html) list2.innerHTML = html;
|
|
15645
|
+
}
|
|
15456
15646
|
/** Every comment box of declaration `name` on the page but `except` shows `value`: the panel and
|
|
15457
15647
|
* a source window on the same declaration write one comment, so that what one of them holds is
|
|
15458
15648
|
* never lost to what the other held. */
|
|
@@ -15605,7 +15795,8 @@ var ReviewBox = class {
|
|
|
15605
15795
|
root.querySelector("[data-rv-comment]")?.focus();
|
|
15606
15796
|
return;
|
|
15607
15797
|
}
|
|
15608
|
-
|
|
15798
|
+
const f2 = this.forName();
|
|
15799
|
+
this.send(root, v2.name, async () => await this.hooks.edited(i2) ? EDITED : this.hooks.submit({ op: "opinion", name: v2.name, verdict, comment: comment || void 0, ...f2 ? { for: f2 } : {} }));
|
|
15609
15800
|
}
|
|
15610
15801
|
/** Approve or comment on an axiom by its name, from the panel of target `i` (the board has no
|
|
15611
15802
|
* declaration for it): a comment is the one written there. */
|
|
@@ -15619,7 +15810,8 @@ var ReviewBox = class {
|
|
|
15619
15810
|
root.querySelector("[data-rv-comment]")?.focus();
|
|
15620
15811
|
return;
|
|
15621
15812
|
}
|
|
15622
|
-
|
|
15813
|
+
const f2 = this.forName();
|
|
15814
|
+
this.send(root, target, () => this.hooks.submit({ op: "opinion", name, verdict, comment: comment || void 0, ...f2 ? { for: f2 } : {} }));
|
|
15623
15815
|
}
|
|
15624
15816
|
/** Send an opinion with the comment written under `key` (the declaration's name; for an axiom,
|
|
15625
15817
|
* its target's): the comment is spent as it goes, so that the boxes drawn again with the change
|
|
@@ -15786,8 +15978,12 @@ var ReviewBox = class {
|
|
|
15786
15978
|
const quote = (o2) => o2.comment && writable ? ` <button type="button" class="rv-quote" data-rv-quote="${key(o2)}" title="Quote this comment in yours, linking back to it">Quote</button>` : "";
|
|
15787
15979
|
const opinion = (o2, compare = false) => {
|
|
15788
15980
|
const now2 = fresh(o2, v2.hash);
|
|
15789
|
-
return
|
|
15790
|
-
|
|
15981
|
+
return (
|
|
15982
|
+
// recorded for them by someone else: theirs, without the mark they did not give it under
|
|
15983
|
+
// (ReviewSpec decision 8)
|
|
15984
|
+
`<div class="rv-op rv-${o2.verdict}${now2 ? "" : " old"}" data-rv-op="${key(o2)}"><span class="rv-dot"></span><b>${esc2(o2.by)}</b>${o2.person && !o2.recordedBy ? VERIFIED : ""} ${VERB[o2.verdict]} <span class="muted">\xB7 ${ago(o2.at)}${o2.recordedBy ? ` \xB7 recorded by ${esc2(o2.recordedBy.by)}` : ""}${now2 ? "" : " \xB7 on an earlier version"}</span>${quote(o2)}` + (o2.comment ? `<div class="rv-c md">${this.comment(o2.comment, v2.name)}</div>` : "") + `</div>` + // beside the opinion, not in it: a stale opinion is faded, what changed must read clearly
|
|
15985
|
+
(compare && !now2 ? this.changes(i2, o2) : "")
|
|
15986
|
+
);
|
|
15791
15987
|
};
|
|
15792
15988
|
const now = current(d2, v2.name);
|
|
15793
15989
|
const all = (Object.hasOwn(d2.opinions, v2.name) ? d2.opinions[v2.name] : []).filter((o2) => !o2.agent);
|
|
@@ -15803,8 +15999,11 @@ var ReviewBox = class {
|
|
|
15803
15999
|
else if (R2.blocked) controls = `<div class="banner bad">${esc2(R2.blocked)}</div>`;
|
|
15804
16000
|
else if (this.hooks.editedNow(i2) === true)
|
|
15805
16001
|
controls = `<div class="banner warn">${EDITED}</div>`;
|
|
15806
|
-
else
|
|
15807
|
-
|
|
16002
|
+
else {
|
|
16003
|
+
this.people(d2);
|
|
16004
|
+
const f2 = this.forName();
|
|
16005
|
+
controls = `<div class="rv-for-on" data-rv-for-on${f2 ? "" : " hidden"}>${f2 ? `Recording for <b>${esc2(f2)}</b> \u2014 their opinion, recorded by you` : ""}</div><textarea class="rv-text" data-rv-comment="${esc2(v2.name)}" rows="${rowsFor(this.drafts.get(v2.name) ?? "")}" placeholder="Comment (optional with an approval) \xB7 Markdown" title="Markdown: > quote, \`code\`, \`\`\`lean blocks, **bold**, *italics*; a declaration's full name in backticks links to it. Quote an opinion with its Quote, lines of code by selecting them in the source window">${esc2(this.drafts.get(v2.name) ?? "")}</textarea><div class="rv-actions"><button class="rv-btn approve" data-rv="approve" title="Approve (a), with the comment if there is one">Approve<span data-rv-for-who>${f2 ? ` for ${esc2(f2)}` : ""}</span></button><button class="rv-btn comment" data-rv="comment" title="Comment (m): send what you wrote in the box">Comment<span data-rv-for-who>${f2 ? ` for ${esc2(f2)}` : ""}</span></button><label class="rv-for" title="Record the opinion as another person's, at the same screen: it counts as theirs, recorded by you. Empty: your own">Speaking for <input class="rv-for-in" data-rv-for list="rv-people" value="${esc2(this.speakingFor ?? "")}" placeholder="${esc2(R2.reviewer ?? "yourself")}" maxlength="${MAX_NAME}" spellcheck="false" autocomplete="off"></label></div><div class="rv-err" hidden></div>`;
|
|
16006
|
+
}
|
|
15808
16007
|
if (R2.error && !R2.readOnly && !R2.blocked) controls += `<div class="banner warn">${esc2(R2.error)}</div>`;
|
|
15809
16008
|
const by = trustedBy(d2, v2.name, v2.module, ext);
|
|
15810
16009
|
const why = STANDARD_AXIOMS.includes(v2.name) ? ` <span class="muted">\u2014 a standard axiom, trusted without review</span>` : nonStandardAxiom(g2, i2) ? ` <span class="muted">\u2014 a non-standard axiom: it needs review whatever is trusted</span>` : by.length ? ` <span class="muted rv-trusted-by">\u2014 trusted without review, under ${by.map((p2) => `<code>${esc2(p2)}</code>`).join(" and ")}</span>` : "";
|
|
@@ -15877,6 +16076,14 @@ function quotedCode(link) {
|
|
|
15877
16076
|
const pre = link.matches(".md-cq-h") ? link.closest(".md-cq")?.querySelector("pre") : null;
|
|
15878
16077
|
return pre?.textContent ? pre.textContent.split("\n") : null;
|
|
15879
16078
|
}
|
|
16079
|
+
var FOR_KEY = "visualproof.speakingFor";
|
|
16080
|
+
function readFor() {
|
|
16081
|
+
try {
|
|
16082
|
+
return typeof sessionStorage === "undefined" ? null : sessionStorage.getItem(FOR_KEY);
|
|
16083
|
+
} catch {
|
|
16084
|
+
return null;
|
|
16085
|
+
}
|
|
16086
|
+
}
|
|
15880
16087
|
|
|
15881
16088
|
// src/web/src/talk.ts
|
|
15882
16089
|
var Talk = class {
|
|
@@ -17941,13 +18148,14 @@ var Activity = class {
|
|
|
17941
18148
|
for (const [name, ops] of Object.entries(r2.opinions)) {
|
|
17942
18149
|
for (const o2 of ops) {
|
|
17943
18150
|
const verb = o2.verdict === "approve" ? o2.agent ? `suggests <span class="ok">approving</span>` : `<span class="ok">approved</span>` : `<span class="commented">${o2.agent ? "comments" : "commented"}</span> on`;
|
|
18151
|
+
const rec = o2.recordedBy;
|
|
17944
18152
|
out.push({
|
|
17945
18153
|
key: `o:${o2.agent ? "a" : "p"}:${name}:${o2.by}:${o2.at}`,
|
|
17946
18154
|
at: o2.at,
|
|
17947
|
-
by: now(o2.by, o2.person),
|
|
18155
|
+
by: rec ? now(rec.by, rec.person) : now(o2.by, o2.person),
|
|
17948
18156
|
agent: !!o2.agent,
|
|
17949
18157
|
cat: "review",
|
|
17950
|
-
html: `${who(o2.by, o2.agent, o2.person)} ${verb} ${decl(name)}`,
|
|
18158
|
+
html: rec ? `${who(rec.by, false, rec.person)} recorded <b>${escapeHtml(o2.by)}</b>'s ${o2.verdict === "approve" ? `<span class="ok">approval</span> of` : `<span class="commented">comment</span> on`} ${decl(name)}` : `${who(o2.by, o2.agent, o2.person)} ${verb} ${decl(name)}`,
|
|
17951
18159
|
...o2.comment ? { comment: { text: o2.comment, on: name } } : {},
|
|
17952
18160
|
names: [name],
|
|
17953
18161
|
targets: [],
|
|
@@ -18444,7 +18652,9 @@ var LostAndFound = class {
|
|
|
18444
18652
|
switch (e2.kind) {
|
|
18445
18653
|
case "opinion": {
|
|
18446
18654
|
const verb = e2.verdict === "approve" ? e2.agent ? `suggested <span class="ok">approving</span>` : `<span class="ok">approved</span>` : `<span class="commented">${e2.agent ? "commented (advice)" : "commented"}</span>`;
|
|
18447
|
-
|
|
18655
|
+
const rec = e2.recordedBy;
|
|
18656
|
+
const whose = rec ? `<b>${escapeHtml(e2.by ?? "")}</b>` : by;
|
|
18657
|
+
return `${whose} ${verb}${rec ? ` <span class="muted">\xB7 recorded by ${this.who(rec.by, void 0, rec.person)}</span>` : ""} ${when}` + (e2.comment ? `<div class="act-quote md">${this.hooks.comment(e2.comment, e2.name)}</div>` : "");
|
|
18448
18658
|
}
|
|
18449
18659
|
case "target":
|
|
18450
18660
|
return `<span class="star">\u2605</span> a target, marked by ${by} ${when}`;
|
|
@@ -18784,6 +18994,7 @@ var Phone = class {
|
|
|
18784
18994
|
|
|
18785
18995
|
// src/web/src/guide.ts
|
|
18786
18996
|
var $2 = (sel) => document.querySelector(sel);
|
|
18997
|
+
var REPO = "https://github.com/LucianoXu/VisualProof";
|
|
18787
18998
|
function around(...sels) {
|
|
18788
18999
|
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
18789
19000
|
for (const s2 of sels) {
|
|
@@ -19119,7 +19330,7 @@ var Guide = class {
|
|
|
19119
19330
|
([group, rows]) => `<div class="help-group"><div class="help-h">${group}</div>${rows.map(
|
|
19120
19331
|
(r2) => `<div class="help-row"><span class="help-k">${(r2.k ?? []).map((x2) => `<kbd>${escapeHtml(x2)}</kbd>`).join(" ")}${r2.k && r2.g ? " " : ""}${r2.g ? `<span class="help-g">${escapeHtml(r2.g)}</span>` : ""}</span><span>${escapeHtml(r2.what)}</span></div>`
|
|
19121
19332
|
).join("")}</div>`
|
|
19122
|
-
).join("")}</div>` + (phone ? "" : `<div class="help-editor"><div class="help-h">Open in editor</div><div class="help-note">A link from the panel and the source window to the declaration in your editor. <code>{path}</code> is the file on the server, <code>{rel}</code> the file in the project, <code>{line}</code> and <code>{col}</code> where it starts.</div><input class="help-input" data-help-editor value="${escapeHtml(mine ?? "")}" placeholder="${escapeHtml(server ?? "vscode://file/{path}:{line}:{col}")}" spellcheck="false" autocomplete="off" aria-label="Open in editor: URL template"><div class="modal-err" data-help-editor-err hidden></div><div class="help-note" data-help-editor-now>${this.editorNow()}</div></div>`) + `<div class="help-about"><div class="help-h">About ${escapeHtml(this.hooks.project())}</div><div class="help-note">${escapeHtml(this.hooks.about())}</div></div><div class="modal-actions"><button class="rv-btn" data-help-tour>Take the tour again</button><button class="rv-btn primary" data-help-close>Close</button></div></div>`;
|
|
19333
|
+
).join("")}</div>` + (phone ? "" : `<div class="help-editor"><div class="help-h">Open in editor</div><div class="help-note">A link from the panel and the source window to the declaration in your editor. <code>{path}</code> is the file on the server, <code>{rel}</code> the file in the project, <code>{line}</code> and <code>{col}</code> where it starts.</div><input class="help-input" data-help-editor value="${escapeHtml(mine ?? "")}" placeholder="${escapeHtml(server ?? "vscode://file/{path}:{line}:{col}")}" spellcheck="false" autocomplete="off" aria-label="Open in editor: URL template"><div class="modal-err" data-help-editor-err hidden></div><div class="help-note" data-help-editor-now>${this.editorNow()}</div></div>`) + `<div class="help-about"><div class="help-h">About ${escapeHtml(this.hooks.project())}</div><div class="help-note">${escapeHtml(this.hooks.about())}</div></div><div class="help-about"><div class="help-h">About VisualProof</div><div class="help-note">Open source: its documentation, its code and its issues are at <a class="help-repo" href="${REPO}" target="_blank" rel="noopener noreferrer">${REPO.replace("https://", "")}</a>.</div></div><div class="modal-actions"><button class="rv-btn" data-help-tour>Take the tour again</button><button class="rv-btn primary" data-help-close>Close</button></div></div>`;
|
|
19123
19334
|
this.help.hidden = false;
|
|
19124
19335
|
this.help.querySelector(".help-box")?.focus();
|
|
19125
19336
|
}
|
|
@@ -21166,7 +21377,7 @@ var SourceWindow = class {
|
|
|
21166
21377
|
const scroll = this.el.querySelector(".sw-scroll");
|
|
21167
21378
|
const keep = !fresh3 && scroll ? scroll.scrollTop : null;
|
|
21168
21379
|
const typed = document.activeElement;
|
|
21169
|
-
const hadFocus = typed instanceof HTMLTextAreaElement && this.el.contains(typed) && typed.matches("[data-rv-comment]") ? { from: typed.selectionStart, to: typed.selectionEnd } : null;
|
|
21380
|
+
const hadFocus = (typed instanceof HTMLTextAreaElement || typed instanceof HTMLInputElement) && this.el.contains(typed) && typed.matches("[data-rv-comment], [data-rv-for]") ? { sel: typed.matches("[data-rv-for]") ? "[data-rv-for]" : "[data-rv-comment]", from: typed.selectionStart, to: typed.selectionEnd } : null;
|
|
21170
21381
|
const short6 = escapeHtml(v2.display ?? parts[parts.length - 1]);
|
|
21171
21382
|
const pinned = this.pinned;
|
|
21172
21383
|
const steps = () => this.hooks.steps();
|
|
@@ -21189,9 +21400,9 @@ var SourceWindow = class {
|
|
|
21189
21400
|
const scrolled = this.el.querySelector(".sw-scroll");
|
|
21190
21401
|
if (scrolled && keep !== null) scrolled.scrollTop = keep;
|
|
21191
21402
|
if (hadFocus) {
|
|
21192
|
-
const box = this.el.querySelector(
|
|
21403
|
+
const box = this.el.querySelector(hadFocus.sel);
|
|
21193
21404
|
box?.focus();
|
|
21194
|
-
box?.setSelectionRange(hadFocus.from, hadFocus.to);
|
|
21405
|
+
if (hadFocus.from !== null && hadFocus.to !== null) box?.setSelectionRange(hadFocus.from, hadFocus.to);
|
|
21195
21406
|
}
|
|
21196
21407
|
this.box.reveal(this.el);
|
|
21197
21408
|
this.flashLines();
|
|
@@ -44403,6 +44614,231 @@ var Papers = class {
|
|
|
44403
44614
|
}
|
|
44404
44615
|
};
|
|
44405
44616
|
|
|
44617
|
+
// src/shared/artrecipe.ts
|
|
44618
|
+
var ART_PRESETS = [
|
|
44619
|
+
{ key: "hd", name: "HD", w: 1920, h: 1080 },
|
|
44620
|
+
{ key: "4k", name: "4K", w: 3840, h: 2160 },
|
|
44621
|
+
{ key: "square", name: "Square", w: 4096, h: 4096 },
|
|
44622
|
+
{ key: "a3", name: "A3, 300 dpi", w: 4961, h: 3508 },
|
|
44623
|
+
{ key: "a2", name: "A2, 300 dpi", w: 7016, h: 4961 }
|
|
44624
|
+
];
|
|
44625
|
+
var RECIPE_MODES = ["plain", "deps", "tcb", "keys", "refs", "apps"];
|
|
44626
|
+
var RECIPE_BYTES = 6e3;
|
|
44627
|
+
var FIELDS = ["size", "orientation", "style", "theme", "transparent", "colouring", "border", "title", "subtitle", "titleSize", "footSize", "source", "names", "targets", "keys", "notes", "edges", "selection", "lit", "view"];
|
|
44628
|
+
var NAME_FIELDS = ["from", "clusters", "minShare", "targets", "decls", "omit"];
|
|
44629
|
+
function readRecipe(v2) {
|
|
44630
|
+
const bad = (error, hint) => ({ error, hint });
|
|
44631
|
+
if (!v2 || typeof v2 !== "object" || Array.isArray(v2)) return bad("recipe is an object.", 'E.g. {"size": "a2", "orientation": "portrait", "names": {"clusters": 1}}.');
|
|
44632
|
+
if (new TextEncoder().encode(JSON.stringify(v2)).length > RECIPE_BYTES) return bad(`A recipe is at most ${RECIPE_BYTES} bytes.`, "Name clusters by a depth rather than one by one.");
|
|
44633
|
+
const o2 = v2;
|
|
44634
|
+
const unknown = Object.keys(o2).filter((k2) => !FIELDS.includes(k2));
|
|
44635
|
+
if (unknown.length) return bad(`recipe has no field ${unknown.join(", ")}.`, `Its fields: ${FIELDS.join(", ")}.`);
|
|
44636
|
+
const r2 = {};
|
|
44637
|
+
const bool = (k2) => {
|
|
44638
|
+
if (o2[k2] === void 0) return null;
|
|
44639
|
+
if (typeof o2[k2] !== "boolean") return `${k2} is true or false.`;
|
|
44640
|
+
r2[k2] = o2[k2];
|
|
44641
|
+
return null;
|
|
44642
|
+
};
|
|
44643
|
+
const oneOf = (k2, of) => {
|
|
44644
|
+
if (o2[k2] === void 0) return null;
|
|
44645
|
+
if (!of.includes(o2[k2])) return `${k2} is ${of.join(", ")}.`;
|
|
44646
|
+
r2[k2] = o2[k2];
|
|
44647
|
+
return null;
|
|
44648
|
+
};
|
|
44649
|
+
const num = (k2, least2, most) => {
|
|
44650
|
+
if (o2[k2] === void 0) return null;
|
|
44651
|
+
if (typeof o2[k2] !== "number" || !Number.isFinite(o2[k2]) || o2[k2] < least2 || o2[k2] > most) return `${k2} is a number from ${least2} to ${most} (hundredths of the picture's height).`;
|
|
44652
|
+
r2[k2] = o2[k2];
|
|
44653
|
+
return null;
|
|
44654
|
+
};
|
|
44655
|
+
const text2 = (k2) => {
|
|
44656
|
+
if (o2[k2] === void 0) return null;
|
|
44657
|
+
if (o2[k2] !== null && (typeof o2[k2] !== "string" || o2[k2].length > TITLE_CHARS)) return `${k2} is text of at most ${TITLE_CHARS} characters, "" for none, or null for the project's own.`;
|
|
44658
|
+
r2[k2] = o2[k2];
|
|
44659
|
+
return null;
|
|
44660
|
+
};
|
|
44661
|
+
const names2 = (x2) => Array.isArray(x2) && x2.length <= 2e3 && x2.every((n3) => typeof n3 === "string" && n3.length > 0 && n3.length <= 1e3);
|
|
44662
|
+
const fault = bool("transparent") ?? bool("border") ?? bool("source") ?? bool("keys") ?? bool("notes") ?? bool("lit") ?? oneOf("orientation", ["landscape", "portrait"]) ?? oneOf("style", ART_STYLES) ?? oneOf("theme", ["light", "dark"]) ?? oneOf("colouring", SCHEMES) ?? oneOf("edges", ["all", "selection", "none"]) ?? num("titleSize", TITLE_SIZE.least, TITLE_SIZE.most) ?? num("footSize", FOOT_SIZE.least, FOOT_SIZE.most) ?? text2("title") ?? text2("subtitle");
|
|
44663
|
+
if (fault) return bad(fault, 'See GET /api/agent/docs/export, "Artwork".');
|
|
44664
|
+
if (o2.size !== void 0) {
|
|
44665
|
+
const s2 = o2.size;
|
|
44666
|
+
if (typeof s2 === "string") {
|
|
44667
|
+
if (!ART_PRESETS.some((p2) => p2.key === s2)) return bad(`size ${s2} is no size.`, `A size by name (${ART_PRESETS.map((p2) => p2.key).join(", ")}) or {w, h} in pixels.`);
|
|
44668
|
+
r2.size = s2;
|
|
44669
|
+
} else {
|
|
44670
|
+
const sw = s2?.w, sh = s2?.h;
|
|
44671
|
+
const side2 = (x2) => Number.isInteger(x2) && x2 >= 16 && x2 <= ART_MAX_SIDE;
|
|
44672
|
+
if (!side2(sw) || !side2(sh)) return bad(`size is a name or {w, h}: whole pixels from 16 to ${ART_MAX_SIDE}.`, `Names: ${ART_PRESETS.map((p2) => p2.key).join(", ")}.`);
|
|
44673
|
+
r2.size = { w: sw, h: sh };
|
|
44674
|
+
}
|
|
44675
|
+
}
|
|
44676
|
+
if (o2.names !== void 0) {
|
|
44677
|
+
const n3 = o2.names;
|
|
44678
|
+
if (n3 === "board" || n3 === "clusters" || n3 === "none") r2.names = n3;
|
|
44679
|
+
else if (n3 && typeof n3 === "object" && !Array.isArray(n3)) {
|
|
44680
|
+
const m3 = n3;
|
|
44681
|
+
const extra = Object.keys(m3).filter((k2) => !NAME_FIELDS.includes(k2));
|
|
44682
|
+
if (extra.length) return bad(`names has no field ${extra.join(", ")}.`, `Its fields: ${NAME_FIELDS.join(", ")}.`);
|
|
44683
|
+
const rule = {};
|
|
44684
|
+
if (m3.from !== void 0) {
|
|
44685
|
+
if (m3.from !== "none" && m3.from !== "board") return bad("names.from is none or board.", "From none: only what the rule names; from board: what the board writes, the rule's besides.");
|
|
44686
|
+
rule.from = m3.from;
|
|
44687
|
+
}
|
|
44688
|
+
if (m3.clusters !== void 0) {
|
|
44689
|
+
if (Number.isInteger(m3.clusters) && m3.clusters >= 0 && m3.clusters <= 20) rule.clusters = m3.clusters;
|
|
44690
|
+
else if (names2(m3.clusters)) rule.clusters = m3.clusters;
|
|
44691
|
+
else return bad("names.clusters is a depth (0: the top level) or a list of clusters' keys.", "E.g. 1: the top level and the clusters right under it.");
|
|
44692
|
+
}
|
|
44693
|
+
if (m3.minShare !== void 0) {
|
|
44694
|
+
if (typeof m3.minShare !== "number" || !(m3.minShare >= 0 && m3.minShare <= 1)) return bad("names.minShare is a share from 0 to 1.", "0.01: a cluster below the top named where it holds at least 1% of the project's declarations.");
|
|
44695
|
+
rule.minShare = m3.minShare;
|
|
44696
|
+
}
|
|
44697
|
+
if (m3.targets !== void 0) {
|
|
44698
|
+
if (typeof m3.targets !== "boolean") return bad("names.targets is true or false.", "The shown targets' names.");
|
|
44699
|
+
rule.targets = m3.targets;
|
|
44700
|
+
}
|
|
44701
|
+
for (const k2 of ["decls", "omit"]) {
|
|
44702
|
+
if (m3[k2] === void 0) continue;
|
|
44703
|
+
if (!names2(m3[k2])) return bad(`names.${k2} is a list of names.`, k2 === "decls" ? "Declarations by full name." : "Clusters by key, declarations by full name.");
|
|
44704
|
+
rule[k2] = m3[k2];
|
|
44705
|
+
}
|
|
44706
|
+
r2.names = rule;
|
|
44707
|
+
} else return bad('names is "board", "clusters", "none" or a rule {from, clusters, minShare, targets, decls, omit}.', 'E.g. {"clusters": 1}: the top level and the clusters right under it, and the targets.');
|
|
44708
|
+
}
|
|
44709
|
+
if (o2.targets !== void 0) {
|
|
44710
|
+
const t2 = o2.targets;
|
|
44711
|
+
if (t2 === "all" || t2 === "none" || t2 === "faded") r2.targets = t2;
|
|
44712
|
+
else if (names2(t2)) r2.targets = t2;
|
|
44713
|
+
else return bad(`targets is "all", "none", "faded" or a list of targets' full names.`, "Those named are marked, the others drawn as any declaration.");
|
|
44714
|
+
}
|
|
44715
|
+
if (o2.selection !== void 0 && o2.selection !== null) {
|
|
44716
|
+
const s2 = o2.selection;
|
|
44717
|
+
if (!s2 || typeof s2 !== "object" || typeof s2.name !== "string" || !s2.name || s2.mode !== void 0 && !RECIPE_MODES.includes(s2.mode) || Object.keys(s2).some((k2) => k2 !== "name" && k2 !== "mode"))
|
|
44718
|
+
return bad("selection is {name, mode?}: a declaration's full name, and how it is selected.", `mode: ${RECIPE_MODES.join(", ")} (default tcb).`);
|
|
44719
|
+
r2.selection = { name: s2.name, ...s2.mode !== void 0 ? { mode: s2.mode } : {} };
|
|
44720
|
+
} else if (o2.selection === null) r2.selection = null;
|
|
44721
|
+
if (o2.view !== void 0 && o2.view !== null) {
|
|
44722
|
+
if (typeof o2.view !== "string" || !o2.view || o2.view.length > 200) return bad("view is a view's id.", "GET /api/agent/views lists them; left out, the Project.");
|
|
44723
|
+
r2.view = o2.view;
|
|
44724
|
+
}
|
|
44725
|
+
return { recipe: r2 };
|
|
44726
|
+
}
|
|
44727
|
+
var EXTERNAL = "\u27E8external\u27E9";
|
|
44728
|
+
var keyOf = (k2) => k2 === "External" ? EXTERNAL : k2.startsWith("External.") ? EXTERNAL + k2.slice(8) : k2;
|
|
44729
|
+
function resolveRecipe(r2, b2) {
|
|
44730
|
+
const problems = [];
|
|
44731
|
+
const preset = typeof r2.size === "string" ? ART_PRESETS.find((p2) => p2.key === r2.size) : ART_PRESETS[1];
|
|
44732
|
+
const given = typeof r2.size === "object" ? r2.size : { w: preset.w, h: preset.h };
|
|
44733
|
+
const portrait = r2.orientation ? r2.orientation === "portrait" : given.h > given.w;
|
|
44734
|
+
const [long, short6] = given.w >= given.h ? [given.w, given.h] : [given.h, given.w];
|
|
44735
|
+
const { w: w2, h: h2 } = portrait ? { w: short6, h: long } : { w: long, h: short6 };
|
|
44736
|
+
let left = 0;
|
|
44737
|
+
const targetSet = new Set(b2.targets);
|
|
44738
|
+
const held = (n4) => b2.clusterOf(n4) !== void 0;
|
|
44739
|
+
let targets = "marked";
|
|
44740
|
+
let shownTargets = [];
|
|
44741
|
+
let marked2 = b2.targets.filter(held);
|
|
44742
|
+
if (r2.targets === "none") [targets, marked2] = ["plain", []];
|
|
44743
|
+
else if (r2.targets === "faded") targets = "highlight";
|
|
44744
|
+
else if (Array.isArray(r2.targets)) {
|
|
44745
|
+
const unknown = r2.targets.filter((t2) => !targetSet.has(t2));
|
|
44746
|
+
if (unknown.length) problems.push(`not targets of the review: ${unknown.join(", ")}`);
|
|
44747
|
+
const off = r2.targets.filter((t2) => targetSet.has(t2) && !held(t2));
|
|
44748
|
+
if (off.length) problems.push(`targets not on the board: ${off.join(", ")}`);
|
|
44749
|
+
const on = r2.targets.filter((t2) => targetSet.has(t2) && held(t2));
|
|
44750
|
+
shownTargets = cleanNames(on, ART_TARGETS_BYTES);
|
|
44751
|
+
left += new Set(on).size - shownTargets.length;
|
|
44752
|
+
targets = "chosen";
|
|
44753
|
+
marked2 = shownTargets;
|
|
44754
|
+
}
|
|
44755
|
+
let labels = "board";
|
|
44756
|
+
let namesFrom = "board";
|
|
44757
|
+
let shown = [];
|
|
44758
|
+
let hidden = [];
|
|
44759
|
+
let ruledIds = [];
|
|
44760
|
+
const named2 = { clusters: [], ruled: [], decls: [] };
|
|
44761
|
+
const n3 = r2.names;
|
|
44762
|
+
if (n3 === "board" || n3 === "clusters" || n3 === "none") labels = n3;
|
|
44763
|
+
else if (n3) {
|
|
44764
|
+
labels = "chosen";
|
|
44765
|
+
namesFrom = n3.from ?? "none";
|
|
44766
|
+
const omit = new Set((n3.omit ?? []).map(keyOf));
|
|
44767
|
+
const byKey = new Map(b2.clusters.map((c3, i2) => [c3.key, i2]));
|
|
44768
|
+
if (typeof n3.clusters === "number") {
|
|
44769
|
+
const least2 = Math.ceil((n3.minShare ?? 0.01) * b2.projectCount);
|
|
44770
|
+
b2.clusters.forEach((c3) => {
|
|
44771
|
+
if ((c3.ext ? c3.depth === 0 : c3.depth <= n3.clusters && c3.count >= least2) && !omit.has(c3.key)) named2.ruled.push(c3.key);
|
|
44772
|
+
});
|
|
44773
|
+
} else if (Array.isArray(n3.clusters)) {
|
|
44774
|
+
for (const k2 of n3.clusters) {
|
|
44775
|
+
const key = keyOf(k2);
|
|
44776
|
+
if (!byKey.has(key)) problems.push(`no cluster ${k2} on the board`);
|
|
44777
|
+
else if (!omit.has(key) && !named2.clusters.includes(key)) named2.clusters.push(key);
|
|
44778
|
+
}
|
|
44779
|
+
}
|
|
44780
|
+
const decls = n3.targets === false ? [] : [...marked2];
|
|
44781
|
+
for (const d2 of n3.decls ?? []) {
|
|
44782
|
+
if (!held(d2)) problems.push(`no declaration ${d2} on the board`);
|
|
44783
|
+
else decls.push(d2);
|
|
44784
|
+
}
|
|
44785
|
+
for (const d2 of decls) if (!omit.has(d2) && !named2.decls.includes(d2)) named2.decls.push(d2);
|
|
44786
|
+
const ids = [...named2.clusters.map((k2) => artNameId("cluster", k2)), ...named2.decls.map((d2) => artNameId("decl", d2))];
|
|
44787
|
+
shown = cleanNames(ids, ART_NAMES_BYTES);
|
|
44788
|
+
const ruled = named2.ruled.map((k2) => artNameId("cluster", k2));
|
|
44789
|
+
ruledIds = cleanNames(ruled, ART_RULED_BYTES);
|
|
44790
|
+
left += ids.length + ruled.length - shown.length - ruledIds.length;
|
|
44791
|
+
const kept = /* @__PURE__ */ new Set([...shown, ...ruledIds]);
|
|
44792
|
+
named2.clusters = named2.clusters.filter((k2) => kept.has(artNameId("cluster", k2)));
|
|
44793
|
+
named2.ruled = named2.ruled.filter((k2) => kept.has(artNameId("cluster", k2)));
|
|
44794
|
+
named2.decls = named2.decls.filter((d2) => kept.has(artNameId("decl", d2)));
|
|
44795
|
+
if (namesFrom === "board") hidden = cleanNames([...omit].map((k2) => byKey.has(k2) ? artNameId("cluster", k2) : artNameId("decl", k2)), ART_NAMES_BYTES);
|
|
44796
|
+
}
|
|
44797
|
+
const look = {
|
|
44798
|
+
w: w2,
|
|
44799
|
+
h: h2,
|
|
44800
|
+
scheme: r2.colouring ?? "kind",
|
|
44801
|
+
theme: r2.theme ?? "light",
|
|
44802
|
+
background: r2.transparent ? "transparent" : "theme",
|
|
44803
|
+
labels,
|
|
44804
|
+
namesFrom,
|
|
44805
|
+
hiddenNames: hidden,
|
|
44806
|
+
shownNames: shown,
|
|
44807
|
+
ruledNames: ruledIds,
|
|
44808
|
+
edges: r2.edges ?? "all",
|
|
44809
|
+
notes: r2.notes ?? true,
|
|
44810
|
+
targets,
|
|
44811
|
+
shownTargets,
|
|
44812
|
+
keys: r2.keys ?? true,
|
|
44813
|
+
selection: r2.lit ?? true,
|
|
44814
|
+
source: r2.source ?? false,
|
|
44815
|
+
style: r2.style ?? "board",
|
|
44816
|
+
title: r2.title ?? null,
|
|
44817
|
+
subtitle: r2.subtitle ?? null,
|
|
44818
|
+
titleSize: r2.titleSize ?? TITLE_SIZE.first,
|
|
44819
|
+
footSize: r2.footSize ?? FOOT_SIZE.first,
|
|
44820
|
+
border: r2.border ?? false
|
|
44821
|
+
};
|
|
44822
|
+
return { look, named: named2, left, problems };
|
|
44823
|
+
}
|
|
44824
|
+
function decodeRecipe(text2) {
|
|
44825
|
+
try {
|
|
44826
|
+
const s2 = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
44827
|
+
return JSON.parse(new TextDecoder().decode(Uint8Array.from(s2, (c3) => c3.charCodeAt(0))));
|
|
44828
|
+
} catch {
|
|
44829
|
+
return null;
|
|
44830
|
+
}
|
|
44831
|
+
}
|
|
44832
|
+
var RECIPE_PARAM = "art";
|
|
44833
|
+
var SAVE_PARAM = "save";
|
|
44834
|
+
function recipeIn(hash) {
|
|
44835
|
+
const p2 = new URLSearchParams(hash.startsWith("#") ? hash.slice(1) : hash);
|
|
44836
|
+
const text2 = p2.get(RECIPE_PARAM);
|
|
44837
|
+
if (!text2) return null;
|
|
44838
|
+
const save = (p2.get(SAVE_PARAM) ?? "").split(",").filter((k2) => k2 === "png" || k2 === "svg");
|
|
44839
|
+
return { text: text2, save: [...new Set(save)] };
|
|
44840
|
+
}
|
|
44841
|
+
|
|
44406
44842
|
// src/web/src/tabs.ts
|
|
44407
44843
|
function starred(targets, short6) {
|
|
44408
44844
|
const s2 = targets.map((t2) => `\u2605${short6(t2)}`);
|
|
@@ -45444,7 +45880,7 @@ var EDGE_SPEED = 0.9;
|
|
|
45444
45880
|
var ARCS_MOST = 6e3;
|
|
45445
45881
|
var FOLD_MORE = 10;
|
|
45446
45882
|
var START = "\0start";
|
|
45447
|
-
var
|
|
45883
|
+
var keyOf2 = (r2) => `${r2.kind}:${r2.name}`;
|
|
45448
45884
|
var ease2 = (t2) => t2 < 0.5 ? 2 * t2 * t2 : 1 - (-2 * t2 + 2) ** 2 / 2;
|
|
45449
45885
|
var lerp = (a3, b2, t2) => a3 + (b2 - a3) * t2;
|
|
45450
45886
|
var LinearForm = class {
|
|
@@ -45805,8 +46241,8 @@ var LinearForm = class {
|
|
|
45805
46241
|
const p2 = ease2(this.t);
|
|
45806
46242
|
const sy = (y2) => v2.top + (y2 - this.cam.y) * k2;
|
|
45807
46243
|
const onScreen = (y0, y1) => sy(y1) > -40 && sy(y0) < h2 + 40;
|
|
45808
|
-
const oy = (r2) => this.off.get(
|
|
45809
|
-
const ox = (r2) => this.off.get(
|
|
46244
|
+
const oy = (r2) => this.off.get(keyOf2(r2))?.dy ?? 0;
|
|
46245
|
+
const ox = (r2) => this.off.get(keyOf2(r2))?.dx ?? 0;
|
|
45810
46246
|
const shown = (r2, y0, y1) => onScreen(y0 + oy(r2), y1 + oy(r2));
|
|
45811
46247
|
const at = (r2, y2, rad) => {
|
|
45812
46248
|
const f2 = r2.from;
|
|
@@ -45923,7 +46359,7 @@ var LinearForm = class {
|
|
|
45923
46359
|
const still = held.size ? this.rows.filter((r2) => !held.has(r2)) : this.rows;
|
|
45924
46360
|
circles(still);
|
|
45925
46361
|
if (this.hooks.edges() && p2 > 0.6) this.arcs(c3, (r2) => [v2.x + ox(r2), sy(r2.cy + oy(r2))], (p2 - 0.6) / 0.4, css);
|
|
45926
|
-
const on = this.hover && !this.drag ? this.rows.find((r2) =>
|
|
46362
|
+
const on = this.hover && !this.drag ? this.rows.find((r2) => keyOf2(r2) === this.hover) : void 0;
|
|
45927
46363
|
if (on && p2 >= 1) {
|
|
45928
46364
|
const top = sy(on.y0 + oy(on)), bottom = sy((on.kind === "section" ? on.y0 + ROW : on.y1) + oy(on));
|
|
45929
46365
|
const x0 = v2.x - 12 + ox(on), x1 = this.nameEnd(on) + 8 + ox(on);
|
|
@@ -46046,7 +46482,7 @@ var LinearForm = class {
|
|
|
46046
46482
|
}
|
|
46047
46483
|
/** The row the pointer is on, lit; drawn again where it changed. */
|
|
46048
46484
|
setHover(r2) {
|
|
46049
|
-
const key = r2 ?
|
|
46485
|
+
const key = r2 ? keyOf2(r2) : null;
|
|
46050
46486
|
if (key === this.hover) return;
|
|
46051
46487
|
this.hover = key;
|
|
46052
46488
|
this.request();
|
|
@@ -46241,7 +46677,7 @@ var LinearForm = class {
|
|
|
46241
46677
|
}
|
|
46242
46678
|
}
|
|
46243
46679
|
d2.at = at;
|
|
46244
|
-
for (const r2 of d2.block) this.off.set(
|
|
46680
|
+
for (const r2 of d2.block) this.off.set(keyOf2(r2), { dy, dx, ty: dy, tx: dx });
|
|
46245
46681
|
d2.sibs.forEach((_2, i2) => {
|
|
46246
46682
|
if (i2 === d2.from) return;
|
|
46247
46683
|
const shift = d2.from < i2 && i2 < at ? -d2.h : at <= i2 && i2 < d2.from ? d2.h : 0;
|
|
@@ -46250,22 +46686,22 @@ var LinearForm = class {
|
|
|
46250
46686
|
}
|
|
46251
46687
|
/** Ease a row to `ty` off its place on the line. */
|
|
46252
46688
|
slide(r2, ty) {
|
|
46253
|
-
const o2 = this.off.get(
|
|
46689
|
+
const o2 = this.off.get(keyOf2(r2));
|
|
46254
46690
|
if (o2) {
|
|
46255
46691
|
o2.ty = ty;
|
|
46256
46692
|
o2.tx = 0;
|
|
46257
|
-
} else if (ty) this.off.set(
|
|
46693
|
+
} else if (ty) this.off.set(keyOf2(r2), { dy: 0, dx: 0, ty, tx: 0 });
|
|
46258
46694
|
}
|
|
46259
46695
|
/** Where each row is drawn now, in the outline's units, and how far off the line. */
|
|
46260
46696
|
drawnAt() {
|
|
46261
|
-
return new Map(this.rows.map((r2) => [
|
|
46697
|
+
return new Map(this.rows.map((r2) => [keyOf2(r2), { y: r2.y0 + (this.off.get(keyOf2(r2))?.dy ?? 0), dx: this.off.get(keyOf2(r2))?.dx ?? 0 }]));
|
|
46262
46698
|
}
|
|
46263
46699
|
/** The rows came in another order: each eases from where it was drawn to its new place. */
|
|
46264
46700
|
settleFrom(before) {
|
|
46265
46701
|
for (const r2 of this.rows) {
|
|
46266
|
-
const b2 = before.get(
|
|
46267
|
-
if (b2 && (Math.abs(b2.y - r2.y0) > 0.5 || b2.dx)) this.off.set(
|
|
46268
|
-
else this.off.delete(
|
|
46702
|
+
const b2 = before.get(keyOf2(r2));
|
|
46703
|
+
if (b2 && (Math.abs(b2.y - r2.y0) > 0.5 || b2.dx)) this.off.set(keyOf2(r2), { dy: b2.y - r2.y0, dx: b2.dx, ty: 0, tx: 0 });
|
|
46704
|
+
else this.off.delete(keyOf2(r2));
|
|
46269
46705
|
}
|
|
46270
46706
|
}
|
|
46271
46707
|
/** Everything off its place on the line eases back to it. */
|
|
@@ -46421,13 +46857,7 @@ function triangle(c3, x2, y2, open) {
|
|
|
46421
46857
|
var FONT2 = `-apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", sans-serif`;
|
|
46422
46858
|
|
|
46423
46859
|
// src/web/src/artwork.ts
|
|
46424
|
-
var PRESETS =
|
|
46425
|
-
{ name: "HD", w: 1920, h: 1080 },
|
|
46426
|
-
{ name: "4K", w: 3840, h: 2160 },
|
|
46427
|
-
{ name: "Square", w: 4096, h: 4096 },
|
|
46428
|
-
{ name: "A3, 300 dpi", w: 4961, h: 3508 },
|
|
46429
|
-
{ name: "A2, 300 dpi", w: 7016, h: 4961 }
|
|
46430
|
-
];
|
|
46860
|
+
var PRESETS = ART_PRESETS;
|
|
46431
46861
|
function presetOf(w2, h2) {
|
|
46432
46862
|
return PRESETS.findIndex((p2) => p2.w === w2 && p2.h === h2 || p2.w === h2 && p2.h === w2);
|
|
46433
46863
|
}
|
|
@@ -46446,6 +46876,7 @@ var DEFAULTS = {
|
|
|
46446
46876
|
namesFrom: "board",
|
|
46447
46877
|
hiddenNames: [],
|
|
46448
46878
|
shownNames: [],
|
|
46879
|
+
ruledNames: [],
|
|
46449
46880
|
edges: "all",
|
|
46450
46881
|
notes: true,
|
|
46451
46882
|
targets: "marked",
|
|
@@ -46475,6 +46906,7 @@ function cleanSettings(s2, now) {
|
|
|
46475
46906
|
namesFrom: o2.namesFrom === "none" ? "none" : "board",
|
|
46476
46907
|
hiddenNames: cleanNames(o2.hiddenNames, ART_NAMES_BYTES, isArtNameId),
|
|
46477
46908
|
shownNames: cleanNames(o2.shownNames, ART_NAMES_BYTES, isArtNameId),
|
|
46909
|
+
ruledNames: cleanNames(o2.ruledNames, ART_RULED_BYTES, isArtNameId),
|
|
46478
46910
|
edges: one(o2.edges, ["all", "selection", "none"], DEFAULTS.edges),
|
|
46479
46911
|
notes: typeof o2.notes === "boolean" ? o2.notes : DEFAULTS.notes,
|
|
46480
46912
|
targets,
|
|
@@ -46508,8 +46940,28 @@ function withChosen(list2, add, budget) {
|
|
|
46508
46940
|
const had = new Set(list2), kept = new Set(out);
|
|
46509
46941
|
return { list: out, left: new Set(add.filter((x2) => !had.has(x2) && !kept.has(x2))).size };
|
|
46510
46942
|
}
|
|
46511
|
-
function themeBackground() {
|
|
46512
|
-
return
|
|
46943
|
+
function themeBackground(v2 = browserVar) {
|
|
46944
|
+
return v2("--canvas");
|
|
46945
|
+
}
|
|
46946
|
+
var browserVar = (name) => getComputedStyle(document.body).getPropertyValue(name).trim();
|
|
46947
|
+
var FRAME_ROOM = { panel: 300, margin: 32, gap: 12, head: 60, tail: 34, bar: 60 };
|
|
46948
|
+
function frameRect(screen, size) {
|
|
46949
|
+
const R2 = FRAME_ROOM;
|
|
46950
|
+
const roomW = Math.max(40, screen.w - R2.panel - 3 * R2.margin), roomH = Math.max(40, screen.h - R2.bar - R2.head - R2.tail - 2 * R2.gap);
|
|
46951
|
+
const scale = Math.min(roomW / size.w, roomH / size.h);
|
|
46952
|
+
const w2 = size.w * scale, h2 = size.h * scale;
|
|
46953
|
+
return { x: R2.margin + (roomW - w2) / 2, y: R2.bar + R2.gap + R2.head + (roomH - h2) / 2, w: w2, h: h2 };
|
|
46954
|
+
}
|
|
46955
|
+
function titleRoom(h2, s2, own2) {
|
|
46956
|
+
const titled = !!(s2.title ?? own2.title) || !!(s2.subtitle ?? own2.subtitle);
|
|
46957
|
+
return titled ? Math.min(h2 * 0.4, s2.titleSize * 1.7 / 100 * h2 + Math.max(14, h2 * 0.035)) : 0;
|
|
46958
|
+
}
|
|
46959
|
+
function writeOver(ctx, s2, rect, own2, source, v2 = browserVar) {
|
|
46960
|
+
const halo = s2.style === "cells" ? "#0f1520" : s2.style === "stars" ? "#090d1a" : v2("--canvas");
|
|
46961
|
+
if (s2.border) drawBorder(ctx, rect, s2.style === "cells" ? "rgba(200, 212, 236, 0.55)" : s2.style === "stars" ? "rgba(241, 230, 200, 0.55)" : v2("--text-2"));
|
|
46962
|
+
drawTitle(ctx, rect, s2.title ?? own2.title, s2.subtitle ?? own2.subtitle, s2.titleSize, s2.style, { ink: v2("--text"), quiet: v2("--text-2"), halo });
|
|
46963
|
+
const quiet = s2.style === "board" ? v2("--text-2") : "rgba(200, 208, 224, 0.7)";
|
|
46964
|
+
drawFoot(ctx, rect, s2.source ? source : null, { text: quiet, halo }, ART_SANS, s2.footSize);
|
|
46513
46965
|
}
|
|
46514
46966
|
function roundRect(ctx, x2, y2, w2, h2, r2) {
|
|
46515
46967
|
r2 = Math.min(r2, w2 / 2, h2 / 2);
|
|
@@ -46813,6 +47265,7 @@ function svgColour(css, alpha = 1) {
|
|
|
46813
47265
|
function glColour(c3) {
|
|
46814
47266
|
return { rgb: `rgb(${Math.round(c3[0] * 255)},${Math.round(c3[1] * 255)},${Math.round(c3[2] * 255)})`, a: +c3[3].toFixed(3) };
|
|
46815
47267
|
}
|
|
47268
|
+
var BUNDLE_PIECES = 12;
|
|
46816
47269
|
var paint = (attr, c3) => ` ${attr}="${c3.rgb}"${c3.a < 1 ? ` ${attr}-opacity="${c3.a}"` : ""}`;
|
|
46817
47270
|
var SvgGradient = class {
|
|
46818
47271
|
stops = [];
|
|
@@ -47032,18 +47485,20 @@ var SvgContext = class {
|
|
|
47032
47485
|
} else if (s2.kind === "line") {
|
|
47033
47486
|
this.body.push(`<line x1="${n2(s2.x0)}" y1="${n2(s2.y0)}" x2="${n2(s2.x1)}" y2="${n2(s2.y1)}"${paint("stroke", glColour(s2.colour))} stroke-width="${n2(s2.width)}"/>`);
|
|
47034
47487
|
} else {
|
|
47035
|
-
const pts = bundlePoints(s2.ctrl, s2.beta,
|
|
47036
|
-
const
|
|
47488
|
+
const pts = bundlePoints(s2.ctrl, s2.beta, BUNDLE_PIECES * 4);
|
|
47489
|
+
const n3 = pts.length / 2 - 1;
|
|
47490
|
+
const path = (i0, i1, c3) => `<path d="M${n2(pts[2 * i0])},${n2(pts[2 * i0 + 1])}${Array.from({ length: i1 - i0 }, (_2, k2) => `L${n2(pts[2 * (i0 + k2 + 1)])},${n2(pts[2 * (i0 + k2 + 1) + 1])}`).join("")}" fill="none"${paint("stroke", c3)} stroke-width="${n2(s2.width)}"/>`;
|
|
47037
47491
|
const a3 = glColour(s2.c0), b2 = glColour(s2.c1);
|
|
47038
|
-
|
|
47039
|
-
if (a3.rgb === b2.rgb && a3.a === b2.a) stroke = paint("stroke", a3);
|
|
47492
|
+
if (a3.rgb === b2.rgb && a3.a === b2.a) this.body.push(path(0, n3, a3));
|
|
47040
47493
|
else {
|
|
47041
|
-
const
|
|
47042
|
-
const
|
|
47043
|
-
|
|
47044
|
-
|
|
47494
|
+
const apart = Math.max(...s2.c0.map((v2, k2) => Math.abs(v2 - s2.c1[k2])));
|
|
47495
|
+
const pieces = Math.min(BUNDLE_PIECES, Math.max(2, Math.ceil(apart * BUNDLE_PIECES)));
|
|
47496
|
+
for (let p2 = 0; p2 < pieces; p2++) {
|
|
47497
|
+
const i0 = Math.floor(p2 * n3 / pieces), i1 = Math.floor((p2 + 1) * n3 / pieces);
|
|
47498
|
+
const t2 = (i0 + i1) / (2 * n3);
|
|
47499
|
+
this.body.push(path(i0, i1, glColour(s2.c0.map((v2, k2) => v2 + (s2.c1[k2] - v2) * t2))));
|
|
47500
|
+
}
|
|
47045
47501
|
}
|
|
47046
|
-
this.body.push(`<path d="${d2}" fill="none"${stroke} stroke-width="${n2(s2.width)}"/>`);
|
|
47047
47502
|
}
|
|
47048
47503
|
}
|
|
47049
47504
|
/** A circle of an artwork's style, as the renderer draws it: a glow fading from its centre; a
|
|
@@ -47200,13 +47655,37 @@ async function encodePng(w2, h2, bands) {
|
|
|
47200
47655
|
const sig = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
47201
47656
|
return new Blob([sig, ...chunk("IHDR", ihdr), ...chunk("IDAT", idat), ...chunk("IEND", new Uint8Array(0))], { type: "image/png" });
|
|
47202
47657
|
}
|
|
47658
|
+
function chooseIn(s2, written2, ids, on) {
|
|
47659
|
+
const hidden = new Set(s2.hiddenNames), shown = new Set(s2.shownNames), ruled = new Set(s2.ruledNames);
|
|
47660
|
+
const add = [], hide = [];
|
|
47661
|
+
for (const id of ids) {
|
|
47662
|
+
if (on) {
|
|
47663
|
+
hidden.delete(id);
|
|
47664
|
+
if (!written2.has(id) && !shown.has(id)) {
|
|
47665
|
+
add.push(id);
|
|
47666
|
+
ruled.delete(id);
|
|
47667
|
+
}
|
|
47668
|
+
} else if (shown.has(id)) shown.delete(id);
|
|
47669
|
+
else if (ruled.has(id)) {
|
|
47670
|
+
ruled.delete(id);
|
|
47671
|
+
if (s2.namesFrom === "board") hide.push(id);
|
|
47672
|
+
} else if (s2.namesFrom === "board" && written2.has(id)) hide.push(id);
|
|
47673
|
+
}
|
|
47674
|
+
return { hidden: [...hidden], shown: [...shown], ruled: [...ruled], add, hide };
|
|
47675
|
+
}
|
|
47676
|
+
async function untilStill(o2, since, quiet = 800, most = 2e4) {
|
|
47677
|
+
const t0 = o2.now();
|
|
47678
|
+
for (; ; ) {
|
|
47679
|
+
const t2 = o2.now();
|
|
47680
|
+
if (o2.drawnAt() >= since && t2 - o2.drawnAt() >= quiet) return true;
|
|
47681
|
+
if (t2 - t0 >= most) return false;
|
|
47682
|
+
await o2.sleep(100);
|
|
47683
|
+
}
|
|
47684
|
+
}
|
|
47203
47685
|
|
|
47204
47686
|
// src/web/src/artframe.ts
|
|
47205
|
-
var PANEL_W =
|
|
47206
|
-
var MARGIN =
|
|
47207
|
-
var GAP5 = 12;
|
|
47208
|
-
var HEAD_ROOM = 60;
|
|
47209
|
-
var TAIL_ROOM = 34;
|
|
47687
|
+
var PANEL_W = FRAME_ROOM.panel;
|
|
47688
|
+
var MARGIN = FRAME_ROOM.margin;
|
|
47210
47689
|
var EASE_MS2 = 480;
|
|
47211
47690
|
var DIP_MS = 180;
|
|
47212
47691
|
var BAND_PIXELS = 4096 * 4096;
|
|
@@ -47332,23 +47811,27 @@ var ArtFrame = class {
|
|
|
47332
47811
|
refresh() {
|
|
47333
47812
|
if (this.isOpen) this.renderPanel();
|
|
47334
47813
|
}
|
|
47335
|
-
/** The mode, from the Export window: the reader's last settings, the whole board in the frame
|
|
47336
|
-
|
|
47337
|
-
|
|
47338
|
-
|
|
47339
|
-
|
|
47340
|
-
|
|
47341
|
-
|
|
47342
|
-
|
|
47814
|
+
/** The mode, from the Export window: the reader's last settings, the whole board in the frame; or,
|
|
47815
|
+
* from a link's recipe (ExportSpec decision 20, shared/artrecipe.ts), the settings it gives — not
|
|
47816
|
+
* kept as the reader's until they change one. Resolves once the mode has come: the board drawn
|
|
47817
|
+
* with its look, the camera where it goes. */
|
|
47818
|
+
open(given) {
|
|
47819
|
+
if (this.isOpen && !this.leaving) return Promise.resolve();
|
|
47820
|
+
const s2 = cleanSettings(given ?? this.hooks.saved(), this.hooks.now());
|
|
47821
|
+
const came = this.start(s2, (r2) => {
|
|
47822
|
+
const below = titleRoom(r2.h, s2, this.hooks.titles());
|
|
47343
47823
|
return this.canvas.fitCam({ x: r2.x, y: r2.y + below, w: r2.w, h: r2.h - below });
|
|
47344
47824
|
});
|
|
47345
47825
|
this.hooks.changed();
|
|
47826
|
+
return came;
|
|
47346
47827
|
}
|
|
47347
|
-
/** The reader leaves: the board drawn as before, the camera where it was.
|
|
47828
|
+
/** The reader leaves: the board drawn as before, the camera where it was. Resolves once the mode
|
|
47829
|
+
* has gone. */
|
|
47348
47830
|
close() {
|
|
47349
|
-
if (!this.isOpen || this.leaving) return;
|
|
47350
|
-
this.end(true);
|
|
47831
|
+
if (!this.isOpen || this.leaving) return Promise.resolve();
|
|
47832
|
+
const gone = this.end(true);
|
|
47351
47833
|
this.hooks.changed();
|
|
47834
|
+
return gone;
|
|
47352
47835
|
}
|
|
47353
47836
|
/** The mode as presence carries it: its settings, and the rectangle of the board's world the frame
|
|
47354
47837
|
* holds — its middle, its width and height. */
|
|
@@ -47361,9 +47844,15 @@ var ArtFrame = class {
|
|
|
47361
47844
|
/** A followed reader's artwork, or a link's: on with their settings — not kept as this reader's —
|
|
47362
47845
|
* the frame holding what theirs holds; off where theirs is. */
|
|
47363
47846
|
follow(v2) {
|
|
47364
|
-
if (!v2)
|
|
47847
|
+
if (!v2) {
|
|
47848
|
+
void this.end(false);
|
|
47849
|
+
return;
|
|
47850
|
+
}
|
|
47365
47851
|
const { region, ...s2 } = v2;
|
|
47366
|
-
if (!this.isOpen || this.leaving)
|
|
47852
|
+
if (!this.isOpen || this.leaving) {
|
|
47853
|
+
void this.start(s2, (r2) => this.camHolding(region, r2));
|
|
47854
|
+
return;
|
|
47855
|
+
}
|
|
47367
47856
|
if (!sameLook(this.s, s2)) {
|
|
47368
47857
|
this.s = s2;
|
|
47369
47858
|
this.hooks.look(s2);
|
|
@@ -47376,7 +47865,8 @@ var ArtFrame = class {
|
|
|
47376
47865
|
/** The mode comes (ExportSpec decision 19, the owner's decision of 2026-10-15: as smoothly as the
|
|
47377
47866
|
* linear form, web/src/linear.ts): the frame closes in from the whole screen as its shade comes,
|
|
47378
47867
|
* the camera glides to where `aim` puts it in the frame, the panel slides in; the board dips to
|
|
47379
|
-
* its ground a moment, takes the artwork's look and comes back; the title and the foot come last.
|
|
47868
|
+
* its ground a moment, takes the artwork's look and comes back; the title and the foot come last.
|
|
47869
|
+
* Resolves when it has come (or a newer step took its place). */
|
|
47380
47870
|
start(s2, aim2) {
|
|
47381
47871
|
const turn = ++this.turn;
|
|
47382
47872
|
const body = document.body;
|
|
@@ -47411,13 +47901,19 @@ var ArtFrame = class {
|
|
|
47411
47901
|
this.renderSky();
|
|
47412
47902
|
this.renderInk();
|
|
47413
47903
|
}, DIP_MS);
|
|
47414
|
-
|
|
47904
|
+
return new Promise(
|
|
47905
|
+
(done) => setTimeout(() => {
|
|
47906
|
+
if (turn === this.turn) body.classList.remove("art-moving");
|
|
47907
|
+
done();
|
|
47908
|
+
}, EASE_MS2)
|
|
47909
|
+
);
|
|
47415
47910
|
}
|
|
47416
47911
|
/** The mode goes, as it came: the frame opens out to the whole screen as its shade goes, the
|
|
47417
47912
|
* panel slides away, the camera glides back where it was (`back`); the board dips, takes its own
|
|
47418
|
-
* look again, and comes back. A follower's goes with the other's, its camera theirs.
|
|
47913
|
+
* look again, and comes back. A follower's goes with the other's, its camera theirs. Resolves when
|
|
47914
|
+
* it has gone (or a newer step took its place). */
|
|
47419
47915
|
end(back) {
|
|
47420
|
-
if (!this.isOpen || this.leaving) return;
|
|
47916
|
+
if (!this.isOpen || this.leaving) return Promise.resolve();
|
|
47421
47917
|
const turn = ++this.turn;
|
|
47422
47918
|
const body = document.body;
|
|
47423
47919
|
this.leaving = true;
|
|
@@ -47436,17 +47932,20 @@ var ArtFrame = class {
|
|
|
47436
47932
|
this.renderSky();
|
|
47437
47933
|
body.classList.remove("art-dip");
|
|
47438
47934
|
}, DIP_MS);
|
|
47439
|
-
|
|
47440
|
-
|
|
47441
|
-
|
|
47442
|
-
|
|
47443
|
-
|
|
47444
|
-
|
|
47445
|
-
|
|
47446
|
-
|
|
47447
|
-
|
|
47448
|
-
|
|
47449
|
-
|
|
47935
|
+
return new Promise(
|
|
47936
|
+
(done) => setTimeout(() => {
|
|
47937
|
+
done();
|
|
47938
|
+
if (turn !== this.turn) return;
|
|
47939
|
+
this.leaving = false;
|
|
47940
|
+
this.rect = { x: 0, y: 0, w: 0, h: 0 };
|
|
47941
|
+
this.root.hidden = true;
|
|
47942
|
+
this.panel.hidden = true;
|
|
47943
|
+
this.sky.hidden = true;
|
|
47944
|
+
this.root.classList.remove("art-away");
|
|
47945
|
+
this.panel.classList.remove("art-away");
|
|
47946
|
+
body.classList.remove("art-moving");
|
|
47947
|
+
}, EASE_MS2)
|
|
47948
|
+
);
|
|
47450
47949
|
}
|
|
47451
47950
|
/** The camera that holds `region` of the board's world in the frame `r`: its middle at the frame's,
|
|
47452
47951
|
* its width the frame's. */
|
|
@@ -47459,10 +47958,7 @@ var ArtFrame = class {
|
|
|
47459
47958
|
* there keeps what it showed — the same middle, as much of the board. */
|
|
47460
47959
|
place(keep) {
|
|
47461
47960
|
const s2 = this.s;
|
|
47462
|
-
const
|
|
47463
|
-
const scale = Math.min(roomW / s2.w, roomH / s2.h);
|
|
47464
|
-
const w2 = Math.max(40, s2.w * scale), h2 = Math.max(40, s2.h * scale);
|
|
47465
|
-
const x2 = MARGIN + (roomW - w2) / 2, y2 = 60 + GAP5 + HEAD_ROOM + (roomH - h2) / 2;
|
|
47961
|
+
const { x: x2, y: y2, w: w2, h: h2 } = frameRect({ w: innerWidth, h: innerHeight }, s2);
|
|
47466
47962
|
const was = this.rect;
|
|
47467
47963
|
if (keep && was.w > 0) {
|
|
47468
47964
|
const C2 = this.canvas;
|
|
@@ -47536,14 +48032,7 @@ var ArtFrame = class {
|
|
|
47536
48032
|
}
|
|
47537
48033
|
/** What an artwork writes over the board within `rect`: its border, its title and its foot. */
|
|
47538
48034
|
writeOver(ctx, s2, rect) {
|
|
47539
|
-
|
|
47540
|
-
const cs = getComputedStyle(document.body);
|
|
47541
|
-
const v2 = (name) => cs.getPropertyValue(name).trim();
|
|
47542
|
-
const halo = s2.style === "cells" ? "#0f1520" : s2.style === "stars" ? "#090d1a" : v2("--canvas");
|
|
47543
|
-
if (s2.border) drawBorder(ctx, rect, s2.style === "cells" ? "rgba(200, 212, 236, 0.55)" : s2.style === "stars" ? "rgba(241, 230, 200, 0.55)" : v2("--text-2"));
|
|
47544
|
-
drawTitle(ctx, rect, s2.title ?? own2.title, s2.subtitle ?? own2.subtitle, s2.titleSize, s2.style, { ink: v2("--text"), quiet: v2("--text-2"), halo });
|
|
47545
|
-
const quiet = s2.style === "board" ? v2("--text-2") : "rgba(200, 208, 224, 0.7)";
|
|
47546
|
-
drawFoot(ctx, rect, s2.source ? this.hooks.source() : null, { text: quiet, halo }, ART_SANS, s2.footSize);
|
|
48035
|
+
writeOver(ctx, s2, rect, this.hooks.titles(), this.hooks.source());
|
|
47547
48036
|
}
|
|
47548
48037
|
/** The title, its line or its size changed beside the frame, or the foot's size: kept, drawn over
|
|
47549
48038
|
* the board at once, and said to those following once it has been still a moment. */
|
|
@@ -47600,7 +48089,7 @@ var ArtFrame = class {
|
|
|
47600
48089
|
else {
|
|
47601
48090
|
s2[k2] = t2.value;
|
|
47602
48091
|
if (k2 === "labels" && t2.value === "chosen") {
|
|
47603
|
-
[s2.namesFrom, s2.hiddenNames, s2.shownNames] = ["board", [], []];
|
|
48092
|
+
[s2.namesFrom, s2.hiddenNames, s2.shownNames, s2.ruledNames] = ["board", [], [], []];
|
|
47604
48093
|
this.filter = "";
|
|
47605
48094
|
} else if (k2 === "targets" && t2.value === "chosen") {
|
|
47606
48095
|
const r2 = withChosen([], this.hooks.targets().map((x2) => x2.name), ART_TARGETS_BYTES);
|
|
@@ -47632,21 +48121,17 @@ var ArtFrame = class {
|
|
|
47632
48121
|
/** Names shown, or hidden (By selection): from their list, its All and None, or the board's menu.
|
|
47633
48122
|
* A name ticked is no longer hidden, and is shown besides where the board does not write it now; a
|
|
47634
48123
|
* name unticked is no longer shown besides, and is hidden where the board would write it all the
|
|
47635
|
-
* same. What does not fit in what the artwork carries is left out, and said.
|
|
48124
|
+
* same. What does not fit in what the artwork carries is left out, and said. A name a recipe's
|
|
48125
|
+
* rule chose (ExportSpec decision 20) is the reader's once ticked — written wherever it falls, as
|
|
48126
|
+
* one ticked by hand — and no longer the rule's once unticked, hidden where the board would write
|
|
48127
|
+
* it (artwork chooseIn). */
|
|
47636
48128
|
chooseNames(ids, on) {
|
|
47637
48129
|
const s2 = this.s;
|
|
47638
48130
|
if (!s2 || this.busy) return this.renderPanel();
|
|
47639
48131
|
const now = this.canvas.writtenNames();
|
|
47640
|
-
const hidden
|
|
47641
|
-
|
|
47642
|
-
|
|
47643
|
-
if (on) {
|
|
47644
|
-
hidden.delete(id);
|
|
47645
|
-
if (!now.has(id) && !shown.has(id)) add.push(id);
|
|
47646
|
-
} else if (shown.has(id)) shown.delete(id);
|
|
47647
|
-
else if (s2.namesFrom === "board" && now.has(id)) hide.push(id);
|
|
47648
|
-
}
|
|
47649
|
-
const a3 = withChosen([...shown], add, ART_NAMES_BYTES), h2 = withChosen([...hidden], hide, ART_NAMES_BYTES);
|
|
48132
|
+
const { hidden, shown, ruled, add, hide } = chooseIn(s2, now, ids, on);
|
|
48133
|
+
s2.ruledNames = ruled;
|
|
48134
|
+
const a3 = withChosen(shown, add, ART_NAMES_BYTES), h2 = withChosen(hidden, hide, ART_NAMES_BYTES);
|
|
47650
48135
|
[s2.shownNames, s2.hiddenNames] = [a3.list, h2.list];
|
|
47651
48136
|
if (a3.left + h2.left) this.hooks.say(NO_ROOM(a3.left + h2.left, "name"));
|
|
47652
48137
|
const wrote = new Set(now);
|
|
@@ -47678,7 +48163,7 @@ var ArtFrame = class {
|
|
|
47678
48163
|
}
|
|
47679
48164
|
if (!this.filter.trim()) {
|
|
47680
48165
|
if (this.busy) return this.renderPanel();
|
|
47681
|
-
[s2.namesFrom, s2.hiddenNames, s2.shownNames] = [all === "all" ? "board" : "none", [], []];
|
|
48166
|
+
[s2.namesFrom, s2.hiddenNames, s2.shownNames, s2.ruledNames] = [all === "all" ? "board" : "none", [], [], []];
|
|
47682
48167
|
return this.settled();
|
|
47683
48168
|
}
|
|
47684
48169
|
this.chooseNames(nameRows(this.rows, this.filter, Infinity).rows.map((r2) => r2.id), all === "all");
|
|
@@ -47715,7 +48200,7 @@ var ArtFrame = class {
|
|
|
47715
48200
|
const s2 = this.s;
|
|
47716
48201
|
const here = this.rows.filter((r2) => this.wrote.has(r2.id)).length;
|
|
47717
48202
|
const n3 = (k2, what) => k2 ? ` \xB7 ${k2.toLocaleString("en")} ${what}` : "";
|
|
47718
|
-
return `${here.toLocaleString("en")} of ${this.rows.length.toLocaleString("en")} in the frame written${s2?.namesFrom === "none" ? " \xB7 from none" : ""}${n3(s2?.hiddenNames.length ?? 0, "hidden")}${n3(s2?.shownNames.length ?? 0, "shown besides")}`;
|
|
48203
|
+
return `${here.toLocaleString("en")} of ${this.rows.length.toLocaleString("en")} in the frame written${s2?.namesFrom === "none" ? " \xB7 from none" : ""}${n3(s2?.hiddenNames.length ?? 0, "hidden")}${n3(s2?.shownNames.length ?? 0, "shown besides")}${n3(s2?.ruledNames.length ?? 0, "by a rule")}`;
|
|
47719
48204
|
}
|
|
47720
48205
|
renderPanel(held = false) {
|
|
47721
48206
|
const s2 = this.s;
|
|
@@ -47791,10 +48276,18 @@ var ArtFrame = class {
|
|
|
47791
48276
|
const n3 = all.filter((t2) => on.has(t2.name)).length;
|
|
47792
48277
|
return `<div class="art-pick"><div class="art-pick-head"><span class="muted">Their star and name, each ticked</span><button class="rv-btn" data-art-pick="targets-all">All</button><button class="rv-btn" data-art-pick="targets-none">None</button></div><div class="art-rows" data-art-rows="targets">${rows || `<div class="art-rows-h">The review has no target</div>`}</div><div class="art-note muted">${n3} of ${all.length} marked</div></div>`;
|
|
47793
48278
|
}
|
|
48279
|
+
/** The files a link's recipe asks for (ExportSpec decision 20), one after the other, as Save SVG
|
|
48280
|
+
* and Save PNG make them; those made. */
|
|
48281
|
+
async save(kinds) {
|
|
48282
|
+
const made2 = [];
|
|
48283
|
+
if (!this.isOpen || !this.s) return made2;
|
|
48284
|
+
for (const k2 of kinds) if (await this.saveFile(k2)) made2.push(k2);
|
|
48285
|
+
return made2;
|
|
48286
|
+
}
|
|
47794
48287
|
/** Make the file, at the full size, from the board as it is drawn now, and hand it to the browser
|
|
47795
|
-
* to save. */
|
|
48288
|
+
* to save; whether it was made. */
|
|
47796
48289
|
async saveFile(kind) {
|
|
47797
|
-
if (this.busy || !this.s) return;
|
|
48290
|
+
if (this.busy || !this.s) return false;
|
|
47798
48291
|
const s2 = { ...this.s };
|
|
47799
48292
|
const C2 = this.canvas;
|
|
47800
48293
|
const rect = { ...this.rect }, cam = { ...C2.cam };
|
|
@@ -47819,7 +48312,8 @@ var ArtFrame = class {
|
|
|
47819
48312
|
if (s2.border) pen.raw(`</g>`);
|
|
47820
48313
|
this.writeOver(ctx, s2, rect);
|
|
47821
48314
|
}, await faces());
|
|
47822
|
-
|
|
48315
|
+
download(new Blob([svg], { type: "image/svg+xml" }), name);
|
|
48316
|
+
return true;
|
|
47823
48317
|
}
|
|
47824
48318
|
this.busy = true;
|
|
47825
48319
|
this.renderPanel();
|
|
@@ -47881,8 +48375,10 @@ var ArtFrame = class {
|
|
|
47881
48375
|
}
|
|
47882
48376
|
}
|
|
47883
48377
|
download(await encodePng(s2.w, s2.h, bands()), name);
|
|
48378
|
+
return true;
|
|
47884
48379
|
} catch {
|
|
47885
48380
|
this.hooks.say(`This browser could not draw a picture of ${s2.w} \xD7 ${s2.h} pixels: choose a smaller size, or save the SVG.`);
|
|
48381
|
+
return false;
|
|
47886
48382
|
} finally {
|
|
47887
48383
|
this.busy = false;
|
|
47888
48384
|
this.renderPanel();
|
|
@@ -50278,8 +50774,10 @@ async function boot() {
|
|
|
50278
50774
|
menu: (t2, x2, y2, world) => openMenu(t2, x2, y2, world),
|
|
50279
50775
|
frozenDrag: () => say(frozenWhy())
|
|
50280
50776
|
});
|
|
50777
|
+
const ownOpen = /* @__PURE__ */ new Map();
|
|
50778
|
+
const asOpenHere = (ns) => ownOpen.size ? ns.map((n3) => ownOpen.has(n3.id) && ownOpen.get(n3.id) !== n3.open ? { ...n3, open: ownOpen.get(n3.id) } : n3) : ns;
|
|
50281
50779
|
const stickies = new Stickies($4("#stickies"), canvas, {
|
|
50282
|
-
notes: () => ({ project: projectNotes.list, view: nav.board?.rec.notes ?? [] }),
|
|
50780
|
+
notes: () => ({ project: asOpenHere(projectNotes.list), view: asOpenHere(nav.board?.rec.notes ?? []) }),
|
|
50283
50781
|
// the Project holds every note of the project; another board what its scope gives and its
|
|
50284
50782
|
// view's lists (the owner's decision of 2026-10-11), the links read as the board reads the review
|
|
50285
50783
|
items: () => ({ all: !nav.board || fixedScope(nav.board.rec), items: nav.board?.rec.items, links: boardReview().links ?? [] }),
|
|
@@ -50294,7 +50792,7 @@ async function boot() {
|
|
|
50294
50792
|
if (i2 !== void 0) go(i2);
|
|
50295
50793
|
},
|
|
50296
50794
|
moves: (h2) => !frozen() && dragMoves(locked, h2),
|
|
50297
|
-
setOpen: (id,
|
|
50795
|
+
setOpen: (id, _kind, open) => setNoteOpen(id, open),
|
|
50298
50796
|
moved: (id, to) => void moveNote(id, to),
|
|
50299
50797
|
// where a card hangs from its icon is shared as its state is, and is no edit
|
|
50300
50798
|
card: (id, kind, card) => void (kind === "project" ? projectOps([{ op: "pnote-card", id, card }]) : noteOps([{ op: "note-card", id, card }])),
|
|
@@ -50326,13 +50824,8 @@ async function boot() {
|
|
|
50326
50824
|
edges: () => S2.showEdges,
|
|
50327
50825
|
moveNote: (id, key, world, detach2) => stickies.moveInto(id, key, world, detach2),
|
|
50328
50826
|
noteText: (id) => noteOf(id)?.note.text ?? null,
|
|
50329
|
-
noteOpen: (id) =>
|
|
50330
|
-
toggleNote: (id) =>
|
|
50331
|
-
const n3 = noteOf(id);
|
|
50332
|
-
if (frozen()) return say(frozenWhy());
|
|
50333
|
-
if (n3?.kind === "project") void projectOps([{ op: "pnote-open", id: n3.note.id, open: !n3.note.open }]);
|
|
50334
|
-
else if (n3) noteOps([{ op: "note-open", id: n3.note.id, open: !n3.note.open }]);
|
|
50335
|
-
},
|
|
50827
|
+
noteOpen: (id) => noteOpenHere(id),
|
|
50828
|
+
toggleNote: (id) => setNoteOpen(id, !noteOpenHere(id)),
|
|
50336
50829
|
// the board's menu on what a row is; Fold and Fit are the form's (menuAct)
|
|
50337
50830
|
menu: (on, x2, y2, spot) => {
|
|
50338
50831
|
if (on?.kind === "note") openNoteMenu(on.id, x2, y2);
|
|
@@ -50352,6 +50845,10 @@ async function boot() {
|
|
|
50352
50845
|
const ART_INERT = "#bar .brand, #bar .library-wrap, #export-btn, #activity-btn, #bar .peers-wrap, #bar .who-wrap, #help-btn, #bar .more-wrap";
|
|
50353
50846
|
const ART_KEYS = /* @__PURE__ */ new Set(["/", "1", "2", "3", "f", "+", "=", "-", "_", "e", "[", "]", "l"]);
|
|
50354
50847
|
let artLook = null;
|
|
50848
|
+
let gitNow = null;
|
|
50849
|
+
let gitAsked = data.git().then((git) => {
|
|
50850
|
+
gitNow ??= git;
|
|
50851
|
+
});
|
|
50355
50852
|
const art = new ArtFrame($4("#art"), $4("#art-panel"), $4("#art-sky"), canvas, {
|
|
50356
50853
|
title: () => views.title(nav.board.id),
|
|
50357
50854
|
saved: () => S2.artwork,
|
|
@@ -50361,13 +50858,21 @@ async function boot() {
|
|
|
50361
50858
|
},
|
|
50362
50859
|
now: () => ({ scheme: canvas.scheme, theme: document.documentElement.dataset.theme === "dark" ? "dark" : "light" }),
|
|
50363
50860
|
look: (s2) => {
|
|
50861
|
+
const coming = !artLook && !!s2;
|
|
50364
50862
|
artLook = s2 && { ...s2 };
|
|
50365
50863
|
drawLook();
|
|
50864
|
+
if (coming)
|
|
50865
|
+
gitAsked = data.git().then((git) => {
|
|
50866
|
+
gitNow = git;
|
|
50867
|
+
art.refresh();
|
|
50868
|
+
});
|
|
50366
50869
|
},
|
|
50367
50870
|
changed: () => people.report(),
|
|
50368
50871
|
notes: () => stickies.asDrawn(),
|
|
50872
|
+
// the commit checked out now; on a past build's board, that build's, with the project's source
|
|
50369
50873
|
source: () => {
|
|
50370
|
-
const
|
|
50874
|
+
const then = vg().meta.project.git;
|
|
50875
|
+
const git = pastShown() ? then && { ...then, source: then.source ?? gitNow?.source ?? null } : gitNow;
|
|
50371
50876
|
return git ? sourceLine(git) : null;
|
|
50372
50877
|
},
|
|
50373
50878
|
// the targets a picture can mark: every one of the review's, by the name the board writes
|
|
@@ -50394,12 +50899,14 @@ async function boot() {
|
|
|
50394
50899
|
linear.hide("reader");
|
|
50395
50900
|
document.documentElement.dataset.theme = a3.style === "board" ? a3.theme : "dark";
|
|
50396
50901
|
} else applyTheme();
|
|
50397
|
-
canvas.setArt(a3 && { labels: a3.labels, namesFrom: a3.namesFrom, hidden: new Set(a3.hiddenNames), shown: new Set(a3.shownNames), edges: a3.edges, targets: a3.targets, shownTargets: new Set(a3.shownTargets), keys: a3.keys, style: a3.style });
|
|
50902
|
+
canvas.setArt(a3 && { labels: a3.labels, namesFrom: a3.namesFrom, hidden: new Set(a3.hiddenNames), shown: new Set(a3.shownNames), ruled: new Set(a3.ruledNames), edges: a3.edges, targets: a3.targets, shownTargets: new Set(a3.shownTargets), keys: a3.keys, style: a3.style });
|
|
50398
50903
|
applyScheme();
|
|
50399
50904
|
syncEdges();
|
|
50400
50905
|
apply();
|
|
50401
50906
|
};
|
|
50907
|
+
let drawnAt = 0;
|
|
50402
50908
|
canvas.onFrame = () => {
|
|
50909
|
+
drawnAt = performance.now();
|
|
50403
50910
|
stickies.frame();
|
|
50404
50911
|
art.frame();
|
|
50405
50912
|
};
|
|
@@ -50813,6 +51320,33 @@ async function boot() {
|
|
|
50813
51320
|
const line = missingLine(gone);
|
|
50814
51321
|
if (line) say(line);
|
|
50815
51322
|
};
|
|
51323
|
+
const openRecipe = async (link) => {
|
|
51324
|
+
const read = readRecipe(decodeRecipe(link.text));
|
|
51325
|
+
if ("error" in read) return say(`This link's artwork could not be read: ${read.error}`);
|
|
51326
|
+
const L2 = canvas.layout;
|
|
51327
|
+
const { look, problems, left } = resolveRecipe(read.recipe, {
|
|
51328
|
+
clusters: L2.clusters,
|
|
51329
|
+
clusterOf: (name) => {
|
|
51330
|
+
const i2 = g2.byName.get(name);
|
|
51331
|
+
const j2 = i2 === void 0 ? -1 : toView(i2);
|
|
51332
|
+
return j2 < 0 ? void 0 : L2.cluster[j2];
|
|
51333
|
+
},
|
|
51334
|
+
targets: Object.keys(review.data.targets),
|
|
51335
|
+
projectCount: g2.projectCount
|
|
51336
|
+
});
|
|
51337
|
+
if (problems.length || left) say(`This link's artwork: ${[...problems, ...left ? [`${left} names left out, more than an artwork carries`] : []].join("; ")}.`);
|
|
51338
|
+
if (art.isOpen) await art.close();
|
|
51339
|
+
await art.open(look);
|
|
51340
|
+
if (!link.save.length) return;
|
|
51341
|
+
const since = performance.now();
|
|
51342
|
+
await gitAsked;
|
|
51343
|
+
await document.fonts?.ready;
|
|
51344
|
+
canvas.request();
|
|
51345
|
+
await untilStill({ now: () => performance.now(), drawnAt: () => drawnAt, sleep: (ms) => new Promise((ok) => setTimeout(ok, ms)) }, since);
|
|
51346
|
+
if (link.save.length > 1) say(`Saving the picture as ${link.save.map((k2) => k2.toUpperCase()).join(" and ")}: your browser may ask to allow this site to download several files.`);
|
|
51347
|
+
const made3 = await art.save(link.save);
|
|
51348
|
+
document.documentElement.dataset.artworkSaved = made3.length ? made3.join(",") : "none";
|
|
51349
|
+
};
|
|
50816
51350
|
handsOver = () => !reader && (!!data.snapshot || !!people.name || !!data.privateSession);
|
|
50817
51351
|
const syncPeers = () => {
|
|
50818
51352
|
const marks = [];
|
|
@@ -51621,7 +52155,12 @@ async function boot() {
|
|
|
51621
52155
|
const cam = e2.state?.cam ?? null;
|
|
51622
52156
|
const tmAt = e2.state?.tm;
|
|
51623
52157
|
const snap2 = snapshotIn(location.hash);
|
|
52158
|
+
const recipe2 = recipeIn(location.hash);
|
|
51624
52159
|
void (st2 ? restore2(st2, cam, typeof tmAt === "number" ? tmAt : void 0) : Promise.resolve()).then(() => snap2 ? openSnapshot(snap2).then(() => writeAddress(false)) : void 0).then(() => {
|
|
52160
|
+
if (!recipe2) return;
|
|
52161
|
+
void openRecipe(recipe2);
|
|
52162
|
+
writeAddress(false);
|
|
52163
|
+
}).then(() => {
|
|
51625
52164
|
renderNav();
|
|
51626
52165
|
people.report();
|
|
51627
52166
|
});
|
|
@@ -52125,6 +52664,16 @@ async function boot() {
|
|
|
52125
52664
|
const p2 = projectNotes.get(id);
|
|
52126
52665
|
return p2 ? { kind: "project", note: p2 } : null;
|
|
52127
52666
|
};
|
|
52667
|
+
const noteOpenHere = (id) => ownOpen.get(id) ?? !!noteOf(id)?.note.open;
|
|
52668
|
+
const setNoteOpen = (id, open) => {
|
|
52669
|
+
const n3 = noteOf(id);
|
|
52670
|
+
if (!n3) return;
|
|
52671
|
+
if (reader) {
|
|
52672
|
+
ownOpen.set(id, open);
|
|
52673
|
+
stickies.render();
|
|
52674
|
+
} else if (n3.kind === "project") void projectOps([{ op: "pnote-open", id, open }]);
|
|
52675
|
+
else noteOps([{ op: "note-open", id, open }]);
|
|
52676
|
+
};
|
|
52128
52677
|
const moveNote = (id, d2) => {
|
|
52129
52678
|
const had = noteOf(id)?.note.decl;
|
|
52130
52679
|
const attach = d2.decl || had ? d2.decl : void 0;
|
|
@@ -52238,8 +52787,9 @@ async function boot() {
|
|
|
52238
52787
|
if (!n3) return;
|
|
52239
52788
|
const t2 = {
|
|
52240
52789
|
kind: "note",
|
|
52241
|
-
open:
|
|
52790
|
+
open: noteOpenHere(id),
|
|
52242
52791
|
view: n3.kind === "view",
|
|
52792
|
+
...reader ? { own: true } : {},
|
|
52243
52793
|
carries: (n3.note.carries ?? []).map(carryLabel),
|
|
52244
52794
|
attach: !data.snapshot && !reader,
|
|
52245
52795
|
...n3.kind === "project" ? { items: noteViews(n3.note) } : {}
|
|
@@ -52462,12 +53012,9 @@ async function boot() {
|
|
|
52462
53012
|
case "note-edit":
|
|
52463
53013
|
if (on.note) await editNote(on.note, on.at);
|
|
52464
53014
|
return;
|
|
52465
|
-
case "note-toggle":
|
|
52466
|
-
|
|
52467
|
-
if (n3?.kind === "project") void projectOps([{ op: "pnote-open", id: n3.note.id, open: !n3.note.open }]);
|
|
52468
|
-
else if (n3) noteOps([{ op: "note-open", id: n3.note.id, open: !n3.note.open }]);
|
|
53015
|
+
case "note-toggle":
|
|
53016
|
+
if (on.note) setNoteOpen(on.note, !noteOpenHere(on.note));
|
|
52469
53017
|
return;
|
|
52470
|
-
}
|
|
52471
53018
|
case "note-kind":
|
|
52472
53019
|
if (on.note) await switchNote(on.note);
|
|
52473
53020
|
return;
|
|
@@ -52606,6 +53153,8 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
52606
53153
|
listen();
|
|
52607
53154
|
const snap = snapshotIn(location.hash);
|
|
52608
53155
|
if (snap) await openSnapshot(snap);
|
|
53156
|
+
const recipe = recipeIn(location.hash);
|
|
53157
|
+
if (recipe) void openRecipe(recipe);
|
|
52609
53158
|
if (!data.snapshot) void tm.load().then(() => renderTabs());
|
|
52610
53159
|
try {
|
|
52611
53160
|
history.replaceState({ n: 0 }, "", "#" + encodeState(current2()));
|