dsh-codex-subscription 2.1.0 → 2.1.1-beta.2
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.en.md +9 -3
- package/README.md +9 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/compatibility.json +2 -1
- package/lib/client.js +2087 -1450
- package/lib/index.js +2183 -1479
- package/package.json +23 -17
package/lib/client.js
CHANGED
|
@@ -105,6 +105,65 @@ window.__ModuleLoader__.load({
|
|
|
105
105
|
context.fillStyle = context.strokeStyle;
|
|
106
106
|
}
|
|
107
107
|
//#endregion
|
|
108
|
+
//#region src/sketch-text.js
|
|
109
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
110
|
+
function layoutSketchText(stroke, context, width, height) {
|
|
111
|
+
const saved = cache.get(stroke);
|
|
112
|
+
if (saved?.width === width && saved?.height === height) return saved.layout;
|
|
113
|
+
const [a, b] = stroke.points;
|
|
114
|
+
const x = Math.min(a.x, b.x) * width, y = Math.min(a.y, b.y) * height;
|
|
115
|
+
const w = Math.abs(b.x - a.x) * width, h = Math.abs(b.y - a.y) * height;
|
|
116
|
+
const wrap = (size) => {
|
|
117
|
+
context.font = `${size}px system-ui, sans-serif`;
|
|
118
|
+
const lines = [];
|
|
119
|
+
for (const paragraph of stroke.text.split("\n")) {
|
|
120
|
+
let line = "";
|
|
121
|
+
for (const char of paragraph) {
|
|
122
|
+
if (line && context.measureText(line + char).width > w) {
|
|
123
|
+
lines.push(line);
|
|
124
|
+
line = "";
|
|
125
|
+
}
|
|
126
|
+
line += char;
|
|
127
|
+
}
|
|
128
|
+
lines.push(line);
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
};
|
|
132
|
+
let size = stroke.width, lines = wrap(size);
|
|
133
|
+
if (lines.length * size * 1.2 > h || lines.some((line) => context.measureText(line).width > w)) {
|
|
134
|
+
let low = 0, high = size;
|
|
135
|
+
for (let i = 0; i < 14; i++) {
|
|
136
|
+
const mid = (low + high) / 2, candidate = wrap(mid);
|
|
137
|
+
if (candidate.length * mid * 1.2 <= h && candidate.every((line) => context.measureText(line).width <= w)) low = mid;
|
|
138
|
+
else high = mid;
|
|
139
|
+
}
|
|
140
|
+
size = low;
|
|
141
|
+
lines = wrap(size);
|
|
142
|
+
}
|
|
143
|
+
const layout = {
|
|
144
|
+
x,
|
|
145
|
+
y,
|
|
146
|
+
size,
|
|
147
|
+
lines
|
|
148
|
+
};
|
|
149
|
+
cache.set(stroke, {
|
|
150
|
+
width,
|
|
151
|
+
height,
|
|
152
|
+
layout
|
|
153
|
+
});
|
|
154
|
+
return layout;
|
|
155
|
+
}
|
|
156
|
+
function newTextBounds(point) {
|
|
157
|
+
const a = {
|
|
158
|
+
x: Math.min(point.x, .65),
|
|
159
|
+
y: Math.min(point.y, .85)
|
|
160
|
+
};
|
|
161
|
+
return [a, {
|
|
162
|
+
x: a.x + .35,
|
|
163
|
+
y: a.y + .15
|
|
164
|
+
}];
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
108
167
|
//#region src/sketch-document.js
|
|
109
168
|
const SKETCH_SIZE = 1024;
|
|
110
169
|
const MAX_SKETCH_STROKES = 2e3;
|
|
@@ -134,11 +193,10 @@ window.__ModuleLoader__.load({
|
|
|
134
193
|
context.beginPath();
|
|
135
194
|
const last = stroke.points.at(-1);
|
|
136
195
|
if (stroke.shape === "text") {
|
|
137
|
-
const
|
|
138
|
-
const lines = stroke.text.split("\n"), fontSize = Math.min(stroke.width, h / Math.max(1, lines.length) / 1.2);
|
|
196
|
+
const { x, y, lines, size: fontSize } = layoutSketchText(stroke, context, size, height);
|
|
139
197
|
context.font = `${fontSize}px system-ui, sans-serif`;
|
|
140
198
|
context.textBaseline = "top";
|
|
141
|
-
lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2
|
|
199
|
+
lines.forEach((line, i) => context.fillText(line, x, y + i * fontSize * 1.2));
|
|
142
200
|
} else if (stroke.shape === "arrow") {
|
|
143
201
|
const x = last.x * size, y = last.y * height, a = Math.atan2(y - first.y * height, x - first.x * size), head = Math.min(Math.hypot(x - first.x * size, y - first.y * height) * .4, Math.max(12, stroke.width * 3));
|
|
144
202
|
context.moveTo(first.x * size, first.y * height);
|
|
@@ -372,21 +430,55 @@ window.__ModuleLoader__.load({
|
|
|
372
430
|
dirty: ref(false),
|
|
373
431
|
documentId: ref(crypto.randomUUID()),
|
|
374
432
|
documentRevision: ref(0),
|
|
433
|
+
restoreId: ref(null),
|
|
434
|
+
mounts: 0,
|
|
375
435
|
agentAdapter: ref({}),
|
|
376
436
|
agentSession: ref(null),
|
|
377
437
|
agentRun: ref(null)
|
|
378
438
|
};
|
|
379
439
|
}
|
|
380
|
-
function createSketchSessionRegistry() {
|
|
381
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
440
|
+
function createSketchSessionRegistry({ maxIdle = 8 } = {}) {
|
|
441
|
+
const sessions = /* @__PURE__ */ new Map(), archived = /* @__PURE__ */ new Map();
|
|
442
|
+
const prune = () => {
|
|
443
|
+
const idle = [...sessions].filter(([, s]) => !s.mounts && !s.dirty.current && !s.agentRun.current?.locked && s.agentRun.current?.state !== "stopped");
|
|
444
|
+
for (const [id, state] of idle.slice(0, Math.max(0, idle.length - maxIdle))) {
|
|
445
|
+
if (!state.saved.current && state.doc.current.layers.some((l) => l.image || l.strokes.length)) continue;
|
|
446
|
+
const restoreId = state.saved.current?.id ?? state.restoreId.current;
|
|
447
|
+
if (restoreId) archived.set(id, restoreId);
|
|
448
|
+
state.agentRun.current?.dispose();
|
|
449
|
+
state.images.current.clear();
|
|
450
|
+
sessions.delete(id);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
382
453
|
return {
|
|
383
454
|
get(id) {
|
|
384
|
-
|
|
385
|
-
|
|
455
|
+
let state = sessions.get(id);
|
|
456
|
+
if (!state) {
|
|
457
|
+
state = createSketchSessionState();
|
|
458
|
+
state.restoreId.current = archived.get(id) ?? null;
|
|
459
|
+
archived.delete(id);
|
|
460
|
+
sessions.set(id, state);
|
|
461
|
+
}
|
|
462
|
+
state.retain = () => {
|
|
463
|
+
state.mounts++;
|
|
464
|
+
return () => {
|
|
465
|
+
state.mounts--;
|
|
466
|
+
prune();
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
sessions.delete(id);
|
|
470
|
+
sessions.set(id, state);
|
|
471
|
+
return state;
|
|
386
472
|
},
|
|
473
|
+
prune,
|
|
474
|
+
stats: () => ({
|
|
475
|
+
resident: sessions.size,
|
|
476
|
+
archived: archived.size
|
|
477
|
+
}),
|
|
387
478
|
dispose() {
|
|
388
479
|
for (const value of sessions.values()) value.agentRun.current?.dispose();
|
|
389
480
|
sessions.clear();
|
|
481
|
+
archived.clear();
|
|
390
482
|
}
|
|
391
483
|
};
|
|
392
484
|
}
|
|
@@ -1068,100 +1160,231 @@ window.__ModuleLoader__.load({
|
|
|
1068
1160
|
});
|
|
1069
1161
|
}
|
|
1070
1162
|
//#endregion
|
|
1071
|
-
//#region src/sketch-
|
|
1072
|
-
function
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
role: "status",
|
|
1078
|
-
"aria-live": "polite",
|
|
1163
|
+
//#region src/sketch-layer-panel.jsx
|
|
1164
|
+
function SketchLayerPanel({ document, disabled, change, t }) {
|
|
1165
|
+
const current = document.layers.find((layer) => layer.id === document.active);
|
|
1166
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
1167
|
+
className: "codexSketchLayers",
|
|
1168
|
+
"aria-label": t("sketchLayers"),
|
|
1079
1169
|
children: [
|
|
1080
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1081
|
-
name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
|
|
1082
|
-
size: 15
|
|
1083
|
-
}),
|
|
1084
|
-
floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchLayers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1085
1171
|
type: "button",
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1172
|
+
title: t("sketchLayerAdd"),
|
|
1173
|
+
"aria-label": t("sketchLayerAdd"),
|
|
1174
|
+
disabled: disabled || document.layers.length >= 8,
|
|
1175
|
+
onClick: () => change("add"),
|
|
1176
|
+
children: "+"
|
|
1177
|
+
})] }),
|
|
1178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1179
|
+
className: "codexLayerList",
|
|
1180
|
+
children: document.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1181
|
+
className: "codexLayerRow",
|
|
1182
|
+
"data-active": layer.id === document.active,
|
|
1183
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1184
|
+
type: "button",
|
|
1185
|
+
disabled,
|
|
1186
|
+
"aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
|
|
1187
|
+
"aria-pressed": layer.visible,
|
|
1188
|
+
onClick: () => change("visible", layer.id),
|
|
1189
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1190
|
+
name: layer.visible ? "eye" : "eyeOff",
|
|
1191
|
+
size: 18
|
|
1192
|
+
})
|
|
1193
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1194
|
+
type: "button",
|
|
1195
|
+
disabled,
|
|
1196
|
+
"aria-pressed": layer.id === document.active,
|
|
1197
|
+
onClick: () => change("select", layer.id),
|
|
1198
|
+
children: layer.name || `${t("sketchLayer")} ${layer.id}`
|
|
1199
|
+
})]
|
|
1200
|
+
}, layer.id))
|
|
1201
|
+
}),
|
|
1202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1203
|
+
className: "codexSketchLayerLabel",
|
|
1204
|
+
children: t("sketchLayerName")
|
|
1205
|
+
}),
|
|
1206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1207
|
+
disabled,
|
|
1208
|
+
"aria-label": t("sketchLayerName"),
|
|
1209
|
+
defaultValue: current.name,
|
|
1210
|
+
placeholder: `${t("sketchLayer")} ${current.id}`,
|
|
1211
|
+
maxLength: 40,
|
|
1212
|
+
onBlur: (e) => {
|
|
1213
|
+
if (e.target.value !== current.name) change("rename", current.id, e.target.value);
|
|
1214
|
+
},
|
|
1215
|
+
onKeyDown: (e) => {
|
|
1216
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
1217
|
+
}
|
|
1218
|
+
}, current.id + "-" + current.name),
|
|
1219
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1220
|
+
className: "codexLayerActions",
|
|
1221
|
+
children: [
|
|
1222
|
+
"duplicate",
|
|
1223
|
+
"up",
|
|
1224
|
+
"down",
|
|
1225
|
+
"delete"
|
|
1226
|
+
].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1227
|
+
type: "button",
|
|
1228
|
+
title: t(`sketchLayer_${action}`),
|
|
1229
|
+
"aria-label": t(`sketchLayer_${action}`),
|
|
1230
|
+
disabled: disabled || action === "delete" && document.layers.length === 1 || action === "duplicate" && (document.layers.length >= 8 || strokeCount(document) + current.strokes.length > 2e3) || action === "up" && current === document.layers.at(-1) || action === "down" && current === document.layers[0],
|
|
1231
|
+
onClick: () => change(action),
|
|
1232
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1233
|
+
name: action === "delete" ? "clear" : action,
|
|
1234
|
+
size: 17
|
|
1235
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
|
|
1236
|
+
}, action))
|
|
1237
|
+
}),
|
|
1238
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1090
1239
|
type: "button",
|
|
1091
|
-
className: "
|
|
1092
|
-
|
|
1240
|
+
className: "codexSketchClearLayer",
|
|
1241
|
+
disabled: disabled || !current.strokes.length && !current.image,
|
|
1242
|
+
onClick: () => change("clear"),
|
|
1093
1243
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1094
|
-
name: "
|
|
1095
|
-
size:
|
|
1096
|
-
}), t("
|
|
1097
|
-
})
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1244
|
+
name: "clear",
|
|
1245
|
+
size: 16
|
|
1246
|
+
}), t("sketchClearLayer")]
|
|
1247
|
+
})
|
|
1248
|
+
]
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/sketch-tool-picker.jsx
|
|
1253
|
+
function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
|
|
1254
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1255
|
+
className: "codexSketchPill",
|
|
1256
|
+
role: "toolbar",
|
|
1257
|
+
"aria-label": t("sketchTitle"),
|
|
1258
|
+
title: t("sketchShortcuts"),
|
|
1259
|
+
children: [
|
|
1260
|
+
[
|
|
1261
|
+
"select",
|
|
1262
|
+
"pen",
|
|
1263
|
+
"pencil",
|
|
1264
|
+
"marker",
|
|
1265
|
+
"text",
|
|
1266
|
+
"eraser"
|
|
1267
|
+
].map((name) => {
|
|
1268
|
+
const drawing = [
|
|
1269
|
+
"pen",
|
|
1270
|
+
"pencil",
|
|
1271
|
+
"marker"
|
|
1272
|
+
].includes(name);
|
|
1273
|
+
const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
|
|
1274
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1275
|
+
type: "button",
|
|
1276
|
+
"aria-label": label,
|
|
1277
|
+
title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
|
|
1278
|
+
"aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
|
|
1279
|
+
disabled,
|
|
1280
|
+
onClick: () => {
|
|
1281
|
+
if (drawing) chooseBrush(name);
|
|
1282
|
+
else chooseTool(name);
|
|
1283
|
+
},
|
|
1284
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1285
|
+
name,
|
|
1286
|
+
size: 23
|
|
1287
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
1288
|
+
}, name);
|
|
1289
|
+
}),
|
|
1290
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1291
|
+
className: "codexSketchShapeToggle",
|
|
1103
1292
|
type: "button",
|
|
1104
|
-
"aria-label": t("
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1293
|
+
"aria-label": t("sketchShapes"),
|
|
1294
|
+
"aria-expanded": shapesOpen,
|
|
1295
|
+
"aria-pressed": [
|
|
1296
|
+
"line",
|
|
1297
|
+
"arrow",
|
|
1298
|
+
"rectangle",
|
|
1299
|
+
"circle"
|
|
1300
|
+
].includes(tool),
|
|
1301
|
+
disabled,
|
|
1302
|
+
onClick: () => setShapesOpen((v) => !v),
|
|
1303
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1304
|
+
name: [
|
|
1305
|
+
"line",
|
|
1306
|
+
"arrow",
|
|
1307
|
+
"rectangle",
|
|
1308
|
+
"circle"
|
|
1309
|
+
].includes(tool) ? tool : "rectangle",
|
|
1310
|
+
size: 23
|
|
1311
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
|
|
1312
|
+
"line",
|
|
1313
|
+
"arrow",
|
|
1314
|
+
"rectangle",
|
|
1315
|
+
"circle"
|
|
1316
|
+
].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
|
|
1317
|
+
}),
|
|
1318
|
+
shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1319
|
+
className: "codexSketchShapeMenu",
|
|
1320
|
+
children: [
|
|
1321
|
+
"line",
|
|
1322
|
+
"arrow",
|
|
1323
|
+
"rectangle",
|
|
1324
|
+
"circle"
|
|
1325
|
+
].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1326
|
+
disabled,
|
|
1327
|
+
type: "button",
|
|
1328
|
+
"aria-pressed": tool === name,
|
|
1329
|
+
onClick: () => {
|
|
1330
|
+
chooseTool(name);
|
|
1331
|
+
setShapesOpen(false);
|
|
1332
|
+
},
|
|
1333
|
+
children: t(`sketchTool_${name}`)
|
|
1334
|
+
}, name))
|
|
1335
|
+
}) : null
|
|
1112
1336
|
]
|
|
1113
1337
|
});
|
|
1114
1338
|
}
|
|
1115
1339
|
//#endregion
|
|
1116
|
-
//#region src/sketch-
|
|
1117
|
-
const
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
}
|
|
1340
|
+
//#region src/sketch-objects.js
|
|
1341
|
+
const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
|
|
1342
|
+
const identifyObjects = (doc) => ({
|
|
1343
|
+
...doc,
|
|
1344
|
+
layers: doc.layers.map((layer) => ({
|
|
1345
|
+
...layer,
|
|
1346
|
+
strokes: layer.strokes.map((s, i) => s.id ? s : {
|
|
1347
|
+
...s,
|
|
1348
|
+
id: objectId(s, i)
|
|
1349
|
+
})
|
|
1350
|
+
}))
|
|
1351
|
+
});
|
|
1352
|
+
function objectBounds(stroke) {
|
|
1353
|
+
const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
|
|
1354
|
+
return {
|
|
1355
|
+
x: Math.min(...xs),
|
|
1356
|
+
y: Math.min(...ys),
|
|
1357
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
1358
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
|
|
1362
|
+
if (![
|
|
1363
|
+
dx,
|
|
1364
|
+
dy,
|
|
1365
|
+
scaleX,
|
|
1366
|
+
scaleY
|
|
1367
|
+
].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
|
|
1368
|
+
const box = objectBounds(stroke);
|
|
1369
|
+
const points = stroke.points.map((p) => ({
|
|
1370
|
+
x: box.x + (p.x - box.x) * scaleX + dx,
|
|
1371
|
+
y: box.y + (p.y - box.y) * scaleY + dy
|
|
1372
|
+
}));
|
|
1373
|
+
if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
|
|
1374
|
+
return {
|
|
1375
|
+
...stroke,
|
|
1376
|
+
points
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function sketchObjectSummary(doc) {
|
|
1380
|
+
return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
|
|
1381
|
+
layer: layer.id,
|
|
1382
|
+
id: objectId(stroke, i),
|
|
1383
|
+
shape: stroke.shape,
|
|
1384
|
+
color: stroke.color,
|
|
1385
|
+
bounds: objectBounds(stroke),
|
|
1386
|
+
...stroke.text ? { text: stroke.text } : {}
|
|
1387
|
+
})));
|
|
1165
1388
|
}
|
|
1166
1389
|
//#endregion
|
|
1167
1390
|
//#region src/sketch-input.js
|
|
@@ -1193,39 +1416,192 @@ window.__ModuleLoader__.load({
|
|
|
1193
1416
|
});
|
|
1194
1417
|
}
|
|
1195
1418
|
//#endregion
|
|
1419
|
+
//#region src/sketch-gesture.js
|
|
1420
|
+
function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
|
|
1421
|
+
if (!samples.length) return doc;
|
|
1422
|
+
if (gesture.object) {
|
|
1423
|
+
const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
|
|
1424
|
+
if (!point) return doc;
|
|
1425
|
+
const box = objectBounds(gesture.object);
|
|
1426
|
+
const stroke = gesture.handle === "end" ? {
|
|
1427
|
+
...gesture.object,
|
|
1428
|
+
points: [gesture.object.points[0], point]
|
|
1429
|
+
} : gesture.handle === "size" ? transformObject(gesture.object, {
|
|
1430
|
+
scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
|
|
1431
|
+
scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
|
|
1432
|
+
}) : transformObject(gesture.object, {
|
|
1433
|
+
dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
|
|
1434
|
+
dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
|
|
1435
|
+
});
|
|
1436
|
+
doc = {
|
|
1437
|
+
...doc,
|
|
1438
|
+
layers: doc.layers.map((l) => l.id === gesture.layer ? {
|
|
1439
|
+
...l,
|
|
1440
|
+
strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
|
|
1441
|
+
} : l)
|
|
1442
|
+
};
|
|
1443
|
+
return doc;
|
|
1444
|
+
}
|
|
1445
|
+
for (const sample of samples) {
|
|
1446
|
+
let point = sketchPoint(sample.clientX, sample.clientY, rect);
|
|
1447
|
+
if (!point) continue;
|
|
1448
|
+
const layer = doc.layers.find((layer) => layer.id === gesture.layer);
|
|
1449
|
+
if (!layer) return doc;
|
|
1450
|
+
if (gesture.eraseStroke) {
|
|
1451
|
+
const previous = gesture.last ?? point;
|
|
1452
|
+
const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
|
|
1453
|
+
layer.strokes = layer.strokes.filter((stroke) => {
|
|
1454
|
+
for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
|
|
1455
|
+
x: previous.x + (point.x - previous.x) * i / steps,
|
|
1456
|
+
y: previous.y + (point.y - previous.y) * i / steps
|
|
1457
|
+
}, width / 2, doc.width, doc.height)) return false;
|
|
1458
|
+
return true;
|
|
1459
|
+
});
|
|
1460
|
+
} else {
|
|
1461
|
+
const stroke = layer.strokes.at(-1);
|
|
1462
|
+
if (!stroke) return doc;
|
|
1463
|
+
if ([
|
|
1464
|
+
"line",
|
|
1465
|
+
"arrow",
|
|
1466
|
+
"rectangle",
|
|
1467
|
+
"circle"
|
|
1468
|
+
].includes(stroke.shape)) {
|
|
1469
|
+
if (stroke.shape === "line" && shiftKey) {
|
|
1470
|
+
const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
|
|
1471
|
+
const snapped = snapLine({
|
|
1472
|
+
x: a.x * w,
|
|
1473
|
+
y: a.y * h
|
|
1474
|
+
}, {
|
|
1475
|
+
x: point.x * w,
|
|
1476
|
+
y: point.y * h
|
|
1477
|
+
});
|
|
1478
|
+
point = {
|
|
1479
|
+
x: snapped.x / w,
|
|
1480
|
+
y: snapped.y / h
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
stroke.points = [stroke.points[0], point];
|
|
1484
|
+
} else {
|
|
1485
|
+
const last = stroke.points.at(-1);
|
|
1486
|
+
if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
|
|
1487
|
+
if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
|
|
1488
|
+
stroke.points.push(point);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
gesture.last = point;
|
|
1492
|
+
}
|
|
1493
|
+
return doc;
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1196
1496
|
//#region src/sketch-drafts.js
|
|
1197
1497
|
const DATABASE = "dsh-codex-sketches-v1";
|
|
1198
|
-
|
|
1498
|
+
const MAX_STORAGE = 32 * 1024 * 1024;
|
|
1499
|
+
const metadata = (kind, row) => ({
|
|
1500
|
+
key: `${kind}:${row.id}`,
|
|
1501
|
+
kind,
|
|
1502
|
+
id: row.id,
|
|
1503
|
+
name: row.name,
|
|
1504
|
+
updated: row.updated,
|
|
1505
|
+
size: JSON.stringify(row).length
|
|
1506
|
+
});
|
|
1507
|
+
async function sketchDrafts(action, value, recoverySession) {
|
|
1199
1508
|
const db = await new Promise((resolve, reject) => {
|
|
1200
|
-
|
|
1201
|
-
request
|
|
1202
|
-
request.
|
|
1509
|
+
let blocked = false;
|
|
1510
|
+
const request = indexedDB.open(DATABASE, 2);
|
|
1511
|
+
request.onblocked = () => {
|
|
1512
|
+
blocked = true;
|
|
1513
|
+
reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
|
|
1514
|
+
};
|
|
1515
|
+
request.onupgradeneeded = () => {
|
|
1516
|
+
if (blocked) {
|
|
1517
|
+
request.transaction.abort();
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const db = request.result, tx = request.transaction;
|
|
1521
|
+
if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
|
|
1522
|
+
const meta = db.createObjectStore("metadata", { keyPath: "key" });
|
|
1523
|
+
db.createObjectStore("recovery", { keyPath: "id" });
|
|
1524
|
+
const cursor = tx.objectStore("drafts").openCursor();
|
|
1525
|
+
cursor.onsuccess = () => {
|
|
1526
|
+
const row = cursor.result;
|
|
1527
|
+
if (row) {
|
|
1528
|
+
meta.put(metadata("drafts", row.value));
|
|
1529
|
+
row.continue();
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
};
|
|
1533
|
+
request.onsuccess = () => {
|
|
1534
|
+
if (blocked) {
|
|
1535
|
+
request.result.close();
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
request.result.onversionchange = () => request.result.close();
|
|
1539
|
+
resolve(request.result);
|
|
1540
|
+
};
|
|
1203
1541
|
request.onerror = () => reject(request.error);
|
|
1204
1542
|
});
|
|
1205
1543
|
try {
|
|
1206
1544
|
return await new Promise((resolve, reject) => {
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1545
|
+
const write = [
|
|
1546
|
+
"save",
|
|
1547
|
+
"delete",
|
|
1548
|
+
"checkpoint",
|
|
1549
|
+
"clearRecovery"
|
|
1550
|
+
].includes(action);
|
|
1551
|
+
const tx = db.transaction([
|
|
1552
|
+
"drafts",
|
|
1553
|
+
"metadata",
|
|
1554
|
+
"recovery"
|
|
1555
|
+
], write ? "readwrite" : "readonly");
|
|
1556
|
+
const meta = tx.objectStore("metadata"), kind = [
|
|
1557
|
+
"checkpoint",
|
|
1558
|
+
"recover",
|
|
1559
|
+
"clearRecovery"
|
|
1560
|
+
].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
|
|
1561
|
+
let result, failure;
|
|
1209
1562
|
tx.oncomplete = () => resolve(result);
|
|
1210
1563
|
tx.onerror = () => reject(tx.error);
|
|
1211
|
-
tx.onabort = () => reject(tx.error ?? Error("Draft
|
|
1212
|
-
|
|
1564
|
+
tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
|
|
1565
|
+
if (action === "get" || action === "recover") {
|
|
1566
|
+
const req = store.get(value);
|
|
1567
|
+
req.onsuccess = () => {
|
|
1568
|
+
result = req.result;
|
|
1569
|
+
};
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (action === "delete" || action === "clearRecovery") {
|
|
1573
|
+
store.delete(value);
|
|
1574
|
+
meta.delete(`${kind}:${value}`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (![
|
|
1578
|
+
"list",
|
|
1579
|
+
"save",
|
|
1580
|
+
"checkpoint"
|
|
1581
|
+
].includes(action)) {
|
|
1582
|
+
tx.abort();
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const request = meta.getAll();
|
|
1213
1586
|
request.onsuccess = () => {
|
|
1214
1587
|
const rows = request.result;
|
|
1215
1588
|
if (action === "list") {
|
|
1216
|
-
result = rows.sort((a, b) => b.updated - a.updated);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
if (action === "delete") {
|
|
1220
|
-
store.delete(value);
|
|
1589
|
+
result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
|
|
1221
1590
|
return;
|
|
1222
1591
|
}
|
|
1223
|
-
const others = rows.filter((row) => row.
|
|
1224
|
-
|
|
1592
|
+
const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
|
|
1593
|
+
const code = others.filter((row) => row.kind === kind).length >= 20 ? "SKETCH_DRAFT_LIMIT" : others.reduce((n, row) => n + row.size, 0) + next.size > MAX_STORAGE ? "SKETCH_STORAGE_LIMIT" : null;
|
|
1594
|
+
if (code) {
|
|
1595
|
+
failure = Object.assign(Error("Draft storage limit reached"), { code });
|
|
1225
1596
|
tx.abort();
|
|
1226
1597
|
return;
|
|
1227
1598
|
}
|
|
1228
1599
|
store.put(value);
|
|
1600
|
+
meta.put(next);
|
|
1601
|
+
if (action === "save" && recoverySession) {
|
|
1602
|
+
tx.objectStore("recovery").delete(recoverySession);
|
|
1603
|
+
meta.delete(`recovery:${recoverySession}`);
|
|
1604
|
+
}
|
|
1229
1605
|
result = value;
|
|
1230
1606
|
};
|
|
1231
1607
|
});
|
|
@@ -1268,847 +1644,1155 @@ window.__ModuleLoader__.load({
|
|
|
1268
1644
|
}
|
|
1269
1645
|
}
|
|
1270
1646
|
//#endregion
|
|
1271
|
-
//#region src/sketch-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1647
|
+
//#region src/sketch-presets.js
|
|
1648
|
+
const SKETCH_PRESETS = [
|
|
1649
|
+
"triangle",
|
|
1650
|
+
"diamond",
|
|
1651
|
+
"star"
|
|
1652
|
+
];
|
|
1653
|
+
function expandSketchPreset(command) {
|
|
1654
|
+
if (!SKETCH_PRESETS.includes(command.shape)) return command;
|
|
1655
|
+
const points = command.points;
|
|
1656
|
+
if (!Array.isArray(points) || points.length !== 2 || points.some((p) => !p || ![p.x, p.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))) throw Error("Shape preset requires two normalized bounding-box corners");
|
|
1657
|
+
const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
|
1658
|
+
const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
|
|
1659
|
+
if (!w || !h) throw Error("Shape preset requires a non-empty box");
|
|
1660
|
+
const vertices = command.shape === "triangle" ? [
|
|
1661
|
+
[.5, 0],
|
|
1662
|
+
[1, 1],
|
|
1663
|
+
[0, 1]
|
|
1664
|
+
] : command.shape === "diamond" ? [
|
|
1665
|
+
[.5, 0],
|
|
1666
|
+
[1, .5],
|
|
1667
|
+
[.5, 1],
|
|
1668
|
+
[0, .5]
|
|
1669
|
+
] : Array.from({ length: 10 }, (_, i) => {
|
|
1670
|
+
const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
|
|
1671
|
+
return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
|
|
1672
|
+
});
|
|
1673
|
+
return {
|
|
1674
|
+
...command,
|
|
1675
|
+
shape: "polygon",
|
|
1676
|
+
points: vertices.map(([u, v]) => ({
|
|
1677
|
+
x: x + u * w,
|
|
1678
|
+
y: y + v * h
|
|
1679
|
+
}))
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
const SKETCH_COMMAND_HELP = {
|
|
1683
|
+
coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
|
|
1684
|
+
shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
|
|
1685
|
+
commands: {
|
|
1686
|
+
preset: "triangle/diamond/star: {op:\"stroke\",id:\"badge\",shape:\"star\",points:[{x:0.1,y:0.1},{x:0.3,y:0.3}],color:\"#ffcc00\",fill:true}. Two opposite box corners; stored as editable polygon. Inspect objectId for one object without repeating full help.",
|
|
1687
|
+
stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
|
|
1688
|
+
layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
|
|
1689
|
+
curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
|
|
1690
|
+
object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
|
|
1691
|
+
resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
|
|
1692
|
+
},
|
|
1693
|
+
limits: {
|
|
1694
|
+
strokes: MAX_SKETCH_STROKES,
|
|
1695
|
+
pointsPerStroke: MAX_STROKE_POINTS,
|
|
1696
|
+
pointsTotal: 2e5,
|
|
1697
|
+
commandsPerBatch: 256
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
1701
|
+
function applySketchCommands(source, commands) {
|
|
1702
|
+
if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
|
|
1703
|
+
let doc = identifyObjects(source);
|
|
1704
|
+
for (let command of commands) {
|
|
1705
|
+
if (!command || typeof command !== "object") throw Error("Invalid command");
|
|
1706
|
+
if (command.op === "stroke") command = expandSketchPreset(command);
|
|
1707
|
+
if (command.op === "resize") {
|
|
1708
|
+
doc = resizeSketch(doc, command.ratio);
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (command.op === "layer") {
|
|
1712
|
+
if (![
|
|
1713
|
+
"add",
|
|
1714
|
+
"select",
|
|
1715
|
+
"rename",
|
|
1716
|
+
"visible",
|
|
1717
|
+
"duplicate",
|
|
1718
|
+
"up",
|
|
1719
|
+
"down",
|
|
1720
|
+
"delete",
|
|
1721
|
+
"clear"
|
|
1722
|
+
].includes(command.action)) throw Error("Unknown layer action");
|
|
1723
|
+
if (command.action === "add") {
|
|
1724
|
+
const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
|
|
1725
|
+
if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
|
|
1726
|
+
const next = changeSketchLayer(doc, "add", after);
|
|
1727
|
+
if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
|
|
1728
|
+
doc = {
|
|
1729
|
+
...next,
|
|
1730
|
+
active: id,
|
|
1731
|
+
nextId: Math.max(next.nextId, id + 1),
|
|
1732
|
+
layers: next.layers.map((l) => l.id === next.active ? {
|
|
1733
|
+
...l,
|
|
1734
|
+
id,
|
|
1735
|
+
name: String(command.value ?? "").trim().slice(0, 40)
|
|
1736
|
+
} : l)
|
|
1737
|
+
};
|
|
1738
|
+
continue;
|
|
1286
1739
|
}
|
|
1740
|
+
const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
|
|
1741
|
+
if (next === doc) throw Error("Layer action unavailable; inspect the document first");
|
|
1742
|
+
doc = next;
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
if (command.op === "object") {
|
|
1746
|
+
const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
|
|
1747
|
+
if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
|
|
1748
|
+
const strokes = layer.strokes.slice(), original = strokes[index];
|
|
1749
|
+
if (command.action === "delete") strokes.splice(index, 1);
|
|
1750
|
+
else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
|
|
1751
|
+
...original,
|
|
1752
|
+
id: crypto.randomUUID(),
|
|
1753
|
+
points: original.points.map((p) => ({ ...p }))
|
|
1754
|
+
});
|
|
1755
|
+
else if (command.action === "update") {
|
|
1756
|
+
const patch = command.patch ?? {};
|
|
1757
|
+
if (Object.keys(patch).some((k) => ![
|
|
1758
|
+
"color",
|
|
1759
|
+
"width",
|
|
1760
|
+
"opacity",
|
|
1761
|
+
"fill",
|
|
1762
|
+
"text",
|
|
1763
|
+
"points"
|
|
1764
|
+
].includes(k))) throw Error("Unsupported object property");
|
|
1765
|
+
const changed = command.transform ? transformObject({
|
|
1766
|
+
...original,
|
|
1767
|
+
...patch
|
|
1768
|
+
}, command.transform) : {
|
|
1769
|
+
...original,
|
|
1770
|
+
...patch
|
|
1771
|
+
};
|
|
1772
|
+
strokes[index] = {
|
|
1773
|
+
...applySketchCommands({
|
|
1774
|
+
...doc,
|
|
1775
|
+
layers: [{
|
|
1776
|
+
...layer,
|
|
1777
|
+
strokes: []
|
|
1778
|
+
}]
|
|
1779
|
+
}, [{
|
|
1780
|
+
...changed,
|
|
1781
|
+
op: "stroke",
|
|
1782
|
+
layer: layer.id
|
|
1783
|
+
}]).layers[0].strokes[0],
|
|
1784
|
+
brush: original.brush ?? "pen",
|
|
1785
|
+
...original.pressure !== void 0 ? { pressure: original.pressure } : {},
|
|
1786
|
+
...original.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
1787
|
+
};
|
|
1788
|
+
} else throw Error("Unknown object action");
|
|
1789
|
+
doc = {
|
|
1790
|
+
...doc,
|
|
1791
|
+
layers: doc.layers.map((l) => l === layer ? {
|
|
1792
|
+
...l,
|
|
1793
|
+
strokes
|
|
1794
|
+
} : l)
|
|
1795
|
+
};
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
if (command.op !== "stroke") throw Error("Unknown command");
|
|
1799
|
+
const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
|
|
1800
|
+
const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
|
|
1801
|
+
let points = command.points;
|
|
1802
|
+
if (command.start !== void 0 || command.segments !== void 0) {
|
|
1803
|
+
if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
|
|
1804
|
+
points = [command.start, ...command.segments.flatMap((s) => [
|
|
1805
|
+
s?.control1,
|
|
1806
|
+
s?.control2,
|
|
1807
|
+
s?.end
|
|
1808
|
+
])];
|
|
1809
|
+
}
|
|
1810
|
+
if (![
|
|
1811
|
+
"pen",
|
|
1812
|
+
"line",
|
|
1813
|
+
"rectangle",
|
|
1814
|
+
"circle",
|
|
1815
|
+
"polygon",
|
|
1816
|
+
"bezier",
|
|
1817
|
+
"arrow",
|
|
1818
|
+
"text",
|
|
1819
|
+
"eraser"
|
|
1820
|
+
].includes(shape) || !/^#[0-9a-f]{6}$/i.test(color ?? "") || !finite(width, 1, 256) || !finite(opacity, 0, 1) || typeof fill !== "boolean" || !Array.isArray(points) || !points.length || points.length > 2e3 || points.some((p) => !p || !finite(p.x, 0, 1) || !finite(p.y, 0, 1))) throw Error("Invalid stroke");
|
|
1821
|
+
if ([
|
|
1822
|
+
"line",
|
|
1823
|
+
"arrow",
|
|
1824
|
+
"text",
|
|
1825
|
+
"rectangle",
|
|
1826
|
+
"circle"
|
|
1827
|
+
].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
|
|
1828
|
+
if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
|
|
1829
|
+
if (fill && ![
|
|
1830
|
+
"rectangle",
|
|
1831
|
+
"circle",
|
|
1832
|
+
"polygon",
|
|
1833
|
+
"bezier"
|
|
1834
|
+
].includes(shape)) throw Error("Fill requires a closed shape");
|
|
1835
|
+
const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
|
|
1836
|
+
if (!layer?.visible) throw Error("Target layer is missing or hidden");
|
|
1837
|
+
if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
|
|
1838
|
+
const id = command.id ?? crypto.randomUUID();
|
|
1839
|
+
if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
|
|
1840
|
+
const stroke = {
|
|
1841
|
+
id,
|
|
1842
|
+
...shape === "text" ? { text: command.text } : {},
|
|
1843
|
+
shape,
|
|
1844
|
+
color,
|
|
1845
|
+
width,
|
|
1846
|
+
opacity,
|
|
1847
|
+
fill,
|
|
1848
|
+
brush: "pen",
|
|
1849
|
+
points: points.map((p) => ({
|
|
1850
|
+
x: p.x,
|
|
1851
|
+
y: p.y
|
|
1852
|
+
}))
|
|
1287
1853
|
};
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1854
|
+
doc = {
|
|
1855
|
+
...doc,
|
|
1856
|
+
layers: doc.layers.map((item) => item === layer ? {
|
|
1857
|
+
...item,
|
|
1858
|
+
strokes: [...item.strokes, stroke]
|
|
1859
|
+
} : item)
|
|
1294
1860
|
};
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1861
|
+
}
|
|
1862
|
+
if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
|
|
1863
|
+
return doc;
|
|
1864
|
+
}
|
|
1865
|
+
function createSketchCommandSession(adapter) {
|
|
1866
|
+
const completed = /* @__PURE__ */ new Map();
|
|
1867
|
+
let pending = false, cachedCharacters = 0;
|
|
1868
|
+
return async (request) => {
|
|
1869
|
+
if (!request || typeof request !== "object") throw Error("Invalid sketch request");
|
|
1870
|
+
if (!adapter.available()) throw Error("Open the sketch board for this session first");
|
|
1871
|
+
const current = adapter.snapshot();
|
|
1872
|
+
if (request.action === "inspect") {
|
|
1873
|
+
const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
|
|
1874
|
+
if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
|
|
1875
|
+
return {
|
|
1876
|
+
...current,
|
|
1877
|
+
protocolVersion: 2,
|
|
1878
|
+
objects: request.objectId ? [] : objects.slice(offset, offset + 50),
|
|
1879
|
+
objectCount: objects.length,
|
|
1880
|
+
...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
|
|
1881
|
+
...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
|
|
1882
|
+
recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
|
|
1883
|
+
...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
|
|
1887
|
+
if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
|
|
1888
|
+
if (request.action === "preview") return {
|
|
1889
|
+
...current,
|
|
1890
|
+
png: await adapter.preview()
|
|
1891
|
+
};
|
|
1892
|
+
if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
|
|
1893
|
+
if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
|
|
1894
|
+
const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
|
|
1895
|
+
...request,
|
|
1896
|
+
runId: void 0
|
|
1897
|
+
});
|
|
1898
|
+
const cached = completed.get(key);
|
|
1899
|
+
if (cached) {
|
|
1900
|
+
if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
|
|
1901
|
+
return cached.result;
|
|
1902
|
+
}
|
|
1903
|
+
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
1904
|
+
let changedObjects;
|
|
1905
|
+
if (request.action === "apply") {
|
|
1906
|
+
const before = adapter.document();
|
|
1907
|
+
let next;
|
|
1908
|
+
try {
|
|
1909
|
+
next = applySketchCommands(before, request.commands);
|
|
1910
|
+
} catch (cause) {
|
|
1911
|
+
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
1912
|
+
error.code = "SKETCH_INVALID_BATCH";
|
|
1913
|
+
throw error;
|
|
1914
|
+
}
|
|
1915
|
+
adapter.commit(next);
|
|
1916
|
+
const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
|
|
1917
|
+
changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
|
|
1918
|
+
layer: l.id,
|
|
1919
|
+
id: s.id
|
|
1920
|
+
})));
|
|
1921
|
+
} else {
|
|
1922
|
+
if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
|
|
1923
|
+
pending = true;
|
|
1924
|
+
try {
|
|
1925
|
+
await adapter.save(request.name);
|
|
1926
|
+
} finally {
|
|
1927
|
+
pending = false;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const result = {
|
|
1931
|
+
...adapter.snapshot(),
|
|
1932
|
+
...changedObjects ? {
|
|
1933
|
+
changedObjects: changedObjects.slice(0, 100),
|
|
1934
|
+
changedObjectCount: changedObjects.length
|
|
1935
|
+
} : {}
|
|
1936
|
+
};
|
|
1937
|
+
completed.set(key, {
|
|
1938
|
+
fingerprint,
|
|
1939
|
+
result,
|
|
1940
|
+
receipt: {
|
|
1941
|
+
requestId: request.requestId,
|
|
1942
|
+
action: request.action,
|
|
1943
|
+
revision: result.revision
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
cachedCharacters += fingerprint.length;
|
|
1947
|
+
while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
|
|
1948
|
+
const oldest = completed.keys().next().value;
|
|
1949
|
+
cachedCharacters -= completed.get(oldest).fingerprint.length;
|
|
1950
|
+
completed.delete(oldest);
|
|
1951
|
+
}
|
|
1952
|
+
return result;
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/sketch-formats.js
|
|
1957
|
+
const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
|
|
1958
|
+
const canvas = (w, h) => {
|
|
1959
|
+
const c = document.createElement("canvas");
|
|
1960
|
+
c.width = w;
|
|
1961
|
+
c.height = h;
|
|
1962
|
+
return c;
|
|
1963
|
+
};
|
|
1964
|
+
function runPsdCodec(action, payload) {
|
|
1965
|
+
return new Promise((resolve, reject) => {
|
|
1966
|
+
const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
|
|
1967
|
+
const finish = (callback, value) => {
|
|
1968
|
+
clearTimeout(timer);
|
|
1969
|
+
worker.terminate();
|
|
1970
|
+
callback(value);
|
|
1303
1971
|
};
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1972
|
+
const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
|
|
1973
|
+
worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
|
|
1974
|
+
worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
|
|
1975
|
+
worker.postMessage({
|
|
1976
|
+
action,
|
|
1977
|
+
payload
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1309
1980
|
}
|
|
1310
|
-
function
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1981
|
+
function encodeSketchDocument(doc) {
|
|
1982
|
+
return JSON.stringify({
|
|
1983
|
+
format: "dsh-sketch",
|
|
1984
|
+
version: 1,
|
|
1985
|
+
doc
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
function decodeSketchDocument(text) {
|
|
1989
|
+
if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
|
|
1990
|
+
const file = JSON.parse(text), source = file.doc;
|
|
1991
|
+
if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
|
|
1992
|
+
const w = source.width ?? 1024, h = source.height ?? 1024;
|
|
1993
|
+
if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
|
|
1994
|
+
let doc = {
|
|
1995
|
+
...createSketchLayers(),
|
|
1996
|
+
width: w,
|
|
1997
|
+
height: h,
|
|
1998
|
+
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
|
|
1326
1999
|
};
|
|
1327
|
-
(0
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2000
|
+
for (let i = 0; i < source.layers.length; i++) {
|
|
2001
|
+
const layer = source.layers[i];
|
|
2002
|
+
if (i) doc = applySketchCommands(doc, [{
|
|
2003
|
+
op: "layer",
|
|
2004
|
+
action: "add"
|
|
2005
|
+
}]);
|
|
2006
|
+
if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
|
|
2007
|
+
for (let j = 0; j < layer.strokes.length; j += 256) {
|
|
2008
|
+
const strokes = layer.strokes.slice(j, j + 256);
|
|
2009
|
+
doc = applySketchCommands(doc, strokes.map((s) => ({
|
|
2010
|
+
...s,
|
|
2011
|
+
op: "stroke",
|
|
2012
|
+
layer: doc.active,
|
|
2013
|
+
fill: s.fill ?? false
|
|
2014
|
+
})));
|
|
2015
|
+
const added = doc.layers.at(-1).strokes;
|
|
2016
|
+
for (let k = 0; k < strokes.length; k++) {
|
|
2017
|
+
const s = strokes[k];
|
|
2018
|
+
if (s.brush !== void 0 && ![
|
|
2019
|
+
"pen",
|
|
2020
|
+
"pencil",
|
|
2021
|
+
"marker"
|
|
2022
|
+
].includes(s.brush)) throw Error("Invalid brush");
|
|
2023
|
+
if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
|
|
2024
|
+
if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
|
|
2025
|
+
Object.assign(added[added.length - strokes.length + k], {
|
|
2026
|
+
brush: s.brush ?? "pen",
|
|
2027
|
+
pressure: s.pressure ?? 1,
|
|
2028
|
+
...s.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
const target = doc.layers.at(-1);
|
|
2033
|
+
target.name = String(layer.name ?? "").slice(0, 40);
|
|
2034
|
+
target.visible = layer.visible !== false;
|
|
2035
|
+
if (layer.image) {
|
|
2036
|
+
const image = layer.image;
|
|
2037
|
+
if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
|
|
2038
|
+
"x",
|
|
2039
|
+
"y",
|
|
2040
|
+
"width",
|
|
2041
|
+
"height"
|
|
2042
|
+
].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
|
|
2043
|
+
const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
|
|
2044
|
+
if (bytes.length < 24) throw Error("Invalid draft image");
|
|
2045
|
+
const header = new DataView(bytes.buffer);
|
|
2046
|
+
if (header.getUint32(0) !== 2303741511 || header.getUint32(4) !== 218765834 || header.getUint32(16) < 1 || header.getUint32(20) < 1 || header.getUint32(16) > 4096 || header.getUint32(20) > 4096) throw Error("Invalid draft image size");
|
|
2047
|
+
target.image = {
|
|
2048
|
+
src: image.src,
|
|
2049
|
+
x: image.x,
|
|
2050
|
+
y: image.y,
|
|
2051
|
+
width: image.width,
|
|
2052
|
+
height: image.height
|
|
2053
|
+
};
|
|
1352
2054
|
}
|
|
2055
|
+
}
|
|
2056
|
+
const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
|
|
2057
|
+
doc.active = doc.layers[Math.max(0, activeIndex)].id;
|
|
2058
|
+
return doc;
|
|
2059
|
+
}
|
|
2060
|
+
async function exportSketchPsd(doc, images, composite) {
|
|
2061
|
+
const width = doc.width ?? 1024, height = doc.height ?? 1024;
|
|
2062
|
+
const children = doc.layers.map((layer, i) => {
|
|
2063
|
+
const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
|
|
2064
|
+
if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
|
|
2065
|
+
paintSketch(ctx, layer.strokes, width, true, height);
|
|
2066
|
+
return {
|
|
2067
|
+
name: layer.name || `Layer ${i + 1}`,
|
|
2068
|
+
hidden: !layer.visible,
|
|
2069
|
+
opacity: 1,
|
|
2070
|
+
blendMode: "normal",
|
|
2071
|
+
imageData: ctx.getImageData(0, 0, width, height)
|
|
2072
|
+
};
|
|
2073
|
+
});
|
|
2074
|
+
const context = canvas(width, height).getContext("2d");
|
|
2075
|
+
context.fillStyle = "#fff";
|
|
2076
|
+
context.fillRect(0, 0, width, height);
|
|
2077
|
+
if (children[0] && !children[0].hidden) {
|
|
2078
|
+
const bottom = canvas(width, height);
|
|
2079
|
+
bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
|
|
2080
|
+
context.drawImage(bottom, 0, 0);
|
|
2081
|
+
children[0].imageData = context.getImageData(0, 0, width, height);
|
|
2082
|
+
} else {
|
|
2083
|
+
if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
|
|
2084
|
+
children.unshift({
|
|
2085
|
+
name: "Paper",
|
|
2086
|
+
opacity: 1,
|
|
2087
|
+
blendMode: "normal",
|
|
2088
|
+
imageData: context.getImageData(0, 0, width, height)
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
return runPsdCodec("write", {
|
|
2092
|
+
width,
|
|
2093
|
+
height,
|
|
2094
|
+
children,
|
|
2095
|
+
imageData: composite.getContext("2d").getImageData(0, 0, width, height)
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
async function importSketchPsd(file) {
|
|
2099
|
+
if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
|
|
2100
|
+
const psd = await runPsdCodec("read", await file.arrayBuffer());
|
|
2101
|
+
const scale = Math.min(1, 1024 / Math.max(psd.width, psd.height)), width = Math.max(1, Math.round(psd.width * scale)), height = Math.max(1, Math.round(psd.height * scale));
|
|
2102
|
+
const doc = {
|
|
2103
|
+
...createSketchLayers(),
|
|
2104
|
+
width,
|
|
2105
|
+
height,
|
|
2106
|
+
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
|
|
2107
|
+
layers: [],
|
|
2108
|
+
nextId: psd.layers.length + 1
|
|
1353
2109
|
};
|
|
2110
|
+
for (const [i, layer] of psd.layers.entries()) {
|
|
2111
|
+
const src = canvas(layer.imageData.width, layer.imageData.height);
|
|
2112
|
+
src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
|
|
2113
|
+
const out = canvas(width, height), ctx = out.getContext("2d");
|
|
2114
|
+
ctx.globalAlpha = layer.opacity;
|
|
2115
|
+
ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
|
|
2116
|
+
doc.layers.push({
|
|
2117
|
+
id: i + 1,
|
|
2118
|
+
name: layer.name,
|
|
2119
|
+
visible: !layer.hidden,
|
|
2120
|
+
strokes: [],
|
|
2121
|
+
image: {
|
|
2122
|
+
src: out.toDataURL("image/png"),
|
|
2123
|
+
x: 0,
|
|
2124
|
+
y: 0,
|
|
2125
|
+
width: 1,
|
|
2126
|
+
height: 1
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
return doc;
|
|
1354
2131
|
}
|
|
1355
2132
|
//#endregion
|
|
1356
|
-
//#region src/sketch-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
function useSketchView(canvas, open) {
|
|
1369
|
-
const [view, setView] = (0, react.useState)({
|
|
1370
|
-
scale: 1,
|
|
1371
|
-
x: 0,
|
|
1372
|
-
y: 0
|
|
1373
|
-
}), [keys, setKeys] = (0, react.useState)(() => {
|
|
2133
|
+
//#region src/sketch-document-lifecycle.js
|
|
2134
|
+
function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
|
|
2135
|
+
const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
|
|
2136
|
+
const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
|
|
2137
|
+
const save = async (name) => {
|
|
2138
|
+
const savingDocument = documentId.current, savingRevision = documentRevision.current;
|
|
2139
|
+
const row = {
|
|
2140
|
+
id: saved.current?.id ?? crypto.randomUUID(),
|
|
2141
|
+
name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
2142
|
+
updated: Date.now(),
|
|
2143
|
+
doc: structuredClone(doc.current)
|
|
2144
|
+
};
|
|
1374
2145
|
try {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2146
|
+
await store("save", row, sessionId);
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
|
|
2149
|
+
if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
|
|
2150
|
+
throw error;
|
|
2151
|
+
}
|
|
2152
|
+
if (documentId.current === savingDocument) {
|
|
2153
|
+
saved.current = {
|
|
2154
|
+
id: row.id,
|
|
2155
|
+
name: row.name
|
|
1378
2156
|
};
|
|
1379
|
-
|
|
1380
|
-
|
|
2157
|
+
if (documentRevision.current === savingRevision) {
|
|
2158
|
+
dirty.current = false;
|
|
2159
|
+
setRecovered(false);
|
|
2160
|
+
}
|
|
1381
2161
|
}
|
|
1382
|
-
}
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
2162
|
+
};
|
|
2163
|
+
const saveChanges = async () => {
|
|
2164
|
+
if (!dirty.current) return;
|
|
2165
|
+
if (hasContent() || saved.current) return save();
|
|
2166
|
+
const id = documentId.current, revision = documentRevision.current;
|
|
2167
|
+
await store("clearRecovery", sessionId);
|
|
2168
|
+
if (documentId.current === id && documentRevision.current === revision) {
|
|
2169
|
+
dirty.current = false;
|
|
2170
|
+
setRecovered(false);
|
|
1388
2171
|
}
|
|
1389
|
-
}
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
2172
|
+
};
|
|
2173
|
+
const replace = (next, decoded, identity) => {
|
|
2174
|
+
documentId.current = crypto.randomUUID();
|
|
2175
|
+
documentRevision.current++;
|
|
2176
|
+
doc.current = identifyObjects(structuredClone(next));
|
|
2177
|
+
setSelection(null);
|
|
2178
|
+
setTextEdit(null);
|
|
2179
|
+
images.current = decoded;
|
|
2180
|
+
cache.current.clear();
|
|
2181
|
+
undo.current = [];
|
|
2182
|
+
redo.current = [];
|
|
2183
|
+
saved.current = identity;
|
|
2184
|
+
dirty.current = false;
|
|
2185
|
+
schedule();
|
|
2186
|
+
};
|
|
2187
|
+
const fresh = async () => {
|
|
2188
|
+
await saveChanges();
|
|
2189
|
+
replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
|
|
2190
|
+
};
|
|
2191
|
+
const load = async (row) => {
|
|
2192
|
+
if (row.id === saved.current?.id) return;
|
|
2193
|
+
row = await store("get", row.id);
|
|
2194
|
+
if (!row) throw Error("Draft no longer exists");
|
|
2195
|
+
await saveChanges();
|
|
2196
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2197
|
+
await decodeImages(row.doc, decoded);
|
|
2198
|
+
replace(row.doc, decoded, {
|
|
2199
|
+
id: row.id,
|
|
2200
|
+
name: row.name
|
|
2201
|
+
});
|
|
2202
|
+
};
|
|
2203
|
+
const importImage = async (file) => {
|
|
2204
|
+
if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
|
|
2205
|
+
if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
|
|
2206
|
+
const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
|
|
2207
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2208
|
+
await decodeImages(next, decoded);
|
|
2209
|
+
await saveChanges();
|
|
2210
|
+
replace(next, decoded, null);
|
|
2211
|
+
dirty.current = true;
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (doc.current.layers.length >= 8) throw Error("Layer limit");
|
|
2215
|
+
const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
|
|
2216
|
+
const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
|
|
2217
|
+
const layer = {
|
|
2218
|
+
id: doc.current.nextId,
|
|
2219
|
+
name: file.name?.slice(0, 40) || t("sketchImport"),
|
|
2220
|
+
visible: true,
|
|
2221
|
+
strokes: [],
|
|
2222
|
+
image: {
|
|
2223
|
+
src: image.src,
|
|
2224
|
+
x: (1 - width) / 2,
|
|
2225
|
+
y: (1 - height) / 2,
|
|
2226
|
+
width,
|
|
2227
|
+
height
|
|
2228
|
+
}
|
|
1408
2229
|
};
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
2230
|
+
await decodeImages({ layers: [layer] }, images.current);
|
|
2231
|
+
checkpoint();
|
|
2232
|
+
doc.current = {
|
|
2233
|
+
...doc.current,
|
|
2234
|
+
nextId: layer.id + 1,
|
|
2235
|
+
active: layer.id,
|
|
2236
|
+
layers: [...doc.current.layers, layer]
|
|
1416
2237
|
};
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2238
|
+
schedule();
|
|
2239
|
+
};
|
|
2240
|
+
const restore = async (isCurrent = () => true) => {
|
|
2241
|
+
if (dirty.current || hasContent()) return;
|
|
2242
|
+
const id = documentId.current, revision = documentRevision.current;
|
|
2243
|
+
const recovery = await store("recover", sessionId);
|
|
2244
|
+
const archived = state.restoreId.current;
|
|
2245
|
+
const row = recovery ?? (archived ? await store("get", archived) : null);
|
|
2246
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2247
|
+
if (row) await decodeImages(row.doc, decoded);
|
|
2248
|
+
if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
|
|
2249
|
+
if (row) {
|
|
2250
|
+
replace(row.doc, decoded, recovery ? null : {
|
|
2251
|
+
id: row.id,
|
|
2252
|
+
name: row.name
|
|
2253
|
+
});
|
|
2254
|
+
dirty.current = Boolean(recovery);
|
|
2255
|
+
setRecovered(Boolean(recovery));
|
|
1424
2256
|
}
|
|
1425
|
-
|
|
1426
|
-
const setKey = (action, key) => {
|
|
1427
|
-
key = key.toLowerCase();
|
|
1428
|
-
if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
|
|
1429
|
-
const next = {
|
|
1430
|
-
...keys,
|
|
1431
|
-
[action]: key
|
|
1432
|
-
};
|
|
1433
|
-
setKeys(next);
|
|
1434
|
-
try {
|
|
1435
|
-
localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
|
|
1436
|
-
} catch {}
|
|
2257
|
+
state.restoreId.current = null;
|
|
1437
2258
|
};
|
|
1438
2259
|
return {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
2260
|
+
hasContent,
|
|
2261
|
+
save,
|
|
2262
|
+
saveChanges,
|
|
2263
|
+
replace,
|
|
2264
|
+
fresh,
|
|
2265
|
+
load,
|
|
2266
|
+
importImage,
|
|
2267
|
+
restore
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
//#endregion
|
|
2271
|
+
//#region src/sketch-operation-gate.js
|
|
2272
|
+
function createSketchOperationGate() {
|
|
2273
|
+
let running = false;
|
|
2274
|
+
return {
|
|
2275
|
+
get running() {
|
|
2276
|
+
return running;
|
|
2277
|
+
},
|
|
2278
|
+
async run(operation, { blocked = false, working, report, rethrow = false }) {
|
|
2279
|
+
if (blocked || running) return false;
|
|
2280
|
+
running = true;
|
|
1447
2281
|
try {
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
}),
|
|
1452
|
-
keyDown: (e) => {
|
|
1453
|
-
if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
1454
|
-
const key = e.key.toLowerCase();
|
|
1455
|
-
if (key === keys.pan && !e.ctrlKey && !e.metaKey) {
|
|
1456
|
-
e.preventDefault();
|
|
1457
|
-
setSpace(true);
|
|
1458
|
-
return true;
|
|
1459
|
-
}
|
|
1460
|
-
if (key === keys.zoomIn || key === "+" || key === keys.zoomOut || key === keys.fit) {
|
|
1461
|
-
e.preventDefault();
|
|
1462
|
-
if (key === keys.fit) reset();
|
|
1463
|
-
else zoom(key === keys.zoomOut ? 1 / 1.2 : 1.2);
|
|
2282
|
+
working(true);
|
|
2283
|
+
report(null);
|
|
2284
|
+
await operation();
|
|
1464
2285
|
return true;
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
report(error);
|
|
2288
|
+
if (rethrow) throw error;
|
|
2289
|
+
return false;
|
|
2290
|
+
} finally {
|
|
2291
|
+
running = false;
|
|
2292
|
+
working(false);
|
|
1465
2293
|
}
|
|
1466
|
-
return false;
|
|
1467
|
-
},
|
|
1468
|
-
keyUp: (e) => {
|
|
1469
|
-
if (e.key.toLowerCase() === keys.pan) setSpace(false);
|
|
1470
|
-
},
|
|
1471
|
-
down: (e) => {
|
|
1472
|
-
if (e.button !== 1 && !space) return false;
|
|
1473
|
-
e.preventDefault();
|
|
1474
|
-
drag.current = {
|
|
1475
|
-
id: e.pointerId,
|
|
1476
|
-
x: e.clientX,
|
|
1477
|
-
y: e.clientY,
|
|
1478
|
-
view: viewRef.current
|
|
1479
|
-
};
|
|
1480
|
-
canvas.current.setPointerCapture(e.pointerId);
|
|
1481
|
-
return true;
|
|
1482
|
-
},
|
|
1483
|
-
move: (e) => {
|
|
1484
|
-
const d = drag.current;
|
|
1485
|
-
if (!d || d.id !== e.pointerId) return false;
|
|
1486
|
-
setView({
|
|
1487
|
-
...d.view,
|
|
1488
|
-
x: d.view.x + e.clientX - d.x,
|
|
1489
|
-
y: d.view.y + e.clientY - d.y
|
|
1490
|
-
});
|
|
1491
|
-
return true;
|
|
1492
|
-
},
|
|
1493
|
-
end: (e) => {
|
|
1494
|
-
if (drag.current?.id !== e.pointerId) return false;
|
|
1495
|
-
drag.current = null;
|
|
1496
|
-
if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
|
|
1497
|
-
return true;
|
|
1498
2294
|
}
|
|
1499
2295
|
};
|
|
1500
2296
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
const width = Math.min(360, box.width - 24);
|
|
1515
|
-
setPlacement({
|
|
1516
|
-
dialog,
|
|
1517
|
-
style: {
|
|
1518
|
-
width,
|
|
1519
|
-
left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
|
|
1520
|
-
bottom: box.bottom - anchor.top + 8,
|
|
1521
|
-
maxHeight: Math.max(80, anchor.top - box.top - 24)
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
};
|
|
1525
|
-
place();
|
|
1526
|
-
const observer = new ResizeObserver(place);
|
|
1527
|
-
observer.observe(dialog);
|
|
1528
|
-
observer.observe(host.current);
|
|
1529
|
-
window.addEventListener("resize", place);
|
|
1530
|
-
return () => {
|
|
1531
|
-
observer.disconnect();
|
|
1532
|
-
window.removeEventListener("resize", place);
|
|
2297
|
+
//#endregion
|
|
2298
|
+
//#region src/sketch-agent-export.js
|
|
2299
|
+
async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
|
|
2300
|
+
let result;
|
|
2301
|
+
if (!await gate.run(async () => {
|
|
2302
|
+
const { blob, extension } = await exportFile(format);
|
|
2303
|
+
const data = new Uint8Array(await blob.arrayBuffer());
|
|
2304
|
+
let raw = "";
|
|
2305
|
+
for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
|
|
2306
|
+
result = {
|
|
2307
|
+
extension,
|
|
2308
|
+
mediaType: blob.type,
|
|
2309
|
+
base64: btoa(raw)
|
|
1533
2310
|
};
|
|
1534
|
-
},
|
|
2311
|
+
}, {
|
|
2312
|
+
blocked,
|
|
2313
|
+
working,
|
|
2314
|
+
report,
|
|
2315
|
+
rethrow: true
|
|
2316
|
+
})) throw Error("Sketch is being edited; retry after it settles");
|
|
2317
|
+
return result;
|
|
2318
|
+
}
|
|
2319
|
+
//#endregion
|
|
2320
|
+
//#region src/sketch-run-status.jsx
|
|
2321
|
+
function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
|
|
2322
|
+
if (state === "idle") return null;
|
|
2323
|
+
const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
|
|
1535
2324
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1536
|
-
|
|
1537
|
-
|
|
2325
|
+
className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
|
|
2326
|
+
role: "status",
|
|
2327
|
+
"aria-live": "polite",
|
|
1538
2328
|
children: [
|
|
1539
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
onClick: () => navigation.zoom(1 / 1.2),
|
|
1543
|
-
children: "−"
|
|
1544
|
-
}),
|
|
1545
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1546
|
-
type: "button",
|
|
1547
|
-
title: t("sketchFit"),
|
|
1548
|
-
onClick: navigation.reset,
|
|
1549
|
-
children: [Math.round(navigation.view.scale * 100), "%"]
|
|
1550
|
-
}),
|
|
1551
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1552
|
-
type: "button",
|
|
1553
|
-
"aria-label": t("sketchZoomIn"),
|
|
1554
|
-
onClick: () => navigation.zoom(1.2),
|
|
1555
|
-
children: "+"
|
|
2329
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2330
|
+
name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
|
|
2331
|
+
size: 15
|
|
1556
2332
|
}),
|
|
1557
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1558
|
-
ref: trigger,
|
|
2333
|
+
floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1559
2334
|
type: "button",
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
className: "
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
children:
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
checked: navigation.shortcuts,
|
|
1587
|
-
onChange: navigation.toggle
|
|
1588
|
-
})]
|
|
1589
|
-
}),
|
|
1590
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
|
|
1591
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1592
|
-
className: "codexSketchKeyGrid",
|
|
1593
|
-
children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1594
|
-
"aria-label": t(`sketchKey_${action}`),
|
|
1595
|
-
value: key === " " ? "Space" : key,
|
|
1596
|
-
readOnly: true,
|
|
1597
|
-
onKeyDown: (e) => {
|
|
1598
|
-
if (e.key === "Tab" || e.key === "Escape") return;
|
|
1599
|
-
e.preventDefault();
|
|
1600
|
-
e.stopPropagation();
|
|
1601
|
-
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
|
|
1602
|
-
}
|
|
1603
|
-
})] }, action))
|
|
1604
|
-
}),
|
|
1605
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
|
|
1606
|
-
]
|
|
1607
|
-
}), placement.dialog) : null
|
|
2335
|
+
onClick: onOpen,
|
|
2336
|
+
children: t(`sketchRun_${state}`)
|
|
2337
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
|
|
2338
|
+
drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2339
|
+
type: "button",
|
|
2340
|
+
className: "codexSketchStop",
|
|
2341
|
+
onClick: onStop,
|
|
2342
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2343
|
+
name: "stop",
|
|
2344
|
+
size: 12
|
|
2345
|
+
}), t("sketchRunStop")]
|
|
2346
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2347
|
+
type: "button",
|
|
2348
|
+
title: t("sketchRunResumeHint"),
|
|
2349
|
+
onClick: onResume,
|
|
2350
|
+
children: t("sketchRunResume")
|
|
2351
|
+
}) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2352
|
+
type: "button",
|
|
2353
|
+
"aria-label": t("sketchDismissStatus"),
|
|
2354
|
+
title: t("sketchDismissStatus"),
|
|
2355
|
+
onClick: onDismiss,
|
|
2356
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2357
|
+
name: "close",
|
|
2358
|
+
size: 14
|
|
2359
|
+
})
|
|
2360
|
+
}) : null] })
|
|
1608
2361
|
]
|
|
1609
2362
|
});
|
|
1610
2363
|
}
|
|
1611
2364
|
//#endregion
|
|
1612
|
-
//#region src/sketch-
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
height: Math.max(...ys) - Math.min(...ys)
|
|
1631
|
-
};
|
|
2365
|
+
//#region src/sketch-tool-widths.js
|
|
2366
|
+
const defaults = {
|
|
2367
|
+
pen: 12,
|
|
2368
|
+
pencil: 6,
|
|
2369
|
+
marker: 28,
|
|
2370
|
+
eraser: 24,
|
|
2371
|
+
text: 32,
|
|
2372
|
+
line: 12,
|
|
2373
|
+
arrow: 12,
|
|
2374
|
+
rectangle: 12,
|
|
2375
|
+
circle: 12
|
|
2376
|
+
};
|
|
2377
|
+
const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
|
|
2378
|
+
function switchSketchToolWidth(memory, current, next) {
|
|
2379
|
+
if (current.tool !== "select") memory[keyFor(current)] = current.width;
|
|
2380
|
+
if (next.tool === "select") return current.width;
|
|
2381
|
+
const key = keyFor(next);
|
|
2382
|
+
return memory[key] ?? defaults[key] ?? 12;
|
|
1632
2383
|
}
|
|
1633
|
-
function
|
|
1634
|
-
|
|
1635
|
-
dx,
|
|
1636
|
-
dy,
|
|
1637
|
-
scaleX,
|
|
1638
|
-
scaleY
|
|
1639
|
-
].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
|
|
1640
|
-
const box = objectBounds(stroke);
|
|
1641
|
-
const points = stroke.points.map((p) => ({
|
|
1642
|
-
x: box.x + (p.x - box.x) * scaleX + dx,
|
|
1643
|
-
y: box.y + (p.y - box.y) * scaleY + dy
|
|
1644
|
-
}));
|
|
1645
|
-
if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
|
|
1646
|
-
return {
|
|
1647
|
-
...stroke,
|
|
1648
|
-
points
|
|
1649
|
-
};
|
|
2384
|
+
function stepSketchWidth(width, direction) {
|
|
2385
|
+
return Math.max(1, Math.min(256, width + direction * 2));
|
|
1650
2386
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
}
|
|
2387
|
+
//#endregion
|
|
2388
|
+
//#region src/sketch-shortcuts.js
|
|
2389
|
+
function sketchShortcutAction(keys, key) {
|
|
2390
|
+
key = key.toLowerCase();
|
|
2391
|
+
return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
|
|
2392
|
+
v: "select",
|
|
2393
|
+
t: "text",
|
|
2394
|
+
"+": "zoomIn"
|
|
2395
|
+
}[key];
|
|
1660
2396
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
|
|
1670
|
-
},
|
|
1671
|
-
limits: {
|
|
1672
|
-
strokes: MAX_SKETCH_STROKES,
|
|
1673
|
-
pointsPerStroke: MAX_STROKE_POINTS,
|
|
1674
|
-
pointsTotal: 2e5,
|
|
1675
|
-
commandsPerBatch: 256
|
|
1676
|
-
}
|
|
2397
|
+
//#endregion
|
|
2398
|
+
//#region src/sketch-layer-renderer.js
|
|
2399
|
+
const NO_IMAGES = /* @__PURE__ */ new Map();
|
|
2400
|
+
const surface = (width, height) => {
|
|
2401
|
+
const c = document.createElement("canvas");
|
|
2402
|
+
c.width = width;
|
|
2403
|
+
c.height = height;
|
|
2404
|
+
return c;
|
|
1677
2405
|
};
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
"select",
|
|
1692
|
-
"rename",
|
|
1693
|
-
"visible",
|
|
1694
|
-
"duplicate",
|
|
1695
|
-
"up",
|
|
1696
|
-
"down",
|
|
1697
|
-
"delete",
|
|
1698
|
-
"clear"
|
|
1699
|
-
].includes(command.action)) throw Error("Unknown layer action");
|
|
1700
|
-
if (command.action === "add") {
|
|
1701
|
-
const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
|
|
1702
|
-
if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
|
|
1703
|
-
const next = changeSketchLayer(doc, "add", after);
|
|
1704
|
-
if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
|
|
1705
|
-
doc = {
|
|
1706
|
-
...next,
|
|
1707
|
-
active: id,
|
|
1708
|
-
nextId: Math.max(next.nextId, id + 1),
|
|
1709
|
-
layers: next.layers.map((l) => l.id === next.active ? {
|
|
1710
|
-
...l,
|
|
1711
|
-
id,
|
|
1712
|
-
name: String(command.value ?? "").trim().slice(0, 40)
|
|
1713
|
-
} : l)
|
|
1714
|
-
};
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
|
|
1718
|
-
if (next === doc) throw Error("Layer action unavailable; inspect the document first");
|
|
1719
|
-
doc = next;
|
|
1720
|
-
continue;
|
|
1721
|
-
}
|
|
1722
|
-
if (command.op === "object") {
|
|
1723
|
-
const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
|
|
1724
|
-
if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
|
|
1725
|
-
const strokes = layer.strokes.slice(), original = strokes[index];
|
|
1726
|
-
if (command.action === "delete") strokes.splice(index, 1);
|
|
1727
|
-
else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
|
|
1728
|
-
...original,
|
|
1729
|
-
id: crypto.randomUUID(),
|
|
1730
|
-
points: original.points.map((p) => ({ ...p }))
|
|
1731
|
-
});
|
|
1732
|
-
else if (command.action === "update") {
|
|
1733
|
-
const patch = command.patch ?? {};
|
|
1734
|
-
if (Object.keys(patch).some((k) => ![
|
|
1735
|
-
"color",
|
|
1736
|
-
"width",
|
|
1737
|
-
"opacity",
|
|
1738
|
-
"fill",
|
|
1739
|
-
"text",
|
|
1740
|
-
"points"
|
|
1741
|
-
].includes(k))) throw Error("Unsupported object property");
|
|
1742
|
-
const changed = command.transform ? transformObject({
|
|
1743
|
-
...original,
|
|
1744
|
-
...patch
|
|
1745
|
-
}, command.transform) : {
|
|
1746
|
-
...original,
|
|
1747
|
-
...patch
|
|
1748
|
-
};
|
|
1749
|
-
strokes[index] = {
|
|
1750
|
-
...applySketchCommands({
|
|
1751
|
-
...doc,
|
|
1752
|
-
layers: [{
|
|
1753
|
-
...layer,
|
|
1754
|
-
strokes: []
|
|
1755
|
-
}]
|
|
1756
|
-
}, [{
|
|
1757
|
-
...changed,
|
|
1758
|
-
op: "stroke",
|
|
1759
|
-
layer: layer.id
|
|
1760
|
-
}]).layers[0].strokes[0],
|
|
1761
|
-
brush: original.brush ?? "pen",
|
|
1762
|
-
...original.pressure !== void 0 ? { pressure: original.pressure } : {},
|
|
1763
|
-
...original.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
1764
|
-
};
|
|
1765
|
-
} else throw Error("Unknown object action");
|
|
1766
|
-
doc = {
|
|
1767
|
-
...doc,
|
|
1768
|
-
layers: doc.layers.map((l) => l === layer ? {
|
|
1769
|
-
...l,
|
|
1770
|
-
strokes
|
|
1771
|
-
} : l)
|
|
2406
|
+
function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
|
|
2407
|
+
context.globalCompositeOperation = "source-over";
|
|
2408
|
+
context.globalAlpha = 1;
|
|
2409
|
+
context.fillStyle = "#fff";
|
|
2410
|
+
context.fillRect(0, 0, size, height);
|
|
2411
|
+
for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
|
|
2412
|
+
for (const layer of doc.layers) {
|
|
2413
|
+
if (!layer.visible) continue;
|
|
2414
|
+
let entry = cache.get(layer.id);
|
|
2415
|
+
if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
|
|
2416
|
+
entry = {
|
|
2417
|
+
surface: surface(size, height),
|
|
2418
|
+
base: surface(size, height)
|
|
1772
2419
|
};
|
|
1773
|
-
|
|
2420
|
+
cache.set(layer.id, entry);
|
|
1774
2421
|
}
|
|
1775
|
-
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2422
|
+
const moving = layer.id === activeLayer;
|
|
2423
|
+
const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
|
|
2424
|
+
const prefix = layer.strokes[count - 1];
|
|
2425
|
+
if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
|
|
2426
|
+
const ctx = entry.base.getContext("2d");
|
|
2427
|
+
const append = entry.count !== void 0 && count >= entry.count && entry.image === layer.image && (entry.strokes === layer.strokes || entry.strokes.slice(0, entry.count).every((stroke, index) => stroke === layer.strokes[index]));
|
|
2428
|
+
if (!append) {
|
|
2429
|
+
ctx.clearRect(0, 0, size, height);
|
|
2430
|
+
const ref = layer.image, image = ref && images.get(ref.src);
|
|
2431
|
+
if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
|
|
2432
|
+
}
|
|
2433
|
+
paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
|
|
2434
|
+
entry.count = count;
|
|
2435
|
+
entry.prefix = prefix;
|
|
2436
|
+
entry.image = layer.image;
|
|
2437
|
+
entry.strokes = layer.strokes;
|
|
1786
2438
|
}
|
|
1787
|
-
if (
|
|
1788
|
-
"
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
"
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
if (!layer?.visible) throw Error("Target layer is missing or hidden");
|
|
1814
|
-
if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
|
|
1815
|
-
const id = command.id ?? crypto.randomUUID();
|
|
1816
|
-
if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
|
|
1817
|
-
const stroke = {
|
|
1818
|
-
id,
|
|
1819
|
-
...shape === "text" ? { text: command.text } : {},
|
|
1820
|
-
shape,
|
|
1821
|
-
color,
|
|
1822
|
-
width,
|
|
1823
|
-
opacity,
|
|
1824
|
-
fill,
|
|
1825
|
-
brush: "pen",
|
|
1826
|
-
points: points.map((p) => ({
|
|
1827
|
-
x: p.x,
|
|
1828
|
-
y: p.y
|
|
1829
|
-
}))
|
|
2439
|
+
if (moving) {
|
|
2440
|
+
const ctx = entry.surface.getContext("2d");
|
|
2441
|
+
ctx.clearRect(0, 0, size, height);
|
|
2442
|
+
ctx.drawImage(entry.base, 0, 0);
|
|
2443
|
+
paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
|
|
2444
|
+
context.drawImage(entry.surface, 0, 0);
|
|
2445
|
+
} else context.drawImage(entry.base, 0, 0);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
//#endregion
|
|
2449
|
+
//#region src/sketch-interactions.js
|
|
2450
|
+
function useSketchDismiss(open, close, host, selectors) {
|
|
2451
|
+
const latest = (0, react.useRef)(close);
|
|
2452
|
+
latest.current = close;
|
|
2453
|
+
(0, react.useEffect)(() => {
|
|
2454
|
+
if (!open) return;
|
|
2455
|
+
const dialog = host.current?.closest("dialog") ?? host.current;
|
|
2456
|
+
if (!dialog) return;
|
|
2457
|
+
const pointer = (event) => {
|
|
2458
|
+
if (selectors.some((selector) => event.target.closest?.(selector))) return;
|
|
2459
|
+
latest.current(false);
|
|
2460
|
+
if (event.target.matches?.("canvas")) {
|
|
2461
|
+
event.preventDefault();
|
|
2462
|
+
event.stopPropagation();
|
|
2463
|
+
event.target.focus({ preventScroll: true });
|
|
2464
|
+
}
|
|
1830
2465
|
};
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2466
|
+
const key = (event) => {
|
|
2467
|
+
if (event.key !== "Escape") return;
|
|
2468
|
+
event.preventDefault();
|
|
2469
|
+
event.stopPropagation();
|
|
2470
|
+
latest.current(false);
|
|
2471
|
+
dialog.querySelector("canvas")?.focus({ preventScroll: true });
|
|
1837
2472
|
};
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2473
|
+
const hidden = () => latest.current(false);
|
|
2474
|
+
document.addEventListener("pointerdown", pointer, true);
|
|
2475
|
+
document.addEventListener("keydown", key, true);
|
|
2476
|
+
dialog.addEventListener("close", hidden);
|
|
2477
|
+
return () => {
|
|
2478
|
+
document.removeEventListener("pointerdown", pointer, true);
|
|
2479
|
+
document.removeEventListener("keydown", key, true);
|
|
2480
|
+
dialog.removeEventListener("close", hidden);
|
|
2481
|
+
};
|
|
2482
|
+
}, [
|
|
2483
|
+
open,
|
|
2484
|
+
host,
|
|
2485
|
+
selectors.join("|")
|
|
2486
|
+
]);
|
|
1841
2487
|
}
|
|
1842
|
-
function
|
|
1843
|
-
const
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
if (!
|
|
1848
|
-
const
|
|
1849
|
-
if (
|
|
1850
|
-
|
|
1851
|
-
|
|
2488
|
+
function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
|
|
2489
|
+
const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
|
|
2490
|
+
const update = (event, bounds) => {
|
|
2491
|
+
if (event) last.current = event;
|
|
2492
|
+
const pointer = last.current, node = canvas.current, cursor = ring.current;
|
|
2493
|
+
if (!pointer || !node || !cursor) return;
|
|
2494
|
+
const rect = bounds ?? node.getBoundingClientRect();
|
|
2495
|
+
if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
|
|
2496
|
+
cursor.hidden = true;
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
|
|
2500
|
+
cursor.hidden = false;
|
|
2501
|
+
cursor.style.width = `${diameter}px`;
|
|
2502
|
+
cursor.style.height = `${diameter}px`;
|
|
2503
|
+
cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
|
|
2504
|
+
};
|
|
2505
|
+
(0, react.useEffect)(() => {
|
|
2506
|
+
update();
|
|
2507
|
+
const observer = new ResizeObserver(() => update());
|
|
2508
|
+
if (canvas.current) observer.observe(canvas.current);
|
|
2509
|
+
return () => observer.disconnect();
|
|
2510
|
+
}, [
|
|
2511
|
+
width,
|
|
2512
|
+
brush,
|
|
2513
|
+
zoom,
|
|
2514
|
+
hidden
|
|
2515
|
+
]);
|
|
2516
|
+
return {
|
|
2517
|
+
down: (event) => {
|
|
2518
|
+
heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
|
|
2519
|
+
},
|
|
2520
|
+
move: (event, bounds) => update({
|
|
2521
|
+
clientX: event.clientX,
|
|
2522
|
+
clientY: event.clientY,
|
|
2523
|
+
pointerId: event.pointerId,
|
|
2524
|
+
pointerType: event.pointerType,
|
|
2525
|
+
pressure: event.pressure
|
|
2526
|
+
}, bounds),
|
|
2527
|
+
leave: () => {
|
|
2528
|
+
last.current = null;
|
|
2529
|
+
if (ring.current) ring.current.hidden = true;
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
//#endregion
|
|
2534
|
+
//#region src/sketch-view.jsx
|
|
2535
|
+
const DEFAULT_KEYS = {
|
|
2536
|
+
pen: "b",
|
|
2537
|
+
eraser: "e",
|
|
2538
|
+
line: "l",
|
|
2539
|
+
rectangle: "r",
|
|
2540
|
+
circle: "o",
|
|
2541
|
+
pan: " ",
|
|
2542
|
+
zoomIn: "=",
|
|
2543
|
+
zoomOut: "-",
|
|
2544
|
+
fit: "0"
|
|
2545
|
+
};
|
|
2546
|
+
function useSketchView(canvas, open) {
|
|
2547
|
+
const [view, setView] = (0, react.useState)({
|
|
2548
|
+
scale: 1,
|
|
2549
|
+
x: 0,
|
|
2550
|
+
y: 0
|
|
2551
|
+
}), [keys, setKeys] = (0, react.useState)(() => {
|
|
2552
|
+
try {
|
|
1852
2553
|
return {
|
|
1853
|
-
...
|
|
1854
|
-
|
|
1855
|
-
objects: objects.slice(offset, offset + 50),
|
|
1856
|
-
objectCount: objects.length,
|
|
1857
|
-
...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
|
|
1858
|
-
...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
|
|
1859
|
-
recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
|
|
1860
|
-
help: SKETCH_COMMAND_HELP
|
|
2554
|
+
...DEFAULT_KEYS,
|
|
2555
|
+
...JSON.parse(localStorage.getItem("codex-sketch-keys"))
|
|
1861
2556
|
};
|
|
2557
|
+
} catch {
|
|
2558
|
+
return DEFAULT_KEYS;
|
|
1862
2559
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
|
|
1870
|
-
if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
|
|
1871
|
-
const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
|
|
1872
|
-
...request,
|
|
1873
|
-
runId: void 0
|
|
1874
|
-
});
|
|
1875
|
-
const cached = completed.get(key);
|
|
1876
|
-
if (cached) {
|
|
1877
|
-
if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
|
|
1878
|
-
return cached.result;
|
|
1879
|
-
}
|
|
1880
|
-
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
1881
|
-
let changedObjects;
|
|
1882
|
-
if (request.action === "apply") {
|
|
1883
|
-
const before = adapter.document();
|
|
1884
|
-
let next;
|
|
1885
|
-
try {
|
|
1886
|
-
next = applySketchCommands(before, request.commands);
|
|
1887
|
-
} catch (cause) {
|
|
1888
|
-
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
1889
|
-
error.code = "SKETCH_INVALID_BATCH";
|
|
1890
|
-
throw error;
|
|
1891
|
-
}
|
|
1892
|
-
adapter.commit(next);
|
|
1893
|
-
const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
|
|
1894
|
-
changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
|
|
1895
|
-
layer: l.id,
|
|
1896
|
-
id: s.id
|
|
1897
|
-
})));
|
|
1898
|
-
} else {
|
|
1899
|
-
if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
|
|
1900
|
-
pending = true;
|
|
1901
|
-
try {
|
|
1902
|
-
await adapter.save(request.name);
|
|
1903
|
-
} finally {
|
|
1904
|
-
pending = false;
|
|
1905
|
-
}
|
|
2560
|
+
});
|
|
2561
|
+
const [shortcuts, setShortcuts] = (0, react.useState)(() => {
|
|
2562
|
+
try {
|
|
2563
|
+
return localStorage.getItem("codex-sketch-shortcuts") !== "off";
|
|
2564
|
+
} catch {
|
|
2565
|
+
return true;
|
|
1906
2566
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2567
|
+
}), [space, setSpace] = (0, react.useState)(false);
|
|
2568
|
+
const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
|
|
2569
|
+
viewRef.current = view;
|
|
2570
|
+
const zoom = (factor) => setView((v) => ({
|
|
2571
|
+
...v,
|
|
2572
|
+
scale: Math.max(.25, Math.min(8, v.scale * factor))
|
|
2573
|
+
}));
|
|
2574
|
+
const reset = () => setView({
|
|
2575
|
+
scale: 1,
|
|
2576
|
+
x: 0,
|
|
2577
|
+
y: 0
|
|
2578
|
+
});
|
|
2579
|
+
(0, react.useEffect)(() => {
|
|
2580
|
+
const node = canvas.current;
|
|
2581
|
+
if (!node || !open) return;
|
|
2582
|
+
const wheel = (e) => {
|
|
2583
|
+
if (!shortcuts || !e.altKey) return;
|
|
2584
|
+
e.preventDefault();
|
|
2585
|
+
zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
|
|
1913
2586
|
};
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2587
|
+
node.addEventListener("wheel", wheel, { passive: false });
|
|
2588
|
+
return () => node.removeEventListener("wheel", wheel);
|
|
2589
|
+
}, [open, shortcuts]);
|
|
2590
|
+
(0, react.useEffect)(() => {
|
|
2591
|
+
const stop = () => {
|
|
2592
|
+
drag.current = null;
|
|
2593
|
+
setSpace(false);
|
|
2594
|
+
};
|
|
2595
|
+
window.addEventListener("blur", stop);
|
|
2596
|
+
return () => window.removeEventListener("blur", stop);
|
|
2597
|
+
}, []);
|
|
2598
|
+
(0, react.useEffect)(() => {
|
|
2599
|
+
if (!open || !shortcuts) {
|
|
2600
|
+
setSpace(false);
|
|
2601
|
+
drag.current = null;
|
|
1928
2602
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
const c = document.createElement("canvas");
|
|
1937
|
-
c.width = w;
|
|
1938
|
-
c.height = h;
|
|
1939
|
-
return c;
|
|
1940
|
-
};
|
|
1941
|
-
function runPsdCodec(action, payload) {
|
|
1942
|
-
return new Promise((resolve, reject) => {
|
|
1943
|
-
const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
|
|
1944
|
-
const finish = (callback, value) => {
|
|
1945
|
-
clearTimeout(timer);
|
|
1946
|
-
worker.terminate();
|
|
1947
|
-
callback(value);
|
|
2603
|
+
}, [open, shortcuts]);
|
|
2604
|
+
const setKey = (action, key) => {
|
|
2605
|
+
key = key.toLowerCase();
|
|
2606
|
+
if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
|
|
2607
|
+
const next = {
|
|
2608
|
+
...keys,
|
|
2609
|
+
[action]: key
|
|
1948
2610
|
};
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
action,
|
|
1954
|
-
payload
|
|
1955
|
-
});
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
function encodeSketchDocument(doc) {
|
|
1959
|
-
return JSON.stringify({
|
|
1960
|
-
format: "dsh-sketch",
|
|
1961
|
-
version: 1,
|
|
1962
|
-
doc
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
function decodeSketchDocument(text) {
|
|
1966
|
-
if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
|
|
1967
|
-
const file = JSON.parse(text), source = file.doc;
|
|
1968
|
-
if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
|
|
1969
|
-
const w = source.width ?? 1024, h = source.height ?? 1024;
|
|
1970
|
-
if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
|
|
1971
|
-
let doc = {
|
|
1972
|
-
...createSketchLayers(),
|
|
1973
|
-
width: w,
|
|
1974
|
-
height: h,
|
|
1975
|
-
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
|
|
2611
|
+
setKeys(next);
|
|
2612
|
+
try {
|
|
2613
|
+
localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
|
|
2614
|
+
} catch {}
|
|
1976
2615
|
};
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
"marker"
|
|
1999
|
-
].includes(s.brush)) throw Error("Invalid brush");
|
|
2000
|
-
if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
|
|
2001
|
-
if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
|
|
2002
|
-
Object.assign(added[added.length - strokes.length + k], {
|
|
2003
|
-
brush: s.brush ?? "pen",
|
|
2004
|
-
pressure: s.pressure ?? 1,
|
|
2005
|
-
...s.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
2006
|
-
});
|
|
2616
|
+
return {
|
|
2617
|
+
view,
|
|
2618
|
+
keys,
|
|
2619
|
+
shortcuts,
|
|
2620
|
+
space,
|
|
2621
|
+
zoom,
|
|
2622
|
+
reset,
|
|
2623
|
+
setKey,
|
|
2624
|
+
toggle: () => setShortcuts((v) => {
|
|
2625
|
+
try {
|
|
2626
|
+
localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
|
|
2627
|
+
} catch {}
|
|
2628
|
+
return !v;
|
|
2629
|
+
}),
|
|
2630
|
+
keyDown: (e) => {
|
|
2631
|
+
if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
2632
|
+
const action = sketchShortcutAction(keys, e.key);
|
|
2633
|
+
if (action === "pan") {
|
|
2634
|
+
e.preventDefault();
|
|
2635
|
+
setSpace(true);
|
|
2636
|
+
return true;
|
|
2007
2637
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
"
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2638
|
+
if ([
|
|
2639
|
+
"zoomIn",
|
|
2640
|
+
"zoomOut",
|
|
2641
|
+
"fit"
|
|
2642
|
+
].includes(action)) {
|
|
2643
|
+
e.preventDefault();
|
|
2644
|
+
if (action === "fit") reset();
|
|
2645
|
+
else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
|
|
2646
|
+
return true;
|
|
2647
|
+
}
|
|
2648
|
+
return false;
|
|
2649
|
+
},
|
|
2650
|
+
keyUp: (e) => {
|
|
2651
|
+
if (e.key.toLowerCase() === keys.pan) setSpace(false);
|
|
2652
|
+
},
|
|
2653
|
+
down: (e) => {
|
|
2654
|
+
if (e.button !== 1 && !space) return false;
|
|
2655
|
+
e.preventDefault();
|
|
2656
|
+
drag.current = {
|
|
2657
|
+
id: e.pointerId,
|
|
2658
|
+
x: e.clientX,
|
|
2659
|
+
y: e.clientY,
|
|
2660
|
+
view: viewRef.current
|
|
2030
2661
|
};
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
const context = canvas(width, height).getContext("2d");
|
|
2052
|
-
context.fillStyle = "#fff";
|
|
2053
|
-
context.fillRect(0, 0, width, height);
|
|
2054
|
-
if (children[0] && !children[0].hidden) {
|
|
2055
|
-
const bottom = canvas(width, height);
|
|
2056
|
-
bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
|
|
2057
|
-
context.drawImage(bottom, 0, 0);
|
|
2058
|
-
children[0].imageData = context.getImageData(0, 0, width, height);
|
|
2059
|
-
} else {
|
|
2060
|
-
if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
|
|
2061
|
-
children.unshift({
|
|
2062
|
-
name: "Paper",
|
|
2063
|
-
opacity: 1,
|
|
2064
|
-
blendMode: "normal",
|
|
2065
|
-
imageData: context.getImageData(0, 0, width, height)
|
|
2066
|
-
});
|
|
2067
|
-
}
|
|
2068
|
-
return runPsdCodec("write", {
|
|
2069
|
-
width,
|
|
2070
|
-
height,
|
|
2071
|
-
children,
|
|
2072
|
-
imageData: composite.getContext("2d").getImageData(0, 0, width, height)
|
|
2073
|
-
});
|
|
2662
|
+
canvas.current.setPointerCapture(e.pointerId);
|
|
2663
|
+
return true;
|
|
2664
|
+
},
|
|
2665
|
+
move: (e) => {
|
|
2666
|
+
const d = drag.current;
|
|
2667
|
+
if (!d || d.id !== e.pointerId) return false;
|
|
2668
|
+
setView({
|
|
2669
|
+
...d.view,
|
|
2670
|
+
x: d.view.x + e.clientX - d.x,
|
|
2671
|
+
y: d.view.y + e.clientY - d.y
|
|
2672
|
+
});
|
|
2673
|
+
return true;
|
|
2674
|
+
},
|
|
2675
|
+
end: (e) => {
|
|
2676
|
+
if (drag.current?.id !== e.pointerId) return false;
|
|
2677
|
+
drag.current = null;
|
|
2678
|
+
if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
|
|
2679
|
+
return true;
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2074
2682
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
const
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
width,
|
|
2082
|
-
height,
|
|
2083
|
-
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
|
|
2084
|
-
layers: [],
|
|
2085
|
-
nextId: psd.layers.length + 1
|
|
2683
|
+
function SketchViewControls({ navigation, t }) {
|
|
2684
|
+
const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
|
|
2685
|
+
const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
|
|
2686
|
+
const close = () => {
|
|
2687
|
+
setOpen(false);
|
|
2688
|
+
trigger.current?.focus({ preventScroll: true });
|
|
2086
2689
|
};
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2690
|
+
useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
|
|
2691
|
+
(0, react.useLayoutEffect)(() => {
|
|
2692
|
+
if (!open) return;
|
|
2693
|
+
const dialog = host.current.closest("dialog");
|
|
2694
|
+
const place = () => {
|
|
2695
|
+
const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
|
|
2696
|
+
const width = Math.min(360, box.width - 24);
|
|
2697
|
+
setPlacement({
|
|
2698
|
+
dialog,
|
|
2699
|
+
style: {
|
|
2700
|
+
width,
|
|
2701
|
+
left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
|
|
2702
|
+
bottom: box.bottom - anchor.top + 8,
|
|
2703
|
+
maxHeight: Math.max(80, anchor.top - box.top - 24)
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
};
|
|
2707
|
+
place();
|
|
2708
|
+
const observer = new ResizeObserver(place);
|
|
2709
|
+
observer.observe(dialog);
|
|
2710
|
+
observer.observe(host.current);
|
|
2711
|
+
window.addEventListener("resize", place);
|
|
2712
|
+
return () => {
|
|
2713
|
+
observer.disconnect();
|
|
2714
|
+
window.removeEventListener("resize", place);
|
|
2715
|
+
};
|
|
2716
|
+
}, [open]);
|
|
2717
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2718
|
+
ref: host,
|
|
2719
|
+
className: "codexSketchViewControls",
|
|
2720
|
+
children: [
|
|
2721
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2722
|
+
type: "button",
|
|
2723
|
+
"aria-label": t("sketchZoomOut"),
|
|
2724
|
+
onClick: () => navigation.zoom(1 / 1.2),
|
|
2725
|
+
children: "−"
|
|
2726
|
+
}),
|
|
2727
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2728
|
+
type: "button",
|
|
2729
|
+
title: t("sketchFit"),
|
|
2730
|
+
onClick: navigation.reset,
|
|
2731
|
+
children: [Math.round(navigation.view.scale * 100), "%"]
|
|
2732
|
+
}),
|
|
2733
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2734
|
+
type: "button",
|
|
2735
|
+
"aria-label": t("sketchZoomIn"),
|
|
2736
|
+
onClick: () => navigation.zoom(1.2),
|
|
2737
|
+
children: "+"
|
|
2738
|
+
}),
|
|
2739
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2740
|
+
ref: trigger,
|
|
2741
|
+
type: "button",
|
|
2742
|
+
"aria-expanded": open,
|
|
2743
|
+
onClick: () => setOpen(!open),
|
|
2744
|
+
children: t("sketchKeys")
|
|
2745
|
+
}),
|
|
2746
|
+
open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2747
|
+
className: "codexSketchKeyPanel",
|
|
2748
|
+
"aria-label": t("sketchKeys"),
|
|
2749
|
+
style: placement.style,
|
|
2750
|
+
onKeyDown: (e) => {
|
|
2751
|
+
if (e.key === "Escape") {
|
|
2752
|
+
e.preventDefault();
|
|
2753
|
+
e.stopPropagation();
|
|
2754
|
+
close();
|
|
2755
|
+
}
|
|
2756
|
+
},
|
|
2757
|
+
children: [
|
|
2758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2759
|
+
type: "button",
|
|
2760
|
+
"aria-label": t("sketchFileClose"),
|
|
2761
|
+
onClick: close,
|
|
2762
|
+
children: "×"
|
|
2763
|
+
})] }),
|
|
2764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2765
|
+
className: "codexSketchKeysEnabled",
|
|
2766
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2767
|
+
type: "checkbox",
|
|
2768
|
+
checked: navigation.shortcuts,
|
|
2769
|
+
onChange: navigation.toggle
|
|
2770
|
+
})]
|
|
2771
|
+
}),
|
|
2772
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
|
|
2773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2774
|
+
className: "codexSketchKeyGrid",
|
|
2775
|
+
children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2776
|
+
"aria-label": t(`sketchKey_${action}`),
|
|
2777
|
+
value: key === " " ? "Space" : key,
|
|
2778
|
+
readOnly: true,
|
|
2779
|
+
onKeyDown: (e) => {
|
|
2780
|
+
if (e.key === "Tab" || e.key === "Escape") return;
|
|
2781
|
+
e.preventDefault();
|
|
2782
|
+
e.stopPropagation();
|
|
2783
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
|
|
2784
|
+
}
|
|
2785
|
+
})] }, action))
|
|
2786
|
+
}),
|
|
2787
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
|
|
2788
|
+
]
|
|
2789
|
+
}), placement.dialog) : null
|
|
2790
|
+
]
|
|
2791
|
+
});
|
|
2108
2792
|
}
|
|
2109
2793
|
//#endregion
|
|
2110
2794
|
//#region src/sketch-files.jsx
|
|
2111
|
-
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t,
|
|
2795
|
+
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
|
|
2112
2796
|
const [open, setOpen] = (0, react.useState)(false), [rows, setRows] = (0, react.useState)([]), [name, setName] = (0, react.useState)(""), [remove, setRemove] = (0, react.useState)(null), [working, setWorking] = (0, react.useState)(false);
|
|
2113
2797
|
const [format, setFormat] = (0, react.useState)("png");
|
|
2114
2798
|
const input = (0, react.useRef)(null), host = (0, react.useRef)(null);
|
|
@@ -2120,19 +2804,14 @@ window.__ModuleLoader__.load({
|
|
|
2120
2804
|
}
|
|
2121
2805
|
}, [open, working]);
|
|
2122
2806
|
useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
|
|
2123
|
-
const run = async (
|
|
2124
|
-
if (disabled || working) return;
|
|
2807
|
+
const run = (operation) => runOperation(async () => {
|
|
2125
2808
|
setWorking(true);
|
|
2126
|
-
onWorking(true);
|
|
2127
2809
|
try {
|
|
2128
2810
|
await operation();
|
|
2129
|
-
} catch (error) {
|
|
2130
|
-
report(error?.message || t("sketchStorageFailed"));
|
|
2131
2811
|
} finally {
|
|
2132
2812
|
setWorking(false);
|
|
2133
|
-
onWorking(false);
|
|
2134
2813
|
}
|
|
2135
|
-
};
|
|
2814
|
+
});
|
|
2136
2815
|
const refresh = async () => setRows(await sketchDrafts("list"));
|
|
2137
2816
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2138
2817
|
ref: host,
|
|
@@ -2429,13 +3108,15 @@ window.__ModuleLoader__.load({
|
|
|
2429
3108
|
//#endregion
|
|
2430
3109
|
//#region src/sketch-agent-client.js
|
|
2431
3110
|
function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
|
|
2432
|
-
let stopped = false, token, timer, attempts = 0, failures = 0;
|
|
3111
|
+
let stopped = false, token, timer, attempts = 0, failures = 0, pending = false;
|
|
2433
3112
|
const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
|
|
2434
3113
|
sessionId,
|
|
2435
3114
|
token,
|
|
2436
3115
|
...payload
|
|
2437
3116
|
}).then(unwrap);
|
|
2438
3117
|
const poll = async () => {
|
|
3118
|
+
if (stopped || pending) return;
|
|
3119
|
+
pending = true;
|
|
2439
3120
|
try {
|
|
2440
3121
|
const tasks = await call("poll");
|
|
2441
3122
|
for (const task of tasks) {
|
|
@@ -2470,25 +3151,53 @@ window.__ModuleLoader__.load({
|
|
|
2470
3151
|
timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
|
|
2471
3152
|
}
|
|
2472
3153
|
return;
|
|
3154
|
+
} finally {
|
|
3155
|
+
pending = false;
|
|
2473
3156
|
}
|
|
2474
3157
|
failures = 0;
|
|
2475
3158
|
if (!stopped) timer = setTimeout(poll, pollDelay());
|
|
2476
3159
|
};
|
|
2477
|
-
const connect = () =>
|
|
2478
|
-
|
|
3160
|
+
const connect = () => {
|
|
3161
|
+
if (stopped || pending) return;
|
|
3162
|
+
pending = true;
|
|
3163
|
+
call("connect").then((value) => {
|
|
3164
|
+
pending = false;
|
|
3165
|
+
token = value.token;
|
|
3166
|
+
attempts = 0;
|
|
3167
|
+
if (stopped) call("disconnect").catch(() => {});
|
|
3168
|
+
else poll();
|
|
3169
|
+
}, (error) => {
|
|
3170
|
+
pending = false;
|
|
3171
|
+
if (stopped) return;
|
|
3172
|
+
const leaseConflict = /Another board is connected/.test(error.message);
|
|
3173
|
+
if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
|
|
3174
|
+
else report(error.message);
|
|
3175
|
+
});
|
|
3176
|
+
};
|
|
3177
|
+
const wake = () => {
|
|
3178
|
+
if (stopped || pending) return;
|
|
3179
|
+
clearTimeout(timer);
|
|
2479
3180
|
attempts = 0;
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
3181
|
+
failures = 0;
|
|
3182
|
+
token ? poll() : connect();
|
|
3183
|
+
};
|
|
3184
|
+
const visible = () => {
|
|
3185
|
+
if (document.visibilityState === "visible") wake();
|
|
3186
|
+
};
|
|
3187
|
+
if (typeof window !== "undefined") {
|
|
3188
|
+
window.addEventListener("online", wake);
|
|
3189
|
+
window.addEventListener("focus", wake);
|
|
3190
|
+
document.addEventListener("visibilitychange", visible);
|
|
3191
|
+
}
|
|
2488
3192
|
connect();
|
|
2489
3193
|
return () => {
|
|
2490
3194
|
stopped = true;
|
|
2491
3195
|
clearTimeout(timer);
|
|
3196
|
+
if (typeof window !== "undefined") {
|
|
3197
|
+
window.removeEventListener("online", wake);
|
|
3198
|
+
window.removeEventListener("focus", wake);
|
|
3199
|
+
document.removeEventListener("visibilitychange", visible);
|
|
3200
|
+
}
|
|
2492
3201
|
if (token) call("disconnect").catch(() => {});
|
|
2493
3202
|
};
|
|
2494
3203
|
}
|
|
@@ -2515,11 +3224,7 @@ window.__ModuleLoader__.load({
|
|
|
2515
3224
|
const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
|
|
2516
3225
|
const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
|
|
2517
3226
|
const navigation = useSketchView(canvas, open);
|
|
2518
|
-
const
|
|
2519
|
-
pen: 12,
|
|
2520
|
-
pencil: 6,
|
|
2521
|
-
marker: 28
|
|
2522
|
-
});
|
|
3227
|
+
const toolWidths = (0, react.useRef)({});
|
|
2523
3228
|
const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
|
|
2524
3229
|
const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
|
|
2525
3230
|
const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
|
|
@@ -2549,15 +3254,24 @@ window.__ModuleLoader__.load({
|
|
|
2549
3254
|
setColor(value);
|
|
2550
3255
|
if (selected) editObject({ color: value });
|
|
2551
3256
|
};
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
3257
|
+
const chooseTool = (name, nextBrush = brush) => {
|
|
3258
|
+
setWidth(switchSketchToolWidth(toolWidths.current, {
|
|
3259
|
+
tool,
|
|
3260
|
+
brush,
|
|
3261
|
+
width
|
|
3262
|
+
}, {
|
|
3263
|
+
tool: name,
|
|
3264
|
+
brush: nextBrush
|
|
3265
|
+
}));
|
|
3266
|
+
setBrush(nextBrush);
|
|
3267
|
+
setTool(name);
|
|
3268
|
+
if (name !== "select") setSelection(null);
|
|
2558
3269
|
};
|
|
3270
|
+
const chooseBrush = (name) => chooseTool("pen", name);
|
|
2559
3271
|
const [fillShape, setFillShape] = (0, react.useState)(false);
|
|
2560
|
-
const [
|
|
3272
|
+
const [hydrated, setHydrated] = (0, react.useState)(false), [recovered, setRecovered] = (0, react.useState)(false);
|
|
3273
|
+
const [restoreAttempt, retryRestore] = (0, react.useState)(0);
|
|
3274
|
+
const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(true), [error, setError] = (0, react.useState)("");
|
|
2561
3275
|
const cursorRing = (0, react.useRef)(null);
|
|
2562
3276
|
const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
|
|
2563
3277
|
useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
|
|
@@ -2595,144 +3309,100 @@ window.__ModuleLoader__.load({
|
|
|
2595
3309
|
if (next === doc.current) return;
|
|
2596
3310
|
if (action !== "select") checkpoint();
|
|
2597
3311
|
else documentRevision.current++;
|
|
2598
|
-
doc.current = next;
|
|
2599
|
-
setError("");
|
|
2600
|
-
schedule();
|
|
2601
|
-
};
|
|
2602
|
-
(0, react.useEffect)(() => {
|
|
2603
|
-
if (open) {
|
|
2604
|
-
dialog.current.showModal();
|
|
2605
|
-
canvas.current.width = doc.current.width ?? 1024;
|
|
2606
|
-
paint();
|
|
2607
|
-
} else dialog.current?.close();
|
|
2608
|
-
}, [open]);
|
|
2609
|
-
(0, react.useEffect)(() => () => {
|
|
2610
|
-
cancelAnimationFrame(frame.current);
|
|
2611
|
-
cache.current.clear();
|
|
2612
|
-
}, []);
|
|
2613
|
-
const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
|
|
2614
|
-
const save = async (name) => {
|
|
2615
|
-
const savingDocument = documentId.current, savingRevision = documentRevision.current;
|
|
2616
|
-
const row = {
|
|
2617
|
-
id: saved.current?.id ?? crypto.randomUUID(),
|
|
2618
|
-
name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
2619
|
-
updated: Date.now(),
|
|
2620
|
-
doc: structuredClone(doc.current)
|
|
2621
|
-
};
|
|
2622
|
-
await sketchDrafts("save", row);
|
|
2623
|
-
if (documentId.current === savingDocument) {
|
|
2624
|
-
saved.current = {
|
|
2625
|
-
id: row.id,
|
|
2626
|
-
name: row.name
|
|
2627
|
-
};
|
|
2628
|
-
if (documentRevision.current === savingRevision) dirty.current = false;
|
|
2629
|
-
}
|
|
2630
|
-
};
|
|
2631
|
-
const saveChanges = async () => {
|
|
2632
|
-
if (dirty.current && (hasContent() || saved.current)) await save();
|
|
2633
|
-
};
|
|
2634
|
-
const replace = (next, decoded, identity) => {
|
|
2635
|
-
documentId.current = crypto.randomUUID();
|
|
2636
|
-
documentRevision.current++;
|
|
2637
|
-
doc.current = identifyObjects(structuredClone(next));
|
|
2638
|
-
setSelection(null);
|
|
2639
|
-
setTextEdit(null);
|
|
2640
|
-
images.current = decoded;
|
|
2641
|
-
cache.current.clear();
|
|
2642
|
-
undo.current = [];
|
|
2643
|
-
redo.current = [];
|
|
2644
|
-
saved.current = identity;
|
|
2645
|
-
dirty.current = false;
|
|
2646
|
-
schedule();
|
|
2647
|
-
};
|
|
2648
|
-
const fresh = async () => {
|
|
2649
|
-
await saveChanges();
|
|
2650
|
-
replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
|
|
2651
|
-
};
|
|
2652
|
-
const load = async (row) => {
|
|
2653
|
-
if (row.id === saved.current?.id) return;
|
|
2654
|
-
await saveChanges();
|
|
2655
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2656
|
-
await decodeSketchImages(row.doc, decoded);
|
|
2657
|
-
replace(row.doc, decoded, {
|
|
2658
|
-
id: row.id,
|
|
2659
|
-
name: row.name
|
|
2660
|
-
});
|
|
2661
|
-
};
|
|
2662
|
-
const importImage = async (file) => {
|
|
2663
|
-
if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
|
|
2664
|
-
if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
|
|
2665
|
-
const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
|
|
2666
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2667
|
-
await decodeSketchImages(next, decoded);
|
|
2668
|
-
await saveChanges();
|
|
2669
|
-
replace(next, decoded, null);
|
|
2670
|
-
dirty.current = true;
|
|
2671
|
-
return;
|
|
2672
|
-
}
|
|
2673
|
-
if (doc.current.layers.length >= 8) throw Error("Layer limit");
|
|
2674
|
-
const image = await importSketchImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
|
|
2675
|
-
const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
|
|
2676
|
-
const layer = {
|
|
2677
|
-
id: doc.current.nextId,
|
|
2678
|
-
name: file.name?.slice(0, 40) || t("sketchImport"),
|
|
2679
|
-
visible: true,
|
|
2680
|
-
strokes: [],
|
|
2681
|
-
image: {
|
|
2682
|
-
src: image.src,
|
|
2683
|
-
x: (1 - width) / 2,
|
|
2684
|
-
y: (1 - height) / 2,
|
|
2685
|
-
width,
|
|
2686
|
-
height
|
|
2687
|
-
}
|
|
2688
|
-
};
|
|
2689
|
-
await decodeSketchImages({ layers: [layer] }, images.current);
|
|
2690
|
-
checkpoint();
|
|
2691
|
-
doc.current = {
|
|
2692
|
-
...doc.current,
|
|
2693
|
-
nextId: layer.id + 1,
|
|
2694
|
-
active: layer.id,
|
|
2695
|
-
layers: [...doc.current.layers, layer]
|
|
2696
|
-
};
|
|
3312
|
+
doc.current = next;
|
|
3313
|
+
setError("");
|
|
2697
3314
|
schedule();
|
|
2698
3315
|
};
|
|
2699
|
-
|
|
2700
|
-
if (
|
|
3316
|
+
(0, react.useEffect)(() => {
|
|
3317
|
+
if (open) {
|
|
3318
|
+
dialog.current.showModal();
|
|
3319
|
+
canvas.current.width = doc.current.width ?? 1024;
|
|
3320
|
+
paint();
|
|
3321
|
+
} else dialog.current?.close();
|
|
3322
|
+
}, [open]);
|
|
3323
|
+
(0, react.useEffect)(() => () => {
|
|
3324
|
+
cancelAnimationFrame(frame.current);
|
|
3325
|
+
cache.current.clear();
|
|
3326
|
+
}, []);
|
|
3327
|
+
const { save, saveChanges, fresh, load, importImage, restore } = createSketchDocumentLifecycle(localSession.current, {
|
|
3328
|
+
sessionId,
|
|
3329
|
+
t,
|
|
3330
|
+
schedule,
|
|
3331
|
+
checkpoint,
|
|
3332
|
+
cache,
|
|
3333
|
+
setSelection,
|
|
3334
|
+
setTextEdit,
|
|
3335
|
+
setRecovered
|
|
3336
|
+
});
|
|
3337
|
+
(0, react.useEffect)(() => localSession.current.retain?.(), []);
|
|
3338
|
+
(0, react.useEffect)(() => {
|
|
3339
|
+
let live = true;
|
|
3340
|
+
setHydrated(false);
|
|
3341
|
+
setBusy(true);
|
|
3342
|
+
setError("");
|
|
3343
|
+
(enabled ? restore(() => live) : Promise.resolve()).then(() => {
|
|
3344
|
+
if (live) {
|
|
3345
|
+
setHydrated(true);
|
|
3346
|
+
setBusy(false);
|
|
3347
|
+
}
|
|
3348
|
+
}).catch((error) => {
|
|
3349
|
+
if (live) setError(t(error.code === "SKETCH_STORAGE_BLOCKED" ? "sketchStorageBlocked" : "sketchStorageFailed"));
|
|
3350
|
+
});
|
|
3351
|
+
return () => {
|
|
3352
|
+
live = false;
|
|
3353
|
+
};
|
|
3354
|
+
}, [
|
|
3355
|
+
enabled,
|
|
3356
|
+
sessionId,
|
|
3357
|
+
restoreAttempt
|
|
3358
|
+
]);
|
|
3359
|
+
(0, react.useEffect)(() => {
|
|
3360
|
+
if (!hydrated || !enabled || !dirty.current || busy || agentLocked) return;
|
|
3361
|
+
const timer = setTimeout(() => {
|
|
3362
|
+
if (active.current || sizeGesture.current || !dirty.current) return;
|
|
3363
|
+
sketchDrafts("checkpoint", {
|
|
3364
|
+
id: sessionId,
|
|
3365
|
+
updated: Date.now(),
|
|
3366
|
+
doc: structuredClone(doc.current)
|
|
3367
|
+
}).catch(() => setError(t("sketchRecoveryFailed")));
|
|
3368
|
+
}, 1500);
|
|
3369
|
+
return () => clearTimeout(timer);
|
|
3370
|
+
}, [
|
|
3371
|
+
revision,
|
|
3372
|
+
hydrated,
|
|
3373
|
+
enabled,
|
|
3374
|
+
busy,
|
|
3375
|
+
agentLocked,
|
|
3376
|
+
sessionId
|
|
3377
|
+
]);
|
|
3378
|
+
const close = () => {
|
|
3379
|
+
if (!hydrated || agentRun.current?.locked) {
|
|
2701
3380
|
onClose();
|
|
2702
3381
|
return;
|
|
2703
3382
|
}
|
|
2704
|
-
|
|
2705
|
-
setBusy(true);
|
|
2706
|
-
try {
|
|
3383
|
+
return runFile(async () => {
|
|
2707
3384
|
await saveChanges();
|
|
2708
3385
|
onClose();
|
|
2709
|
-
}
|
|
2710
|
-
setError(t("sketchStorageFailed"));
|
|
2711
|
-
} finally {
|
|
2712
|
-
setBusy(false);
|
|
2713
|
-
}
|
|
2714
|
-
};
|
|
2715
|
-
const runFile = async (operation) => {
|
|
2716
|
-
if (busy || agentRun.current?.locked || active.current) return;
|
|
2717
|
-
setBusy(true);
|
|
2718
|
-
setError("");
|
|
2719
|
-
try {
|
|
2720
|
-
await operation();
|
|
2721
|
-
} catch {
|
|
2722
|
-
setError(t("sketchStorageFailed"));
|
|
2723
|
-
} finally {
|
|
2724
|
-
setBusy(false);
|
|
2725
|
-
}
|
|
3386
|
+
});
|
|
2726
3387
|
};
|
|
3388
|
+
const operationGate = (0, react.useRef)(null);
|
|
3389
|
+
operationGate.current ??= createSketchOperationGate();
|
|
3390
|
+
const runFile = (operation) => operationGate.current.run(operation, {
|
|
3391
|
+
blocked: busy || agentRun.current?.locked || Boolean(active.current),
|
|
3392
|
+
working: setBusy,
|
|
3393
|
+
report: (error) => setError(error ? error?.message || t("sketchStorageFailed") : "")
|
|
3394
|
+
});
|
|
2727
3395
|
(0, react.useEffect)(() => {
|
|
2728
|
-
if (open && !agentLocked && incoming && incoming !== received.current) {
|
|
3396
|
+
if (open && hydrated && !busy && !agentLocked && incoming && incoming !== received.current) {
|
|
2729
3397
|
received.current = incoming;
|
|
2730
3398
|
runFile(() => importImage(incoming.file));
|
|
2731
3399
|
}
|
|
2732
3400
|
}, [
|
|
2733
3401
|
open,
|
|
2734
3402
|
incoming,
|
|
2735
|
-
agentLocked
|
|
3403
|
+
agentLocked,
|
|
3404
|
+
hydrated,
|
|
3405
|
+
busy
|
|
2736
3406
|
]);
|
|
2737
3407
|
const keyDown = (event) => {
|
|
2738
3408
|
if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
|
|
@@ -2744,14 +3414,6 @@ window.__ModuleLoader__.load({
|
|
|
2744
3414
|
editObject({}, "delete");
|
|
2745
3415
|
return;
|
|
2746
3416
|
}
|
|
2747
|
-
if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "v") {
|
|
2748
|
-
setTool("select");
|
|
2749
|
-
return;
|
|
2750
|
-
}
|
|
2751
|
-
if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.toLowerCase() === "t") {
|
|
2752
|
-
setTool("text");
|
|
2753
|
-
return;
|
|
2754
|
-
}
|
|
2755
3417
|
const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
|
|
2756
3418
|
if (command && [
|
|
2757
3419
|
"z",
|
|
@@ -2765,20 +3427,24 @@ window.__ModuleLoader__.load({
|
|
|
2765
3427
|
return;
|
|
2766
3428
|
}
|
|
2767
3429
|
if (command || event.altKey) return;
|
|
2768
|
-
const
|
|
3430
|
+
const action = sketchShortcutAction(navigation.keys, key);
|
|
3431
|
+
if ([
|
|
2769
3432
|
"pen",
|
|
2770
3433
|
"eraser",
|
|
2771
3434
|
"line",
|
|
2772
3435
|
"rectangle",
|
|
2773
|
-
"circle"
|
|
2774
|
-
|
|
2775
|
-
|
|
3436
|
+
"circle",
|
|
3437
|
+
"select",
|
|
3438
|
+
"text"
|
|
3439
|
+
].includes(action)) {
|
|
2776
3440
|
event.preventDefault();
|
|
2777
|
-
|
|
3441
|
+
chooseTool(action);
|
|
2778
3442
|
}
|
|
2779
3443
|
if (key === "[" || key === "]") {
|
|
2780
3444
|
event.preventDefault();
|
|
2781
|
-
|
|
3445
|
+
const value = stepSketchWidth(selected?.width ?? width, key === "]" ? 1 : -1);
|
|
3446
|
+
if (selected) editObject({ width: value });
|
|
3447
|
+
else setWidth(value);
|
|
2782
3448
|
}
|
|
2783
3449
|
};
|
|
2784
3450
|
const current = doc.current.layers.find((layer) => layer.id === doc.current.active);
|
|
@@ -2787,81 +3453,14 @@ window.__ModuleLoader__.load({
|
|
|
2787
3453
|
const gesture = active.current;
|
|
2788
3454
|
if (!gesture || gesture.id !== event.pointerId || busy) return;
|
|
2789
3455
|
const rect = bounds ?? canvas.current.getBoundingClientRect();
|
|
2790
|
-
if (gesture.object) {
|
|
2791
|
-
const point = sketchPoint(event.clientX, event.clientY, rect);
|
|
2792
|
-
if (!point) return;
|
|
2793
|
-
try {
|
|
2794
|
-
const box = objectBounds(gesture.object);
|
|
2795
|
-
const stroke = gesture.handle === "end" ? {
|
|
2796
|
-
...gesture.object,
|
|
2797
|
-
points: [gesture.object.points[0], point]
|
|
2798
|
-
} : gesture.handle === "size" ? transformObject(gesture.object, {
|
|
2799
|
-
scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
|
|
2800
|
-
scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
|
|
2801
|
-
}) : transformObject(gesture.object, {
|
|
2802
|
-
dx: point.x - gesture.start.x,
|
|
2803
|
-
dy: point.y - gesture.start.y
|
|
2804
|
-
});
|
|
2805
|
-
doc.current = {
|
|
2806
|
-
...doc.current,
|
|
2807
|
-
layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
|
|
2808
|
-
...l,
|
|
2809
|
-
strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
|
|
2810
|
-
} : l)
|
|
2811
|
-
};
|
|
2812
|
-
schedule();
|
|
2813
|
-
} catch {}
|
|
2814
|
-
return;
|
|
2815
|
-
}
|
|
2816
3456
|
const native = event.nativeEvent ?? event;
|
|
2817
|
-
const
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
const previous = gesture.last ?? point;
|
|
2824
|
-
const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
|
|
2825
|
-
layer.strokes = layer.strokes.filter((stroke) => {
|
|
2826
|
-
for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
|
|
2827
|
-
x: previous.x + (point.x - previous.x) * i / steps,
|
|
2828
|
-
y: previous.y + (point.y - previous.y) * i / steps
|
|
2829
|
-
}, width / 2, doc.current.width, doc.current.height)) return false;
|
|
2830
|
-
return true;
|
|
2831
|
-
});
|
|
2832
|
-
} else {
|
|
2833
|
-
const stroke = layer.strokes.at(-1);
|
|
2834
|
-
if ([
|
|
2835
|
-
"line",
|
|
2836
|
-
"arrow",
|
|
2837
|
-
"rectangle",
|
|
2838
|
-
"circle"
|
|
2839
|
-
].includes(stroke.shape)) {
|
|
2840
|
-
if (stroke.shape === "line" && event.shiftKey) {
|
|
2841
|
-
const w = doc.current.width ?? 1024, h = doc.current.height ?? 1024, a = stroke.points[0];
|
|
2842
|
-
const snapped = snapLine({
|
|
2843
|
-
x: a.x * w,
|
|
2844
|
-
y: a.y * h
|
|
2845
|
-
}, {
|
|
2846
|
-
x: point.x * w,
|
|
2847
|
-
y: point.y * h
|
|
2848
|
-
});
|
|
2849
|
-
point = {
|
|
2850
|
-
x: snapped.x / w,
|
|
2851
|
-
y: snapped.y / h
|
|
2852
|
-
};
|
|
2853
|
-
}
|
|
2854
|
-
stroke.points = [stroke.points[0], point];
|
|
2855
|
-
} else {
|
|
2856
|
-
const last = stroke.points.at(-1);
|
|
2857
|
-
if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
|
|
2858
|
-
if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
|
|
2859
|
-
stroke.points.push(point);
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
gesture.last = point;
|
|
3457
|
+
const samples = native.getCoalescedEvents?.() ?? [];
|
|
3458
|
+
try {
|
|
3459
|
+
doc.current = updateSketchGesture(doc.current, gesture, samples.length ? [...samples, native] : [native], rect, width, event.shiftKey);
|
|
3460
|
+
schedule(Boolean(gesture.object));
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
setError(error?.message || t("sketchFailed"));
|
|
2863
3463
|
}
|
|
2864
|
-
schedule(false);
|
|
2865
3464
|
};
|
|
2866
3465
|
const end = (event, cancel = false) => {
|
|
2867
3466
|
if (navigation.end(event)) return;
|
|
@@ -2884,7 +3483,7 @@ window.__ModuleLoader__.load({
|
|
|
2884
3483
|
layer: drawn.layer,
|
|
2885
3484
|
id: stroke.id
|
|
2886
3485
|
});
|
|
2887
|
-
|
|
3486
|
+
chooseTool("select");
|
|
2888
3487
|
}
|
|
2889
3488
|
}
|
|
2890
3489
|
active.current = null;
|
|
@@ -2910,7 +3509,7 @@ window.__ModuleLoader__.load({
|
|
|
2910
3509
|
available: () => enabled && agentEnabled,
|
|
2911
3510
|
previewEnabled: () => agentPreview,
|
|
2912
3511
|
open: () => onOpen(),
|
|
2913
|
-
busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
|
|
3512
|
+
busy: () => operationGate.current.running || busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
|
|
2914
3513
|
document: () => doc.current,
|
|
2915
3514
|
snapshot: () => ({
|
|
2916
3515
|
documentId: documentId.current,
|
|
@@ -2957,23 +3556,18 @@ window.__ModuleLoader__.load({
|
|
|
2957
3556
|
previewEnabled: () => agentAdapter.current.previewEnabled()
|
|
2958
3557
|
});
|
|
2959
3558
|
(0, react.useEffect)(() => {
|
|
2960
|
-
if (!enabled || !agentEnabled) return;
|
|
3559
|
+
if (!enabled || !agentEnabled || !hydrated) return;
|
|
2961
3560
|
const api = Object.freeze({
|
|
2962
3561
|
version: 2,
|
|
2963
3562
|
sessionId,
|
|
2964
3563
|
execute: (request) => agentRun.current.execute(request),
|
|
2965
|
-
export:
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
extension,
|
|
2973
|
-
mediaType: blob.type,
|
|
2974
|
-
base64: btoa(raw)
|
|
2975
|
-
};
|
|
2976
|
-
}
|
|
3564
|
+
export: (format) => exportSketchAgentFile(format, {
|
|
3565
|
+
gate: operationGate.current,
|
|
3566
|
+
blocked: agentAdapter.current.busy() || agentRun.current.locked,
|
|
3567
|
+
working: setBusy,
|
|
3568
|
+
report: (error) => setError(error ? error.message || t("sketchFailed") : ""),
|
|
3569
|
+
exportFile: (value) => agentAdapter.current.export(value)
|
|
3570
|
+
})
|
|
2977
3571
|
});
|
|
2978
3572
|
window.dshSketchAgent = api;
|
|
2979
3573
|
return () => {
|
|
@@ -2983,14 +3577,15 @@ window.__ModuleLoader__.load({
|
|
|
2983
3577
|
enabled,
|
|
2984
3578
|
agentEnabled,
|
|
2985
3579
|
rpc,
|
|
2986
|
-
sessionId
|
|
3580
|
+
sessionId,
|
|
3581
|
+
hydrated
|
|
2987
3582
|
]);
|
|
2988
3583
|
(0, react.useEffect)(() => {
|
|
2989
3584
|
if (!enabled || !agentEnabled) {
|
|
2990
3585
|
if (agentRun.current.locked) agentRun.current.stop();
|
|
2991
3586
|
return;
|
|
2992
3587
|
}
|
|
2993
|
-
if (!rpc || !sessionId) return;
|
|
3588
|
+
if (!rpc || !sessionId || !hydrated) return;
|
|
2994
3589
|
let live = true;
|
|
2995
3590
|
const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
|
|
2996
3591
|
if (!live) throw Error("Sketch session disconnected");
|
|
@@ -2998,7 +3593,7 @@ window.__ModuleLoader__.load({
|
|
|
2998
3593
|
}, (message) => {
|
|
2999
3594
|
agentRun.current.fail();
|
|
3000
3595
|
setError(message);
|
|
3001
|
-
}, () => 350);
|
|
3596
|
+
}, () => agentRun.current.locked ? 350 : 2e3);
|
|
3002
3597
|
return () => {
|
|
3003
3598
|
live = false;
|
|
3004
3599
|
disconnect();
|
|
@@ -3007,23 +3602,18 @@ window.__ModuleLoader__.load({
|
|
|
3007
3602
|
enabled,
|
|
3008
3603
|
agentEnabled,
|
|
3009
3604
|
rpc,
|
|
3010
|
-
sessionId
|
|
3605
|
+
sessionId,
|
|
3606
|
+
hydrated
|
|
3011
3607
|
]);
|
|
3012
|
-
const attach =
|
|
3013
|
-
if (!enabled
|
|
3014
|
-
|
|
3015
|
-
setError("");
|
|
3016
|
-
try {
|
|
3608
|
+
const attach = () => {
|
|
3609
|
+
if (!enabled) return;
|
|
3610
|
+
return runFile(async () => {
|
|
3017
3611
|
paint();
|
|
3018
|
-
const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error("
|
|
3612
|
+
const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error(t("sketchFailed"))), "image/png"));
|
|
3019
3613
|
await saveChanges();
|
|
3020
3614
|
await attachSketch(blob);
|
|
3021
3615
|
onClose();
|
|
3022
|
-
}
|
|
3023
|
-
setError(t("sketchFailed"));
|
|
3024
|
-
} finally {
|
|
3025
|
-
setBusy(false);
|
|
3026
|
-
}
|
|
3616
|
+
});
|
|
3027
3617
|
};
|
|
3028
3618
|
const exportFile = async (format = "png") => {
|
|
3029
3619
|
paint();
|
|
@@ -3043,8 +3633,8 @@ window.__ModuleLoader__.load({
|
|
|
3043
3633
|
};
|
|
3044
3634
|
agentAdapter.current.export = exportFile;
|
|
3045
3635
|
const download = async (format) => {
|
|
3636
|
+
setError("");
|
|
3046
3637
|
const { blob, extension } = await exportFile(format);
|
|
3047
|
-
await saveChanges();
|
|
3048
3638
|
const url = URL.createObjectURL(blob), link = document.createElement("a");
|
|
3049
3639
|
link.href = url;
|
|
3050
3640
|
link.download = `${(saved.current?.name || "sketch").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").slice(0, 80)}.${extension}`;
|
|
@@ -3110,7 +3700,7 @@ window.__ModuleLoader__.load({
|
|
|
3110
3700
|
type: "button",
|
|
3111
3701
|
"aria-label": t("sketchCancel"),
|
|
3112
3702
|
title: t("sketchCancel"),
|
|
3113
|
-
disabled: busy && !agentLocked,
|
|
3703
|
+
disabled: busy && hydrated && !agentLocked,
|
|
3114
3704
|
onClick: close,
|
|
3115
3705
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
|
|
3116
3706
|
}),
|
|
@@ -3123,8 +3713,7 @@ window.__ModuleLoader__.load({
|
|
|
3123
3713
|
hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
|
|
3124
3714
|
disabled: agentLocked || busy,
|
|
3125
3715
|
t,
|
|
3126
|
-
|
|
3127
|
-
onWorking: setBusy
|
|
3716
|
+
runOperation: runFile
|
|
3128
3717
|
}),
|
|
3129
3718
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3130
3719
|
className: "codexSketchHeading",
|
|
@@ -3215,6 +3804,19 @@ window.__ModuleLoader__.load({
|
|
|
3215
3804
|
},
|
|
3216
3805
|
onDismiss: () => setNoticeHidden(true)
|
|
3217
3806
|
}) : null,
|
|
3807
|
+
recovered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3808
|
+
className: "codexSketchAgentStatus",
|
|
3809
|
+
role: "status",
|
|
3810
|
+
children: [t("sketchRecovered"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3811
|
+
type: "button",
|
|
3812
|
+
onClick: () => setRecovered(false),
|
|
3813
|
+
"aria-label": t("sketchDismissStatus"),
|
|
3814
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3815
|
+
name: "close",
|
|
3816
|
+
size: 14
|
|
3817
|
+
})
|
|
3818
|
+
})]
|
|
3819
|
+
}) : null,
|
|
3218
3820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3219
3821
|
className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
|
|
3220
3822
|
children: [
|
|
@@ -3229,6 +3831,12 @@ window.__ModuleLoader__.load({
|
|
|
3229
3831
|
width: SKETCH_SIZE,
|
|
3230
3832
|
height: SKETCH_SIZE,
|
|
3231
3833
|
"aria-label": t("sketchTitle"),
|
|
3834
|
+
onDoubleClick: () => {
|
|
3835
|
+
if (!busy && !agentRun.current?.locked && tool === "select" && selected?.shape === "text") setTextEdit({
|
|
3836
|
+
selection,
|
|
3837
|
+
value: selected.text
|
|
3838
|
+
});
|
|
3839
|
+
},
|
|
3232
3840
|
onPointerDown: (event) => {
|
|
3233
3841
|
if (busy || !enabled || active.current) return;
|
|
3234
3842
|
if (navigation.down(event)) return;
|
|
@@ -3405,13 +4013,10 @@ window.__ModuleLoader__.load({
|
|
|
3405
4013
|
sizeGesture.current = false;
|
|
3406
4014
|
},
|
|
3407
4015
|
onChange: (value) => {
|
|
3408
|
-
if (opacityMode) {
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
setWidth(value);
|
|
3413
|
-
if (selected) editObject({ width: value });
|
|
3414
|
-
}
|
|
4016
|
+
if (opacityMode) if (selected) editObject({ opacity: value / 100 });
|
|
4017
|
+
else setFlow(value);
|
|
4018
|
+
else if (selected) editObject({ width: value });
|
|
4019
|
+
else setWidth(value);
|
|
3415
4020
|
}
|
|
3416
4021
|
}),
|
|
3417
4022
|
textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
@@ -3425,10 +4030,7 @@ window.__ModuleLoader__.load({
|
|
|
3425
4030
|
try {
|
|
3426
4031
|
if (textEdit.selection) editObject({ text: textEdit.value });
|
|
3427
4032
|
else {
|
|
3428
|
-
const a = textEdit.point,
|
|
3429
|
-
x: Math.min(1, a.x + .35),
|
|
3430
|
-
y: Math.min(1, a.y + .15)
|
|
3431
|
-
}, id = crypto.randomUUID();
|
|
4033
|
+
const [a, b] = newTextBounds(textEdit.point), id = crypto.randomUUID();
|
|
3432
4034
|
const next = applySketchCommands(doc.current, [{
|
|
3433
4035
|
op: "stroke",
|
|
3434
4036
|
id,
|
|
@@ -3447,7 +4049,7 @@ window.__ModuleLoader__.load({
|
|
|
3447
4049
|
schedule();
|
|
3448
4050
|
}
|
|
3449
4051
|
setTextEdit(null);
|
|
3450
|
-
|
|
4052
|
+
chooseTool("select");
|
|
3451
4053
|
} catch (e) {
|
|
3452
4054
|
setError(e.message);
|
|
3453
4055
|
}
|
|
@@ -3457,6 +4059,18 @@ window.__ModuleLoader__.load({
|
|
|
3457
4059
|
autoFocus: true,
|
|
3458
4060
|
"aria-label": t("sketchText"),
|
|
3459
4061
|
maxLength: 500,
|
|
4062
|
+
onKeyDown: (event) => {
|
|
4063
|
+
if (event.nativeEvent.isComposing) return;
|
|
4064
|
+
if (event.key === "Escape") {
|
|
4065
|
+
event.preventDefault();
|
|
4066
|
+
event.stopPropagation();
|
|
4067
|
+
setTextEdit(null);
|
|
4068
|
+
}
|
|
4069
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
4070
|
+
event.preventDefault();
|
|
4071
|
+
event.currentTarget.form.requestSubmit();
|
|
4072
|
+
}
|
|
4073
|
+
},
|
|
3460
4074
|
value: textEdit.value,
|
|
3461
4075
|
onChange: (e) => setTextEdit({
|
|
3462
4076
|
...textEdit,
|
|
@@ -3516,89 +4130,11 @@ window.__ModuleLoader__.load({
|
|
|
3516
4130
|
!doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
|
|
3517
4131
|
]
|
|
3518
4132
|
}) : null,
|
|
3519
|
-
layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
type: "button",
|
|
3525
|
-
title: t("sketchLayerAdd"),
|
|
3526
|
-
"aria-label": t("sketchLayerAdd"),
|
|
3527
|
-
disabled: agentLocked || busy || doc.current.layers.length >= 8,
|
|
3528
|
-
onClick: () => change("add"),
|
|
3529
|
-
children: "+"
|
|
3530
|
-
})] }),
|
|
3531
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3532
|
-
className: "codexLayerList",
|
|
3533
|
-
children: doc.current.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3534
|
-
className: "codexLayerRow",
|
|
3535
|
-
"data-active": layer.id === doc.current.active,
|
|
3536
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3537
|
-
type: "button",
|
|
3538
|
-
disabled: agentLocked || busy,
|
|
3539
|
-
"aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
|
|
3540
|
-
"aria-pressed": layer.visible,
|
|
3541
|
-
onClick: () => change("visible", layer.id),
|
|
3542
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3543
|
-
name: layer.visible ? "eye" : "eyeOff",
|
|
3544
|
-
size: 18
|
|
3545
|
-
})
|
|
3546
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3547
|
-
type: "button",
|
|
3548
|
-
disabled: agentLocked || busy,
|
|
3549
|
-
"aria-pressed": layer.id === doc.current.active,
|
|
3550
|
-
onClick: () => change("select", layer.id),
|
|
3551
|
-
children: layer.name || `${t("sketchLayer")} ${layer.id}`
|
|
3552
|
-
})]
|
|
3553
|
-
}, layer.id))
|
|
3554
|
-
}),
|
|
3555
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
3556
|
-
className: "codexSketchLayerLabel",
|
|
3557
|
-
children: t("sketchLayerName")
|
|
3558
|
-
}),
|
|
3559
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3560
|
-
disabled: agentLocked || busy,
|
|
3561
|
-
"aria-label": t("sketchLayerName"),
|
|
3562
|
-
defaultValue: current.name,
|
|
3563
|
-
placeholder: `${t("sketchLayer")} ${current.id}`,
|
|
3564
|
-
maxLength: 40,
|
|
3565
|
-
onBlur: (e) => {
|
|
3566
|
-
if (e.target.value !== current.name) change("rename", current.id, e.target.value);
|
|
3567
|
-
},
|
|
3568
|
-
onKeyDown: (e) => {
|
|
3569
|
-
if (e.key === "Enter") e.currentTarget.blur();
|
|
3570
|
-
}
|
|
3571
|
-
}, current.id + "-" + current.name),
|
|
3572
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3573
|
-
className: "codexLayerActions",
|
|
3574
|
-
children: [
|
|
3575
|
-
"duplicate",
|
|
3576
|
-
"up",
|
|
3577
|
-
"down",
|
|
3578
|
-
"delete"
|
|
3579
|
-
].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3580
|
-
type: "button",
|
|
3581
|
-
title: t(`sketchLayer_${action}`),
|
|
3582
|
-
"aria-label": t(`sketchLayer_${action}`),
|
|
3583
|
-
disabled: agentLocked || busy || action === "delete" && doc.current.layers.length === 1 || action === "duplicate" && (doc.current.layers.length >= 8 || strokeCount(doc.current) + current.strokes.length > 2e3) || action === "up" && current === doc.current.layers.at(-1) || action === "down" && current === doc.current.layers[0],
|
|
3584
|
-
onClick: () => change(action),
|
|
3585
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3586
|
-
name: action === "delete" ? "clear" : action,
|
|
3587
|
-
size: 17
|
|
3588
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
|
|
3589
|
-
}, action))
|
|
3590
|
-
}),
|
|
3591
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3592
|
-
type: "button",
|
|
3593
|
-
className: "codexSketchClearLayer",
|
|
3594
|
-
disabled: agentLocked || busy || !current.strokes.length && !current.image,
|
|
3595
|
-
onClick: () => change("clear"),
|
|
3596
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3597
|
-
name: "clear",
|
|
3598
|
-
size: 16
|
|
3599
|
-
}), t("sketchClearLayer")]
|
|
3600
|
-
})
|
|
3601
|
-
]
|
|
4133
|
+
layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
|
|
4134
|
+
document: doc.current,
|
|
4135
|
+
disabled: agentLocked || busy,
|
|
4136
|
+
change,
|
|
4137
|
+
t
|
|
3602
4138
|
}) : null
|
|
3603
4139
|
]
|
|
3604
4140
|
}),
|
|
@@ -3616,92 +4152,15 @@ window.__ModuleLoader__.load({
|
|
|
3616
4152
|
navigation,
|
|
3617
4153
|
t
|
|
3618
4154
|
}),
|
|
3619
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
"pencil",
|
|
3629
|
-
"marker",
|
|
3630
|
-
"text",
|
|
3631
|
-
"eraser"
|
|
3632
|
-
].map((name) => {
|
|
3633
|
-
const drawing = [
|
|
3634
|
-
"pen",
|
|
3635
|
-
"pencil",
|
|
3636
|
-
"marker"
|
|
3637
|
-
].includes(name);
|
|
3638
|
-
const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
|
|
3639
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3640
|
-
type: "button",
|
|
3641
|
-
"aria-label": label,
|
|
3642
|
-
title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
|
|
3643
|
-
"aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
|
|
3644
|
-
disabled: agentLocked || busy,
|
|
3645
|
-
onClick: () => {
|
|
3646
|
-
if (drawing) chooseBrush(name);
|
|
3647
|
-
else {
|
|
3648
|
-
setTool(name);
|
|
3649
|
-
if (name !== "select") setSelection(null);
|
|
3650
|
-
}
|
|
3651
|
-
},
|
|
3652
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3653
|
-
name,
|
|
3654
|
-
size: 23
|
|
3655
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
3656
|
-
}, name);
|
|
3657
|
-
}),
|
|
3658
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3659
|
-
className: "codexSketchShapeToggle",
|
|
3660
|
-
type: "button",
|
|
3661
|
-
"aria-label": t("sketchShapes"),
|
|
3662
|
-
"aria-expanded": shapesOpen,
|
|
3663
|
-
"aria-pressed": [
|
|
3664
|
-
"line",
|
|
3665
|
-
"arrow",
|
|
3666
|
-
"rectangle",
|
|
3667
|
-
"circle"
|
|
3668
|
-
].includes(tool),
|
|
3669
|
-
disabled: agentLocked || busy,
|
|
3670
|
-
onClick: () => setShapesOpen((v) => !v),
|
|
3671
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3672
|
-
name: [
|
|
3673
|
-
"line",
|
|
3674
|
-
"arrow",
|
|
3675
|
-
"rectangle",
|
|
3676
|
-
"circle"
|
|
3677
|
-
].includes(tool) ? tool : "rectangle",
|
|
3678
|
-
size: 23
|
|
3679
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
|
|
3680
|
-
"line",
|
|
3681
|
-
"arrow",
|
|
3682
|
-
"rectangle",
|
|
3683
|
-
"circle"
|
|
3684
|
-
].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
|
|
3685
|
-
}),
|
|
3686
|
-
shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3687
|
-
className: "codexSketchShapeMenu",
|
|
3688
|
-
children: [
|
|
3689
|
-
"line",
|
|
3690
|
-
"arrow",
|
|
3691
|
-
"rectangle",
|
|
3692
|
-
"circle"
|
|
3693
|
-
].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3694
|
-
type: "button",
|
|
3695
|
-
"aria-pressed": tool === name,
|
|
3696
|
-
onClick: () => {
|
|
3697
|
-
setTool(name);
|
|
3698
|
-
setSelection(null);
|
|
3699
|
-
setShapesOpen(false);
|
|
3700
|
-
},
|
|
3701
|
-
children: t(`sketchTool_${name}`)
|
|
3702
|
-
}, name))
|
|
3703
|
-
}) : null
|
|
3704
|
-
]
|
|
4155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
|
|
4156
|
+
t,
|
|
4157
|
+
disabled: agentLocked || busy,
|
|
4158
|
+
tool,
|
|
4159
|
+
brush,
|
|
4160
|
+
chooseBrush,
|
|
4161
|
+
chooseTool,
|
|
4162
|
+
shapesOpen,
|
|
4163
|
+
setShapesOpen
|
|
3705
4164
|
}),
|
|
3706
4165
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3707
4166
|
className: "codexLayerBrush",
|
|
@@ -3793,10 +4252,14 @@ window.__ModuleLoader__.load({
|
|
|
3793
4252
|
})
|
|
3794
4253
|
]
|
|
3795
4254
|
}),
|
|
3796
|
-
error ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4255
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3797
4256
|
className: "codexSketchHint",
|
|
3798
4257
|
role: "alert",
|
|
3799
|
-
children: error
|
|
4258
|
+
children: [error, !hydrated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4259
|
+
type: "button",
|
|
4260
|
+
onClick: () => retryRestore((value) => value + 1),
|
|
4261
|
+
children: t("accountRetry")
|
|
4262
|
+
}) : null]
|
|
3800
4263
|
}) : null
|
|
3801
4264
|
]
|
|
3802
4265
|
})] });
|
|
@@ -3999,6 +4462,23 @@ window.__ModuleLoader__.load({
|
|
|
3999
4462
|
//#endregion
|
|
4000
4463
|
//#region src/client-locales.js
|
|
4001
4464
|
const zh = {
|
|
4465
|
+
compactionTitle: "云端压缩",
|
|
4466
|
+
compaction_dsh: "关闭",
|
|
4467
|
+
compaction_cloud: "开启",
|
|
4468
|
+
compactionHint: "实验功能,默认关闭。长上下文由 Codex 压缩后继续请求,保留 DSH 历史与原生压缩。开启时该请求使用 SSE;历史改变后重新建立压缩状态。",
|
|
4469
|
+
advancedModelSearch: "模型与搜索",
|
|
4470
|
+
subagentBackendTitle: "独立子任务",
|
|
4471
|
+
subagentBackend_dsh: "DSH",
|
|
4472
|
+
subagentBackend_codex: "Codex",
|
|
4473
|
+
connectionTitle: "连接方式",
|
|
4474
|
+
connectionHint: "默认 SSE。WebSocket 实验性复用连接与上下文传输;连接失败可回退 SSE。下次请求生效,不扩大上下文容量。",
|
|
4475
|
+
subagentBackendHint: "Codex 复用订阅登录,跟随当前订阅模型和工作区权限;其他模型会话使用 Luna low。共享上下文子任务仍用 DSH。",
|
|
4476
|
+
subagentBackendUnavailable: "当前宿主缺少子代理服务,请更新 DSH。",
|
|
4477
|
+
sketchRecovered: "已恢复未保存草稿",
|
|
4478
|
+
sketchRecoveryFailed: "恢复检查点保存失败,请手动保存或导出草稿。",
|
|
4479
|
+
sketchStorageBlocked: "草稿正被其他窗口占用,请关闭其他草图窗口后重试。",
|
|
4480
|
+
sketchDraftLimit: "已达 20 份草稿上限。请先导出,或删除不需要的草稿后保存。",
|
|
4481
|
+
sketchStorageLimit: "草稿存储空间已满。请先导出,或删除不需要的草稿后保存。",
|
|
4002
4482
|
sketchSizeShort: "粗细",
|
|
4003
4483
|
sketchObjectDuplicate: "复制对象",
|
|
4004
4484
|
sketchObjectDelete: "删除对象",
|
|
@@ -4410,6 +4890,23 @@ window.__ModuleLoader__.load({
|
|
|
4410
4890
|
imageRemoveAnnotation: "删除标注"
|
|
4411
4891
|
};
|
|
4412
4892
|
const en = {
|
|
4893
|
+
compactionTitle: "Cloud compaction",
|
|
4894
|
+
compaction_dsh: "Off",
|
|
4895
|
+
compaction_cloud: "On",
|
|
4896
|
+
compactionHint: "Experimental, off by default. Codex compacts long requests while DSH history and native compaction remain available. These requests use SSE; changed history invalidates previous checkpoints.",
|
|
4897
|
+
advancedModelSearch: "Models and search",
|
|
4898
|
+
subagentBackendTitle: "Independent subtasks",
|
|
4899
|
+
subagentBackend_dsh: "DSH",
|
|
4900
|
+
subagentBackend_codex: "Codex",
|
|
4901
|
+
connectionTitle: "Connection",
|
|
4902
|
+
connectionHint: "SSE by default. Experimental WebSocket reuses connections and context transfers and can fall back to SSE on connection failure. Applies to the next request; context limits stay the same.",
|
|
4903
|
+
subagentBackendHint: "Codex uses your subscription login, current subscription model and workspace permissions; other model sessions use Luna low. Shared-context subtasks stay in DSH.",
|
|
4904
|
+
subagentBackendUnavailable: "Subagent services are unavailable. Update DSH to use this option.",
|
|
4905
|
+
sketchRecovered: "Unsaved sketch recovered",
|
|
4906
|
+
sketchRecoveryFailed: "Recovery checkpoint failed. Save or export your draft.",
|
|
4907
|
+
sketchStorageBlocked: "Draft storage is in use. Close other sketch windows and retry.",
|
|
4908
|
+
sketchDraftLimit: "The 20-draft limit is reached. Export first, or remove an unwanted draft before saving.",
|
|
4909
|
+
sketchStorageLimit: "Draft storage is full. Export first, or remove an unwanted draft before saving.",
|
|
4413
4910
|
sketchSizeShort: "Size",
|
|
4414
4911
|
sketchObjectDuplicate: "Duplicate object",
|
|
4415
4912
|
sketchObjectDelete: "Delete object",
|
|
@@ -4823,6 +5320,15 @@ window.__ModuleLoader__.load({
|
|
|
4823
5320
|
//#endregion
|
|
4824
5321
|
//#region src/client-styles.js
|
|
4825
5322
|
const STYLE = `
|
|
5323
|
+
.codexSubscriptionAdvancedPreferences{display:flex;flex-direction:column;gap:10px}
|
|
5324
|
+
.codexSubscriptionSearchChoices.codexSubscriptionQuotaModes{display:flex;flex:0 0 auto;gap:0;grid-template-columns:none}
|
|
5325
|
+
.codexSubscriptionSettingsDisclosure>summary{display:flex;align-items:center;gap:10px;min-height:28px;cursor:pointer;list-style:none;font-size:14px;font-weight:500}
|
|
5326
|
+
.codexSubscriptionSettingsDisclosure>summary::-webkit-details-marker{display:none}
|
|
5327
|
+
.codexSubscriptionSettingsDisclosure>summary>.codexSubscriptionPreferenceHint{margin-left:auto;font-weight:400}
|
|
5328
|
+
.codexSubscriptionSettingsDisclosure>summary>svg{flex:none;transition:transform .15s}
|
|
5329
|
+
.codexSubscriptionSettingsDisclosure[open]>summary>svg{transform:rotate(180deg)}
|
|
5330
|
+
.codexSubscriptionSettingsDisclosure>summary:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:4px;border-radius:4px}
|
|
5331
|
+
.codexSubscriptionSettingsDisclosureBody{padding-top:8px;margin-top:8px;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
4826
5332
|
.codexComposerQuota[data-warning=true]{color:var(--dsw-alias-state-error-primary)}
|
|
4827
5333
|
.codexComposerQuota[data-warning=true] progress{accent-color:var(--dsw-alias-state-error-primary)}
|
|
4828
5334
|
.codexComposerQuota[data-warning=true] progress::-webkit-progress-value{background:var(--dsw-alias-state-error-primary)}
|
|
@@ -4875,20 +5381,9 @@ window.__ModuleLoader__.load({
|
|
|
4875
5381
|
.codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
4876
5382
|
.codexSubscriptionContextInput{width:116px}
|
|
4877
5383
|
.codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}
|
|
4878
|
-
.codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}
|
|
4879
|
-
.codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}
|
|
4880
|
-
.codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}
|
|
4881
|
-
.codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
4882
|
-
.codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
|
|
4883
|
-
.codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}
|
|
4884
|
-
.codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}
|
|
4885
|
-
.codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}
|
|
4886
|
-
.codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}
|
|
4887
|
-
.codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}
|
|
4888
|
-
.codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
4889
5384
|
.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
|
|
4890
5385
|
.codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
|
|
4891
|
-
.codexSubscriptionSearchChoices[data-saving=true] .
|
|
5386
|
+
.codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
|
|
4892
5387
|
.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
4893
5388
|
.codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
|
|
4894
5389
|
.codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
@@ -6176,6 +6671,7 @@ window.__ModuleLoader__.load({
|
|
|
6176
6671
|
let modelRefreshGeneration = 0;
|
|
6177
6672
|
let modelRefreshStarted = false;
|
|
6178
6673
|
let disposed = false;
|
|
6674
|
+
let subagentBackendAvailable = false;
|
|
6179
6675
|
const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
|
|
6180
6676
|
const nativeSnapshot = () => scope.getSnapshot();
|
|
6181
6677
|
const read = () => {
|
|
@@ -6189,6 +6685,10 @@ window.__ModuleLoader__.load({
|
|
|
6189
6685
|
return Object.freeze({
|
|
6190
6686
|
status: current.status,
|
|
6191
6687
|
...capabilities,
|
|
6688
|
+
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6689
|
+
compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
|
|
6690
|
+
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
6691
|
+
subagentBackendAvailable,
|
|
6192
6692
|
quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
|
|
6193
6693
|
searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
|
|
6194
6694
|
speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
|
|
@@ -6222,6 +6722,7 @@ window.__ModuleLoader__.load({
|
|
|
6222
6722
|
publish();
|
|
6223
6723
|
});
|
|
6224
6724
|
const acceptFallback = (value) => {
|
|
6725
|
+
subagentBackendAvailable = value?.subagentBackendAvailable === true;
|
|
6225
6726
|
if (!modelRefreshStarted) {
|
|
6226
6727
|
contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
|
|
6227
6728
|
verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
|
|
@@ -6232,6 +6733,9 @@ window.__ModuleLoader__.load({
|
|
|
6232
6733
|
fallback = {
|
|
6233
6734
|
status: "ready",
|
|
6234
6735
|
value: {
|
|
6736
|
+
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6737
|
+
compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
|
|
6738
|
+
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
6235
6739
|
...readCapabilitySettings(value),
|
|
6236
6740
|
[QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
|
|
6237
6741
|
[SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
|
|
@@ -6255,6 +6759,7 @@ window.__ModuleLoader__.load({
|
|
|
6255
6759
|
try {
|
|
6256
6760
|
const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
|
|
6257
6761
|
if (current !== generation || disposed) return;
|
|
6762
|
+
subagentBackendAvailable = value?.subagentBackendAvailable === true;
|
|
6258
6763
|
if (nativeSnapshot().status === "ready") {
|
|
6259
6764
|
if (!modelRefreshStarted) {
|
|
6260
6765
|
contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
|
|
@@ -6315,7 +6820,7 @@ window.__ModuleLoader__.load({
|
|
|
6315
6820
|
failedPatch = void 0;
|
|
6316
6821
|
publish();
|
|
6317
6822
|
try {
|
|
6318
|
-
if (nativeSnapshot().status === "ready") {
|
|
6823
|
+
if (nativeSnapshot().status === "ready" && !Object.hasOwn(patch, "subagentBackend")) {
|
|
6319
6824
|
for (const [field, value] of entries) {
|
|
6320
6825
|
if (current !== generation) return;
|
|
6321
6826
|
await scope.set(field, value);
|
|
@@ -6641,7 +7146,7 @@ window.__ModuleLoader__.load({
|
|
|
6641
7146
|
}
|
|
6642
7147
|
//#endregion
|
|
6643
7148
|
//#region src/version.js
|
|
6644
|
-
const PACKAGE_VERSION = "2.1.
|
|
7149
|
+
const PACKAGE_VERSION = "2.1.1-beta.2";
|
|
6645
7150
|
//#endregion
|
|
6646
7151
|
//#region src/client-recovery.js
|
|
6647
7152
|
async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
|
|
@@ -7280,64 +7785,76 @@ window.__ModuleLoader__.load({
|
|
|
7280
7785
|
const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
|
|
7281
7786
|
const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
|
|
7282
7787
|
const active = snapshot.imageGeneration || snapshot.imageEditing;
|
|
7283
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7788
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
7284
7789
|
className: "codexSubscriptionCard codexImageSettings",
|
|
7285
7790
|
"aria-label": t("imageSettings"),
|
|
7286
|
-
children:
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
imageModel,
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7791
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
7792
|
+
className: "codexSubscriptionSettingsDisclosure",
|
|
7793
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
7794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSettings") }),
|
|
7795
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7796
|
+
className: "codexSubscriptionPreferenceHint",
|
|
7797
|
+
children: active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
|
|
7798
|
+
}),
|
|
7799
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
7800
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7801
|
+
className: "codexSubscriptionSettingsDisclosureBody",
|
|
7802
|
+
children: [
|
|
7803
|
+
Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
|
|
7804
|
+
const value = imageGroupValue(snapshot, group);
|
|
7805
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7806
|
+
label: t(group),
|
|
7807
|
+
hint: t(`${group}Hint`),
|
|
7808
|
+
value,
|
|
7809
|
+
text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
|
|
7810
|
+
disabled,
|
|
7811
|
+
items: ["on", "off"].map((id) => ({
|
|
7812
|
+
id,
|
|
7813
|
+
label: t(`${group}_${id}`)
|
|
7814
|
+
})),
|
|
7815
|
+
onSelect: (id) => {
|
|
7816
|
+
preference.set(imageGroupPatch(group, id === "on"));
|
|
7817
|
+
}
|
|
7818
|
+
}, group);
|
|
7819
|
+
}),
|
|
7820
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7821
|
+
className: "codexImageDefaultsGroup",
|
|
7822
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7823
|
+
label: t("imageModel"),
|
|
7824
|
+
value: snapshot.imageModel,
|
|
7825
|
+
text: modelLabel(snapshot.imageModel),
|
|
7826
|
+
disabled: disabled || !active,
|
|
7827
|
+
items: Object.keys(IMAGE_MODELS).map((id) => ({
|
|
7828
|
+
id,
|
|
7829
|
+
label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
|
|
7830
|
+
})),
|
|
7831
|
+
onSelect: (imageModel) => {
|
|
7832
|
+
preference.set({
|
|
7833
|
+
imageModel,
|
|
7834
|
+
imageQuality: "auto"
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7837
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7838
|
+
label: t("imageQuality"),
|
|
7839
|
+
value: snapshot.imageQuality,
|
|
7840
|
+
text: t(`imageQuality_${snapshot.imageQuality}`),
|
|
7841
|
+
disabled: disabled || !active,
|
|
7842
|
+
items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
|
|
7843
|
+
id,
|
|
7844
|
+
label: t(`imageQuality_${id}`)
|
|
7845
|
+
})),
|
|
7846
|
+
onSelect: (imageQuality) => {
|
|
7847
|
+
preference.set({ imageQuality });
|
|
7848
|
+
}
|
|
7849
|
+
})]
|
|
7850
|
+
}),
|
|
7851
|
+
snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7852
|
+
className: "codexSubscriptionPreferenceHint",
|
|
7853
|
+
children: t("imageModelHint")
|
|
7854
|
+
}) : null
|
|
7855
|
+
]
|
|
7856
|
+
})]
|
|
7857
|
+
})
|
|
7341
7858
|
});
|
|
7342
7859
|
}
|
|
7343
7860
|
//#endregion
|
|
@@ -7552,10 +8069,9 @@ window.__ModuleLoader__.load({
|
|
|
7552
8069
|
function SearchProviderPreference({ preference, t }) {
|
|
7553
8070
|
const snapshot = usePreferenceSnapshot(preference);
|
|
7554
8071
|
const writable = snapshot.status === "ready" && snapshot.writable === true;
|
|
7555
|
-
const choice = (value, label
|
|
7556
|
-
className: "
|
|
8072
|
+
const choice = (value, label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8073
|
+
className: "codexSubscriptionQuotaMode",
|
|
7557
8074
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7558
|
-
className: "codexSubscriptionSearchInput",
|
|
7559
8075
|
type: "radio",
|
|
7560
8076
|
name: "codex-subscription-search-provider",
|
|
7561
8077
|
checked: snapshot.searchProvider === value,
|
|
@@ -7563,39 +8079,39 @@ window.__ModuleLoader__.load({
|
|
|
7563
8079
|
onChange: () => {
|
|
7564
8080
|
preference.set({ [SEARCH_PROVIDER_FIELD]: value });
|
|
7565
8081
|
}
|
|
7566
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7567
|
-
className: "codexSubscriptionSearchCopy",
|
|
7568
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hint })]
|
|
7569
|
-
})]
|
|
8082
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
7570
8083
|
});
|
|
8084
|
+
const hint = snapshot.searchProvider === "dsh" ? "searchDshHint" : snapshot.searchProvider === "codex" ? "searchCodexHint" : "searchAutoHint";
|
|
7571
8085
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7572
8086
|
className: "codexSubscriptionSearch",
|
|
7573
|
-
children: [
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
8087
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8088
|
+
className: "codexSubscriptionPreference",
|
|
8089
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8090
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8091
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8092
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8093
|
+
children: t("searchTitle")
|
|
8094
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8095
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8096
|
+
children: t(hint)
|
|
7579
8097
|
})]
|
|
7580
|
-
}),
|
|
7581
|
-
|
|
7582
|
-
className: "codexSubscriptionSearchChoices",
|
|
8098
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8099
|
+
className: "codexSubscriptionSearchChoices codexSubscriptionQuotaModes",
|
|
7583
8100
|
"data-saving": snapshot.saving || void 0,
|
|
7584
8101
|
"aria-busy": snapshot.saving || void 0,
|
|
7585
8102
|
role: "radiogroup",
|
|
7586
8103
|
"aria-label": t("searchTitle"),
|
|
7587
8104
|
children: [
|
|
7588
|
-
choice(SEARCH_PROVIDER_AUTO, t("searchAuto")
|
|
7589
|
-
choice("dsh",
|
|
7590
|
-
choice(SEARCH_PROVIDER_CODEX,
|
|
8105
|
+
choice(SEARCH_PROVIDER_AUTO, t("searchAuto")),
|
|
8106
|
+
choice("dsh", "DSH"),
|
|
8107
|
+
choice(SEARCH_PROVIDER_CODEX, "Codex")
|
|
7591
8108
|
]
|
|
7592
|
-
})
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
]
|
|
8109
|
+
})]
|
|
8110
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
|
|
8111
|
+
preference,
|
|
8112
|
+
t,
|
|
8113
|
+
section: "search"
|
|
8114
|
+
})]
|
|
7599
8115
|
});
|
|
7600
8116
|
}
|
|
7601
8117
|
function ContextWindowPreference({ preference, t }) {
|
|
@@ -7761,16 +8277,137 @@ window.__ModuleLoader__.load({
|
|
|
7761
8277
|
function PreferencesCard({ preference, t, section = "display" }) {
|
|
7762
8278
|
const snapshot = usePreferenceSnapshot(preference);
|
|
7763
8279
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7764
|
-
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8280
|
+
className: section === "advanced" ? "codexSubscriptionAdvancedPreferences" : "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
7765
8281
|
children: [section === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7766
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
7767
|
-
|
|
7768
|
-
t
|
|
8282
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
8283
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8284
|
+
"aria-label": t("advancedModelSearch"),
|
|
8285
|
+
children: [
|
|
8286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("advancedModelSearch") }),
|
|
8287
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
|
|
8288
|
+
preference,
|
|
8289
|
+
t
|
|
8290
|
+
}),
|
|
8291
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
|
|
8292
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
|
|
8293
|
+
preference,
|
|
8294
|
+
t
|
|
8295
|
+
})
|
|
8296
|
+
]
|
|
7769
8297
|
}),
|
|
7770
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
8298
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8299
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8300
|
+
"aria-label": t("connectionTitle"),
|
|
8301
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8302
|
+
className: "codexSubscriptionPreference",
|
|
8303
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8304
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8305
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8306
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8307
|
+
children: [
|
|
8308
|
+
t("connectionTitle"),
|
|
8309
|
+
" ",
|
|
8310
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
|
|
8311
|
+
]
|
|
8312
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8313
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8314
|
+
children: t("connectionHint")
|
|
8315
|
+
})]
|
|
8316
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8317
|
+
className: "codexSubscriptionQuotaModes",
|
|
8318
|
+
role: "radiogroup",
|
|
8319
|
+
"aria-label": t("connectionTitle"),
|
|
8320
|
+
"aria-busy": snapshot.saving || void 0,
|
|
8321
|
+
children: ["sse", "websocket"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8322
|
+
className: "codexSubscriptionQuotaMode",
|
|
8323
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
8324
|
+
type: "radio",
|
|
8325
|
+
name: "codex-connection-mode",
|
|
8326
|
+
checked: snapshot.connectionMode === value,
|
|
8327
|
+
disabled: !snapshot.writable,
|
|
8328
|
+
onChange: () => {
|
|
8329
|
+
preference.set({ connectionMode: value });
|
|
8330
|
+
}
|
|
8331
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === "sse" ? "SSE" : "WebSocket" })]
|
|
8332
|
+
}, value))
|
|
8333
|
+
})]
|
|
8334
|
+
})
|
|
8335
|
+
}),
|
|
8336
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8337
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8338
|
+
"aria-label": t("compactionTitle"),
|
|
8339
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8340
|
+
className: "codexSubscriptionPreference",
|
|
8341
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8342
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8343
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8344
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8345
|
+
children: [
|
|
8346
|
+
t("compactionTitle"),
|
|
8347
|
+
" ",
|
|
8348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
|
|
8349
|
+
]
|
|
8350
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8351
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8352
|
+
children: t("compactionHint")
|
|
8353
|
+
})]
|
|
8354
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8355
|
+
className: "codexSubscriptionQuotaModes",
|
|
8356
|
+
role: "radiogroup",
|
|
8357
|
+
"aria-label": t("compactionTitle"),
|
|
8358
|
+
"aria-busy": snapshot.saving || void 0,
|
|
8359
|
+
children: ["dsh", "cloud"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8360
|
+
className: "codexSubscriptionQuotaMode",
|
|
8361
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
8362
|
+
type: "radio",
|
|
8363
|
+
name: "codex-compaction-mode",
|
|
8364
|
+
checked: snapshot.compactionMode === value,
|
|
8365
|
+
disabled: !snapshot.writable,
|
|
8366
|
+
onChange: () => {
|
|
8367
|
+
preference.set({ compactionMode: value });
|
|
8368
|
+
}
|
|
8369
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`compaction_${value}`) })]
|
|
8370
|
+
}, value))
|
|
8371
|
+
})]
|
|
8372
|
+
})
|
|
8373
|
+
}),
|
|
8374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8375
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8376
|
+
"aria-label": t("subagentBackendTitle"),
|
|
8377
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8378
|
+
className: "codexSubscriptionPreference",
|
|
8379
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8380
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8381
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8382
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8383
|
+
children: [
|
|
8384
|
+
t("subagentBackendTitle"),
|
|
8385
|
+
" ",
|
|
8386
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
|
|
8387
|
+
]
|
|
8388
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8389
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8390
|
+
children: t(snapshot.subagentBackendAvailable ? "subagentBackendHint" : "subagentBackendUnavailable")
|
|
8391
|
+
})]
|
|
8392
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8393
|
+
className: "codexSubscriptionQuotaModes",
|
|
8394
|
+
role: "radiogroup",
|
|
8395
|
+
"aria-label": t("subagentBackendTitle"),
|
|
8396
|
+
"aria-busy": snapshot.saving || void 0,
|
|
8397
|
+
children: ["dsh", "codex"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8398
|
+
className: "codexSubscriptionQuotaMode",
|
|
8399
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
8400
|
+
type: "radio",
|
|
8401
|
+
name: "codex-subagent-backend",
|
|
8402
|
+
checked: snapshot.subagentBackend === value,
|
|
8403
|
+
disabled: !snapshot.writable || !snapshot.subagentBackendAvailable,
|
|
8404
|
+
onChange: () => {
|
|
8405
|
+
preference.set({ subagentBackend: value });
|
|
8406
|
+
}
|
|
8407
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`subagentBackend_${value}`) })]
|
|
8408
|
+
}, value))
|
|
8409
|
+
})]
|
|
8410
|
+
})
|
|
7774
8411
|
})
|
|
7775
8412
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
|
|
7776
8413
|
preference,
|