dsh-codex-subscription 2.1.0 → 2.1.1-beta.1
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 +7 -0
- package/README.md +7 -0
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/client.js +1839 -1361
- package/lib/index.js +1984 -1484
- package/package.json +8 -2
package/lib/client.js
CHANGED
|
@@ -372,21 +372,55 @@ window.__ModuleLoader__.load({
|
|
|
372
372
|
dirty: ref(false),
|
|
373
373
|
documentId: ref(crypto.randomUUID()),
|
|
374
374
|
documentRevision: ref(0),
|
|
375
|
+
restoreId: ref(null),
|
|
376
|
+
mounts: 0,
|
|
375
377
|
agentAdapter: ref({}),
|
|
376
378
|
agentSession: ref(null),
|
|
377
379
|
agentRun: ref(null)
|
|
378
380
|
};
|
|
379
381
|
}
|
|
380
|
-
function createSketchSessionRegistry() {
|
|
381
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
382
|
+
function createSketchSessionRegistry({ maxIdle = 8 } = {}) {
|
|
383
|
+
const sessions = /* @__PURE__ */ new Map(), archived = /* @__PURE__ */ new Map();
|
|
384
|
+
const prune = () => {
|
|
385
|
+
const idle = [...sessions].filter(([, s]) => !s.mounts && !s.dirty.current && !s.agentRun.current?.locked && s.agentRun.current?.state !== "stopped");
|
|
386
|
+
for (const [id, state] of idle.slice(0, Math.max(0, idle.length - maxIdle))) {
|
|
387
|
+
if (!state.saved.current && state.doc.current.layers.some((l) => l.image || l.strokes.length)) continue;
|
|
388
|
+
const restoreId = state.saved.current?.id ?? state.restoreId.current;
|
|
389
|
+
if (restoreId) archived.set(id, restoreId);
|
|
390
|
+
state.agentRun.current?.dispose();
|
|
391
|
+
state.images.current.clear();
|
|
392
|
+
sessions.delete(id);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
382
395
|
return {
|
|
383
396
|
get(id) {
|
|
384
|
-
|
|
385
|
-
|
|
397
|
+
let state = sessions.get(id);
|
|
398
|
+
if (!state) {
|
|
399
|
+
state = createSketchSessionState();
|
|
400
|
+
state.restoreId.current = archived.get(id) ?? null;
|
|
401
|
+
archived.delete(id);
|
|
402
|
+
sessions.set(id, state);
|
|
403
|
+
}
|
|
404
|
+
state.retain = () => {
|
|
405
|
+
state.mounts++;
|
|
406
|
+
return () => {
|
|
407
|
+
state.mounts--;
|
|
408
|
+
prune();
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
sessions.delete(id);
|
|
412
|
+
sessions.set(id, state);
|
|
413
|
+
return state;
|
|
386
414
|
},
|
|
415
|
+
prune,
|
|
416
|
+
stats: () => ({
|
|
417
|
+
resident: sessions.size,
|
|
418
|
+
archived: archived.size
|
|
419
|
+
}),
|
|
387
420
|
dispose() {
|
|
388
421
|
for (const value of sessions.values()) value.agentRun.current?.dispose();
|
|
389
422
|
sessions.clear();
|
|
423
|
+
archived.clear();
|
|
390
424
|
}
|
|
391
425
|
};
|
|
392
426
|
}
|
|
@@ -1068,100 +1102,231 @@ window.__ModuleLoader__.load({
|
|
|
1068
1102
|
});
|
|
1069
1103
|
}
|
|
1070
1104
|
//#endregion
|
|
1071
|
-
//#region src/sketch-
|
|
1072
|
-
function
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
role: "status",
|
|
1078
|
-
"aria-live": "polite",
|
|
1105
|
+
//#region src/sketch-layer-panel.jsx
|
|
1106
|
+
function SketchLayerPanel({ document, disabled, change, t }) {
|
|
1107
|
+
const current = document.layers.find((layer) => layer.id === document.active);
|
|
1108
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
1109
|
+
className: "codexSketchLayers",
|
|
1110
|
+
"aria-label": t("sketchLayers"),
|
|
1079
1111
|
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", {
|
|
1112
|
+
/* @__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
1113
|
type: "button",
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1114
|
+
title: t("sketchLayerAdd"),
|
|
1115
|
+
"aria-label": t("sketchLayerAdd"),
|
|
1116
|
+
disabled: disabled || document.layers.length >= 8,
|
|
1117
|
+
onClick: () => change("add"),
|
|
1118
|
+
children: "+"
|
|
1119
|
+
})] }),
|
|
1120
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1121
|
+
className: "codexLayerList",
|
|
1122
|
+
children: document.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1123
|
+
className: "codexLayerRow",
|
|
1124
|
+
"data-active": layer.id === document.active,
|
|
1125
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1126
|
+
type: "button",
|
|
1127
|
+
disabled,
|
|
1128
|
+
"aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
|
|
1129
|
+
"aria-pressed": layer.visible,
|
|
1130
|
+
onClick: () => change("visible", layer.id),
|
|
1131
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1132
|
+
name: layer.visible ? "eye" : "eyeOff",
|
|
1133
|
+
size: 18
|
|
1134
|
+
})
|
|
1135
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1136
|
+
type: "button",
|
|
1137
|
+
disabled,
|
|
1138
|
+
"aria-pressed": layer.id === document.active,
|
|
1139
|
+
onClick: () => change("select", layer.id),
|
|
1140
|
+
children: layer.name || `${t("sketchLayer")} ${layer.id}`
|
|
1141
|
+
})]
|
|
1142
|
+
}, layer.id))
|
|
1143
|
+
}),
|
|
1144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1145
|
+
className: "codexSketchLayerLabel",
|
|
1146
|
+
children: t("sketchLayerName")
|
|
1147
|
+
}),
|
|
1148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1149
|
+
disabled,
|
|
1150
|
+
"aria-label": t("sketchLayerName"),
|
|
1151
|
+
defaultValue: current.name,
|
|
1152
|
+
placeholder: `${t("sketchLayer")} ${current.id}`,
|
|
1153
|
+
maxLength: 40,
|
|
1154
|
+
onBlur: (e) => {
|
|
1155
|
+
if (e.target.value !== current.name) change("rename", current.id, e.target.value);
|
|
1156
|
+
},
|
|
1157
|
+
onKeyDown: (e) => {
|
|
1158
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
1159
|
+
}
|
|
1160
|
+
}, current.id + "-" + current.name),
|
|
1161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1162
|
+
className: "codexLayerActions",
|
|
1163
|
+
children: [
|
|
1164
|
+
"duplicate",
|
|
1165
|
+
"up",
|
|
1166
|
+
"down",
|
|
1167
|
+
"delete"
|
|
1168
|
+
].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1169
|
+
type: "button",
|
|
1170
|
+
title: t(`sketchLayer_${action}`),
|
|
1171
|
+
"aria-label": t(`sketchLayer_${action}`),
|
|
1172
|
+
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],
|
|
1173
|
+
onClick: () => change(action),
|
|
1174
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1175
|
+
name: action === "delete" ? "clear" : action,
|
|
1176
|
+
size: 17
|
|
1177
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
|
|
1178
|
+
}, action))
|
|
1179
|
+
}),
|
|
1180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1090
1181
|
type: "button",
|
|
1091
|
-
className: "
|
|
1092
|
-
|
|
1182
|
+
className: "codexSketchClearLayer",
|
|
1183
|
+
disabled: disabled || !current.strokes.length && !current.image,
|
|
1184
|
+
onClick: () => change("clear"),
|
|
1093
1185
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1094
|
-
name: "
|
|
1095
|
-
size:
|
|
1096
|
-
}), t("
|
|
1097
|
-
})
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1186
|
+
name: "clear",
|
|
1187
|
+
size: 16
|
|
1188
|
+
}), t("sketchClearLayer")]
|
|
1189
|
+
})
|
|
1190
|
+
]
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
//#endregion
|
|
1194
|
+
//#region src/sketch-tool-picker.jsx
|
|
1195
|
+
function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
|
|
1196
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1197
|
+
className: "codexSketchPill",
|
|
1198
|
+
role: "toolbar",
|
|
1199
|
+
"aria-label": t("sketchTitle"),
|
|
1200
|
+
title: t("sketchShortcuts"),
|
|
1201
|
+
children: [
|
|
1202
|
+
[
|
|
1203
|
+
"select",
|
|
1204
|
+
"pen",
|
|
1205
|
+
"pencil",
|
|
1206
|
+
"marker",
|
|
1207
|
+
"text",
|
|
1208
|
+
"eraser"
|
|
1209
|
+
].map((name) => {
|
|
1210
|
+
const drawing = [
|
|
1211
|
+
"pen",
|
|
1212
|
+
"pencil",
|
|
1213
|
+
"marker"
|
|
1214
|
+
].includes(name);
|
|
1215
|
+
const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
|
|
1216
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1217
|
+
type: "button",
|
|
1218
|
+
"aria-label": label,
|
|
1219
|
+
title: drawing ? `${label} · ${t(`sketchBrushHint_${name}`)}` : label,
|
|
1220
|
+
"aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
|
|
1221
|
+
disabled,
|
|
1222
|
+
onClick: () => {
|
|
1223
|
+
if (drawing) chooseBrush(name);
|
|
1224
|
+
else chooseTool(name);
|
|
1225
|
+
},
|
|
1226
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1227
|
+
name,
|
|
1228
|
+
size: 23
|
|
1229
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
1230
|
+
}, name);
|
|
1231
|
+
}),
|
|
1232
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1233
|
+
className: "codexSketchShapeToggle",
|
|
1103
1234
|
type: "button",
|
|
1104
|
-
"aria-label": t("
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1235
|
+
"aria-label": t("sketchShapes"),
|
|
1236
|
+
"aria-expanded": shapesOpen,
|
|
1237
|
+
"aria-pressed": [
|
|
1238
|
+
"line",
|
|
1239
|
+
"arrow",
|
|
1240
|
+
"rectangle",
|
|
1241
|
+
"circle"
|
|
1242
|
+
].includes(tool),
|
|
1243
|
+
disabled,
|
|
1244
|
+
onClick: () => setShapesOpen((v) => !v),
|
|
1245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
1246
|
+
name: [
|
|
1247
|
+
"line",
|
|
1248
|
+
"arrow",
|
|
1249
|
+
"rectangle",
|
|
1250
|
+
"circle"
|
|
1251
|
+
].includes(tool) ? tool : "rectangle",
|
|
1252
|
+
size: 23
|
|
1253
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t([
|
|
1254
|
+
"line",
|
|
1255
|
+
"arrow",
|
|
1256
|
+
"rectangle",
|
|
1257
|
+
"circle"
|
|
1258
|
+
].includes(tool) ? `sketchTool_${tool}` : "sketchShapes") })]
|
|
1259
|
+
}),
|
|
1260
|
+
shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1261
|
+
className: "codexSketchShapeMenu",
|
|
1262
|
+
children: [
|
|
1263
|
+
"line",
|
|
1264
|
+
"arrow",
|
|
1265
|
+
"rectangle",
|
|
1266
|
+
"circle"
|
|
1267
|
+
].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1268
|
+
disabled,
|
|
1269
|
+
type: "button",
|
|
1270
|
+
"aria-pressed": tool === name,
|
|
1271
|
+
onClick: () => {
|
|
1272
|
+
chooseTool(name);
|
|
1273
|
+
setShapesOpen(false);
|
|
1274
|
+
},
|
|
1275
|
+
children: t(`sketchTool_${name}`)
|
|
1276
|
+
}, name))
|
|
1277
|
+
}) : null
|
|
1112
1278
|
]
|
|
1113
1279
|
});
|
|
1114
1280
|
}
|
|
1115
1281
|
//#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
|
-
}
|
|
1282
|
+
//#region src/sketch-objects.js
|
|
1283
|
+
const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
|
|
1284
|
+
const identifyObjects = (doc) => ({
|
|
1285
|
+
...doc,
|
|
1286
|
+
layers: doc.layers.map((layer) => ({
|
|
1287
|
+
...layer,
|
|
1288
|
+
strokes: layer.strokes.map((s, i) => s.id ? s : {
|
|
1289
|
+
...s,
|
|
1290
|
+
id: objectId(s, i)
|
|
1291
|
+
})
|
|
1292
|
+
}))
|
|
1293
|
+
});
|
|
1294
|
+
function objectBounds(stroke) {
|
|
1295
|
+
const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
|
|
1296
|
+
return {
|
|
1297
|
+
x: Math.min(...xs),
|
|
1298
|
+
y: Math.min(...ys),
|
|
1299
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
1300
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
|
|
1304
|
+
if (![
|
|
1305
|
+
dx,
|
|
1306
|
+
dy,
|
|
1307
|
+
scaleX,
|
|
1308
|
+
scaleY
|
|
1309
|
+
].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
|
|
1310
|
+
const box = objectBounds(stroke);
|
|
1311
|
+
const points = stroke.points.map((p) => ({
|
|
1312
|
+
x: box.x + (p.x - box.x) * scaleX + dx,
|
|
1313
|
+
y: box.y + (p.y - box.y) * scaleY + dy
|
|
1314
|
+
}));
|
|
1315
|
+
if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
|
|
1316
|
+
return {
|
|
1317
|
+
...stroke,
|
|
1318
|
+
points
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function sketchObjectSummary(doc) {
|
|
1322
|
+
return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
|
|
1323
|
+
layer: layer.id,
|
|
1324
|
+
id: objectId(stroke, i),
|
|
1325
|
+
shape: stroke.shape,
|
|
1326
|
+
color: stroke.color,
|
|
1327
|
+
bounds: objectBounds(stroke),
|
|
1328
|
+
...stroke.text ? { text: stroke.text } : {}
|
|
1329
|
+
})));
|
|
1165
1330
|
}
|
|
1166
1331
|
//#endregion
|
|
1167
1332
|
//#region src/sketch-input.js
|
|
@@ -1193,39 +1358,192 @@ window.__ModuleLoader__.load({
|
|
|
1193
1358
|
});
|
|
1194
1359
|
}
|
|
1195
1360
|
//#endregion
|
|
1361
|
+
//#region src/sketch-gesture.js
|
|
1362
|
+
function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
|
|
1363
|
+
if (!samples.length) return doc;
|
|
1364
|
+
if (gesture.object) {
|
|
1365
|
+
const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
|
|
1366
|
+
if (!point) return doc;
|
|
1367
|
+
const box = objectBounds(gesture.object);
|
|
1368
|
+
const stroke = gesture.handle === "end" ? {
|
|
1369
|
+
...gesture.object,
|
|
1370
|
+
points: [gesture.object.points[0], point]
|
|
1371
|
+
} : gesture.handle === "size" ? transformObject(gesture.object, {
|
|
1372
|
+
scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
|
|
1373
|
+
scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
|
|
1374
|
+
}) : transformObject(gesture.object, {
|
|
1375
|
+
dx: point.x - gesture.start.x,
|
|
1376
|
+
dy: point.y - gesture.start.y
|
|
1377
|
+
});
|
|
1378
|
+
doc = {
|
|
1379
|
+
...doc,
|
|
1380
|
+
layers: doc.layers.map((l) => l.id === gesture.layer ? {
|
|
1381
|
+
...l,
|
|
1382
|
+
strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
|
|
1383
|
+
} : l)
|
|
1384
|
+
};
|
|
1385
|
+
return doc;
|
|
1386
|
+
}
|
|
1387
|
+
for (const sample of samples) {
|
|
1388
|
+
let point = sketchPoint(sample.clientX, sample.clientY, rect);
|
|
1389
|
+
if (!point) continue;
|
|
1390
|
+
const layer = doc.layers.find((layer) => layer.id === gesture.layer);
|
|
1391
|
+
if (!layer) return doc;
|
|
1392
|
+
if (gesture.eraseStroke) {
|
|
1393
|
+
const previous = gesture.last ?? point;
|
|
1394
|
+
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))));
|
|
1395
|
+
layer.strokes = layer.strokes.filter((stroke) => {
|
|
1396
|
+
for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
|
|
1397
|
+
x: previous.x + (point.x - previous.x) * i / steps,
|
|
1398
|
+
y: previous.y + (point.y - previous.y) * i / steps
|
|
1399
|
+
}, width / 2, doc.width, doc.height)) return false;
|
|
1400
|
+
return true;
|
|
1401
|
+
});
|
|
1402
|
+
} else {
|
|
1403
|
+
const stroke = layer.strokes.at(-1);
|
|
1404
|
+
if (!stroke) return doc;
|
|
1405
|
+
if ([
|
|
1406
|
+
"line",
|
|
1407
|
+
"arrow",
|
|
1408
|
+
"rectangle",
|
|
1409
|
+
"circle"
|
|
1410
|
+
].includes(stroke.shape)) {
|
|
1411
|
+
if (stroke.shape === "line" && shiftKey) {
|
|
1412
|
+
const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
|
|
1413
|
+
const snapped = snapLine({
|
|
1414
|
+
x: a.x * w,
|
|
1415
|
+
y: a.y * h
|
|
1416
|
+
}, {
|
|
1417
|
+
x: point.x * w,
|
|
1418
|
+
y: point.y * h
|
|
1419
|
+
});
|
|
1420
|
+
point = {
|
|
1421
|
+
x: snapped.x / w,
|
|
1422
|
+
y: snapped.y / h
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
stroke.points = [stroke.points[0], point];
|
|
1426
|
+
} else {
|
|
1427
|
+
const last = stroke.points.at(-1);
|
|
1428
|
+
if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
|
|
1429
|
+
if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
|
|
1430
|
+
stroke.points.push(point);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
gesture.last = point;
|
|
1434
|
+
}
|
|
1435
|
+
return doc;
|
|
1436
|
+
}
|
|
1437
|
+
//#endregion
|
|
1196
1438
|
//#region src/sketch-drafts.js
|
|
1197
1439
|
const DATABASE = "dsh-codex-sketches-v1";
|
|
1198
|
-
|
|
1440
|
+
const MAX_STORAGE = 32 * 1024 * 1024;
|
|
1441
|
+
const metadata = (kind, row) => ({
|
|
1442
|
+
key: `${kind}:${row.id}`,
|
|
1443
|
+
kind,
|
|
1444
|
+
id: row.id,
|
|
1445
|
+
name: row.name,
|
|
1446
|
+
updated: row.updated,
|
|
1447
|
+
size: JSON.stringify(row).length
|
|
1448
|
+
});
|
|
1449
|
+
async function sketchDrafts(action, value, recoverySession) {
|
|
1199
1450
|
const db = await new Promise((resolve, reject) => {
|
|
1200
|
-
|
|
1201
|
-
request
|
|
1202
|
-
request.
|
|
1451
|
+
let blocked = false;
|
|
1452
|
+
const request = indexedDB.open(DATABASE, 2);
|
|
1453
|
+
request.onblocked = () => {
|
|
1454
|
+
blocked = true;
|
|
1455
|
+
reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
|
|
1456
|
+
};
|
|
1457
|
+
request.onupgradeneeded = () => {
|
|
1458
|
+
if (blocked) {
|
|
1459
|
+
request.transaction.abort();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const db = request.result, tx = request.transaction;
|
|
1463
|
+
if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
|
|
1464
|
+
const meta = db.createObjectStore("metadata", { keyPath: "key" });
|
|
1465
|
+
db.createObjectStore("recovery", { keyPath: "id" });
|
|
1466
|
+
const cursor = tx.objectStore("drafts").openCursor();
|
|
1467
|
+
cursor.onsuccess = () => {
|
|
1468
|
+
const row = cursor.result;
|
|
1469
|
+
if (row) {
|
|
1470
|
+
meta.put(metadata("drafts", row.value));
|
|
1471
|
+
row.continue();
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
};
|
|
1475
|
+
request.onsuccess = () => {
|
|
1476
|
+
if (blocked) {
|
|
1477
|
+
request.result.close();
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
request.result.onversionchange = () => request.result.close();
|
|
1481
|
+
resolve(request.result);
|
|
1482
|
+
};
|
|
1203
1483
|
request.onerror = () => reject(request.error);
|
|
1204
1484
|
});
|
|
1205
1485
|
try {
|
|
1206
1486
|
return await new Promise((resolve, reject) => {
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1487
|
+
const write = [
|
|
1488
|
+
"save",
|
|
1489
|
+
"delete",
|
|
1490
|
+
"checkpoint",
|
|
1491
|
+
"clearRecovery"
|
|
1492
|
+
].includes(action);
|
|
1493
|
+
const tx = db.transaction([
|
|
1494
|
+
"drafts",
|
|
1495
|
+
"metadata",
|
|
1496
|
+
"recovery"
|
|
1497
|
+
], write ? "readwrite" : "readonly");
|
|
1498
|
+
const meta = tx.objectStore("metadata"), kind = [
|
|
1499
|
+
"checkpoint",
|
|
1500
|
+
"recover",
|
|
1501
|
+
"clearRecovery"
|
|
1502
|
+
].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
|
|
1503
|
+
let result, failure;
|
|
1209
1504
|
tx.oncomplete = () => resolve(result);
|
|
1210
1505
|
tx.onerror = () => reject(tx.error);
|
|
1211
|
-
tx.onabort = () => reject(tx.error ?? Error("Draft
|
|
1212
|
-
|
|
1506
|
+
tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
|
|
1507
|
+
if (action === "get" || action === "recover") {
|
|
1508
|
+
const req = store.get(value);
|
|
1509
|
+
req.onsuccess = () => {
|
|
1510
|
+
result = req.result;
|
|
1511
|
+
};
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (action === "delete" || action === "clearRecovery") {
|
|
1515
|
+
store.delete(value);
|
|
1516
|
+
meta.delete(`${kind}:${value}`);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (![
|
|
1520
|
+
"list",
|
|
1521
|
+
"save",
|
|
1522
|
+
"checkpoint"
|
|
1523
|
+
].includes(action)) {
|
|
1524
|
+
tx.abort();
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const request = meta.getAll();
|
|
1213
1528
|
request.onsuccess = () => {
|
|
1214
1529
|
const rows = request.result;
|
|
1215
1530
|
if (action === "list") {
|
|
1216
|
-
result = rows.sort((a, b) => b.updated - a.updated);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
if (action === "delete") {
|
|
1220
|
-
store.delete(value);
|
|
1531
|
+
result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
|
|
1221
1532
|
return;
|
|
1222
1533
|
}
|
|
1223
|
-
const others = rows.filter((row) => row.
|
|
1224
|
-
|
|
1534
|
+
const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
|
|
1535
|
+
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;
|
|
1536
|
+
if (code) {
|
|
1537
|
+
failure = Object.assign(Error("Draft storage limit reached"), { code });
|
|
1225
1538
|
tx.abort();
|
|
1226
1539
|
return;
|
|
1227
1540
|
}
|
|
1228
1541
|
store.put(value);
|
|
1542
|
+
meta.put(next);
|
|
1543
|
+
if (action === "save" && recoverySession) {
|
|
1544
|
+
tx.objectStore("recovery").delete(recoverySession);
|
|
1545
|
+
meta.delete(`recovery:${recoverySession}`);
|
|
1546
|
+
}
|
|
1229
1547
|
result = value;
|
|
1230
1548
|
};
|
|
1231
1549
|
});
|
|
@@ -1267,397 +1585,6 @@ window.__ModuleLoader__.load({
|
|
|
1267
1585
|
bitmap.close();
|
|
1268
1586
|
}
|
|
1269
1587
|
}
|
|
1270
|
-
//#endregion
|
|
1271
|
-
//#region src/sketch-interactions.js
|
|
1272
|
-
function useSketchDismiss(open, close, host, selectors) {
|
|
1273
|
-
const latest = (0, react.useRef)(close);
|
|
1274
|
-
latest.current = close;
|
|
1275
|
-
(0, react.useEffect)(() => {
|
|
1276
|
-
if (!open) return;
|
|
1277
|
-
const dialog = host.current?.closest("dialog") ?? host.current;
|
|
1278
|
-
if (!dialog) return;
|
|
1279
|
-
const pointer = (event) => {
|
|
1280
|
-
if (selectors.some((selector) => event.target.closest?.(selector))) return;
|
|
1281
|
-
latest.current(false);
|
|
1282
|
-
if (event.target.matches?.("canvas")) {
|
|
1283
|
-
event.preventDefault();
|
|
1284
|
-
event.stopPropagation();
|
|
1285
|
-
event.target.focus({ preventScroll: true });
|
|
1286
|
-
}
|
|
1287
|
-
};
|
|
1288
|
-
const key = (event) => {
|
|
1289
|
-
if (event.key !== "Escape") return;
|
|
1290
|
-
event.preventDefault();
|
|
1291
|
-
event.stopPropagation();
|
|
1292
|
-
latest.current(false);
|
|
1293
|
-
dialog.querySelector("canvas")?.focus({ preventScroll: true });
|
|
1294
|
-
};
|
|
1295
|
-
const hidden = () => latest.current(false);
|
|
1296
|
-
document.addEventListener("pointerdown", pointer, true);
|
|
1297
|
-
document.addEventListener("keydown", key, true);
|
|
1298
|
-
dialog.addEventListener("close", hidden);
|
|
1299
|
-
return () => {
|
|
1300
|
-
document.removeEventListener("pointerdown", pointer, true);
|
|
1301
|
-
document.removeEventListener("keydown", key, true);
|
|
1302
|
-
dialog.removeEventListener("close", hidden);
|
|
1303
|
-
};
|
|
1304
|
-
}, [
|
|
1305
|
-
open,
|
|
1306
|
-
host,
|
|
1307
|
-
selectors.join("|")
|
|
1308
|
-
]);
|
|
1309
|
-
}
|
|
1310
|
-
function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
|
|
1311
|
-
const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
|
|
1312
|
-
const update = (event, bounds) => {
|
|
1313
|
-
if (event) last.current = event;
|
|
1314
|
-
const pointer = last.current, node = canvas.current, cursor = ring.current;
|
|
1315
|
-
if (!pointer || !node || !cursor) return;
|
|
1316
|
-
const rect = bounds ?? node.getBoundingClientRect();
|
|
1317
|
-
if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
|
|
1318
|
-
cursor.hidden = true;
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
|
|
1322
|
-
cursor.hidden = false;
|
|
1323
|
-
cursor.style.width = `${diameter}px`;
|
|
1324
|
-
cursor.style.height = `${diameter}px`;
|
|
1325
|
-
cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
|
|
1326
|
-
};
|
|
1327
|
-
(0, react.useEffect)(() => {
|
|
1328
|
-
update();
|
|
1329
|
-
const observer = new ResizeObserver(() => update());
|
|
1330
|
-
if (canvas.current) observer.observe(canvas.current);
|
|
1331
|
-
return () => observer.disconnect();
|
|
1332
|
-
}, [
|
|
1333
|
-
width,
|
|
1334
|
-
brush,
|
|
1335
|
-
zoom,
|
|
1336
|
-
hidden
|
|
1337
|
-
]);
|
|
1338
|
-
return {
|
|
1339
|
-
down: (event) => {
|
|
1340
|
-
heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
|
|
1341
|
-
},
|
|
1342
|
-
move: (event, bounds) => update({
|
|
1343
|
-
clientX: event.clientX,
|
|
1344
|
-
clientY: event.clientY,
|
|
1345
|
-
pointerId: event.pointerId,
|
|
1346
|
-
pointerType: event.pointerType,
|
|
1347
|
-
pressure: event.pressure
|
|
1348
|
-
}, bounds),
|
|
1349
|
-
leave: () => {
|
|
1350
|
-
last.current = null;
|
|
1351
|
-
if (ring.current) ring.current.hidden = true;
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
//#endregion
|
|
1356
|
-
//#region src/sketch-view.jsx
|
|
1357
|
-
const DEFAULT_KEYS = {
|
|
1358
|
-
pen: "b",
|
|
1359
|
-
eraser: "e",
|
|
1360
|
-
line: "l",
|
|
1361
|
-
rectangle: "r",
|
|
1362
|
-
circle: "o",
|
|
1363
|
-
pan: " ",
|
|
1364
|
-
zoomIn: "=",
|
|
1365
|
-
zoomOut: "-",
|
|
1366
|
-
fit: "0"
|
|
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)(() => {
|
|
1374
|
-
try {
|
|
1375
|
-
return {
|
|
1376
|
-
...DEFAULT_KEYS,
|
|
1377
|
-
...JSON.parse(localStorage.getItem("codex-sketch-keys"))
|
|
1378
|
-
};
|
|
1379
|
-
} catch {
|
|
1380
|
-
return DEFAULT_KEYS;
|
|
1381
|
-
}
|
|
1382
|
-
});
|
|
1383
|
-
const [shortcuts, setShortcuts] = (0, react.useState)(() => {
|
|
1384
|
-
try {
|
|
1385
|
-
return localStorage.getItem("codex-sketch-shortcuts") !== "off";
|
|
1386
|
-
} catch {
|
|
1387
|
-
return true;
|
|
1388
|
-
}
|
|
1389
|
-
}), [space, setSpace] = (0, react.useState)(false);
|
|
1390
|
-
const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
|
|
1391
|
-
viewRef.current = view;
|
|
1392
|
-
const zoom = (factor) => setView((v) => ({
|
|
1393
|
-
...v,
|
|
1394
|
-
scale: Math.max(.25, Math.min(8, v.scale * factor))
|
|
1395
|
-
}));
|
|
1396
|
-
const reset = () => setView({
|
|
1397
|
-
scale: 1,
|
|
1398
|
-
x: 0,
|
|
1399
|
-
y: 0
|
|
1400
|
-
});
|
|
1401
|
-
(0, react.useEffect)(() => {
|
|
1402
|
-
const node = canvas.current;
|
|
1403
|
-
if (!node || !open) return;
|
|
1404
|
-
const wheel = (e) => {
|
|
1405
|
-
if (!shortcuts || !e.altKey) return;
|
|
1406
|
-
e.preventDefault();
|
|
1407
|
-
zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
|
|
1408
|
-
};
|
|
1409
|
-
node.addEventListener("wheel", wheel, { passive: false });
|
|
1410
|
-
return () => node.removeEventListener("wheel", wheel);
|
|
1411
|
-
}, [open, shortcuts]);
|
|
1412
|
-
(0, react.useEffect)(() => {
|
|
1413
|
-
const stop = () => {
|
|
1414
|
-
drag.current = null;
|
|
1415
|
-
setSpace(false);
|
|
1416
|
-
};
|
|
1417
|
-
window.addEventListener("blur", stop);
|
|
1418
|
-
return () => window.removeEventListener("blur", stop);
|
|
1419
|
-
}, []);
|
|
1420
|
-
(0, react.useEffect)(() => {
|
|
1421
|
-
if (!open || !shortcuts) {
|
|
1422
|
-
setSpace(false);
|
|
1423
|
-
drag.current = null;
|
|
1424
|
-
}
|
|
1425
|
-
}, [open, shortcuts]);
|
|
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 {}
|
|
1437
|
-
};
|
|
1438
|
-
return {
|
|
1439
|
-
view,
|
|
1440
|
-
keys,
|
|
1441
|
-
shortcuts,
|
|
1442
|
-
space,
|
|
1443
|
-
zoom,
|
|
1444
|
-
reset,
|
|
1445
|
-
setKey,
|
|
1446
|
-
toggle: () => setShortcuts((v) => {
|
|
1447
|
-
try {
|
|
1448
|
-
localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
|
|
1449
|
-
} catch {}
|
|
1450
|
-
return !v;
|
|
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);
|
|
1464
|
-
return true;
|
|
1465
|
-
}
|
|
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
|
-
}
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
function SketchViewControls({ navigation, t }) {
|
|
1502
|
-
const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
|
|
1503
|
-
const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
|
|
1504
|
-
const close = () => {
|
|
1505
|
-
setOpen(false);
|
|
1506
|
-
trigger.current?.focus({ preventScroll: true });
|
|
1507
|
-
};
|
|
1508
|
-
useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
|
|
1509
|
-
(0, react.useLayoutEffect)(() => {
|
|
1510
|
-
if (!open) return;
|
|
1511
|
-
const dialog = host.current.closest("dialog");
|
|
1512
|
-
const place = () => {
|
|
1513
|
-
const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
|
|
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);
|
|
1533
|
-
};
|
|
1534
|
-
}, [open]);
|
|
1535
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1536
|
-
ref: host,
|
|
1537
|
-
className: "codexSketchViewControls",
|
|
1538
|
-
children: [
|
|
1539
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1540
|
-
type: "button",
|
|
1541
|
-
"aria-label": t("sketchZoomOut"),
|
|
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: "+"
|
|
1556
|
-
}),
|
|
1557
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1558
|
-
ref: trigger,
|
|
1559
|
-
type: "button",
|
|
1560
|
-
"aria-expanded": open,
|
|
1561
|
-
onClick: () => setOpen(!open),
|
|
1562
|
-
children: t("sketchKeys")
|
|
1563
|
-
}),
|
|
1564
|
-
open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1565
|
-
className: "codexSketchKeyPanel",
|
|
1566
|
-
"aria-label": t("sketchKeys"),
|
|
1567
|
-
style: placement.style,
|
|
1568
|
-
onKeyDown: (e) => {
|
|
1569
|
-
if (e.key === "Escape") {
|
|
1570
|
-
e.preventDefault();
|
|
1571
|
-
e.stopPropagation();
|
|
1572
|
-
close();
|
|
1573
|
-
}
|
|
1574
|
-
},
|
|
1575
|
-
children: [
|
|
1576
|
-
/* @__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", {
|
|
1577
|
-
type: "button",
|
|
1578
|
-
"aria-label": t("sketchFileClose"),
|
|
1579
|
-
onClick: close,
|
|
1580
|
-
children: "×"
|
|
1581
|
-
})] }),
|
|
1582
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1583
|
-
className: "codexSketchKeysEnabled",
|
|
1584
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1585
|
-
type: "checkbox",
|
|
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
|
|
1608
|
-
]
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
//#endregion
|
|
1612
|
-
//#region src/sketch-objects.js
|
|
1613
|
-
const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
|
|
1614
|
-
const identifyObjects = (doc) => ({
|
|
1615
|
-
...doc,
|
|
1616
|
-
layers: doc.layers.map((layer) => ({
|
|
1617
|
-
...layer,
|
|
1618
|
-
strokes: layer.strokes.map((s, i) => s.id ? s : {
|
|
1619
|
-
...s,
|
|
1620
|
-
id: objectId(s, i)
|
|
1621
|
-
})
|
|
1622
|
-
}))
|
|
1623
|
-
});
|
|
1624
|
-
function objectBounds(stroke) {
|
|
1625
|
-
const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
|
|
1626
|
-
return {
|
|
1627
|
-
x: Math.min(...xs),
|
|
1628
|
-
y: Math.min(...ys),
|
|
1629
|
-
width: Math.max(...xs) - Math.min(...xs),
|
|
1630
|
-
height: Math.max(...ys) - Math.min(...ys)
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
|
|
1634
|
-
if (![
|
|
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
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
function sketchObjectSummary(doc) {
|
|
1652
|
-
return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
|
|
1653
|
-
layer: layer.id,
|
|
1654
|
-
id: objectId(stroke, i),
|
|
1655
|
-
shape: stroke.shape,
|
|
1656
|
-
color: stroke.color,
|
|
1657
|
-
bounds: objectBounds(stroke),
|
|
1658
|
-
...stroke.text ? { text: stroke.text } : {}
|
|
1659
|
-
})));
|
|
1660
|
-
}
|
|
1661
1588
|
const SKETCH_COMMAND_HELP = {
|
|
1662
1589
|
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.",
|
|
1663
1590
|
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.",
|
|
@@ -1784,331 +1711,992 @@ window.__ModuleLoader__.load({
|
|
|
1784
1711
|
s?.end
|
|
1785
1712
|
])];
|
|
1786
1713
|
}
|
|
1787
|
-
if (![
|
|
1788
|
-
"pen",
|
|
1789
|
-
"line",
|
|
1790
|
-
"rectangle",
|
|
1791
|
-
"circle",
|
|
1792
|
-
"polygon",
|
|
1793
|
-
"bezier",
|
|
1794
|
-
"arrow",
|
|
1795
|
-
"text",
|
|
1796
|
-
"eraser"
|
|
1797
|
-
].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");
|
|
1798
|
-
if ([
|
|
1799
|
-
"line",
|
|
1800
|
-
"arrow",
|
|
1801
|
-
"text",
|
|
1802
|
-
"rectangle",
|
|
1803
|
-
"circle"
|
|
1804
|
-
].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
|
|
1805
|
-
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.`);
|
|
1806
|
-
if (fill && ![
|
|
1807
|
-
"rectangle",
|
|
1808
|
-
"circle",
|
|
1809
|
-
"polygon",
|
|
1810
|
-
"bezier"
|
|
1811
|
-
].includes(shape)) throw Error("Fill requires a closed shape");
|
|
1812
|
-
const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
|
|
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
|
-
}))
|
|
1830
|
-
};
|
|
1831
|
-
doc = {
|
|
1832
|
-
...doc,
|
|
1833
|
-
layers: doc.layers.map((item) => item === layer ? {
|
|
1834
|
-
...item,
|
|
1835
|
-
strokes: [...item.strokes, stroke]
|
|
1836
|
-
} : item)
|
|
1714
|
+
if (![
|
|
1715
|
+
"pen",
|
|
1716
|
+
"line",
|
|
1717
|
+
"rectangle",
|
|
1718
|
+
"circle",
|
|
1719
|
+
"polygon",
|
|
1720
|
+
"bezier",
|
|
1721
|
+
"arrow",
|
|
1722
|
+
"text",
|
|
1723
|
+
"eraser"
|
|
1724
|
+
].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");
|
|
1725
|
+
if ([
|
|
1726
|
+
"line",
|
|
1727
|
+
"arrow",
|
|
1728
|
+
"text",
|
|
1729
|
+
"rectangle",
|
|
1730
|
+
"circle"
|
|
1731
|
+
].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
|
|
1732
|
+
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.`);
|
|
1733
|
+
if (fill && ![
|
|
1734
|
+
"rectangle",
|
|
1735
|
+
"circle",
|
|
1736
|
+
"polygon",
|
|
1737
|
+
"bezier"
|
|
1738
|
+
].includes(shape)) throw Error("Fill requires a closed shape");
|
|
1739
|
+
const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
|
|
1740
|
+
if (!layer?.visible) throw Error("Target layer is missing or hidden");
|
|
1741
|
+
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");
|
|
1742
|
+
const id = command.id ?? crypto.randomUUID();
|
|
1743
|
+
if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
|
|
1744
|
+
const stroke = {
|
|
1745
|
+
id,
|
|
1746
|
+
...shape === "text" ? { text: command.text } : {},
|
|
1747
|
+
shape,
|
|
1748
|
+
color,
|
|
1749
|
+
width,
|
|
1750
|
+
opacity,
|
|
1751
|
+
fill,
|
|
1752
|
+
brush: "pen",
|
|
1753
|
+
points: points.map((p) => ({
|
|
1754
|
+
x: p.x,
|
|
1755
|
+
y: p.y
|
|
1756
|
+
}))
|
|
1757
|
+
};
|
|
1758
|
+
doc = {
|
|
1759
|
+
...doc,
|
|
1760
|
+
layers: doc.layers.map((item) => item === layer ? {
|
|
1761
|
+
...item,
|
|
1762
|
+
strokes: [...item.strokes, stroke]
|
|
1763
|
+
} : item)
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
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");
|
|
1767
|
+
return doc;
|
|
1768
|
+
}
|
|
1769
|
+
function createSketchCommandSession(adapter) {
|
|
1770
|
+
const completed = /* @__PURE__ */ new Map();
|
|
1771
|
+
let pending = false, cachedCharacters = 0;
|
|
1772
|
+
return async (request) => {
|
|
1773
|
+
if (!request || typeof request !== "object") throw Error("Invalid sketch request");
|
|
1774
|
+
if (!adapter.available()) throw Error("Open the sketch board for this session first");
|
|
1775
|
+
const current = adapter.snapshot();
|
|
1776
|
+
if (request.action === "inspect") {
|
|
1777
|
+
const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
|
|
1778
|
+
if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
|
|
1779
|
+
return {
|
|
1780
|
+
...current,
|
|
1781
|
+
protocolVersion: 2,
|
|
1782
|
+
objects: objects.slice(offset, offset + 50),
|
|
1783
|
+
objectCount: objects.length,
|
|
1784
|
+
...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
|
|
1785
|
+
...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
|
|
1786
|
+
recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
|
|
1787
|
+
help: SKETCH_COMMAND_HELP
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
|
|
1791
|
+
if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
|
|
1792
|
+
if (request.action === "preview") return {
|
|
1793
|
+
...current,
|
|
1794
|
+
png: await adapter.preview()
|
|
1795
|
+
};
|
|
1796
|
+
if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
|
|
1797
|
+
if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
|
|
1798
|
+
const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
|
|
1799
|
+
...request,
|
|
1800
|
+
runId: void 0
|
|
1801
|
+
});
|
|
1802
|
+
const cached = completed.get(key);
|
|
1803
|
+
if (cached) {
|
|
1804
|
+
if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
|
|
1805
|
+
return cached.result;
|
|
1806
|
+
}
|
|
1807
|
+
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
1808
|
+
let changedObjects;
|
|
1809
|
+
if (request.action === "apply") {
|
|
1810
|
+
const before = adapter.document();
|
|
1811
|
+
let next;
|
|
1812
|
+
try {
|
|
1813
|
+
next = applySketchCommands(before, request.commands);
|
|
1814
|
+
} catch (cause) {
|
|
1815
|
+
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
1816
|
+
error.code = "SKETCH_INVALID_BATCH";
|
|
1817
|
+
throw error;
|
|
1818
|
+
}
|
|
1819
|
+
adapter.commit(next);
|
|
1820
|
+
const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
|
|
1821
|
+
changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
|
|
1822
|
+
layer: l.id,
|
|
1823
|
+
id: s.id
|
|
1824
|
+
})));
|
|
1825
|
+
} else {
|
|
1826
|
+
if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
|
|
1827
|
+
pending = true;
|
|
1828
|
+
try {
|
|
1829
|
+
await adapter.save(request.name);
|
|
1830
|
+
} finally {
|
|
1831
|
+
pending = false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
const result = {
|
|
1835
|
+
...adapter.snapshot(),
|
|
1836
|
+
...changedObjects ? {
|
|
1837
|
+
changedObjects: changedObjects.slice(0, 100),
|
|
1838
|
+
changedObjectCount: changedObjects.length
|
|
1839
|
+
} : {}
|
|
1840
|
+
};
|
|
1841
|
+
completed.set(key, {
|
|
1842
|
+
fingerprint,
|
|
1843
|
+
result,
|
|
1844
|
+
receipt: {
|
|
1845
|
+
requestId: request.requestId,
|
|
1846
|
+
action: request.action,
|
|
1847
|
+
revision: result.revision
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
cachedCharacters += fingerprint.length;
|
|
1851
|
+
while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
|
|
1852
|
+
const oldest = completed.keys().next().value;
|
|
1853
|
+
cachedCharacters -= completed.get(oldest).fingerprint.length;
|
|
1854
|
+
completed.delete(oldest);
|
|
1855
|
+
}
|
|
1856
|
+
return result;
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
//#endregion
|
|
1860
|
+
//#region src/sketch-formats.js
|
|
1861
|
+
const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
|
|
1862
|
+
const canvas = (w, h) => {
|
|
1863
|
+
const c = document.createElement("canvas");
|
|
1864
|
+
c.width = w;
|
|
1865
|
+
c.height = h;
|
|
1866
|
+
return c;
|
|
1867
|
+
};
|
|
1868
|
+
function runPsdCodec(action, payload) {
|
|
1869
|
+
return new Promise((resolve, reject) => {
|
|
1870
|
+
const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
|
|
1871
|
+
const finish = (callback, value) => {
|
|
1872
|
+
clearTimeout(timer);
|
|
1873
|
+
worker.terminate();
|
|
1874
|
+
callback(value);
|
|
1875
|
+
};
|
|
1876
|
+
const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
|
|
1877
|
+
worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
|
|
1878
|
+
worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
|
|
1879
|
+
worker.postMessage({
|
|
1880
|
+
action,
|
|
1881
|
+
payload
|
|
1882
|
+
});
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
function encodeSketchDocument(doc) {
|
|
1886
|
+
return JSON.stringify({
|
|
1887
|
+
format: "dsh-sketch",
|
|
1888
|
+
version: 1,
|
|
1889
|
+
doc
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
function decodeSketchDocument(text) {
|
|
1893
|
+
if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
|
|
1894
|
+
const file = JSON.parse(text), source = file.doc;
|
|
1895
|
+
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");
|
|
1896
|
+
const w = source.width ?? 1024, h = source.height ?? 1024;
|
|
1897
|
+
if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
|
|
1898
|
+
let doc = {
|
|
1899
|
+
...createSketchLayers(),
|
|
1900
|
+
width: w,
|
|
1901
|
+
height: h,
|
|
1902
|
+
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
|
|
1903
|
+
};
|
|
1904
|
+
for (let i = 0; i < source.layers.length; i++) {
|
|
1905
|
+
const layer = source.layers[i];
|
|
1906
|
+
if (i) doc = applySketchCommands(doc, [{
|
|
1907
|
+
op: "layer",
|
|
1908
|
+
action: "add"
|
|
1909
|
+
}]);
|
|
1910
|
+
if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
|
|
1911
|
+
for (let j = 0; j < layer.strokes.length; j += 256) {
|
|
1912
|
+
const strokes = layer.strokes.slice(j, j + 256);
|
|
1913
|
+
doc = applySketchCommands(doc, strokes.map((s) => ({
|
|
1914
|
+
...s,
|
|
1915
|
+
op: "stroke",
|
|
1916
|
+
layer: doc.active,
|
|
1917
|
+
fill: s.fill ?? false
|
|
1918
|
+
})));
|
|
1919
|
+
const added = doc.layers.at(-1).strokes;
|
|
1920
|
+
for (let k = 0; k < strokes.length; k++) {
|
|
1921
|
+
const s = strokes[k];
|
|
1922
|
+
if (s.brush !== void 0 && ![
|
|
1923
|
+
"pen",
|
|
1924
|
+
"pencil",
|
|
1925
|
+
"marker"
|
|
1926
|
+
].includes(s.brush)) throw Error("Invalid brush");
|
|
1927
|
+
if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
|
|
1928
|
+
if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
|
|
1929
|
+
Object.assign(added[added.length - strokes.length + k], {
|
|
1930
|
+
brush: s.brush ?? "pen",
|
|
1931
|
+
pressure: s.pressure ?? 1,
|
|
1932
|
+
...s.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
const target = doc.layers.at(-1);
|
|
1937
|
+
target.name = String(layer.name ?? "").slice(0, 40);
|
|
1938
|
+
target.visible = layer.visible !== false;
|
|
1939
|
+
if (layer.image) {
|
|
1940
|
+
const image = layer.image;
|
|
1941
|
+
if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
|
|
1942
|
+
"x",
|
|
1943
|
+
"y",
|
|
1944
|
+
"width",
|
|
1945
|
+
"height"
|
|
1946
|
+
].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
|
|
1947
|
+
const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
|
|
1948
|
+
if (bytes.length < 24) throw Error("Invalid draft image");
|
|
1949
|
+
const header = new DataView(bytes.buffer);
|
|
1950
|
+
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");
|
|
1951
|
+
target.image = {
|
|
1952
|
+
src: image.src,
|
|
1953
|
+
x: image.x,
|
|
1954
|
+
y: image.y,
|
|
1955
|
+
width: image.width,
|
|
1956
|
+
height: image.height
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
|
|
1961
|
+
doc.active = doc.layers[Math.max(0, activeIndex)].id;
|
|
1962
|
+
return doc;
|
|
1963
|
+
}
|
|
1964
|
+
async function exportSketchPsd(doc, images, composite) {
|
|
1965
|
+
const width = doc.width ?? 1024, height = doc.height ?? 1024;
|
|
1966
|
+
const children = doc.layers.map((layer, i) => {
|
|
1967
|
+
const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
|
|
1968
|
+
if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
|
|
1969
|
+
paintSketch(ctx, layer.strokes, width, true, height);
|
|
1970
|
+
return {
|
|
1971
|
+
name: layer.name || `Layer ${i + 1}`,
|
|
1972
|
+
hidden: !layer.visible,
|
|
1973
|
+
opacity: 1,
|
|
1974
|
+
blendMode: "normal",
|
|
1975
|
+
imageData: ctx.getImageData(0, 0, width, height)
|
|
1837
1976
|
};
|
|
1977
|
+
});
|
|
1978
|
+
const context = canvas(width, height).getContext("2d");
|
|
1979
|
+
context.fillStyle = "#fff";
|
|
1980
|
+
context.fillRect(0, 0, width, height);
|
|
1981
|
+
if (children[0] && !children[0].hidden) {
|
|
1982
|
+
const bottom = canvas(width, height);
|
|
1983
|
+
bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
|
|
1984
|
+
context.drawImage(bottom, 0, 0);
|
|
1985
|
+
children[0].imageData = context.getImageData(0, 0, width, height);
|
|
1986
|
+
} else {
|
|
1987
|
+
if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
|
|
1988
|
+
children.unshift({
|
|
1989
|
+
name: "Paper",
|
|
1990
|
+
opacity: 1,
|
|
1991
|
+
blendMode: "normal",
|
|
1992
|
+
imageData: context.getImageData(0, 0, width, height)
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
return runPsdCodec("write", {
|
|
1996
|
+
width,
|
|
1997
|
+
height,
|
|
1998
|
+
children,
|
|
1999
|
+
imageData: composite.getContext("2d").getImageData(0, 0, width, height)
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
async function importSketchPsd(file) {
|
|
2003
|
+
if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
|
|
2004
|
+
const psd = await runPsdCodec("read", await file.arrayBuffer());
|
|
2005
|
+
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));
|
|
2006
|
+
const doc = {
|
|
2007
|
+
...createSketchLayers(),
|
|
2008
|
+
width,
|
|
2009
|
+
height,
|
|
2010
|
+
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
|
|
2011
|
+
layers: [],
|
|
2012
|
+
nextId: psd.layers.length + 1
|
|
2013
|
+
};
|
|
2014
|
+
for (const [i, layer] of psd.layers.entries()) {
|
|
2015
|
+
const src = canvas(layer.imageData.width, layer.imageData.height);
|
|
2016
|
+
src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
|
|
2017
|
+
const out = canvas(width, height), ctx = out.getContext("2d");
|
|
2018
|
+
ctx.globalAlpha = layer.opacity;
|
|
2019
|
+
ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
|
|
2020
|
+
doc.layers.push({
|
|
2021
|
+
id: i + 1,
|
|
2022
|
+
name: layer.name,
|
|
2023
|
+
visible: !layer.hidden,
|
|
2024
|
+
strokes: [],
|
|
2025
|
+
image: {
|
|
2026
|
+
src: out.toDataURL("image/png"),
|
|
2027
|
+
x: 0,
|
|
2028
|
+
y: 0,
|
|
2029
|
+
width: 1,
|
|
2030
|
+
height: 1
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
1838
2033
|
}
|
|
1839
|
-
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");
|
|
1840
2034
|
return doc;
|
|
1841
2035
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
const current =
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2036
|
+
//#endregion
|
|
2037
|
+
//#region src/sketch-document-lifecycle.js
|
|
2038
|
+
function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
|
|
2039
|
+
const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
|
|
2040
|
+
const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
|
|
2041
|
+
const save = async (name) => {
|
|
2042
|
+
const savingDocument = documentId.current, savingRevision = documentRevision.current;
|
|
2043
|
+
const row = {
|
|
2044
|
+
id: saved.current?.id ?? crypto.randomUUID(),
|
|
2045
|
+
name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
2046
|
+
updated: Date.now(),
|
|
2047
|
+
doc: structuredClone(doc.current)
|
|
2048
|
+
};
|
|
2049
|
+
try {
|
|
2050
|
+
await store("save", row, sessionId);
|
|
2051
|
+
} catch (error) {
|
|
2052
|
+
if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
|
|
2053
|
+
if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
|
|
2054
|
+
throw error;
|
|
2055
|
+
}
|
|
2056
|
+
if (documentId.current === savingDocument) {
|
|
2057
|
+
saved.current = {
|
|
2058
|
+
id: row.id,
|
|
2059
|
+
name: row.name
|
|
1861
2060
|
};
|
|
2061
|
+
if (documentRevision.current === savingRevision) {
|
|
2062
|
+
dirty.current = false;
|
|
2063
|
+
setRecovered(false);
|
|
2064
|
+
}
|
|
1862
2065
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
if (
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2066
|
+
};
|
|
2067
|
+
const saveChanges = async () => {
|
|
2068
|
+
if (!dirty.current) return;
|
|
2069
|
+
if (hasContent() || saved.current) return save();
|
|
2070
|
+
const id = documentId.current, revision = documentRevision.current;
|
|
2071
|
+
await store("clearRecovery", sessionId);
|
|
2072
|
+
if (documentId.current === id && documentRevision.current === revision) {
|
|
2073
|
+
dirty.current = false;
|
|
2074
|
+
setRecovered(false);
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
const replace = (next, decoded, identity) => {
|
|
2078
|
+
documentId.current = crypto.randomUUID();
|
|
2079
|
+
documentRevision.current++;
|
|
2080
|
+
doc.current = identifyObjects(structuredClone(next));
|
|
2081
|
+
setSelection(null);
|
|
2082
|
+
setTextEdit(null);
|
|
2083
|
+
images.current = decoded;
|
|
2084
|
+
cache.current.clear();
|
|
2085
|
+
undo.current = [];
|
|
2086
|
+
redo.current = [];
|
|
2087
|
+
saved.current = identity;
|
|
2088
|
+
dirty.current = false;
|
|
2089
|
+
schedule();
|
|
2090
|
+
};
|
|
2091
|
+
const fresh = async () => {
|
|
2092
|
+
await saveChanges();
|
|
2093
|
+
replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
|
|
2094
|
+
};
|
|
2095
|
+
const load = async (row) => {
|
|
2096
|
+
if (row.id === saved.current?.id) return;
|
|
2097
|
+
row = await store("get", row.id);
|
|
2098
|
+
if (!row) throw Error("Draft no longer exists");
|
|
2099
|
+
await saveChanges();
|
|
2100
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2101
|
+
await decodeImages(row.doc, decoded);
|
|
2102
|
+
replace(row.doc, decoded, {
|
|
2103
|
+
id: row.id,
|
|
2104
|
+
name: row.name
|
|
1874
2105
|
});
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2106
|
+
};
|
|
2107
|
+
const importImage = async (file) => {
|
|
2108
|
+
if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
|
|
2109
|
+
if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
|
|
2110
|
+
const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
|
|
2111
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2112
|
+
await decodeImages(next, decoded);
|
|
2113
|
+
await saveChanges();
|
|
2114
|
+
replace(next, decoded, null);
|
|
2115
|
+
dirty.current = true;
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (doc.current.layers.length >= 8) throw Error("Layer limit");
|
|
2119
|
+
const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
|
|
2120
|
+
const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
|
|
2121
|
+
const layer = {
|
|
2122
|
+
id: doc.current.nextId,
|
|
2123
|
+
name: file.name?.slice(0, 40) || t("sketchImport"),
|
|
2124
|
+
visible: true,
|
|
2125
|
+
strokes: [],
|
|
2126
|
+
image: {
|
|
2127
|
+
src: image.src,
|
|
2128
|
+
x: (1 - width) / 2,
|
|
2129
|
+
y: (1 - height) / 2,
|
|
2130
|
+
width,
|
|
2131
|
+
height
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
await decodeImages({ layers: [layer] }, images.current);
|
|
2135
|
+
checkpoint();
|
|
2136
|
+
doc.current = {
|
|
2137
|
+
...doc.current,
|
|
2138
|
+
nextId: layer.id + 1,
|
|
2139
|
+
active: layer.id,
|
|
2140
|
+
layers: [...doc.current.layers, layer]
|
|
2141
|
+
};
|
|
2142
|
+
schedule();
|
|
2143
|
+
};
|
|
2144
|
+
const restore = async (isCurrent = () => true) => {
|
|
2145
|
+
if (dirty.current || hasContent()) return;
|
|
2146
|
+
const id = documentId.current, revision = documentRevision.current;
|
|
2147
|
+
const recovery = await store("recover", sessionId);
|
|
2148
|
+
const archived = state.restoreId.current;
|
|
2149
|
+
const row = recovery ?? (archived ? await store("get", archived) : null);
|
|
2150
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2151
|
+
if (row) await decodeImages(row.doc, decoded);
|
|
2152
|
+
if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
|
|
2153
|
+
if (row) {
|
|
2154
|
+
replace(row.doc, decoded, recovery ? null : {
|
|
2155
|
+
id: row.id,
|
|
2156
|
+
name: row.name
|
|
2157
|
+
});
|
|
2158
|
+
dirty.current = Boolean(recovery);
|
|
2159
|
+
setRecovered(Boolean(recovery));
|
|
1879
2160
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2161
|
+
state.restoreId.current = null;
|
|
2162
|
+
};
|
|
2163
|
+
return {
|
|
2164
|
+
hasContent,
|
|
2165
|
+
save,
|
|
2166
|
+
saveChanges,
|
|
2167
|
+
replace,
|
|
2168
|
+
fresh,
|
|
2169
|
+
load,
|
|
2170
|
+
importImage,
|
|
2171
|
+
restore
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
//#endregion
|
|
2175
|
+
//#region src/sketch-operation-gate.js
|
|
2176
|
+
function createSketchOperationGate() {
|
|
2177
|
+
let running = false;
|
|
2178
|
+
return {
|
|
2179
|
+
get running() {
|
|
2180
|
+
return running;
|
|
2181
|
+
},
|
|
2182
|
+
async run(operation, { blocked = false, working, report, rethrow = false }) {
|
|
2183
|
+
if (blocked || running) return false;
|
|
2184
|
+
running = true;
|
|
1901
2185
|
try {
|
|
1902
|
-
|
|
2186
|
+
working(true);
|
|
2187
|
+
report(null);
|
|
2188
|
+
await operation();
|
|
2189
|
+
return true;
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
report(error);
|
|
2192
|
+
if (rethrow) throw error;
|
|
2193
|
+
return false;
|
|
1903
2194
|
} finally {
|
|
1904
|
-
|
|
2195
|
+
running = false;
|
|
2196
|
+
working(false);
|
|
1905
2197
|
}
|
|
1906
2198
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
//#endregion
|
|
2202
|
+
//#region src/sketch-agent-export.js
|
|
2203
|
+
async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
|
|
2204
|
+
let result;
|
|
2205
|
+
if (!await gate.run(async () => {
|
|
2206
|
+
const { blob, extension } = await exportFile(format);
|
|
2207
|
+
const data = new Uint8Array(await blob.arrayBuffer());
|
|
2208
|
+
let raw = "";
|
|
2209
|
+
for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
|
|
2210
|
+
result = {
|
|
2211
|
+
extension,
|
|
2212
|
+
mediaType: blob.type,
|
|
2213
|
+
base64: btoa(raw)
|
|
1913
2214
|
};
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2215
|
+
}, {
|
|
2216
|
+
blocked,
|
|
2217
|
+
working,
|
|
2218
|
+
report,
|
|
2219
|
+
rethrow: true
|
|
2220
|
+
})) throw Error("Sketch is being edited; retry after it settles");
|
|
2221
|
+
return result;
|
|
2222
|
+
}
|
|
2223
|
+
//#endregion
|
|
2224
|
+
//#region src/sketch-run-status.jsx
|
|
2225
|
+
function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
|
|
2226
|
+
if (state === "idle") return null;
|
|
2227
|
+
const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
|
|
2228
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2229
|
+
className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
|
|
2230
|
+
role: "status",
|
|
2231
|
+
"aria-live": "polite",
|
|
2232
|
+
children: [
|
|
2233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2234
|
+
name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
|
|
2235
|
+
size: 15
|
|
2236
|
+
}),
|
|
2237
|
+
floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2238
|
+
type: "button",
|
|
2239
|
+
onClick: onOpen,
|
|
2240
|
+
children: t(`sketchRun_${state}`)
|
|
2241
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
|
|
2242
|
+
drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2243
|
+
type: "button",
|
|
2244
|
+
className: "codexSketchStop",
|
|
2245
|
+
onClick: onStop,
|
|
2246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2247
|
+
name: "stop",
|
|
2248
|
+
size: 12
|
|
2249
|
+
}), t("sketchRunStop")]
|
|
2250
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2251
|
+
type: "button",
|
|
2252
|
+
title: t("sketchRunResumeHint"),
|
|
2253
|
+
onClick: onResume,
|
|
2254
|
+
children: t("sketchRunResume")
|
|
2255
|
+
}) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2256
|
+
type: "button",
|
|
2257
|
+
"aria-label": t("sketchDismissStatus"),
|
|
2258
|
+
title: t("sketchDismissStatus"),
|
|
2259
|
+
onClick: onDismiss,
|
|
2260
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2261
|
+
name: "close",
|
|
2262
|
+
size: 14
|
|
2263
|
+
})
|
|
2264
|
+
}) : null] })
|
|
2265
|
+
]
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
//#endregion
|
|
2269
|
+
//#region src/sketch-tool-widths.js
|
|
2270
|
+
const defaults = {
|
|
2271
|
+
pen: 12,
|
|
2272
|
+
pencil: 6,
|
|
2273
|
+
marker: 28,
|
|
2274
|
+
eraser: 24,
|
|
2275
|
+
text: 32,
|
|
2276
|
+
line: 12,
|
|
2277
|
+
arrow: 12,
|
|
2278
|
+
rectangle: 12,
|
|
2279
|
+
circle: 12
|
|
2280
|
+
};
|
|
2281
|
+
const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
|
|
2282
|
+
function switchSketchToolWidth(memory, current, next) {
|
|
2283
|
+
if (current.tool !== "select") memory[keyFor(current)] = current.width;
|
|
2284
|
+
if (next.tool === "select") return current.width;
|
|
2285
|
+
const key = keyFor(next);
|
|
2286
|
+
return memory[key] ?? defaults[key] ?? 12;
|
|
2287
|
+
}
|
|
2288
|
+
function stepSketchWidth(width, direction) {
|
|
2289
|
+
return Math.max(1, Math.min(256, width + direction * 2));
|
|
2290
|
+
}
|
|
2291
|
+
//#endregion
|
|
2292
|
+
//#region src/sketch-shortcuts.js
|
|
2293
|
+
function sketchShortcutAction(keys, key) {
|
|
2294
|
+
key = key.toLowerCase();
|
|
2295
|
+
return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
|
|
2296
|
+
v: "select",
|
|
2297
|
+
t: "text",
|
|
2298
|
+
"+": "zoomIn"
|
|
2299
|
+
}[key];
|
|
2300
|
+
}
|
|
2301
|
+
//#endregion
|
|
2302
|
+
//#region src/sketch-layer-renderer.js
|
|
2303
|
+
const NO_IMAGES = /* @__PURE__ */ new Map();
|
|
2304
|
+
const surface = (width, height) => {
|
|
2305
|
+
const c = document.createElement("canvas");
|
|
2306
|
+
c.width = width;
|
|
2307
|
+
c.height = height;
|
|
2308
|
+
return c;
|
|
2309
|
+
};
|
|
2310
|
+
function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
|
|
2311
|
+
context.globalCompositeOperation = "source-over";
|
|
2312
|
+
context.globalAlpha = 1;
|
|
2313
|
+
context.fillStyle = "#fff";
|
|
2314
|
+
context.fillRect(0, 0, size, height);
|
|
2315
|
+
for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
|
|
2316
|
+
for (const layer of doc.layers) {
|
|
2317
|
+
if (!layer.visible) continue;
|
|
2318
|
+
let entry = cache.get(layer.id);
|
|
2319
|
+
if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
|
|
2320
|
+
entry = {
|
|
2321
|
+
surface: surface(size, height),
|
|
2322
|
+
base: surface(size, height)
|
|
2323
|
+
};
|
|
2324
|
+
cache.set(layer.id, entry);
|
|
2325
|
+
}
|
|
2326
|
+
const moving = layer.id === activeLayer;
|
|
2327
|
+
const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
|
|
2328
|
+
const prefix = layer.strokes[count - 1];
|
|
2329
|
+
if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
|
|
2330
|
+
const ctx = entry.base.getContext("2d");
|
|
2331
|
+
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]));
|
|
2332
|
+
if (!append) {
|
|
2333
|
+
ctx.clearRect(0, 0, size, height);
|
|
2334
|
+
const ref = layer.image, image = ref && images.get(ref.src);
|
|
2335
|
+
if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
|
|
1921
2336
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2337
|
+
paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
|
|
2338
|
+
entry.count = count;
|
|
2339
|
+
entry.prefix = prefix;
|
|
2340
|
+
entry.image = layer.image;
|
|
2341
|
+
entry.strokes = layer.strokes;
|
|
2342
|
+
}
|
|
2343
|
+
if (moving) {
|
|
2344
|
+
const ctx = entry.surface.getContext("2d");
|
|
2345
|
+
ctx.clearRect(0, 0, size, height);
|
|
2346
|
+
ctx.drawImage(entry.base, 0, 0);
|
|
2347
|
+
paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
|
|
2348
|
+
context.drawImage(entry.surface, 0, 0);
|
|
2349
|
+
} else context.drawImage(entry.base, 0, 0);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
//#endregion
|
|
2353
|
+
//#region src/sketch-interactions.js
|
|
2354
|
+
function useSketchDismiss(open, close, host, selectors) {
|
|
2355
|
+
const latest = (0, react.useRef)(close);
|
|
2356
|
+
latest.current = close;
|
|
2357
|
+
(0, react.useEffect)(() => {
|
|
2358
|
+
if (!open) return;
|
|
2359
|
+
const dialog = host.current?.closest("dialog") ?? host.current;
|
|
2360
|
+
if (!dialog) return;
|
|
2361
|
+
const pointer = (event) => {
|
|
2362
|
+
if (selectors.some((selector) => event.target.closest?.(selector))) return;
|
|
2363
|
+
latest.current(false);
|
|
2364
|
+
if (event.target.matches?.("canvas")) {
|
|
2365
|
+
event.preventDefault();
|
|
2366
|
+
event.stopPropagation();
|
|
2367
|
+
event.target.focus({ preventScroll: true });
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
const key = (event) => {
|
|
2371
|
+
if (event.key !== "Escape") return;
|
|
2372
|
+
event.preventDefault();
|
|
2373
|
+
event.stopPropagation();
|
|
2374
|
+
latest.current(false);
|
|
2375
|
+
dialog.querySelector("canvas")?.focus({ preventScroll: true });
|
|
2376
|
+
};
|
|
2377
|
+
const hidden = () => latest.current(false);
|
|
2378
|
+
document.addEventListener("pointerdown", pointer, true);
|
|
2379
|
+
document.addEventListener("keydown", key, true);
|
|
2380
|
+
dialog.addEventListener("close", hidden);
|
|
2381
|
+
return () => {
|
|
2382
|
+
document.removeEventListener("pointerdown", pointer, true);
|
|
2383
|
+
document.removeEventListener("keydown", key, true);
|
|
2384
|
+
dialog.removeEventListener("close", hidden);
|
|
2385
|
+
};
|
|
2386
|
+
}, [
|
|
2387
|
+
open,
|
|
2388
|
+
host,
|
|
2389
|
+
selectors.join("|")
|
|
2390
|
+
]);
|
|
2391
|
+
}
|
|
2392
|
+
function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
|
|
2393
|
+
const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
|
|
2394
|
+
const update = (event, bounds) => {
|
|
2395
|
+
if (event) last.current = event;
|
|
2396
|
+
const pointer = last.current, node = canvas.current, cursor = ring.current;
|
|
2397
|
+
if (!pointer || !node || !cursor) return;
|
|
2398
|
+
const rect = bounds ?? node.getBoundingClientRect();
|
|
2399
|
+
if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
|
|
2400
|
+
cursor.hidden = true;
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
|
|
2404
|
+
cursor.hidden = false;
|
|
2405
|
+
cursor.style.width = `${diameter}px`;
|
|
2406
|
+
cursor.style.height = `${diameter}px`;
|
|
2407
|
+
cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
|
|
2408
|
+
};
|
|
2409
|
+
(0, react.useEffect)(() => {
|
|
2410
|
+
update();
|
|
2411
|
+
const observer = new ResizeObserver(() => update());
|
|
2412
|
+
if (canvas.current) observer.observe(canvas.current);
|
|
2413
|
+
return () => observer.disconnect();
|
|
2414
|
+
}, [
|
|
2415
|
+
width,
|
|
2416
|
+
brush,
|
|
2417
|
+
zoom,
|
|
2418
|
+
hidden
|
|
2419
|
+
]);
|
|
2420
|
+
return {
|
|
2421
|
+
down: (event) => {
|
|
2422
|
+
heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
|
|
2423
|
+
},
|
|
2424
|
+
move: (event, bounds) => update({
|
|
2425
|
+
clientX: event.clientX,
|
|
2426
|
+
clientY: event.clientY,
|
|
2427
|
+
pointerId: event.pointerId,
|
|
2428
|
+
pointerType: event.pointerType,
|
|
2429
|
+
pressure: event.pressure
|
|
2430
|
+
}, bounds),
|
|
2431
|
+
leave: () => {
|
|
2432
|
+
last.current = null;
|
|
2433
|
+
if (ring.current) ring.current.hidden = true;
|
|
1928
2434
|
}
|
|
1929
|
-
return result;
|
|
1930
2435
|
};
|
|
1931
2436
|
}
|
|
1932
2437
|
//#endregion
|
|
1933
|
-
//#region src/sketch-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
2438
|
+
//#region src/sketch-view.jsx
|
|
2439
|
+
const DEFAULT_KEYS = {
|
|
2440
|
+
pen: "b",
|
|
2441
|
+
eraser: "e",
|
|
2442
|
+
line: "l",
|
|
2443
|
+
rectangle: "r",
|
|
2444
|
+
circle: "o",
|
|
2445
|
+
pan: " ",
|
|
2446
|
+
zoomIn: "=",
|
|
2447
|
+
zoomOut: "-",
|
|
2448
|
+
fit: "0"
|
|
1940
2449
|
};
|
|
1941
|
-
function
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
});
|
|
2450
|
+
function useSketchView(canvas, open) {
|
|
2451
|
+
const [view, setView] = (0, react.useState)({
|
|
2452
|
+
scale: 1,
|
|
2453
|
+
x: 0,
|
|
2454
|
+
y: 0
|
|
2455
|
+
}), [keys, setKeys] = (0, react.useState)(() => {
|
|
2456
|
+
try {
|
|
2457
|
+
return {
|
|
2458
|
+
...DEFAULT_KEYS,
|
|
2459
|
+
...JSON.parse(localStorage.getItem("codex-sketch-keys"))
|
|
2460
|
+
};
|
|
2461
|
+
} catch {
|
|
2462
|
+
return DEFAULT_KEYS;
|
|
2463
|
+
}
|
|
1956
2464
|
});
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2465
|
+
const [shortcuts, setShortcuts] = (0, react.useState)(() => {
|
|
2466
|
+
try {
|
|
2467
|
+
return localStorage.getItem("codex-sketch-shortcuts") !== "off";
|
|
2468
|
+
} catch {
|
|
2469
|
+
return true;
|
|
2470
|
+
}
|
|
2471
|
+
}), [space, setSpace] = (0, react.useState)(false);
|
|
2472
|
+
const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
|
|
2473
|
+
viewRef.current = view;
|
|
2474
|
+
const zoom = (factor) => setView((v) => ({
|
|
2475
|
+
...v,
|
|
2476
|
+
scale: Math.max(.25, Math.min(8, v.scale * factor))
|
|
2477
|
+
}));
|
|
2478
|
+
const reset = () => setView({
|
|
2479
|
+
scale: 1,
|
|
2480
|
+
x: 0,
|
|
2481
|
+
y: 0
|
|
1963
2482
|
});
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2483
|
+
(0, react.useEffect)(() => {
|
|
2484
|
+
const node = canvas.current;
|
|
2485
|
+
if (!node || !open) return;
|
|
2486
|
+
const wheel = (e) => {
|
|
2487
|
+
if (!shortcuts || !e.altKey) return;
|
|
2488
|
+
e.preventDefault();
|
|
2489
|
+
zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
|
|
2490
|
+
};
|
|
2491
|
+
node.addEventListener("wheel", wheel, { passive: false });
|
|
2492
|
+
return () => node.removeEventListener("wheel", wheel);
|
|
2493
|
+
}, [open, shortcuts]);
|
|
2494
|
+
(0, react.useEffect)(() => {
|
|
2495
|
+
const stop = () => {
|
|
2496
|
+
drag.current = null;
|
|
2497
|
+
setSpace(false);
|
|
2498
|
+
};
|
|
2499
|
+
window.addEventListener("blur", stop);
|
|
2500
|
+
return () => window.removeEventListener("blur", stop);
|
|
2501
|
+
}, []);
|
|
2502
|
+
(0, react.useEffect)(() => {
|
|
2503
|
+
if (!open || !shortcuts) {
|
|
2504
|
+
setSpace(false);
|
|
2505
|
+
drag.current = null;
|
|
2506
|
+
}
|
|
2507
|
+
}, [open, shortcuts]);
|
|
2508
|
+
const setKey = (action, key) => {
|
|
2509
|
+
key = key.toLowerCase();
|
|
2510
|
+
if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
|
|
2511
|
+
const next = {
|
|
2512
|
+
...keys,
|
|
2513
|
+
[action]: key
|
|
2514
|
+
};
|
|
2515
|
+
setKeys(next);
|
|
2516
|
+
try {
|
|
2517
|
+
localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
|
|
2518
|
+
} catch {}
|
|
1976
2519
|
};
|
|
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
|
-
});
|
|
2520
|
+
return {
|
|
2521
|
+
view,
|
|
2522
|
+
keys,
|
|
2523
|
+
shortcuts,
|
|
2524
|
+
space,
|
|
2525
|
+
zoom,
|
|
2526
|
+
reset,
|
|
2527
|
+
setKey,
|
|
2528
|
+
toggle: () => setShortcuts((v) => {
|
|
2529
|
+
try {
|
|
2530
|
+
localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
|
|
2531
|
+
} catch {}
|
|
2532
|
+
return !v;
|
|
2533
|
+
}),
|
|
2534
|
+
keyDown: (e) => {
|
|
2535
|
+
if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
2536
|
+
const action = sketchShortcutAction(keys, e.key);
|
|
2537
|
+
if (action === "pan") {
|
|
2538
|
+
e.preventDefault();
|
|
2539
|
+
setSpace(true);
|
|
2540
|
+
return true;
|
|
2007
2541
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
"
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2542
|
+
if ([
|
|
2543
|
+
"zoomIn",
|
|
2544
|
+
"zoomOut",
|
|
2545
|
+
"fit"
|
|
2546
|
+
].includes(action)) {
|
|
2547
|
+
e.preventDefault();
|
|
2548
|
+
if (action === "fit") reset();
|
|
2549
|
+
else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
|
|
2550
|
+
return true;
|
|
2551
|
+
}
|
|
2552
|
+
return false;
|
|
2553
|
+
},
|
|
2554
|
+
keyUp: (e) => {
|
|
2555
|
+
if (e.key.toLowerCase() === keys.pan) setSpace(false);
|
|
2556
|
+
},
|
|
2557
|
+
down: (e) => {
|
|
2558
|
+
if (e.button !== 1 && !space) return false;
|
|
2559
|
+
e.preventDefault();
|
|
2560
|
+
drag.current = {
|
|
2561
|
+
id: e.pointerId,
|
|
2562
|
+
x: e.clientX,
|
|
2563
|
+
y: e.clientY,
|
|
2564
|
+
view: viewRef.current
|
|
2030
2565
|
};
|
|
2566
|
+
canvas.current.setPointerCapture(e.pointerId);
|
|
2567
|
+
return true;
|
|
2568
|
+
},
|
|
2569
|
+
move: (e) => {
|
|
2570
|
+
const d = drag.current;
|
|
2571
|
+
if (!d || d.id !== e.pointerId) return false;
|
|
2572
|
+
setView({
|
|
2573
|
+
...d.view,
|
|
2574
|
+
x: d.view.x + e.clientX - d.x,
|
|
2575
|
+
y: d.view.y + e.clientY - d.y
|
|
2576
|
+
});
|
|
2577
|
+
return true;
|
|
2578
|
+
},
|
|
2579
|
+
end: (e) => {
|
|
2580
|
+
if (drag.current?.id !== e.pointerId) return false;
|
|
2581
|
+
drag.current = null;
|
|
2582
|
+
if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
|
|
2583
|
+
return true;
|
|
2031
2584
|
}
|
|
2032
|
-
}
|
|
2033
|
-
const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
|
|
2034
|
-
doc.active = doc.layers[Math.max(0, activeIndex)].id;
|
|
2035
|
-
return doc;
|
|
2585
|
+
};
|
|
2036
2586
|
}
|
|
2037
|
-
|
|
2038
|
-
const
|
|
2039
|
-
const
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2587
|
+
function SketchViewControls({ navigation, t }) {
|
|
2588
|
+
const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
|
|
2589
|
+
const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
|
|
2590
|
+
const close = () => {
|
|
2591
|
+
setOpen(false);
|
|
2592
|
+
trigger.current?.focus({ preventScroll: true });
|
|
2593
|
+
};
|
|
2594
|
+
useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
|
|
2595
|
+
(0, react.useLayoutEffect)(() => {
|
|
2596
|
+
if (!open) return;
|
|
2597
|
+
const dialog = host.current.closest("dialog");
|
|
2598
|
+
const place = () => {
|
|
2599
|
+
const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
|
|
2600
|
+
const width = Math.min(360, box.width - 24);
|
|
2601
|
+
setPlacement({
|
|
2602
|
+
dialog,
|
|
2603
|
+
style: {
|
|
2604
|
+
width,
|
|
2605
|
+
left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
|
|
2606
|
+
bottom: box.bottom - anchor.top + 8,
|
|
2607
|
+
maxHeight: Math.max(80, anchor.top - box.top - 24)
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
};
|
|
2611
|
+
place();
|
|
2612
|
+
const observer = new ResizeObserver(place);
|
|
2613
|
+
observer.observe(dialog);
|
|
2614
|
+
observer.observe(host.current);
|
|
2615
|
+
window.addEventListener("resize", place);
|
|
2616
|
+
return () => {
|
|
2617
|
+
observer.disconnect();
|
|
2618
|
+
window.removeEventListener("resize", place);
|
|
2049
2619
|
};
|
|
2620
|
+
}, [open]);
|
|
2621
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2622
|
+
ref: host,
|
|
2623
|
+
className: "codexSketchViewControls",
|
|
2624
|
+
children: [
|
|
2625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2626
|
+
type: "button",
|
|
2627
|
+
"aria-label": t("sketchZoomOut"),
|
|
2628
|
+
onClick: () => navigation.zoom(1 / 1.2),
|
|
2629
|
+
children: "−"
|
|
2630
|
+
}),
|
|
2631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2632
|
+
type: "button",
|
|
2633
|
+
title: t("sketchFit"),
|
|
2634
|
+
onClick: navigation.reset,
|
|
2635
|
+
children: [Math.round(navigation.view.scale * 100), "%"]
|
|
2636
|
+
}),
|
|
2637
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2638
|
+
type: "button",
|
|
2639
|
+
"aria-label": t("sketchZoomIn"),
|
|
2640
|
+
onClick: () => navigation.zoom(1.2),
|
|
2641
|
+
children: "+"
|
|
2642
|
+
}),
|
|
2643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2644
|
+
ref: trigger,
|
|
2645
|
+
type: "button",
|
|
2646
|
+
"aria-expanded": open,
|
|
2647
|
+
onClick: () => setOpen(!open),
|
|
2648
|
+
children: t("sketchKeys")
|
|
2649
|
+
}),
|
|
2650
|
+
open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2651
|
+
className: "codexSketchKeyPanel",
|
|
2652
|
+
"aria-label": t("sketchKeys"),
|
|
2653
|
+
style: placement.style,
|
|
2654
|
+
onKeyDown: (e) => {
|
|
2655
|
+
if (e.key === "Escape") {
|
|
2656
|
+
e.preventDefault();
|
|
2657
|
+
e.stopPropagation();
|
|
2658
|
+
close();
|
|
2659
|
+
}
|
|
2660
|
+
},
|
|
2661
|
+
children: [
|
|
2662
|
+
/* @__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", {
|
|
2663
|
+
type: "button",
|
|
2664
|
+
"aria-label": t("sketchFileClose"),
|
|
2665
|
+
onClick: close,
|
|
2666
|
+
children: "×"
|
|
2667
|
+
})] }),
|
|
2668
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2669
|
+
className: "codexSketchKeysEnabled",
|
|
2670
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2671
|
+
type: "checkbox",
|
|
2672
|
+
checked: navigation.shortcuts,
|
|
2673
|
+
onChange: navigation.toggle
|
|
2674
|
+
})]
|
|
2675
|
+
}),
|
|
2676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
|
|
2677
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2678
|
+
className: "codexSketchKeyGrid",
|
|
2679
|
+
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", {
|
|
2680
|
+
"aria-label": t(`sketchKey_${action}`),
|
|
2681
|
+
value: key === " " ? "Space" : key,
|
|
2682
|
+
readOnly: true,
|
|
2683
|
+
onKeyDown: (e) => {
|
|
2684
|
+
if (e.key === "Tab" || e.key === "Escape") return;
|
|
2685
|
+
e.preventDefault();
|
|
2686
|
+
e.stopPropagation();
|
|
2687
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
|
|
2688
|
+
}
|
|
2689
|
+
})] }, action))
|
|
2690
|
+
}),
|
|
2691
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
|
|
2692
|
+
]
|
|
2693
|
+
}), placement.dialog) : null
|
|
2694
|
+
]
|
|
2050
2695
|
});
|
|
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
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
async function importSketchPsd(file) {
|
|
2076
|
-
if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
|
|
2077
|
-
const psd = await runPsdCodec("read", await file.arrayBuffer());
|
|
2078
|
-
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));
|
|
2079
|
-
const doc = {
|
|
2080
|
-
...createSketchLayers(),
|
|
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
|
|
2086
|
-
};
|
|
2087
|
-
for (const [i, layer] of psd.layers.entries()) {
|
|
2088
|
-
const src = canvas(layer.imageData.width, layer.imageData.height);
|
|
2089
|
-
src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
|
|
2090
|
-
const out = canvas(width, height), ctx = out.getContext("2d");
|
|
2091
|
-
ctx.globalAlpha = layer.opacity;
|
|
2092
|
-
ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
|
|
2093
|
-
doc.layers.push({
|
|
2094
|
-
id: i + 1,
|
|
2095
|
-
name: layer.name,
|
|
2096
|
-
visible: !layer.hidden,
|
|
2097
|
-
strokes: [],
|
|
2098
|
-
image: {
|
|
2099
|
-
src: out.toDataURL("image/png"),
|
|
2100
|
-
x: 0,
|
|
2101
|
-
y: 0,
|
|
2102
|
-
width: 1,
|
|
2103
|
-
height: 1
|
|
2104
|
-
}
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
return doc;
|
|
2108
2696
|
}
|
|
2109
2697
|
//#endregion
|
|
2110
2698
|
//#region src/sketch-files.jsx
|
|
2111
|
-
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t,
|
|
2699
|
+
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
|
|
2112
2700
|
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
2701
|
const [format, setFormat] = (0, react.useState)("png");
|
|
2114
2702
|
const input = (0, react.useRef)(null), host = (0, react.useRef)(null);
|
|
@@ -2120,19 +2708,14 @@ window.__ModuleLoader__.load({
|
|
|
2120
2708
|
}
|
|
2121
2709
|
}, [open, working]);
|
|
2122
2710
|
useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
|
|
2123
|
-
const run = async (
|
|
2124
|
-
if (disabled || working) return;
|
|
2711
|
+
const run = (operation) => runOperation(async () => {
|
|
2125
2712
|
setWorking(true);
|
|
2126
|
-
onWorking(true);
|
|
2127
2713
|
try {
|
|
2128
2714
|
await operation();
|
|
2129
|
-
} catch (error) {
|
|
2130
|
-
report(error?.message || t("sketchStorageFailed"));
|
|
2131
2715
|
} finally {
|
|
2132
2716
|
setWorking(false);
|
|
2133
|
-
onWorking(false);
|
|
2134
2717
|
}
|
|
2135
|
-
};
|
|
2718
|
+
});
|
|
2136
2719
|
const refresh = async () => setRows(await sketchDrafts("list"));
|
|
2137
2720
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2138
2721
|
ref: host,
|
|
@@ -2429,13 +3012,15 @@ window.__ModuleLoader__.load({
|
|
|
2429
3012
|
//#endregion
|
|
2430
3013
|
//#region src/sketch-agent-client.js
|
|
2431
3014
|
function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
|
|
2432
|
-
let stopped = false, token, timer, attempts = 0, failures = 0;
|
|
3015
|
+
let stopped = false, token, timer, attempts = 0, failures = 0, pending = false;
|
|
2433
3016
|
const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
|
|
2434
3017
|
sessionId,
|
|
2435
3018
|
token,
|
|
2436
3019
|
...payload
|
|
2437
3020
|
}).then(unwrap);
|
|
2438
3021
|
const poll = async () => {
|
|
3022
|
+
if (stopped || pending) return;
|
|
3023
|
+
pending = true;
|
|
2439
3024
|
try {
|
|
2440
3025
|
const tasks = await call("poll");
|
|
2441
3026
|
for (const task of tasks) {
|
|
@@ -2470,25 +3055,53 @@ window.__ModuleLoader__.load({
|
|
|
2470
3055
|
timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
|
|
2471
3056
|
}
|
|
2472
3057
|
return;
|
|
3058
|
+
} finally {
|
|
3059
|
+
pending = false;
|
|
2473
3060
|
}
|
|
2474
3061
|
failures = 0;
|
|
2475
3062
|
if (!stopped) timer = setTimeout(poll, pollDelay());
|
|
2476
3063
|
};
|
|
2477
|
-
const connect = () =>
|
|
2478
|
-
|
|
3064
|
+
const connect = () => {
|
|
3065
|
+
if (stopped || pending) return;
|
|
3066
|
+
pending = true;
|
|
3067
|
+
call("connect").then((value) => {
|
|
3068
|
+
pending = false;
|
|
3069
|
+
token = value.token;
|
|
3070
|
+
attempts = 0;
|
|
3071
|
+
if (stopped) call("disconnect").catch(() => {});
|
|
3072
|
+
else poll();
|
|
3073
|
+
}, (error) => {
|
|
3074
|
+
pending = false;
|
|
3075
|
+
if (stopped) return;
|
|
3076
|
+
const leaseConflict = /Another board is connected/.test(error.message);
|
|
3077
|
+
if (++attempts < (leaseConflict ? 8 : 3)) timer = setTimeout(connect, Math.min(3e3, 500 * attempts));
|
|
3078
|
+
else report(error.message);
|
|
3079
|
+
});
|
|
3080
|
+
};
|
|
3081
|
+
const wake = () => {
|
|
3082
|
+
if (stopped || pending) return;
|
|
3083
|
+
clearTimeout(timer);
|
|
2479
3084
|
attempts = 0;
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
3085
|
+
failures = 0;
|
|
3086
|
+
token ? poll() : connect();
|
|
3087
|
+
};
|
|
3088
|
+
const visible = () => {
|
|
3089
|
+
if (document.visibilityState === "visible") wake();
|
|
3090
|
+
};
|
|
3091
|
+
if (typeof window !== "undefined") {
|
|
3092
|
+
window.addEventListener("online", wake);
|
|
3093
|
+
window.addEventListener("focus", wake);
|
|
3094
|
+
document.addEventListener("visibilitychange", visible);
|
|
3095
|
+
}
|
|
2488
3096
|
connect();
|
|
2489
3097
|
return () => {
|
|
2490
3098
|
stopped = true;
|
|
2491
3099
|
clearTimeout(timer);
|
|
3100
|
+
if (typeof window !== "undefined") {
|
|
3101
|
+
window.removeEventListener("online", wake);
|
|
3102
|
+
window.removeEventListener("focus", wake);
|
|
3103
|
+
document.removeEventListener("visibilitychange", visible);
|
|
3104
|
+
}
|
|
2492
3105
|
if (token) call("disconnect").catch(() => {});
|
|
2493
3106
|
};
|
|
2494
3107
|
}
|
|
@@ -2515,11 +3128,7 @@ window.__ModuleLoader__.load({
|
|
|
2515
3128
|
const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
|
|
2516
3129
|
const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
|
|
2517
3130
|
const navigation = useSketchView(canvas, open);
|
|
2518
|
-
const
|
|
2519
|
-
pen: 12,
|
|
2520
|
-
pencil: 6,
|
|
2521
|
-
marker: 28
|
|
2522
|
-
});
|
|
3131
|
+
const toolWidths = (0, react.useRef)({});
|
|
2523
3132
|
const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
|
|
2524
3133
|
const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
|
|
2525
3134
|
const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
|
|
@@ -2549,15 +3158,24 @@ window.__ModuleLoader__.load({
|
|
|
2549
3158
|
setColor(value);
|
|
2550
3159
|
if (selected) editObject({ color: value });
|
|
2551
3160
|
};
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
3161
|
+
const chooseTool = (name, nextBrush = brush) => {
|
|
3162
|
+
setWidth(switchSketchToolWidth(toolWidths.current, {
|
|
3163
|
+
tool,
|
|
3164
|
+
brush,
|
|
3165
|
+
width
|
|
3166
|
+
}, {
|
|
3167
|
+
tool: name,
|
|
3168
|
+
brush: nextBrush
|
|
3169
|
+
}));
|
|
3170
|
+
setBrush(nextBrush);
|
|
3171
|
+
setTool(name);
|
|
3172
|
+
if (name !== "select") setSelection(null);
|
|
2558
3173
|
};
|
|
3174
|
+
const chooseBrush = (name) => chooseTool("pen", name);
|
|
2559
3175
|
const [fillShape, setFillShape] = (0, react.useState)(false);
|
|
2560
|
-
const [
|
|
3176
|
+
const [hydrated, setHydrated] = (0, react.useState)(false), [recovered, setRecovered] = (0, react.useState)(false);
|
|
3177
|
+
const [restoreAttempt, retryRestore] = (0, react.useState)(0);
|
|
3178
|
+
const [layersOpen, setLayersOpen] = (0, react.useState)(false), [busy, setBusy] = (0, react.useState)(true), [error, setError] = (0, react.useState)("");
|
|
2561
3179
|
const cursorRing = (0, react.useRef)(null);
|
|
2562
3180
|
const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
|
|
2563
3181
|
useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
|
|
@@ -2580,159 +3198,115 @@ window.__ModuleLoader__.load({
|
|
|
2580
3198
|
updateUi.current = false;
|
|
2581
3199
|
redraw((value) => value + 1);
|
|
2582
3200
|
}
|
|
2583
|
-
});
|
|
2584
|
-
};
|
|
2585
|
-
const checkpoint = () => {
|
|
2586
|
-
documentRevision.current++;
|
|
2587
|
-
dirty.current = true;
|
|
2588
|
-
undo.current.push(doc.current);
|
|
2589
|
-
if (undo.current.length > 30) undo.current.shift();
|
|
2590
|
-
redo.current = [];
|
|
2591
|
-
};
|
|
2592
|
-
const change = (action, id, value) => {
|
|
2593
|
-
if (busy || agentRun.current?.locked || active.current) return;
|
|
2594
|
-
const next = changeSketchLayer(doc.current, action, id, value);
|
|
2595
|
-
if (next === doc.current) return;
|
|
2596
|
-
if (action !== "select") checkpoint();
|
|
2597
|
-
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
|
-
};
|
|
3201
|
+
});
|
|
3202
|
+
};
|
|
3203
|
+
const checkpoint = () => {
|
|
3204
|
+
documentRevision.current++;
|
|
3205
|
+
dirty.current = true;
|
|
3206
|
+
undo.current.push(doc.current);
|
|
3207
|
+
if (undo.current.length > 30) undo.current.shift();
|
|
3208
|
+
redo.current = [];
|
|
3209
|
+
};
|
|
3210
|
+
const change = (action, id, value) => {
|
|
3211
|
+
if (busy || agentRun.current?.locked || active.current) return;
|
|
3212
|
+
const next = changeSketchLayer(doc.current, action, id, value);
|
|
3213
|
+
if (next === doc.current) return;
|
|
3214
|
+
if (action !== "select") checkpoint();
|
|
3215
|
+
else documentRevision.current++;
|
|
3216
|
+
doc.current = next;
|
|
3217
|
+
setError("");
|
|
2697
3218
|
schedule();
|
|
2698
3219
|
};
|
|
2699
|
-
|
|
2700
|
-
if (
|
|
3220
|
+
(0, react.useEffect)(() => {
|
|
3221
|
+
if (open) {
|
|
3222
|
+
dialog.current.showModal();
|
|
3223
|
+
canvas.current.width = doc.current.width ?? 1024;
|
|
3224
|
+
paint();
|
|
3225
|
+
} else dialog.current?.close();
|
|
3226
|
+
}, [open]);
|
|
3227
|
+
(0, react.useEffect)(() => () => {
|
|
3228
|
+
cancelAnimationFrame(frame.current);
|
|
3229
|
+
cache.current.clear();
|
|
3230
|
+
}, []);
|
|
3231
|
+
const { save, saveChanges, fresh, load, importImage, restore } = createSketchDocumentLifecycle(localSession.current, {
|
|
3232
|
+
sessionId,
|
|
3233
|
+
t,
|
|
3234
|
+
schedule,
|
|
3235
|
+
checkpoint,
|
|
3236
|
+
cache,
|
|
3237
|
+
setSelection,
|
|
3238
|
+
setTextEdit,
|
|
3239
|
+
setRecovered
|
|
3240
|
+
});
|
|
3241
|
+
(0, react.useEffect)(() => localSession.current.retain?.(), []);
|
|
3242
|
+
(0, react.useEffect)(() => {
|
|
3243
|
+
let live = true;
|
|
3244
|
+
setHydrated(false);
|
|
3245
|
+
setBusy(true);
|
|
3246
|
+
setError("");
|
|
3247
|
+
(enabled ? restore(() => live) : Promise.resolve()).then(() => {
|
|
3248
|
+
if (live) {
|
|
3249
|
+
setHydrated(true);
|
|
3250
|
+
setBusy(false);
|
|
3251
|
+
}
|
|
3252
|
+
}).catch((error) => {
|
|
3253
|
+
if (live) setError(t(error.code === "SKETCH_STORAGE_BLOCKED" ? "sketchStorageBlocked" : "sketchStorageFailed"));
|
|
3254
|
+
});
|
|
3255
|
+
return () => {
|
|
3256
|
+
live = false;
|
|
3257
|
+
};
|
|
3258
|
+
}, [
|
|
3259
|
+
enabled,
|
|
3260
|
+
sessionId,
|
|
3261
|
+
restoreAttempt
|
|
3262
|
+
]);
|
|
3263
|
+
(0, react.useEffect)(() => {
|
|
3264
|
+
if (!hydrated || !enabled || !dirty.current || busy || agentLocked) return;
|
|
3265
|
+
const timer = setTimeout(() => {
|
|
3266
|
+
if (active.current || sizeGesture.current || !dirty.current) return;
|
|
3267
|
+
sketchDrafts("checkpoint", {
|
|
3268
|
+
id: sessionId,
|
|
3269
|
+
updated: Date.now(),
|
|
3270
|
+
doc: structuredClone(doc.current)
|
|
3271
|
+
}).catch(() => setError(t("sketchRecoveryFailed")));
|
|
3272
|
+
}, 1500);
|
|
3273
|
+
return () => clearTimeout(timer);
|
|
3274
|
+
}, [
|
|
3275
|
+
revision,
|
|
3276
|
+
hydrated,
|
|
3277
|
+
enabled,
|
|
3278
|
+
busy,
|
|
3279
|
+
agentLocked,
|
|
3280
|
+
sessionId
|
|
3281
|
+
]);
|
|
3282
|
+
const close = () => {
|
|
3283
|
+
if (!hydrated || agentRun.current?.locked) {
|
|
2701
3284
|
onClose();
|
|
2702
3285
|
return;
|
|
2703
3286
|
}
|
|
2704
|
-
|
|
2705
|
-
setBusy(true);
|
|
2706
|
-
try {
|
|
3287
|
+
return runFile(async () => {
|
|
2707
3288
|
await saveChanges();
|
|
2708
3289
|
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
|
-
}
|
|
3290
|
+
});
|
|
2726
3291
|
};
|
|
3292
|
+
const operationGate = (0, react.useRef)(null);
|
|
3293
|
+
operationGate.current ??= createSketchOperationGate();
|
|
3294
|
+
const runFile = (operation) => operationGate.current.run(operation, {
|
|
3295
|
+
blocked: busy || agentRun.current?.locked || Boolean(active.current),
|
|
3296
|
+
working: setBusy,
|
|
3297
|
+
report: (error) => setError(error ? error?.message || t("sketchStorageFailed") : "")
|
|
3298
|
+
});
|
|
2727
3299
|
(0, react.useEffect)(() => {
|
|
2728
|
-
if (open && !agentLocked && incoming && incoming !== received.current) {
|
|
3300
|
+
if (open && hydrated && !busy && !agentLocked && incoming && incoming !== received.current) {
|
|
2729
3301
|
received.current = incoming;
|
|
2730
3302
|
runFile(() => importImage(incoming.file));
|
|
2731
3303
|
}
|
|
2732
3304
|
}, [
|
|
2733
3305
|
open,
|
|
2734
3306
|
incoming,
|
|
2735
|
-
agentLocked
|
|
3307
|
+
agentLocked,
|
|
3308
|
+
hydrated,
|
|
3309
|
+
busy
|
|
2736
3310
|
]);
|
|
2737
3311
|
const keyDown = (event) => {
|
|
2738
3312
|
if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
|
|
@@ -2744,14 +3318,6 @@ window.__ModuleLoader__.load({
|
|
|
2744
3318
|
editObject({}, "delete");
|
|
2745
3319
|
return;
|
|
2746
3320
|
}
|
|
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
3321
|
const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
|
|
2756
3322
|
if (command && [
|
|
2757
3323
|
"z",
|
|
@@ -2765,20 +3331,24 @@ window.__ModuleLoader__.load({
|
|
|
2765
3331
|
return;
|
|
2766
3332
|
}
|
|
2767
3333
|
if (command || event.altKey) return;
|
|
2768
|
-
const
|
|
3334
|
+
const action = sketchShortcutAction(navigation.keys, key);
|
|
3335
|
+
if ([
|
|
2769
3336
|
"pen",
|
|
2770
3337
|
"eraser",
|
|
2771
3338
|
"line",
|
|
2772
3339
|
"rectangle",
|
|
2773
|
-
"circle"
|
|
2774
|
-
|
|
2775
|
-
|
|
3340
|
+
"circle",
|
|
3341
|
+
"select",
|
|
3342
|
+
"text"
|
|
3343
|
+
].includes(action)) {
|
|
2776
3344
|
event.preventDefault();
|
|
2777
|
-
|
|
3345
|
+
chooseTool(action);
|
|
2778
3346
|
}
|
|
2779
3347
|
if (key === "[" || key === "]") {
|
|
2780
3348
|
event.preventDefault();
|
|
2781
|
-
|
|
3349
|
+
const value = stepSketchWidth(selected?.width ?? width, key === "]" ? 1 : -1);
|
|
3350
|
+
if (selected) editObject({ width: value });
|
|
3351
|
+
else setWidth(value);
|
|
2782
3352
|
}
|
|
2783
3353
|
};
|
|
2784
3354
|
const current = doc.current.layers.find((layer) => layer.id === doc.current.active);
|
|
@@ -2787,81 +3357,14 @@ window.__ModuleLoader__.load({
|
|
|
2787
3357
|
const gesture = active.current;
|
|
2788
3358
|
if (!gesture || gesture.id !== event.pointerId || busy) return;
|
|
2789
3359
|
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
3360
|
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;
|
|
3361
|
+
const samples = native.getCoalescedEvents?.() ?? [];
|
|
3362
|
+
try {
|
|
3363
|
+
doc.current = updateSketchGesture(doc.current, gesture, samples.length ? [...samples, native] : [native], rect, width, event.shiftKey);
|
|
3364
|
+
schedule(Boolean(gesture.object));
|
|
3365
|
+
} catch (error) {
|
|
3366
|
+
setError(error?.message || t("sketchFailed"));
|
|
2863
3367
|
}
|
|
2864
|
-
schedule(false);
|
|
2865
3368
|
};
|
|
2866
3369
|
const end = (event, cancel = false) => {
|
|
2867
3370
|
if (navigation.end(event)) return;
|
|
@@ -2884,7 +3387,7 @@ window.__ModuleLoader__.load({
|
|
|
2884
3387
|
layer: drawn.layer,
|
|
2885
3388
|
id: stroke.id
|
|
2886
3389
|
});
|
|
2887
|
-
|
|
3390
|
+
chooseTool("select");
|
|
2888
3391
|
}
|
|
2889
3392
|
}
|
|
2890
3393
|
active.current = null;
|
|
@@ -2910,7 +3413,7 @@ window.__ModuleLoader__.load({
|
|
|
2910
3413
|
available: () => enabled && agentEnabled,
|
|
2911
3414
|
previewEnabled: () => agentPreview,
|
|
2912
3415
|
open: () => onOpen(),
|
|
2913
|
-
busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
|
|
3416
|
+
busy: () => operationGate.current.running || busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
|
|
2914
3417
|
document: () => doc.current,
|
|
2915
3418
|
snapshot: () => ({
|
|
2916
3419
|
documentId: documentId.current,
|
|
@@ -2957,23 +3460,18 @@ window.__ModuleLoader__.load({
|
|
|
2957
3460
|
previewEnabled: () => agentAdapter.current.previewEnabled()
|
|
2958
3461
|
});
|
|
2959
3462
|
(0, react.useEffect)(() => {
|
|
2960
|
-
if (!enabled || !agentEnabled) return;
|
|
3463
|
+
if (!enabled || !agentEnabled || !hydrated) return;
|
|
2961
3464
|
const api = Object.freeze({
|
|
2962
3465
|
version: 2,
|
|
2963
3466
|
sessionId,
|
|
2964
3467
|
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
|
-
}
|
|
3468
|
+
export: (format) => exportSketchAgentFile(format, {
|
|
3469
|
+
gate: operationGate.current,
|
|
3470
|
+
blocked: agentAdapter.current.busy() || agentRun.current.locked,
|
|
3471
|
+
working: setBusy,
|
|
3472
|
+
report: (error) => setError(error ? error.message || t("sketchFailed") : ""),
|
|
3473
|
+
exportFile: (value) => agentAdapter.current.export(value)
|
|
3474
|
+
})
|
|
2977
3475
|
});
|
|
2978
3476
|
window.dshSketchAgent = api;
|
|
2979
3477
|
return () => {
|
|
@@ -2983,14 +3481,15 @@ window.__ModuleLoader__.load({
|
|
|
2983
3481
|
enabled,
|
|
2984
3482
|
agentEnabled,
|
|
2985
3483
|
rpc,
|
|
2986
|
-
sessionId
|
|
3484
|
+
sessionId,
|
|
3485
|
+
hydrated
|
|
2987
3486
|
]);
|
|
2988
3487
|
(0, react.useEffect)(() => {
|
|
2989
3488
|
if (!enabled || !agentEnabled) {
|
|
2990
3489
|
if (agentRun.current.locked) agentRun.current.stop();
|
|
2991
3490
|
return;
|
|
2992
3491
|
}
|
|
2993
|
-
if (!rpc || !sessionId) return;
|
|
3492
|
+
if (!rpc || !sessionId || !hydrated) return;
|
|
2994
3493
|
let live = true;
|
|
2995
3494
|
const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
|
|
2996
3495
|
if (!live) throw Error("Sketch session disconnected");
|
|
@@ -2998,7 +3497,7 @@ window.__ModuleLoader__.load({
|
|
|
2998
3497
|
}, (message) => {
|
|
2999
3498
|
agentRun.current.fail();
|
|
3000
3499
|
setError(message);
|
|
3001
|
-
}, () => 350);
|
|
3500
|
+
}, () => agentRun.current.locked ? 350 : 2e3);
|
|
3002
3501
|
return () => {
|
|
3003
3502
|
live = false;
|
|
3004
3503
|
disconnect();
|
|
@@ -3007,23 +3506,18 @@ window.__ModuleLoader__.load({
|
|
|
3007
3506
|
enabled,
|
|
3008
3507
|
agentEnabled,
|
|
3009
3508
|
rpc,
|
|
3010
|
-
sessionId
|
|
3509
|
+
sessionId,
|
|
3510
|
+
hydrated
|
|
3011
3511
|
]);
|
|
3012
|
-
const attach =
|
|
3013
|
-
if (!enabled
|
|
3014
|
-
|
|
3015
|
-
setError("");
|
|
3016
|
-
try {
|
|
3512
|
+
const attach = () => {
|
|
3513
|
+
if (!enabled) return;
|
|
3514
|
+
return runFile(async () => {
|
|
3017
3515
|
paint();
|
|
3018
|
-
const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error("
|
|
3516
|
+
const blob = await new Promise((resolve, reject) => canvas.current.toBlob((blob) => blob ? resolve(blob) : reject(Error(t("sketchFailed"))), "image/png"));
|
|
3019
3517
|
await saveChanges();
|
|
3020
3518
|
await attachSketch(blob);
|
|
3021
3519
|
onClose();
|
|
3022
|
-
}
|
|
3023
|
-
setError(t("sketchFailed"));
|
|
3024
|
-
} finally {
|
|
3025
|
-
setBusy(false);
|
|
3026
|
-
}
|
|
3520
|
+
});
|
|
3027
3521
|
};
|
|
3028
3522
|
const exportFile = async (format = "png") => {
|
|
3029
3523
|
paint();
|
|
@@ -3043,8 +3537,8 @@ window.__ModuleLoader__.load({
|
|
|
3043
3537
|
};
|
|
3044
3538
|
agentAdapter.current.export = exportFile;
|
|
3045
3539
|
const download = async (format) => {
|
|
3540
|
+
setError("");
|
|
3046
3541
|
const { blob, extension } = await exportFile(format);
|
|
3047
|
-
await saveChanges();
|
|
3048
3542
|
const url = URL.createObjectURL(blob), link = document.createElement("a");
|
|
3049
3543
|
link.href = url;
|
|
3050
3544
|
link.download = `${(saved.current?.name || "sketch").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").slice(0, 80)}.${extension}`;
|
|
@@ -3110,7 +3604,7 @@ window.__ModuleLoader__.load({
|
|
|
3110
3604
|
type: "button",
|
|
3111
3605
|
"aria-label": t("sketchCancel"),
|
|
3112
3606
|
title: t("sketchCancel"),
|
|
3113
|
-
disabled: busy && !agentLocked,
|
|
3607
|
+
disabled: busy && hydrated && !agentLocked,
|
|
3114
3608
|
onClick: close,
|
|
3115
3609
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
|
|
3116
3610
|
}),
|
|
@@ -3123,8 +3617,7 @@ window.__ModuleLoader__.load({
|
|
|
3123
3617
|
hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
|
|
3124
3618
|
disabled: agentLocked || busy,
|
|
3125
3619
|
t,
|
|
3126
|
-
|
|
3127
|
-
onWorking: setBusy
|
|
3620
|
+
runOperation: runFile
|
|
3128
3621
|
}),
|
|
3129
3622
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3130
3623
|
className: "codexSketchHeading",
|
|
@@ -3215,6 +3708,19 @@ window.__ModuleLoader__.load({
|
|
|
3215
3708
|
},
|
|
3216
3709
|
onDismiss: () => setNoticeHidden(true)
|
|
3217
3710
|
}) : null,
|
|
3711
|
+
recovered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3712
|
+
className: "codexSketchAgentStatus",
|
|
3713
|
+
role: "status",
|
|
3714
|
+
children: [t("sketchRecovered"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3715
|
+
type: "button",
|
|
3716
|
+
onClick: () => setRecovered(false),
|
|
3717
|
+
"aria-label": t("sketchDismissStatus"),
|
|
3718
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3719
|
+
name: "close",
|
|
3720
|
+
size: 14
|
|
3721
|
+
})
|
|
3722
|
+
})]
|
|
3723
|
+
}) : null,
|
|
3218
3724
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3219
3725
|
className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
|
|
3220
3726
|
children: [
|
|
@@ -3405,13 +3911,10 @@ window.__ModuleLoader__.load({
|
|
|
3405
3911
|
sizeGesture.current = false;
|
|
3406
3912
|
},
|
|
3407
3913
|
onChange: (value) => {
|
|
3408
|
-
if (opacityMode) {
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
setWidth(value);
|
|
3413
|
-
if (selected) editObject({ width: value });
|
|
3414
|
-
}
|
|
3914
|
+
if (opacityMode) if (selected) editObject({ opacity: value / 100 });
|
|
3915
|
+
else setFlow(value);
|
|
3916
|
+
else if (selected) editObject({ width: value });
|
|
3917
|
+
else setWidth(value);
|
|
3415
3918
|
}
|
|
3416
3919
|
}),
|
|
3417
3920
|
textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
@@ -3447,7 +3950,7 @@ window.__ModuleLoader__.load({
|
|
|
3447
3950
|
schedule();
|
|
3448
3951
|
}
|
|
3449
3952
|
setTextEdit(null);
|
|
3450
|
-
|
|
3953
|
+
chooseTool("select");
|
|
3451
3954
|
} catch (e) {
|
|
3452
3955
|
setError(e.message);
|
|
3453
3956
|
}
|
|
@@ -3516,89 +4019,11 @@ window.__ModuleLoader__.load({
|
|
|
3516
4019
|
!doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
|
|
3517
4020
|
]
|
|
3518
4021
|
}) : 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
|
-
]
|
|
4022
|
+
layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
|
|
4023
|
+
document: doc.current,
|
|
4024
|
+
disabled: agentLocked || busy,
|
|
4025
|
+
change,
|
|
4026
|
+
t
|
|
3602
4027
|
}) : null
|
|
3603
4028
|
]
|
|
3604
4029
|
}),
|
|
@@ -3616,92 +4041,15 @@ window.__ModuleLoader__.load({
|
|
|
3616
4041
|
navigation,
|
|
3617
4042
|
t
|
|
3618
4043
|
}),
|
|
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
|
-
]
|
|
4044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
|
|
4045
|
+
t,
|
|
4046
|
+
disabled: agentLocked || busy,
|
|
4047
|
+
tool,
|
|
4048
|
+
brush,
|
|
4049
|
+
chooseBrush,
|
|
4050
|
+
chooseTool,
|
|
4051
|
+
shapesOpen,
|
|
4052
|
+
setShapesOpen
|
|
3705
4053
|
}),
|
|
3706
4054
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3707
4055
|
className: "codexLayerBrush",
|
|
@@ -3793,10 +4141,14 @@ window.__ModuleLoader__.load({
|
|
|
3793
4141
|
})
|
|
3794
4142
|
]
|
|
3795
4143
|
}),
|
|
3796
|
-
error ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4144
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3797
4145
|
className: "codexSketchHint",
|
|
3798
4146
|
role: "alert",
|
|
3799
|
-
children: error
|
|
4147
|
+
children: [error, !hydrated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4148
|
+
type: "button",
|
|
4149
|
+
onClick: () => retryRestore((value) => value + 1),
|
|
4150
|
+
children: t("accountRetry")
|
|
4151
|
+
}) : null]
|
|
3800
4152
|
}) : null
|
|
3801
4153
|
]
|
|
3802
4154
|
})] });
|
|
@@ -3999,6 +4351,19 @@ window.__ModuleLoader__.load({
|
|
|
3999
4351
|
//#endregion
|
|
4000
4352
|
//#region src/client-locales.js
|
|
4001
4353
|
const zh = {
|
|
4354
|
+
advancedModelSearch: "模型与搜索",
|
|
4355
|
+
subagentBackendTitle: "独立子任务",
|
|
4356
|
+
subagentBackend_dsh: "DSH",
|
|
4357
|
+
subagentBackend_codex: "Codex",
|
|
4358
|
+
connectionTitle: "连接方式",
|
|
4359
|
+
connectionHint: "默认 SSE。WebSocket 实验性复用连接与上下文传输,跟随现有代理;连接失败可回退 SSE。下次请求生效,不扩大上下文容量。",
|
|
4360
|
+
subagentBackendHint: "Codex 复用订阅登录,跟随当前订阅模型和工作区权限;其他模型会话使用 Luna low。共享上下文子任务仍用 DSH。",
|
|
4361
|
+
subagentBackendUnavailable: "当前宿主缺少子代理服务,请更新 DSH。",
|
|
4362
|
+
sketchRecovered: "已恢复未保存草稿",
|
|
4363
|
+
sketchRecoveryFailed: "恢复检查点保存失败,请手动保存或导出草稿。",
|
|
4364
|
+
sketchStorageBlocked: "草稿正被其他窗口占用,请关闭其他草图窗口后重试。",
|
|
4365
|
+
sketchDraftLimit: "已达 20 份草稿上限。请先导出,或删除不需要的草稿后保存。",
|
|
4366
|
+
sketchStorageLimit: "草稿存储空间已满。请先导出,或删除不需要的草稿后保存。",
|
|
4002
4367
|
sketchSizeShort: "粗细",
|
|
4003
4368
|
sketchObjectDuplicate: "复制对象",
|
|
4004
4369
|
sketchObjectDelete: "删除对象",
|
|
@@ -4410,6 +4775,19 @@ window.__ModuleLoader__.load({
|
|
|
4410
4775
|
imageRemoveAnnotation: "删除标注"
|
|
4411
4776
|
};
|
|
4412
4777
|
const en = {
|
|
4778
|
+
advancedModelSearch: "Models and search",
|
|
4779
|
+
subagentBackendTitle: "Independent subtasks",
|
|
4780
|
+
subagentBackend_dsh: "DSH",
|
|
4781
|
+
subagentBackend_codex: "Codex",
|
|
4782
|
+
connectionTitle: "Connection",
|
|
4783
|
+
connectionHint: "SSE by default. Experimental WebSocket reuses connections and context transfers, follows your proxy, and can fall back to SSE on connection failure. Applies to the next request; context limits stay the same.",
|
|
4784
|
+
subagentBackendHint: "Codex uses your subscription login, current subscription model and workspace permissions; other model sessions use Luna low. Shared-context subtasks stay in DSH.",
|
|
4785
|
+
subagentBackendUnavailable: "Subagent services are unavailable. Update DSH to use this option.",
|
|
4786
|
+
sketchRecovered: "Unsaved sketch recovered",
|
|
4787
|
+
sketchRecoveryFailed: "Recovery checkpoint failed. Save or export your draft.",
|
|
4788
|
+
sketchStorageBlocked: "Draft storage is in use. Close other sketch windows and retry.",
|
|
4789
|
+
sketchDraftLimit: "The 20-draft limit is reached. Export first, or remove an unwanted draft before saving.",
|
|
4790
|
+
sketchStorageLimit: "Draft storage is full. Export first, or remove an unwanted draft before saving.",
|
|
4413
4791
|
sketchSizeShort: "Size",
|
|
4414
4792
|
sketchObjectDuplicate: "Duplicate object",
|
|
4415
4793
|
sketchObjectDelete: "Delete object",
|
|
@@ -4823,6 +5201,15 @@ window.__ModuleLoader__.load({
|
|
|
4823
5201
|
//#endregion
|
|
4824
5202
|
//#region src/client-styles.js
|
|
4825
5203
|
const STYLE = `
|
|
5204
|
+
.codexSubscriptionAdvancedPreferences{display:flex;flex-direction:column;gap:10px}
|
|
5205
|
+
.codexSubscriptionSearchChoices.codexSubscriptionQuotaModes{display:flex;flex:0 0 auto;gap:0;grid-template-columns:none}
|
|
5206
|
+
.codexSubscriptionSettingsDisclosure>summary{display:flex;align-items:center;gap:10px;min-height:28px;cursor:pointer;list-style:none;font-size:14px;font-weight:500}
|
|
5207
|
+
.codexSubscriptionSettingsDisclosure>summary::-webkit-details-marker{display:none}
|
|
5208
|
+
.codexSubscriptionSettingsDisclosure>summary>.codexSubscriptionPreferenceHint{margin-left:auto;font-weight:400}
|
|
5209
|
+
.codexSubscriptionSettingsDisclosure>summary>svg{flex:none;transition:transform .15s}
|
|
5210
|
+
.codexSubscriptionSettingsDisclosure[open]>summary>svg{transform:rotate(180deg)}
|
|
5211
|
+
.codexSubscriptionSettingsDisclosure>summary:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:4px;border-radius:4px}
|
|
5212
|
+
.codexSubscriptionSettingsDisclosureBody{padding-top:8px;margin-top:8px;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
4826
5213
|
.codexComposerQuota[data-warning=true]{color:var(--dsw-alias-state-error-primary)}
|
|
4827
5214
|
.codexComposerQuota[data-warning=true] progress{accent-color:var(--dsw-alias-state-error-primary)}
|
|
4828
5215
|
.codexComposerQuota[data-warning=true] progress::-webkit-progress-value{background:var(--dsw-alias-state-error-primary)}
|
|
@@ -4875,20 +5262,9 @@ window.__ModuleLoader__.load({
|
|
|
4875
5262
|
.codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
4876
5263
|
.codexSubscriptionContextInput{width:116px}
|
|
4877
5264
|
.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
5265
|
.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
|
|
4890
5266
|
.codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
|
|
4891
|
-
.codexSubscriptionSearchChoices[data-saving=true] .
|
|
5267
|
+
.codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
|
|
4892
5268
|
.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
4893
5269
|
.codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
|
|
4894
5270
|
.codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
@@ -6176,6 +6552,7 @@ window.__ModuleLoader__.load({
|
|
|
6176
6552
|
let modelRefreshGeneration = 0;
|
|
6177
6553
|
let modelRefreshStarted = false;
|
|
6178
6554
|
let disposed = false;
|
|
6555
|
+
let subagentBackendAvailable = false;
|
|
6179
6556
|
const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
|
|
6180
6557
|
const nativeSnapshot = () => scope.getSnapshot();
|
|
6181
6558
|
const read = () => {
|
|
@@ -6189,6 +6566,9 @@ window.__ModuleLoader__.load({
|
|
|
6189
6566
|
return Object.freeze({
|
|
6190
6567
|
status: current.status,
|
|
6191
6568
|
...capabilities,
|
|
6569
|
+
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6570
|
+
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
6571
|
+
subagentBackendAvailable,
|
|
6192
6572
|
quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
|
|
6193
6573
|
searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
|
|
6194
6574
|
speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
|
|
@@ -6222,6 +6602,7 @@ window.__ModuleLoader__.load({
|
|
|
6222
6602
|
publish();
|
|
6223
6603
|
});
|
|
6224
6604
|
const acceptFallback = (value) => {
|
|
6605
|
+
subagentBackendAvailable = value?.subagentBackendAvailable === true;
|
|
6225
6606
|
if (!modelRefreshStarted) {
|
|
6226
6607
|
contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
|
|
6227
6608
|
verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
|
|
@@ -6232,6 +6613,8 @@ window.__ModuleLoader__.load({
|
|
|
6232
6613
|
fallback = {
|
|
6233
6614
|
status: "ready",
|
|
6234
6615
|
value: {
|
|
6616
|
+
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6617
|
+
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
6235
6618
|
...readCapabilitySettings(value),
|
|
6236
6619
|
[QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
|
|
6237
6620
|
[SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
|
|
@@ -6255,6 +6638,7 @@ window.__ModuleLoader__.load({
|
|
|
6255
6638
|
try {
|
|
6256
6639
|
const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
|
|
6257
6640
|
if (current !== generation || disposed) return;
|
|
6641
|
+
subagentBackendAvailable = value?.subagentBackendAvailable === true;
|
|
6258
6642
|
if (nativeSnapshot().status === "ready") {
|
|
6259
6643
|
if (!modelRefreshStarted) {
|
|
6260
6644
|
contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
|
|
@@ -6315,7 +6699,7 @@ window.__ModuleLoader__.load({
|
|
|
6315
6699
|
failedPatch = void 0;
|
|
6316
6700
|
publish();
|
|
6317
6701
|
try {
|
|
6318
|
-
if (nativeSnapshot().status === "ready") {
|
|
6702
|
+
if (nativeSnapshot().status === "ready" && !Object.hasOwn(patch, "subagentBackend")) {
|
|
6319
6703
|
for (const [field, value] of entries) {
|
|
6320
6704
|
if (current !== generation) return;
|
|
6321
6705
|
await scope.set(field, value);
|
|
@@ -6641,7 +7025,7 @@ window.__ModuleLoader__.load({
|
|
|
6641
7025
|
}
|
|
6642
7026
|
//#endregion
|
|
6643
7027
|
//#region src/version.js
|
|
6644
|
-
const PACKAGE_VERSION = "2.1.
|
|
7028
|
+
const PACKAGE_VERSION = "2.1.1-beta.1";
|
|
6645
7029
|
//#endregion
|
|
6646
7030
|
//#region src/client-recovery.js
|
|
6647
7031
|
async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
|
|
@@ -7280,64 +7664,76 @@ window.__ModuleLoader__.load({
|
|
|
7280
7664
|
const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
|
|
7281
7665
|
const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
|
|
7282
7666
|
const active = snapshot.imageGeneration || snapshot.imageEditing;
|
|
7283
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7667
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
7284
7668
|
className: "codexSubscriptionCard codexImageSettings",
|
|
7285
7669
|
"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
|
-
|
|
7670
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
7671
|
+
className: "codexSubscriptionSettingsDisclosure",
|
|
7672
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
7673
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSettings") }),
|
|
7674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7675
|
+
className: "codexSubscriptionPreferenceHint",
|
|
7676
|
+
children: active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
|
|
7677
|
+
}),
|
|
7678
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
7679
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7680
|
+
className: "codexSubscriptionSettingsDisclosureBody",
|
|
7681
|
+
children: [
|
|
7682
|
+
Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
|
|
7683
|
+
const value = imageGroupValue(snapshot, group);
|
|
7684
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7685
|
+
label: t(group),
|
|
7686
|
+
hint: t(`${group}Hint`),
|
|
7687
|
+
value,
|
|
7688
|
+
text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
|
|
7689
|
+
disabled,
|
|
7690
|
+
items: ["on", "off"].map((id) => ({
|
|
7691
|
+
id,
|
|
7692
|
+
label: t(`${group}_${id}`)
|
|
7693
|
+
})),
|
|
7694
|
+
onSelect: (id) => {
|
|
7695
|
+
preference.set(imageGroupPatch(group, id === "on"));
|
|
7696
|
+
}
|
|
7697
|
+
}, group);
|
|
7698
|
+
}),
|
|
7699
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7700
|
+
className: "codexImageDefaultsGroup",
|
|
7701
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7702
|
+
label: t("imageModel"),
|
|
7703
|
+
value: snapshot.imageModel,
|
|
7704
|
+
text: modelLabel(snapshot.imageModel),
|
|
7705
|
+
disabled: disabled || !active,
|
|
7706
|
+
items: Object.keys(IMAGE_MODELS).map((id) => ({
|
|
7707
|
+
id,
|
|
7708
|
+
label: `${modelLabel(id)}${id.includes("2.5") ? ` · ${t("imageExperimental")}` : ""}`
|
|
7709
|
+
})),
|
|
7710
|
+
onSelect: (imageModel) => {
|
|
7711
|
+
preference.set({
|
|
7712
|
+
imageModel,
|
|
7713
|
+
imageQuality: "auto"
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7716
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7717
|
+
label: t("imageQuality"),
|
|
7718
|
+
value: snapshot.imageQuality,
|
|
7719
|
+
text: t(`imageQuality_${snapshot.imageQuality}`),
|
|
7720
|
+
disabled: disabled || !active,
|
|
7721
|
+
items: IMAGE_MODELS[snapshot.imageModel].map((id) => ({
|
|
7722
|
+
id,
|
|
7723
|
+
label: t(`imageQuality_${id}`)
|
|
7724
|
+
})),
|
|
7725
|
+
onSelect: (imageQuality) => {
|
|
7726
|
+
preference.set({ imageQuality });
|
|
7727
|
+
}
|
|
7728
|
+
})]
|
|
7729
|
+
}),
|
|
7730
|
+
snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7731
|
+
className: "codexSubscriptionPreferenceHint",
|
|
7732
|
+
children: t("imageModelHint")
|
|
7733
|
+
}) : null
|
|
7734
|
+
]
|
|
7735
|
+
})]
|
|
7736
|
+
})
|
|
7341
7737
|
});
|
|
7342
7738
|
}
|
|
7343
7739
|
//#endregion
|
|
@@ -7552,10 +7948,9 @@ window.__ModuleLoader__.load({
|
|
|
7552
7948
|
function SearchProviderPreference({ preference, t }) {
|
|
7553
7949
|
const snapshot = usePreferenceSnapshot(preference);
|
|
7554
7950
|
const writable = snapshot.status === "ready" && snapshot.writable === true;
|
|
7555
|
-
const choice = (value, label
|
|
7556
|
-
className: "
|
|
7951
|
+
const choice = (value, label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7952
|
+
className: "codexSubscriptionQuotaMode",
|
|
7557
7953
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7558
|
-
className: "codexSubscriptionSearchInput",
|
|
7559
7954
|
type: "radio",
|
|
7560
7955
|
name: "codex-subscription-search-provider",
|
|
7561
7956
|
checked: snapshot.searchProvider === value,
|
|
@@ -7563,39 +7958,39 @@ window.__ModuleLoader__.load({
|
|
|
7563
7958
|
onChange: () => {
|
|
7564
7959
|
preference.set({ [SEARCH_PROVIDER_FIELD]: value });
|
|
7565
7960
|
}
|
|
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
|
-
})]
|
|
7961
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
7570
7962
|
});
|
|
7963
|
+
const hint = snapshot.searchProvider === "dsh" ? "searchDshHint" : snapshot.searchProvider === "codex" ? "searchCodexHint" : "searchAutoHint";
|
|
7571
7964
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7572
7965
|
className: "codexSubscriptionSearch",
|
|
7573
|
-
children: [
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7966
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7967
|
+
className: "codexSubscriptionPreference",
|
|
7968
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7969
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
7970
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7971
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
7972
|
+
children: t("searchTitle")
|
|
7973
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7974
|
+
className: "codexSubscriptionPreferenceHint",
|
|
7975
|
+
children: t(hint)
|
|
7579
7976
|
})]
|
|
7580
|
-
}),
|
|
7581
|
-
|
|
7582
|
-
className: "codexSubscriptionSearchChoices",
|
|
7977
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7978
|
+
className: "codexSubscriptionSearchChoices codexSubscriptionQuotaModes",
|
|
7583
7979
|
"data-saving": snapshot.saving || void 0,
|
|
7584
7980
|
"aria-busy": snapshot.saving || void 0,
|
|
7585
7981
|
role: "radiogroup",
|
|
7586
7982
|
"aria-label": t("searchTitle"),
|
|
7587
7983
|
children: [
|
|
7588
|
-
choice(SEARCH_PROVIDER_AUTO, t("searchAuto")
|
|
7589
|
-
choice("dsh",
|
|
7590
|
-
choice(SEARCH_PROVIDER_CODEX,
|
|
7984
|
+
choice(SEARCH_PROVIDER_AUTO, t("searchAuto")),
|
|
7985
|
+
choice("dsh", "DSH"),
|
|
7986
|
+
choice(SEARCH_PROVIDER_CODEX, "Codex")
|
|
7591
7987
|
]
|
|
7592
|
-
})
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
]
|
|
7988
|
+
})]
|
|
7989
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
|
|
7990
|
+
preference,
|
|
7991
|
+
t,
|
|
7992
|
+
section: "search"
|
|
7993
|
+
})]
|
|
7599
7994
|
});
|
|
7600
7995
|
}
|
|
7601
7996
|
function ContextWindowPreference({ preference, t }) {
|
|
@@ -7761,16 +8156,99 @@ window.__ModuleLoader__.load({
|
|
|
7761
8156
|
function PreferencesCard({ preference, t, section = "display" }) {
|
|
7762
8157
|
const snapshot = usePreferenceSnapshot(preference);
|
|
7763
8158
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7764
|
-
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8159
|
+
className: section === "advanced" ? "codexSubscriptionAdvancedPreferences" : "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
7765
8160
|
children: [section === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7766
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
7767
|
-
|
|
7768
|
-
t
|
|
8161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
8162
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8163
|
+
"aria-label": t("advancedModelSearch"),
|
|
8164
|
+
children: [
|
|
8165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("advancedModelSearch") }),
|
|
8166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchProviderPreference, {
|
|
8167
|
+
preference,
|
|
8168
|
+
t
|
|
8169
|
+
}),
|
|
8170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
|
|
8171
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
|
|
8172
|
+
preference,
|
|
8173
|
+
t
|
|
8174
|
+
})
|
|
8175
|
+
]
|
|
7769
8176
|
}),
|
|
7770
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
8177
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8178
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8179
|
+
"aria-label": t("connectionTitle"),
|
|
8180
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8181
|
+
className: "codexSubscriptionPreference",
|
|
8182
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8183
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8184
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8185
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8186
|
+
children: [
|
|
8187
|
+
t("connectionTitle"),
|
|
8188
|
+
" ",
|
|
8189
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
|
|
8190
|
+
]
|
|
8191
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8192
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8193
|
+
children: t("connectionHint")
|
|
8194
|
+
})]
|
|
8195
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8196
|
+
className: "codexSubscriptionQuotaModes",
|
|
8197
|
+
role: "radiogroup",
|
|
8198
|
+
"aria-label": t("connectionTitle"),
|
|
8199
|
+
"aria-busy": snapshot.saving || void 0,
|
|
8200
|
+
children: ["sse", "websocket"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8201
|
+
className: "codexSubscriptionQuotaMode",
|
|
8202
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
8203
|
+
type: "radio",
|
|
8204
|
+
name: "codex-connection-mode",
|
|
8205
|
+
checked: snapshot.connectionMode === value,
|
|
8206
|
+
disabled: !snapshot.writable,
|
|
8207
|
+
onChange: () => {
|
|
8208
|
+
preference.set({ connectionMode: value });
|
|
8209
|
+
}
|
|
8210
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === "sse" ? "SSE" : "WebSocket" })]
|
|
8211
|
+
}, value))
|
|
8212
|
+
})]
|
|
8213
|
+
})
|
|
8214
|
+
}),
|
|
8215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8216
|
+
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8217
|
+
"aria-label": t("subagentBackendTitle"),
|
|
8218
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8219
|
+
className: "codexSubscriptionPreference",
|
|
8220
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8221
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8222
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8223
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8224
|
+
children: [
|
|
8225
|
+
t("subagentBackendTitle"),
|
|
8226
|
+
" ",
|
|
8227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
|
|
8228
|
+
]
|
|
8229
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8230
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8231
|
+
children: t(snapshot.subagentBackendAvailable ? "subagentBackendHint" : "subagentBackendUnavailable")
|
|
8232
|
+
})]
|
|
8233
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8234
|
+
className: "codexSubscriptionQuotaModes",
|
|
8235
|
+
role: "radiogroup",
|
|
8236
|
+
"aria-label": t("subagentBackendTitle"),
|
|
8237
|
+
"aria-busy": snapshot.saving || void 0,
|
|
8238
|
+
children: ["dsh", "codex"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
8239
|
+
className: "codexSubscriptionQuotaMode",
|
|
8240
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
8241
|
+
type: "radio",
|
|
8242
|
+
name: "codex-subagent-backend",
|
|
8243
|
+
checked: snapshot.subagentBackend === value,
|
|
8244
|
+
disabled: !snapshot.writable || !snapshot.subagentBackendAvailable,
|
|
8245
|
+
onChange: () => {
|
|
8246
|
+
preference.set({ subagentBackend: value });
|
|
8247
|
+
}
|
|
8248
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`subagentBackend_${value}`) })]
|
|
8249
|
+
}, value))
|
|
8250
|
+
})]
|
|
8251
|
+
})
|
|
7774
8252
|
})
|
|
7775
8253
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
|
|
7776
8254
|
preference,
|