dsh-codex-subscription 2.1.0-beta.5 → 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 +42 -5
- package/README.md +42 -5
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/client.js +2050 -1478
- package/lib/index.js +1984 -1484
- package/package.json +8 -2
package/lib/client.js
CHANGED
|
@@ -71,6 +71,40 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
return normalized;
|
|
72
72
|
}
|
|
73
73
|
//#endregion
|
|
74
|
+
//#region src/sketch-brushes.js
|
|
75
|
+
const grains = /* @__PURE__ */ new Map();
|
|
76
|
+
function pencilGrain(context, color) {
|
|
77
|
+
if (!context.createPattern || typeof document === "undefined") return color;
|
|
78
|
+
if (!grains.has(color)) {
|
|
79
|
+
const canvas = document.createElement("canvas");
|
|
80
|
+
canvas.width = 64;
|
|
81
|
+
canvas.height = 64;
|
|
82
|
+
const ctx = canvas.getContext("2d");
|
|
83
|
+
ctx.fillStyle = color;
|
|
84
|
+
let seed = 173;
|
|
85
|
+
for (let y = 0; y < 64; y++) for (let x = 0; x < 64; x++) {
|
|
86
|
+
seed = Math.imul(seed, 1664525) + 1013904223 >>> 0;
|
|
87
|
+
const value = seed / 4294967296;
|
|
88
|
+
if (value < .28) continue;
|
|
89
|
+
ctx.globalAlpha = .2 + value * .8;
|
|
90
|
+
ctx.fillRect(x, y, 1, 1);
|
|
91
|
+
}
|
|
92
|
+
if (grains.size >= 16) grains.delete(grains.keys().next().value);
|
|
93
|
+
grains.set(color, canvas);
|
|
94
|
+
}
|
|
95
|
+
return context.createPattern(grains.get(color), "repeat") ?? color;
|
|
96
|
+
}
|
|
97
|
+
function configureSketchBrush(context, stroke) {
|
|
98
|
+
const modern = stroke.brushVersion === 2 && stroke.shape === "pen";
|
|
99
|
+
const brush = stroke.brush ?? "pen";
|
|
100
|
+
context.lineCap = modern && brush === "marker" ? "butt" : "round";
|
|
101
|
+
context.lineJoin = "round";
|
|
102
|
+
context.globalAlpha = (stroke.opacity ?? 1) * (brush === "marker" ? .28 : brush === "pencil" ? modern ? .85 : .65 : 1);
|
|
103
|
+
context.lineWidth = stroke.width * (brush === "pencil" && !modern ? .55 : 1) * (stroke.pressure ?? 1);
|
|
104
|
+
context.strokeStyle = modern && brush === "pencil" ? pencilGrain(context, stroke.color) : stroke.color;
|
|
105
|
+
context.fillStyle = context.strokeStyle;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
74
108
|
//#region src/sketch-document.js
|
|
75
109
|
const SKETCH_SIZE = 1024;
|
|
76
110
|
const MAX_SKETCH_STROKES = 2e3;
|
|
@@ -96,10 +130,7 @@ window.__ModuleLoader__.load({
|
|
|
96
130
|
const first = stroke.points[0];
|
|
97
131
|
if (!first) continue;
|
|
98
132
|
context.globalCompositeOperation = stroke.shape === "eraser" ? "destination-out" : "source-over";
|
|
99
|
-
context
|
|
100
|
-
context.strokeStyle = stroke.color;
|
|
101
|
-
context.fillStyle = stroke.color;
|
|
102
|
-
context.lineWidth = stroke.width * (stroke.brush === "pencil" ? .55 : 1) * (stroke.pressure ?? 1);
|
|
133
|
+
configureSketchBrush(context, stroke);
|
|
103
134
|
context.beginPath();
|
|
104
135
|
const last = stroke.points.at(-1);
|
|
105
136
|
if (stroke.shape === "text") {
|
|
@@ -148,7 +179,8 @@ window.__ModuleLoader__.load({
|
|
|
148
179
|
if (stroke.fill) context.fill();
|
|
149
180
|
else context.stroke();
|
|
150
181
|
} else if (stroke.points.length === 1) {
|
|
151
|
-
context.
|
|
182
|
+
if (stroke.brushVersion === 2 && stroke.brush === "marker") context.rect(first.x * size - context.lineWidth / 2, first.y * height - context.lineWidth / 4, context.lineWidth, context.lineWidth / 2);
|
|
183
|
+
else context.arc(first.x * size, first.y * height, context.lineWidth / 2, 0, Math.PI * 2);
|
|
152
184
|
context.fill();
|
|
153
185
|
} else {
|
|
154
186
|
context.moveTo(first.x * size, first.y * height);
|
|
@@ -340,21 +372,55 @@ window.__ModuleLoader__.load({
|
|
|
340
372
|
dirty: ref(false),
|
|
341
373
|
documentId: ref(crypto.randomUUID()),
|
|
342
374
|
documentRevision: ref(0),
|
|
375
|
+
restoreId: ref(null),
|
|
376
|
+
mounts: 0,
|
|
343
377
|
agentAdapter: ref({}),
|
|
344
378
|
agentSession: ref(null),
|
|
345
379
|
agentRun: ref(null)
|
|
346
380
|
};
|
|
347
381
|
}
|
|
348
|
-
function createSketchSessionRegistry() {
|
|
349
|
-
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
|
+
};
|
|
350
395
|
return {
|
|
351
396
|
get(id) {
|
|
352
|
-
|
|
353
|
-
|
|
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;
|
|
354
414
|
},
|
|
415
|
+
prune,
|
|
416
|
+
stats: () => ({
|
|
417
|
+
resident: sessions.size,
|
|
418
|
+
archived: archived.size
|
|
419
|
+
}),
|
|
355
420
|
dispose() {
|
|
356
421
|
for (const value of sessions.values()) value.agentRun.current?.dispose();
|
|
357
422
|
sessions.clear();
|
|
423
|
+
archived.clear();
|
|
358
424
|
}
|
|
359
425
|
};
|
|
360
426
|
}
|
|
@@ -995,55 +1061,272 @@ window.__ModuleLoader__.load({
|
|
|
995
1061
|
});
|
|
996
1062
|
}
|
|
997
1063
|
//#endregion
|
|
998
|
-
//#region src/
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1064
|
+
//#region src/workspace-icons.jsx
|
|
1065
|
+
function WorkspaceIcon({ name, size = 24 }) {
|
|
1066
|
+
const paths = {
|
|
1067
|
+
select: "M5 3l14 9-7 2-3 7-4-18z",
|
|
1068
|
+
text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
|
|
1069
|
+
arrow: "M4 20L20 4M10 4h10v10",
|
|
1070
|
+
line: "M4 20L20 4",
|
|
1071
|
+
layers: "M12 3L2 8l10 5 10-5-10-5zM2 12l10 5 10-5M2 16l10 5 10-5",
|
|
1072
|
+
eye: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM15 12a3 3 0 11-6 0 3 3 0 016 0",
|
|
1073
|
+
eyeOff: "M3 3l18 18M9 5a10 10 0 013 0c6 0 10 7 10 7l-3 4M6 6C3 8 2 12 2 12s4 7 10 7c2 0 4-1 5-2",
|
|
1074
|
+
duplicate: "M8 8h13v13H8zM16 8V3H3v13h5",
|
|
1075
|
+
up: "M12 20V4M5 11l7-7 7 7",
|
|
1076
|
+
down: "M12 4v16M5 13l7 7 7-7",
|
|
1077
|
+
pencil: "M4 20l2-6L17 3l4 4L10 18l-6 2zM14 6l4 4",
|
|
1078
|
+
marker: "M5 16l9-12 7 5-9 12-7-5zM5 16l-3 4 6 1M12 7l7 5",
|
|
1079
|
+
image: "M4 4h16v16H4zM4 16l5-5 4 4 3-3 4 4M15 8h.01",
|
|
1080
|
+
close: "M6 6l12 12M18 6L6 18",
|
|
1081
|
+
stop: "M6 6h12v12H6z",
|
|
1082
|
+
pen: "M4 17c3-7 12-15 12-11S4 19 8 19s10-10 10-6-6 8-2 7l4-3",
|
|
1083
|
+
eraser: "M4 14l9-10 7 7-9 10H9l-5-5zM8 10l7 7M11 21h10",
|
|
1084
|
+
undo: "M9 5L4 10l5 5M5 10h9a6 6 0 010 12",
|
|
1085
|
+
redo: "M15 5l5 5-5 5M19 10h-9a6 6 0 000 12",
|
|
1086
|
+
check: "M5 12l5 5 9-11",
|
|
1087
|
+
clear: "M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13",
|
|
1088
|
+
rectangle: "M5 5h14v14H5z",
|
|
1089
|
+
circle: "M20 12a8 8 0 11-16 0 8 8 0 0116 0"
|
|
1090
|
+
};
|
|
1091
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1092
|
+
width: size,
|
|
1093
|
+
height: size,
|
|
1094
|
+
viewBox: "0 0 24 24",
|
|
1095
|
+
fill: "none",
|
|
1096
|
+
stroke: "currentColor",
|
|
1097
|
+
strokeWidth: "1.8",
|
|
1098
|
+
strokeLinecap: "round",
|
|
1099
|
+
strokeLinejoin: "round",
|
|
1100
|
+
"aria-hidden": "true",
|
|
1101
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: paths[name] ?? paths.pen })
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
//#endregion
|
|
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"),
|
|
1111
|
+
children: [
|
|
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", {
|
|
1113
|
+
type: "button",
|
|
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", {
|
|
1181
|
+
type: "button",
|
|
1182
|
+
className: "codexSketchClearLayer",
|
|
1183
|
+
disabled: disabled || !current.strokes.length && !current.image,
|
|
1184
|
+
onClick: () => change("clear"),
|
|
1185
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
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",
|
|
1234
|
+
type: "button",
|
|
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
|
|
1278
|
+
]
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
//#endregion
|
|
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
|
+
})));
|
|
1047
1330
|
}
|
|
1048
1331
|
//#endregion
|
|
1049
1332
|
//#region src/sketch-input.js
|
|
@@ -1075,39 +1358,192 @@ window.__ModuleLoader__.load({
|
|
|
1075
1358
|
});
|
|
1076
1359
|
}
|
|
1077
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
|
|
1078
1438
|
//#region src/sketch-drafts.js
|
|
1079
1439
|
const DATABASE = "dsh-codex-sketches-v1";
|
|
1080
|
-
|
|
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) {
|
|
1081
1450
|
const db = await new Promise((resolve, reject) => {
|
|
1082
|
-
|
|
1083
|
-
request
|
|
1084
|
-
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
|
+
};
|
|
1085
1483
|
request.onerror = () => reject(request.error);
|
|
1086
1484
|
});
|
|
1087
1485
|
try {
|
|
1088
1486
|
return await new Promise((resolve, reject) => {
|
|
1089
|
-
const
|
|
1090
|
-
|
|
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;
|
|
1091
1504
|
tx.oncomplete = () => resolve(result);
|
|
1092
1505
|
tx.onerror = () => reject(tx.error);
|
|
1093
|
-
tx.onabort = () => reject(tx.error ?? Error("Draft
|
|
1094
|
-
|
|
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();
|
|
1095
1528
|
request.onsuccess = () => {
|
|
1096
1529
|
const rows = request.result;
|
|
1097
1530
|
if (action === "list") {
|
|
1098
|
-
result = rows.sort((a, b) => b.updated - a.updated);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (action === "delete") {
|
|
1102
|
-
store.delete(value);
|
|
1531
|
+
result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
|
|
1103
1532
|
return;
|
|
1104
1533
|
}
|
|
1105
|
-
const others = rows.filter((row) => row.
|
|
1106
|
-
|
|
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 });
|
|
1107
1538
|
tx.abort();
|
|
1108
1539
|
return;
|
|
1109
1540
|
}
|
|
1110
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
|
+
}
|
|
1111
1547
|
result = value;
|
|
1112
1548
|
};
|
|
1113
1549
|
});
|
|
@@ -1149,846 +1585,1118 @@ window.__ModuleLoader__.load({
|
|
|
1149
1585
|
bitmap.close();
|
|
1150
1586
|
}
|
|
1151
1587
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1588
|
+
const SKETCH_COMMAND_HELP = {
|
|
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.",
|
|
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.",
|
|
1591
|
+
commands: {
|
|
1592
|
+
stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
|
|
1593
|
+
layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
|
|
1594
|
+
curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
|
|
1595
|
+
object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
|
|
1596
|
+
resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
|
|
1597
|
+
},
|
|
1598
|
+
limits: {
|
|
1599
|
+
strokes: MAX_SKETCH_STROKES,
|
|
1600
|
+
pointsPerStroke: MAX_STROKE_POINTS,
|
|
1601
|
+
pointsTotal: 2e5,
|
|
1602
|
+
commandsPerBatch: 256
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
1606
|
+
function applySketchCommands(source, commands) {
|
|
1607
|
+
if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
|
|
1608
|
+
let doc = identifyObjects(source);
|
|
1609
|
+
for (const command of commands) {
|
|
1610
|
+
if (!command || typeof command !== "object") throw Error("Invalid command");
|
|
1611
|
+
if (command.op === "resize") {
|
|
1612
|
+
doc = resizeSketch(doc, command.ratio);
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (command.op === "layer") {
|
|
1616
|
+
if (![
|
|
1617
|
+
"add",
|
|
1618
|
+
"select",
|
|
1619
|
+
"rename",
|
|
1620
|
+
"visible",
|
|
1621
|
+
"duplicate",
|
|
1622
|
+
"up",
|
|
1623
|
+
"down",
|
|
1624
|
+
"delete",
|
|
1625
|
+
"clear"
|
|
1626
|
+
].includes(command.action)) throw Error("Unknown layer action");
|
|
1627
|
+
if (command.action === "add") {
|
|
1628
|
+
const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
|
|
1629
|
+
if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
|
|
1630
|
+
const next = changeSketchLayer(doc, "add", after);
|
|
1631
|
+
if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
|
|
1632
|
+
doc = {
|
|
1633
|
+
...next,
|
|
1634
|
+
active: id,
|
|
1635
|
+
nextId: Math.max(next.nextId, id + 1),
|
|
1636
|
+
layers: next.layers.map((l) => l.id === next.active ? {
|
|
1637
|
+
...l,
|
|
1638
|
+
id,
|
|
1639
|
+
name: String(command.value ?? "").trim().slice(0, 40)
|
|
1640
|
+
} : l)
|
|
1641
|
+
};
|
|
1642
|
+
continue;
|
|
1168
1643
|
}
|
|
1644
|
+
const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
|
|
1645
|
+
if (next === doc) throw Error("Layer action unavailable; inspect the document first");
|
|
1646
|
+
doc = next;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (command.op === "object") {
|
|
1650
|
+
const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
|
|
1651
|
+
if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
|
|
1652
|
+
const strokes = layer.strokes.slice(), original = strokes[index];
|
|
1653
|
+
if (command.action === "delete") strokes.splice(index, 1);
|
|
1654
|
+
else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
|
|
1655
|
+
...original,
|
|
1656
|
+
id: crypto.randomUUID(),
|
|
1657
|
+
points: original.points.map((p) => ({ ...p }))
|
|
1658
|
+
});
|
|
1659
|
+
else if (command.action === "update") {
|
|
1660
|
+
const patch = command.patch ?? {};
|
|
1661
|
+
if (Object.keys(patch).some((k) => ![
|
|
1662
|
+
"color",
|
|
1663
|
+
"width",
|
|
1664
|
+
"opacity",
|
|
1665
|
+
"fill",
|
|
1666
|
+
"text",
|
|
1667
|
+
"points"
|
|
1668
|
+
].includes(k))) throw Error("Unsupported object property");
|
|
1669
|
+
const changed = command.transform ? transformObject({
|
|
1670
|
+
...original,
|
|
1671
|
+
...patch
|
|
1672
|
+
}, command.transform) : {
|
|
1673
|
+
...original,
|
|
1674
|
+
...patch
|
|
1675
|
+
};
|
|
1676
|
+
strokes[index] = {
|
|
1677
|
+
...applySketchCommands({
|
|
1678
|
+
...doc,
|
|
1679
|
+
layers: [{
|
|
1680
|
+
...layer,
|
|
1681
|
+
strokes: []
|
|
1682
|
+
}]
|
|
1683
|
+
}, [{
|
|
1684
|
+
...changed,
|
|
1685
|
+
op: "stroke",
|
|
1686
|
+
layer: layer.id
|
|
1687
|
+
}]).layers[0].strokes[0],
|
|
1688
|
+
brush: original.brush ?? "pen",
|
|
1689
|
+
...original.pressure !== void 0 ? { pressure: original.pressure } : {},
|
|
1690
|
+
...original.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
1691
|
+
};
|
|
1692
|
+
} else throw Error("Unknown object action");
|
|
1693
|
+
doc = {
|
|
1694
|
+
...doc,
|
|
1695
|
+
layers: doc.layers.map((l) => l === layer ? {
|
|
1696
|
+
...l,
|
|
1697
|
+
strokes
|
|
1698
|
+
} : l)
|
|
1699
|
+
};
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (command.op !== "stroke") throw Error("Unknown command");
|
|
1703
|
+
const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
|
|
1704
|
+
const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
|
|
1705
|
+
let points = command.points;
|
|
1706
|
+
if (command.start !== void 0 || command.segments !== void 0) {
|
|
1707
|
+
if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
|
|
1708
|
+
points = [command.start, ...command.segments.flatMap((s) => [
|
|
1709
|
+
s?.control1,
|
|
1710
|
+
s?.control2,
|
|
1711
|
+
s?.end
|
|
1712
|
+
])];
|
|
1713
|
+
}
|
|
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
|
+
}))
|
|
1169
1757
|
};
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
};
|
|
1177
|
-
const hidden = () => latest.current(false);
|
|
1178
|
-
document.addEventListener("pointerdown", pointer, true);
|
|
1179
|
-
document.addEventListener("keydown", key, true);
|
|
1180
|
-
dialog.addEventListener("close", hidden);
|
|
1181
|
-
return () => {
|
|
1182
|
-
document.removeEventListener("pointerdown", pointer, true);
|
|
1183
|
-
document.removeEventListener("keydown", key, true);
|
|
1184
|
-
dialog.removeEventListener("close", hidden);
|
|
1758
|
+
doc = {
|
|
1759
|
+
...doc,
|
|
1760
|
+
layers: doc.layers.map((item) => item === layer ? {
|
|
1761
|
+
...item,
|
|
1762
|
+
strokes: [...item.strokes, stroke]
|
|
1763
|
+
} : item)
|
|
1185
1764
|
};
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
selectors.join("|")
|
|
1190
|
-
]);
|
|
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;
|
|
1191
1768
|
}
|
|
1192
|
-
function
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
if (!
|
|
1198
|
-
const
|
|
1199
|
-
if (
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
+
};
|
|
1202
1789
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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);
|
|
1235
1855
|
}
|
|
1856
|
+
return result;
|
|
1236
1857
|
};
|
|
1237
1858
|
}
|
|
1238
1859
|
//#endregion
|
|
1239
|
-
//#region src/sketch-
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
pan: " ",
|
|
1247
|
-
zoomIn: "=",
|
|
1248
|
-
zoomOut: "-",
|
|
1249
|
-
fit: "0"
|
|
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;
|
|
1250
1867
|
};
|
|
1251
|
-
function
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
|
1261
1957
|
};
|
|
1262
|
-
} catch {
|
|
1263
|
-
return DEFAULT_KEYS;
|
|
1264
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)
|
|
1976
|
+
};
|
|
1265
1977
|
});
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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)
|
|
1283
2000
|
});
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
return doc;
|
|
2035
|
+
}
|
|
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)
|
|
1315
2048
|
};
|
|
1316
|
-
setKeys(next);
|
|
1317
2049
|
try {
|
|
1318
|
-
|
|
1319
|
-
} catch {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
setKey,
|
|
1329
|
-
toggle: () => setShortcuts((v) => {
|
|
1330
|
-
try {
|
|
1331
|
-
localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
|
|
1332
|
-
} catch {}
|
|
1333
|
-
return !v;
|
|
1334
|
-
}),
|
|
1335
|
-
keyDown: (e) => {
|
|
1336
|
-
if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
1337
|
-
const key = e.key.toLowerCase();
|
|
1338
|
-
if (key === keys.pan && !e.ctrlKey && !e.metaKey) {
|
|
1339
|
-
e.preventDefault();
|
|
1340
|
-
setSpace(true);
|
|
1341
|
-
return true;
|
|
1342
|
-
}
|
|
1343
|
-
if (key === keys.zoomIn || key === "+" || key === keys.zoomOut || key === keys.fit) {
|
|
1344
|
-
e.preventDefault();
|
|
1345
|
-
if (key === keys.fit) reset();
|
|
1346
|
-
else zoom(key === keys.zoomOut ? 1 / 1.2 : 1.2);
|
|
1347
|
-
return true;
|
|
1348
|
-
}
|
|
1349
|
-
return false;
|
|
1350
|
-
},
|
|
1351
|
-
keyUp: (e) => {
|
|
1352
|
-
if (e.key.toLowerCase() === keys.pan) setSpace(false);
|
|
1353
|
-
},
|
|
1354
|
-
down: (e) => {
|
|
1355
|
-
if (e.button !== 1 && !space) return false;
|
|
1356
|
-
e.preventDefault();
|
|
1357
|
-
drag.current = {
|
|
1358
|
-
id: e.pointerId,
|
|
1359
|
-
x: e.clientX,
|
|
1360
|
-
y: e.clientY,
|
|
1361
|
-
view: viewRef.current
|
|
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
|
|
1362
2060
|
};
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
const d = drag.current;
|
|
1368
|
-
if (!d || d.id !== e.pointerId) return false;
|
|
1369
|
-
setView({
|
|
1370
|
-
...d.view,
|
|
1371
|
-
x: d.view.x + e.clientX - d.x,
|
|
1372
|
-
y: d.view.y + e.clientY - d.y
|
|
1373
|
-
});
|
|
1374
|
-
return true;
|
|
1375
|
-
},
|
|
1376
|
-
end: (e) => {
|
|
1377
|
-
if (drag.current?.id !== e.pointerId) return false;
|
|
1378
|
-
drag.current = null;
|
|
1379
|
-
if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
|
|
1380
|
-
return true;
|
|
2061
|
+
if (documentRevision.current === savingRevision) {
|
|
2062
|
+
dirty.current = false;
|
|
2063
|
+
setRecovered(false);
|
|
2064
|
+
}
|
|
1381
2065
|
}
|
|
1382
2066
|
};
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
|
2105
|
+
});
|
|
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));
|
|
2160
|
+
}
|
|
2161
|
+
state.restoreId.current = null;
|
|
1390
2162
|
};
|
|
1391
|
-
useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
|
|
1392
|
-
(0, react.useLayoutEffect)(() => {
|
|
1393
|
-
if (!open) return;
|
|
1394
|
-
const dialog = host.current.closest("dialog");
|
|
1395
|
-
const place = () => {
|
|
1396
|
-
const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
|
|
1397
|
-
const width = Math.min(360, box.width - 24);
|
|
1398
|
-
setPlacement({
|
|
1399
|
-
dialog,
|
|
1400
|
-
style: {
|
|
1401
|
-
width,
|
|
1402
|
-
left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
|
|
1403
|
-
bottom: box.bottom - anchor.top + 8,
|
|
1404
|
-
maxHeight: Math.max(80, anchor.top - box.top - 24)
|
|
1405
|
-
}
|
|
1406
|
-
});
|
|
1407
|
-
};
|
|
1408
|
-
place();
|
|
1409
|
-
const observer = new ResizeObserver(place);
|
|
1410
|
-
observer.observe(dialog);
|
|
1411
|
-
observer.observe(host.current);
|
|
1412
|
-
window.addEventListener("resize", place);
|
|
1413
|
-
return () => {
|
|
1414
|
-
observer.disconnect();
|
|
1415
|
-
window.removeEventListener("resize", place);
|
|
1416
|
-
};
|
|
1417
|
-
}, [open]);
|
|
1418
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1419
|
-
ref: host,
|
|
1420
|
-
className: "codexSketchViewControls",
|
|
1421
|
-
children: [
|
|
1422
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1423
|
-
type: "button",
|
|
1424
|
-
"aria-label": t("sketchZoomOut"),
|
|
1425
|
-
onClick: () => navigation.zoom(1 / 1.2),
|
|
1426
|
-
children: "−"
|
|
1427
|
-
}),
|
|
1428
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1429
|
-
type: "button",
|
|
1430
|
-
title: t("sketchFit"),
|
|
1431
|
-
onClick: navigation.reset,
|
|
1432
|
-
children: [Math.round(navigation.view.scale * 100), "%"]
|
|
1433
|
-
}),
|
|
1434
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1435
|
-
type: "button",
|
|
1436
|
-
"aria-label": t("sketchZoomIn"),
|
|
1437
|
-
onClick: () => navigation.zoom(1.2),
|
|
1438
|
-
children: "+"
|
|
1439
|
-
}),
|
|
1440
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1441
|
-
ref: trigger,
|
|
1442
|
-
type: "button",
|
|
1443
|
-
"aria-expanded": open,
|
|
1444
|
-
onClick: () => setOpen(!open),
|
|
1445
|
-
children: t("sketchKeys")
|
|
1446
|
-
}),
|
|
1447
|
-
open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1448
|
-
className: "codexSketchKeyPanel",
|
|
1449
|
-
"aria-label": t("sketchKeys"),
|
|
1450
|
-
style: placement.style,
|
|
1451
|
-
onKeyDown: (e) => {
|
|
1452
|
-
if (e.key === "Escape") {
|
|
1453
|
-
e.preventDefault();
|
|
1454
|
-
e.stopPropagation();
|
|
1455
|
-
close();
|
|
1456
|
-
}
|
|
1457
|
-
},
|
|
1458
|
-
children: [
|
|
1459
|
-
/* @__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", {
|
|
1460
|
-
type: "button",
|
|
1461
|
-
"aria-label": t("sketchFileClose"),
|
|
1462
|
-
onClick: close,
|
|
1463
|
-
children: "×"
|
|
1464
|
-
})] }),
|
|
1465
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1466
|
-
className: "codexSketchKeysEnabled",
|
|
1467
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1468
|
-
type: "checkbox",
|
|
1469
|
-
checked: navigation.shortcuts,
|
|
1470
|
-
onChange: navigation.toggle
|
|
1471
|
-
})]
|
|
1472
|
-
}),
|
|
1473
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
|
|
1474
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1475
|
-
className: "codexSketchKeyGrid",
|
|
1476
|
-
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", {
|
|
1477
|
-
"aria-label": t(`sketchKey_${action}`),
|
|
1478
|
-
value: key === " " ? "Space" : key,
|
|
1479
|
-
readOnly: true,
|
|
1480
|
-
onKeyDown: (e) => {
|
|
1481
|
-
if (e.key === "Tab" || e.key === "Escape") return;
|
|
1482
|
-
e.preventDefault();
|
|
1483
|
-
e.stopPropagation();
|
|
1484
|
-
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
|
|
1485
|
-
}
|
|
1486
|
-
})] }, action))
|
|
1487
|
-
}),
|
|
1488
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
|
|
1489
|
-
]
|
|
1490
|
-
}), placement.dialog) : null
|
|
1491
|
-
]
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
//#endregion
|
|
1495
|
-
//#region src/sketch-objects.js
|
|
1496
|
-
const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
|
|
1497
|
-
const identifyObjects = (doc) => ({
|
|
1498
|
-
...doc,
|
|
1499
|
-
layers: doc.layers.map((layer) => ({
|
|
1500
|
-
...layer,
|
|
1501
|
-
strokes: layer.strokes.map((s, i) => s.id ? s : {
|
|
1502
|
-
...s,
|
|
1503
|
-
id: objectId(s, i)
|
|
1504
|
-
})
|
|
1505
|
-
}))
|
|
1506
|
-
});
|
|
1507
|
-
function objectBounds(stroke) {
|
|
1508
|
-
const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
|
|
1509
2163
|
return {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2164
|
+
hasContent,
|
|
2165
|
+
save,
|
|
2166
|
+
saveChanges,
|
|
2167
|
+
replace,
|
|
2168
|
+
fresh,
|
|
2169
|
+
load,
|
|
2170
|
+
importImage,
|
|
2171
|
+
restore
|
|
1514
2172
|
};
|
|
1515
2173
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
scaleX,
|
|
1521
|
-
scaleY
|
|
1522
|
-
].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
|
|
1523
|
-
const box = objectBounds(stroke);
|
|
1524
|
-
const points = stroke.points.map((p) => ({
|
|
1525
|
-
x: box.x + (p.x - box.x) * scaleX + dx,
|
|
1526
|
-
y: box.y + (p.y - box.y) * scaleY + dy
|
|
1527
|
-
}));
|
|
1528
|
-
if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
|
|
2174
|
+
//#endregion
|
|
2175
|
+
//#region src/sketch-operation-gate.js
|
|
2176
|
+
function createSketchOperationGate() {
|
|
2177
|
+
let running = false;
|
|
1529
2178
|
return {
|
|
1530
|
-
|
|
1531
|
-
|
|
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;
|
|
2185
|
+
try {
|
|
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;
|
|
2194
|
+
} finally {
|
|
2195
|
+
running = false;
|
|
2196
|
+
working(false);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1532
2199
|
};
|
|
1533
2200
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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)
|
|
2214
|
+
};
|
|
2215
|
+
}, {
|
|
2216
|
+
blocked,
|
|
2217
|
+
working,
|
|
2218
|
+
report,
|
|
2219
|
+
rethrow: true
|
|
2220
|
+
})) throw Error("Sketch is being edited; retry after it settles");
|
|
2221
|
+
return result;
|
|
1543
2222
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
|
1560
2280
|
};
|
|
1561
|
-
const
|
|
1562
|
-
function
|
|
1563
|
-
if (
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
...doc,
|
|
1650
|
-
layers: doc.layers.map((l) => l === layer ? {
|
|
1651
|
-
...l,
|
|
1652
|
-
strokes
|
|
1653
|
-
} : l)
|
|
1654
|
-
};
|
|
1655
|
-
continue;
|
|
1656
|
-
}
|
|
1657
|
-
if (command.op !== "stroke") throw Error("Unknown command");
|
|
1658
|
-
const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
|
|
1659
|
-
const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
|
|
1660
|
-
let points = command.points;
|
|
1661
|
-
if (command.start !== void 0 || command.segments !== void 0) {
|
|
1662
|
-
if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
|
|
1663
|
-
points = [command.start, ...command.segments.flatMap((s) => [
|
|
1664
|
-
s?.control1,
|
|
1665
|
-
s?.control2,
|
|
1666
|
-
s?.end
|
|
1667
|
-
])];
|
|
1668
|
-
}
|
|
1669
|
-
if (![
|
|
1670
|
-
"pen",
|
|
1671
|
-
"line",
|
|
1672
|
-
"rectangle",
|
|
1673
|
-
"circle",
|
|
1674
|
-
"polygon",
|
|
1675
|
-
"bezier",
|
|
1676
|
-
"arrow",
|
|
1677
|
-
"text",
|
|
1678
|
-
"eraser"
|
|
1679
|
-
].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");
|
|
1680
|
-
if ([
|
|
1681
|
-
"line",
|
|
1682
|
-
"arrow",
|
|
1683
|
-
"text",
|
|
1684
|
-
"rectangle",
|
|
1685
|
-
"circle"
|
|
1686
|
-
].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
|
|
1687
|
-
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.`);
|
|
1688
|
-
if (fill && ![
|
|
1689
|
-
"rectangle",
|
|
1690
|
-
"circle",
|
|
1691
|
-
"polygon",
|
|
1692
|
-
"bezier"
|
|
1693
|
-
].includes(shape)) throw Error("Fill requires a closed shape");
|
|
1694
|
-
const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
|
|
1695
|
-
if (!layer?.visible) throw Error("Target layer is missing or hidden");
|
|
1696
|
-
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");
|
|
1697
|
-
const id = command.id ?? crypto.randomUUID();
|
|
1698
|
-
if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
|
|
1699
|
-
const stroke = {
|
|
1700
|
-
id,
|
|
1701
|
-
...shape === "text" ? { text: command.text } : {},
|
|
1702
|
-
shape,
|
|
1703
|
-
color,
|
|
1704
|
-
width,
|
|
1705
|
-
opacity,
|
|
1706
|
-
fill,
|
|
1707
|
-
brush: "pen",
|
|
1708
|
-
points: points.map((p) => ({
|
|
1709
|
-
x: p.x,
|
|
1710
|
-
y: p.y
|
|
1711
|
-
}))
|
|
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);
|
|
2336
|
+
}
|
|
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
|
+
}
|
|
1712
2369
|
};
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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 });
|
|
1719
2376
|
};
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
+
]);
|
|
1723
2391
|
}
|
|
1724
|
-
function
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
if (!
|
|
1730
|
-
const
|
|
1731
|
-
if (
|
|
1732
|
-
|
|
1733
|
-
|
|
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;
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
//#endregion
|
|
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"
|
|
2449
|
+
};
|
|
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 {
|
|
1734
2457
|
return {
|
|
1735
|
-
...
|
|
1736
|
-
|
|
1737
|
-
objects: objects.slice(offset, offset + 50),
|
|
1738
|
-
objectCount: objects.length,
|
|
1739
|
-
...offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
|
|
1740
|
-
...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
|
|
1741
|
-
recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
|
|
1742
|
-
help: SKETCH_COMMAND_HELP
|
|
2458
|
+
...DEFAULT_KEYS,
|
|
2459
|
+
...JSON.parse(localStorage.getItem("codex-sketch-keys"))
|
|
1743
2460
|
};
|
|
2461
|
+
} catch {
|
|
2462
|
+
return DEFAULT_KEYS;
|
|
1744
2463
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
|
|
1752
|
-
if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
|
|
1753
|
-
const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
|
|
1754
|
-
...request,
|
|
1755
|
-
runId: void 0
|
|
1756
|
-
});
|
|
1757
|
-
const cached = completed.get(key);
|
|
1758
|
-
if (cached) {
|
|
1759
|
-
if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
|
|
1760
|
-
return cached.result;
|
|
1761
|
-
}
|
|
1762
|
-
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
1763
|
-
let changedObjects;
|
|
1764
|
-
if (request.action === "apply") {
|
|
1765
|
-
const before = adapter.document();
|
|
1766
|
-
let next;
|
|
1767
|
-
try {
|
|
1768
|
-
next = applySketchCommands(before, request.commands);
|
|
1769
|
-
} catch (cause) {
|
|
1770
|
-
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
1771
|
-
error.code = "SKETCH_INVALID_BATCH";
|
|
1772
|
-
throw error;
|
|
1773
|
-
}
|
|
1774
|
-
adapter.commit(next);
|
|
1775
|
-
const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
|
|
1776
|
-
changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
|
|
1777
|
-
layer: l.id,
|
|
1778
|
-
id: s.id
|
|
1779
|
-
})));
|
|
1780
|
-
} else {
|
|
1781
|
-
if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
|
|
1782
|
-
pending = true;
|
|
1783
|
-
try {
|
|
1784
|
-
await adapter.save(request.name);
|
|
1785
|
-
} finally {
|
|
1786
|
-
pending = false;
|
|
1787
|
-
}
|
|
2464
|
+
});
|
|
2465
|
+
const [shortcuts, setShortcuts] = (0, react.useState)(() => {
|
|
2466
|
+
try {
|
|
2467
|
+
return localStorage.getItem("codex-sketch-shortcuts") !== "off";
|
|
2468
|
+
} catch {
|
|
2469
|
+
return true;
|
|
1788
2470
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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
|
|
2482
|
+
});
|
|
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);
|
|
1795
2498
|
};
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
cachedCharacters += fingerprint.length;
|
|
1806
|
-
while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
|
|
1807
|
-
const oldest = completed.keys().next().value;
|
|
1808
|
-
cachedCharacters -= completed.get(oldest).fingerprint.length;
|
|
1809
|
-
completed.delete(oldest);
|
|
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;
|
|
1810
2506
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
const c = document.createElement("canvas");
|
|
1819
|
-
c.width = w;
|
|
1820
|
-
c.height = h;
|
|
1821
|
-
return c;
|
|
1822
|
-
};
|
|
1823
|
-
function runPsdCodec(action, payload) {
|
|
1824
|
-
return new Promise((resolve, reject) => {
|
|
1825
|
-
const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
|
|
1826
|
-
const finish = (callback, value) => {
|
|
1827
|
-
clearTimeout(timer);
|
|
1828
|
-
worker.terminate();
|
|
1829
|
-
callback(value);
|
|
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
|
|
1830
2514
|
};
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
action,
|
|
1836
|
-
payload
|
|
1837
|
-
});
|
|
1838
|
-
});
|
|
1839
|
-
}
|
|
1840
|
-
function encodeSketchDocument(doc) {
|
|
1841
|
-
return JSON.stringify({
|
|
1842
|
-
format: "dsh-sketch",
|
|
1843
|
-
version: 1,
|
|
1844
|
-
doc
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
function decodeSketchDocument(text) {
|
|
1848
|
-
if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
|
|
1849
|
-
const file = JSON.parse(text), source = file.doc;
|
|
1850
|
-
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");
|
|
1851
|
-
const w = source.width ?? 1024, h = source.height ?? 1024;
|
|
1852
|
-
if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
|
|
1853
|
-
let doc = {
|
|
1854
|
-
...createSketchLayers(),
|
|
1855
|
-
width: w,
|
|
1856
|
-
height: h,
|
|
1857
|
-
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
|
|
2515
|
+
setKeys(next);
|
|
2516
|
+
try {
|
|
2517
|
+
localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
|
|
2518
|
+
} catch {}
|
|
1858
2519
|
};
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
"marker"
|
|
1881
|
-
].includes(s.brush)) throw Error("Invalid brush");
|
|
1882
|
-
if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
|
|
1883
|
-
Object.assign(added[added.length - strokes.length + k], {
|
|
1884
|
-
brush: s.brush ?? "pen",
|
|
1885
|
-
pressure: s.pressure ?? 1
|
|
1886
|
-
});
|
|
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;
|
|
1887
2541
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
"
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
if (
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
|
1910
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;
|
|
1911
2584
|
}
|
|
1912
|
-
}
|
|
1913
|
-
const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
|
|
1914
|
-
doc.active = doc.layers[Math.max(0, activeIndex)].id;
|
|
1915
|
-
return doc;
|
|
2585
|
+
};
|
|
1916
2586
|
}
|
|
1917
|
-
|
|
1918
|
-
const
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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
|
+
});
|
|
1929
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);
|
|
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
|
+
]
|
|
1930
2695
|
});
|
|
1931
|
-
const context = canvas(width, height).getContext("2d");
|
|
1932
|
-
context.fillStyle = "#fff";
|
|
1933
|
-
context.fillRect(0, 0, width, height);
|
|
1934
|
-
if (children[0] && !children[0].hidden) {
|
|
1935
|
-
const bottom = canvas(width, height);
|
|
1936
|
-
bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
|
|
1937
|
-
context.drawImage(bottom, 0, 0);
|
|
1938
|
-
children[0].imageData = context.getImageData(0, 0, width, height);
|
|
1939
|
-
} else {
|
|
1940
|
-
if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
|
|
1941
|
-
children.unshift({
|
|
1942
|
-
name: "Paper",
|
|
1943
|
-
opacity: 1,
|
|
1944
|
-
blendMode: "normal",
|
|
1945
|
-
imageData: context.getImageData(0, 0, width, height)
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
|
-
return runPsdCodec("write", {
|
|
1949
|
-
width,
|
|
1950
|
-
height,
|
|
1951
|
-
children,
|
|
1952
|
-
imageData: composite.getContext("2d").getImageData(0, 0, width, height)
|
|
1953
|
-
});
|
|
1954
|
-
}
|
|
1955
|
-
async function importSketchPsd(file) {
|
|
1956
|
-
if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
|
|
1957
|
-
const psd = await runPsdCodec("read", await file.arrayBuffer());
|
|
1958
|
-
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));
|
|
1959
|
-
const doc = {
|
|
1960
|
-
...createSketchLayers(),
|
|
1961
|
-
width,
|
|
1962
|
-
height,
|
|
1963
|
-
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
|
|
1964
|
-
layers: [],
|
|
1965
|
-
nextId: psd.layers.length + 1
|
|
1966
|
-
};
|
|
1967
|
-
for (const [i, layer] of psd.layers.entries()) {
|
|
1968
|
-
const src = canvas(layer.imageData.width, layer.imageData.height);
|
|
1969
|
-
src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
|
|
1970
|
-
const out = canvas(width, height), ctx = out.getContext("2d");
|
|
1971
|
-
ctx.globalAlpha = layer.opacity;
|
|
1972
|
-
ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
|
|
1973
|
-
doc.layers.push({
|
|
1974
|
-
id: i + 1,
|
|
1975
|
-
name: layer.name,
|
|
1976
|
-
visible: !layer.hidden,
|
|
1977
|
-
strokes: [],
|
|
1978
|
-
image: {
|
|
1979
|
-
src: out.toDataURL("image/png"),
|
|
1980
|
-
x: 0,
|
|
1981
|
-
y: 0,
|
|
1982
|
-
width: 1,
|
|
1983
|
-
height: 1
|
|
1984
|
-
}
|
|
1985
|
-
});
|
|
1986
|
-
}
|
|
1987
|
-
return doc;
|
|
1988
2696
|
}
|
|
1989
2697
|
//#endregion
|
|
1990
2698
|
//#region src/sketch-files.jsx
|
|
1991
|
-
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t,
|
|
2699
|
+
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
|
|
1992
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);
|
|
1993
2701
|
const [format, setFormat] = (0, react.useState)("png");
|
|
1994
2702
|
const input = (0, react.useRef)(null), host = (0, react.useRef)(null);
|
|
@@ -2000,19 +2708,14 @@ window.__ModuleLoader__.load({
|
|
|
2000
2708
|
}
|
|
2001
2709
|
}, [open, working]);
|
|
2002
2710
|
useSketchDismiss(open, setOpen, host, [".codexSketchFiles"]);
|
|
2003
|
-
const run = async (
|
|
2004
|
-
if (disabled || working) return;
|
|
2711
|
+
const run = (operation) => runOperation(async () => {
|
|
2005
2712
|
setWorking(true);
|
|
2006
|
-
onWorking(true);
|
|
2007
2713
|
try {
|
|
2008
2714
|
await operation();
|
|
2009
|
-
} catch (error) {
|
|
2010
|
-
report(error?.message || t("sketchStorageFailed"));
|
|
2011
2715
|
} finally {
|
|
2012
2716
|
setWorking(false);
|
|
2013
|
-
onWorking(false);
|
|
2014
2717
|
}
|
|
2015
|
-
};
|
|
2718
|
+
});
|
|
2016
2719
|
const refresh = async () => setRows(await sketchDrafts("list"));
|
|
2017
2720
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2018
2721
|
ref: host,
|
|
@@ -2154,46 +2857,6 @@ window.__ModuleLoader__.load({
|
|
|
2154
2857
|
});
|
|
2155
2858
|
}
|
|
2156
2859
|
//#endregion
|
|
2157
|
-
//#region src/workspace-icons.jsx
|
|
2158
|
-
function WorkspaceIcon({ name, size = 24 }) {
|
|
2159
|
-
const paths = {
|
|
2160
|
-
select: "M5 3l14 9-7 2-3 7-4-18z",
|
|
2161
|
-
text: "M4 5h16M12 5v15M8 20h8M4 5v3M20 5v3",
|
|
2162
|
-
arrow: "M4 20L20 4M10 4h10v10",
|
|
2163
|
-
line: "M4 20L20 4",
|
|
2164
|
-
layers: "M12 3L2 8l10 5 10-5-10-5zM2 12l10 5 10-5M2 16l10 5 10-5",
|
|
2165
|
-
eye: "M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12zM15 12a3 3 0 11-6 0 3 3 0 016 0",
|
|
2166
|
-
eyeOff: "M3 3l18 18M9 5a10 10 0 013 0c6 0 10 7 10 7l-3 4M6 6C3 8 2 12 2 12s4 7 10 7c2 0 4-1 5-2",
|
|
2167
|
-
duplicate: "M8 8h13v13H8zM16 8V3H3v13h5",
|
|
2168
|
-
up: "M12 20V4M5 11l7-7 7 7",
|
|
2169
|
-
down: "M12 4v16M5 13l7 7 7-7",
|
|
2170
|
-
pencil: "M4 20l2-6L17 3l4 4L10 18l-6 2zM14 6l4 4",
|
|
2171
|
-
marker: "M5 16l9-12 7 5-9 12-7-5zM5 16l-3 4 6 1M12 7l7 5",
|
|
2172
|
-
image: "M4 4h16v16H4zM4 16l5-5 4 4 3-3 4 4M15 8h.01",
|
|
2173
|
-
close: "M6 6l12 12M18 6L6 18",
|
|
2174
|
-
pen: "M4 17c3-7 12-15 12-11S4 19 8 19s10-10 10-6-6 8-2 7l4-3",
|
|
2175
|
-
eraser: "M4 14l9-10 7 7-9 10H9l-5-5zM8 10l7 7M11 21h10",
|
|
2176
|
-
undo: "M9 5L4 10l5 5M5 10h9a6 6 0 010 12",
|
|
2177
|
-
redo: "M15 5l5 5-5 5M19 10h-9a6 6 0 000 12",
|
|
2178
|
-
check: "M5 12l5 5 9-11",
|
|
2179
|
-
clear: "M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13",
|
|
2180
|
-
rectangle: "M5 5h14v14H5z",
|
|
2181
|
-
circle: "M20 12a8 8 0 11-16 0 8 8 0 0116 0"
|
|
2182
|
-
};
|
|
2183
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2184
|
-
width: size,
|
|
2185
|
-
height: size,
|
|
2186
|
-
viewBox: "0 0 24 24",
|
|
2187
|
-
fill: "none",
|
|
2188
|
-
stroke: "currentColor",
|
|
2189
|
-
strokeWidth: "1.8",
|
|
2190
|
-
strokeLinecap: "round",
|
|
2191
|
-
strokeLinejoin: "round",
|
|
2192
|
-
"aria-hidden": "true",
|
|
2193
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: paths[name] ?? paths.pen })
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
//#endregion
|
|
2197
2860
|
//#region src/sketch-size-control.jsx
|
|
2198
2861
|
function SketchSizeControl({ value, onChange, onStart, onEnd, label, disabled, min = 2, max = 128, mode, modes, onModeChange, suffix = "" }) {
|
|
2199
2862
|
const active = (0, react.useRef)(false);
|
|
@@ -2349,13 +3012,15 @@ window.__ModuleLoader__.load({
|
|
|
2349
3012
|
//#endregion
|
|
2350
3013
|
//#region src/sketch-agent-client.js
|
|
2351
3014
|
function connectSketchAgent(rpc, sessionId, execute, report, pollDelay = () => 350) {
|
|
2352
|
-
let stopped = false, token, timer, attempts = 0, failures = 0;
|
|
3015
|
+
let stopped = false, token, timer, attempts = 0, failures = 0, pending = false;
|
|
2353
3016
|
const call = (endpoint, payload) => rpc.call(CHANNEL, `sketch/${endpoint}`, {
|
|
2354
3017
|
sessionId,
|
|
2355
3018
|
token,
|
|
2356
3019
|
...payload
|
|
2357
3020
|
}).then(unwrap);
|
|
2358
3021
|
const poll = async () => {
|
|
3022
|
+
if (stopped || pending) return;
|
|
3023
|
+
pending = true;
|
|
2359
3024
|
try {
|
|
2360
3025
|
const tasks = await call("poll");
|
|
2361
3026
|
for (const task of tasks) {
|
|
@@ -2390,25 +3055,53 @@ window.__ModuleLoader__.load({
|
|
|
2390
3055
|
timer = setTimeout(connect, Math.min(1e4, 1e3 * 2 ** (failures - 1)));
|
|
2391
3056
|
}
|
|
2392
3057
|
return;
|
|
3058
|
+
} finally {
|
|
3059
|
+
pending = false;
|
|
2393
3060
|
}
|
|
2394
3061
|
failures = 0;
|
|
2395
3062
|
if (!stopped) timer = setTimeout(poll, pollDelay());
|
|
2396
3063
|
};
|
|
2397
|
-
const connect = () =>
|
|
2398
|
-
|
|
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);
|
|
2399
3084
|
attempts = 0;
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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
|
+
}
|
|
2408
3096
|
connect();
|
|
2409
3097
|
return () => {
|
|
2410
3098
|
stopped = true;
|
|
2411
3099
|
clearTimeout(timer);
|
|
3100
|
+
if (typeof window !== "undefined") {
|
|
3101
|
+
window.removeEventListener("online", wake);
|
|
3102
|
+
window.removeEventListener("focus", wake);
|
|
3103
|
+
document.removeEventListener("visibilitychange", visible);
|
|
3104
|
+
}
|
|
2412
3105
|
if (token) call("disconnect").catch(() => {});
|
|
2413
3106
|
};
|
|
2414
3107
|
}
|
|
@@ -2435,6 +3128,7 @@ window.__ModuleLoader__.load({
|
|
|
2435
3128
|
const [stability, setStability] = (0, react.useState)(0), [flow, setFlow] = (0, react.useState)(100), [picturesOpen, setPicturesOpen] = (0, react.useState)(false);
|
|
2436
3129
|
const pictureInput = (0, react.useRef)(null), received = (0, react.useRef)(null);
|
|
2437
3130
|
const navigation = useSketchView(canvas, open);
|
|
3131
|
+
const toolWidths = (0, react.useRef)({});
|
|
2438
3132
|
const [revision, redraw] = (0, react.useState)(0), [tool, setTool] = (0, react.useState)("pen"), [brush, setBrush] = (0, react.useState)("pen");
|
|
2439
3133
|
const [eraser, setEraser] = (0, react.useState)("pixel"), [color, setColor] = (0, react.useState)("#0088ff"), [width, setWidth] = (0, react.useState)(12);
|
|
2440
3134
|
const [selection, setSelection] = (0, react.useState)(null), [textEdit, setTextEdit] = (0, react.useState)(null), [shapesOpen, setShapesOpen] = (0, react.useState)(false);
|
|
@@ -2464,8 +3158,24 @@ window.__ModuleLoader__.load({
|
|
|
2464
3158
|
setColor(value);
|
|
2465
3159
|
if (selected) editObject({ color: value });
|
|
2466
3160
|
};
|
|
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);
|
|
3173
|
+
};
|
|
3174
|
+
const chooseBrush = (name) => chooseTool("pen", name);
|
|
2467
3175
|
const [fillShape, setFillShape] = (0, react.useState)(false);
|
|
2468
|
-
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)("");
|
|
2469
3179
|
const cursorRing = (0, react.useRef)(null);
|
|
2470
3180
|
const cursor = useSketchCursor(canvas, cursorRing, width, tool === "pen" ? brush : "pen", navigation.view.scale, !open || navigation.space || busy || agentLocked || tool === "select" || tool === "text");
|
|
2471
3181
|
useSketchDismiss(shapesOpen, setShapesOpen, dialog, [".codexSketchShapeMenu", ".codexSketchShapeToggle"]);
|
|
@@ -2500,147 +3210,103 @@ window.__ModuleLoader__.load({
|
|
|
2500
3210
|
const change = (action, id, value) => {
|
|
2501
3211
|
if (busy || agentRun.current?.locked || active.current) return;
|
|
2502
3212
|
const next = changeSketchLayer(doc.current, action, id, value);
|
|
2503
|
-
if (next === doc.current) return;
|
|
2504
|
-
if (action !== "select") checkpoint();
|
|
2505
|
-
else documentRevision.current++;
|
|
2506
|
-
doc.current = next;
|
|
2507
|
-
setError("");
|
|
2508
|
-
schedule();
|
|
2509
|
-
};
|
|
2510
|
-
(0, react.useEffect)(() => {
|
|
2511
|
-
if (open) {
|
|
2512
|
-
dialog.current.showModal();
|
|
2513
|
-
canvas.current.width = doc.current.width ?? 1024;
|
|
2514
|
-
paint();
|
|
2515
|
-
} else dialog.current?.close();
|
|
2516
|
-
}, [open]);
|
|
2517
|
-
(0, react.useEffect)(() => () => {
|
|
2518
|
-
cancelAnimationFrame(frame.current);
|
|
2519
|
-
cache.current.clear();
|
|
2520
|
-
}, []);
|
|
2521
|
-
const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
|
|
2522
|
-
const save = async (name) => {
|
|
2523
|
-
const savingDocument = documentId.current, savingRevision = documentRevision.current;
|
|
2524
|
-
const row = {
|
|
2525
|
-
id: saved.current?.id ?? crypto.randomUUID(),
|
|
2526
|
-
name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
2527
|
-
updated: Date.now(),
|
|
2528
|
-
doc: structuredClone(doc.current)
|
|
2529
|
-
};
|
|
2530
|
-
await sketchDrafts("save", row);
|
|
2531
|
-
if (documentId.current === savingDocument) {
|
|
2532
|
-
saved.current = {
|
|
2533
|
-
id: row.id,
|
|
2534
|
-
name: row.name
|
|
2535
|
-
};
|
|
2536
|
-
if (documentRevision.current === savingRevision) dirty.current = false;
|
|
2537
|
-
}
|
|
2538
|
-
};
|
|
2539
|
-
const saveChanges = async () => {
|
|
2540
|
-
if (dirty.current && (hasContent() || saved.current)) await save();
|
|
2541
|
-
};
|
|
2542
|
-
const replace = (next, decoded, identity) => {
|
|
2543
|
-
documentId.current = crypto.randomUUID();
|
|
2544
|
-
documentRevision.current++;
|
|
2545
|
-
doc.current = identifyObjects(structuredClone(next));
|
|
2546
|
-
setSelection(null);
|
|
2547
|
-
setTextEdit(null);
|
|
2548
|
-
images.current = decoded;
|
|
2549
|
-
cache.current.clear();
|
|
2550
|
-
undo.current = [];
|
|
2551
|
-
redo.current = [];
|
|
2552
|
-
saved.current = identity;
|
|
2553
|
-
dirty.current = false;
|
|
2554
|
-
schedule();
|
|
2555
|
-
};
|
|
2556
|
-
const fresh = async () => {
|
|
2557
|
-
await saveChanges();
|
|
2558
|
-
replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
|
|
2559
|
-
};
|
|
2560
|
-
const load = async (row) => {
|
|
2561
|
-
if (row.id === saved.current?.id) return;
|
|
2562
|
-
await saveChanges();
|
|
2563
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2564
|
-
await decodeSketchImages(row.doc, decoded);
|
|
2565
|
-
replace(row.doc, decoded, {
|
|
2566
|
-
id: row.id,
|
|
2567
|
-
name: row.name
|
|
2568
|
-
});
|
|
2569
|
-
};
|
|
2570
|
-
const importImage = async (file) => {
|
|
2571
|
-
if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
|
|
2572
|
-
if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
|
|
2573
|
-
const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
|
|
2574
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2575
|
-
await decodeSketchImages(next, decoded);
|
|
2576
|
-
await saveChanges();
|
|
2577
|
-
replace(next, decoded, null);
|
|
2578
|
-
dirty.current = true;
|
|
2579
|
-
return;
|
|
2580
|
-
}
|
|
2581
|
-
if (doc.current.layers.length >= 8) throw Error("Layer limit");
|
|
2582
|
-
const image = await importSketchImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
|
|
2583
|
-
const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
|
|
2584
|
-
const layer = {
|
|
2585
|
-
id: doc.current.nextId,
|
|
2586
|
-
name: file.name?.slice(0, 40) || t("sketchImport"),
|
|
2587
|
-
visible: true,
|
|
2588
|
-
strokes: [],
|
|
2589
|
-
image: {
|
|
2590
|
-
src: image.src,
|
|
2591
|
-
x: (1 - width) / 2,
|
|
2592
|
-
y: (1 - height) / 2,
|
|
2593
|
-
width,
|
|
2594
|
-
height
|
|
2595
|
-
}
|
|
2596
|
-
};
|
|
2597
|
-
await decodeSketchImages({ layers: [layer] }, images.current);
|
|
2598
|
-
checkpoint();
|
|
2599
|
-
doc.current = {
|
|
2600
|
-
...doc.current,
|
|
2601
|
-
nextId: layer.id + 1,
|
|
2602
|
-
active: layer.id,
|
|
2603
|
-
layers: [...doc.current.layers, layer]
|
|
2604
|
-
};
|
|
3213
|
+
if (next === doc.current) return;
|
|
3214
|
+
if (action !== "select") checkpoint();
|
|
3215
|
+
else documentRevision.current++;
|
|
3216
|
+
doc.current = next;
|
|
3217
|
+
setError("");
|
|
2605
3218
|
schedule();
|
|
2606
3219
|
};
|
|
2607
|
-
|
|
2608
|
-
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) {
|
|
2609
3284
|
onClose();
|
|
2610
3285
|
return;
|
|
2611
3286
|
}
|
|
2612
|
-
|
|
2613
|
-
setBusy(true);
|
|
2614
|
-
try {
|
|
3287
|
+
return runFile(async () => {
|
|
2615
3288
|
await saveChanges();
|
|
2616
3289
|
onClose();
|
|
2617
|
-
}
|
|
2618
|
-
setError(t("sketchStorageFailed"));
|
|
2619
|
-
} finally {
|
|
2620
|
-
setBusy(false);
|
|
2621
|
-
}
|
|
2622
|
-
};
|
|
2623
|
-
const runFile = async (operation) => {
|
|
2624
|
-
if (busy || agentRun.current?.locked || active.current) return;
|
|
2625
|
-
setBusy(true);
|
|
2626
|
-
setError("");
|
|
2627
|
-
try {
|
|
2628
|
-
await operation();
|
|
2629
|
-
} catch {
|
|
2630
|
-
setError(t("sketchStorageFailed"));
|
|
2631
|
-
} finally {
|
|
2632
|
-
setBusy(false);
|
|
2633
|
-
}
|
|
3290
|
+
});
|
|
2634
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
|
+
});
|
|
2635
3299
|
(0, react.useEffect)(() => {
|
|
2636
|
-
if (open && !agentLocked && incoming && incoming !== received.current) {
|
|
3300
|
+
if (open && hydrated && !busy && !agentLocked && incoming && incoming !== received.current) {
|
|
2637
3301
|
received.current = incoming;
|
|
2638
3302
|
runFile(() => importImage(incoming.file));
|
|
2639
3303
|
}
|
|
2640
3304
|
}, [
|
|
2641
3305
|
open,
|
|
2642
3306
|
incoming,
|
|
2643
|
-
agentLocked
|
|
3307
|
+
agentLocked,
|
|
3308
|
+
hydrated,
|
|
3309
|
+
busy
|
|
2644
3310
|
]);
|
|
2645
3311
|
const keyDown = (event) => {
|
|
2646
3312
|
if (event.target.closest("input,textarea,select,[contenteditable=true]") || event.isComposing || busy || active.current) return;
|
|
@@ -2652,14 +3318,6 @@ window.__ModuleLoader__.load({
|
|
|
2652
3318
|
editObject({}, "delete");
|
|
2653
3319
|
return;
|
|
2654
3320
|
}
|
|
2655
|
-
if (event.key.toLowerCase() === "v") {
|
|
2656
|
-
setTool("select");
|
|
2657
|
-
return;
|
|
2658
|
-
}
|
|
2659
|
-
if (event.key.toLowerCase() === "t") {
|
|
2660
|
-
setTool("text");
|
|
2661
|
-
return;
|
|
2662
|
-
}
|
|
2663
3321
|
const key = event.key.toLowerCase(), command = event.ctrlKey || event.metaKey;
|
|
2664
3322
|
if (command && [
|
|
2665
3323
|
"z",
|
|
@@ -2673,21 +3331,24 @@ window.__ModuleLoader__.load({
|
|
|
2673
3331
|
return;
|
|
2674
3332
|
}
|
|
2675
3333
|
if (command || event.altKey) return;
|
|
2676
|
-
const
|
|
3334
|
+
const action = sketchShortcutAction(navigation.keys, key);
|
|
3335
|
+
if ([
|
|
2677
3336
|
"pen",
|
|
2678
3337
|
"eraser",
|
|
2679
3338
|
"line",
|
|
2680
3339
|
"rectangle",
|
|
2681
|
-
"circle"
|
|
2682
|
-
|
|
2683
|
-
|
|
3340
|
+
"circle",
|
|
3341
|
+
"select",
|
|
3342
|
+
"text"
|
|
3343
|
+
].includes(action)) {
|
|
2684
3344
|
event.preventDefault();
|
|
2685
|
-
|
|
2686
|
-
if (tools[key] === "pen") setBrush("pen");
|
|
3345
|
+
chooseTool(action);
|
|
2687
3346
|
}
|
|
2688
3347
|
if (key === "[" || key === "]") {
|
|
2689
3348
|
event.preventDefault();
|
|
2690
|
-
|
|
3349
|
+
const value = stepSketchWidth(selected?.width ?? width, key === "]" ? 1 : -1);
|
|
3350
|
+
if (selected) editObject({ width: value });
|
|
3351
|
+
else setWidth(value);
|
|
2691
3352
|
}
|
|
2692
3353
|
};
|
|
2693
3354
|
const current = doc.current.layers.find((layer) => layer.id === doc.current.active);
|
|
@@ -2696,81 +3357,14 @@ window.__ModuleLoader__.load({
|
|
|
2696
3357
|
const gesture = active.current;
|
|
2697
3358
|
if (!gesture || gesture.id !== event.pointerId || busy) return;
|
|
2698
3359
|
const rect = bounds ?? canvas.current.getBoundingClientRect();
|
|
2699
|
-
if (gesture.object) {
|
|
2700
|
-
const point = sketchPoint(event.clientX, event.clientY, rect);
|
|
2701
|
-
if (!point) return;
|
|
2702
|
-
try {
|
|
2703
|
-
const box = objectBounds(gesture.object);
|
|
2704
|
-
const stroke = gesture.handle === "end" ? {
|
|
2705
|
-
...gesture.object,
|
|
2706
|
-
points: [gesture.object.points[0], point]
|
|
2707
|
-
} : gesture.handle === "size" ? transformObject(gesture.object, {
|
|
2708
|
-
scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
|
|
2709
|
-
scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
|
|
2710
|
-
}) : transformObject(gesture.object, {
|
|
2711
|
-
dx: point.x - gesture.start.x,
|
|
2712
|
-
dy: point.y - gesture.start.y
|
|
2713
|
-
});
|
|
2714
|
-
doc.current = {
|
|
2715
|
-
...doc.current,
|
|
2716
|
-
layers: doc.current.layers.map((l) => l.id === gesture.layer ? {
|
|
2717
|
-
...l,
|
|
2718
|
-
strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
|
|
2719
|
-
} : l)
|
|
2720
|
-
};
|
|
2721
|
-
schedule();
|
|
2722
|
-
} catch {}
|
|
2723
|
-
return;
|
|
2724
|
-
}
|
|
2725
3360
|
const native = event.nativeEvent ?? event;
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
const previous = gesture.last ?? point;
|
|
2733
|
-
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))));
|
|
2734
|
-
layer.strokes = layer.strokes.filter((stroke) => {
|
|
2735
|
-
for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
|
|
2736
|
-
x: previous.x + (point.x - previous.x) * i / steps,
|
|
2737
|
-
y: previous.y + (point.y - previous.y) * i / steps
|
|
2738
|
-
}, width / 2, doc.current.width, doc.current.height)) return false;
|
|
2739
|
-
return true;
|
|
2740
|
-
});
|
|
2741
|
-
} else {
|
|
2742
|
-
const stroke = layer.strokes.at(-1);
|
|
2743
|
-
if ([
|
|
2744
|
-
"line",
|
|
2745
|
-
"arrow",
|
|
2746
|
-
"rectangle",
|
|
2747
|
-
"circle"
|
|
2748
|
-
].includes(stroke.shape)) {
|
|
2749
|
-
if (stroke.shape === "line" && event.shiftKey) {
|
|
2750
|
-
const w = doc.current.width ?? 1024, h = doc.current.height ?? 1024, a = stroke.points[0];
|
|
2751
|
-
const snapped = snapLine({
|
|
2752
|
-
x: a.x * w,
|
|
2753
|
-
y: a.y * h
|
|
2754
|
-
}, {
|
|
2755
|
-
x: point.x * w,
|
|
2756
|
-
y: point.y * h
|
|
2757
|
-
});
|
|
2758
|
-
point = {
|
|
2759
|
-
x: snapped.x / w,
|
|
2760
|
-
y: snapped.y / h
|
|
2761
|
-
};
|
|
2762
|
-
}
|
|
2763
|
-
stroke.points = [stroke.points[0], point];
|
|
2764
|
-
} else {
|
|
2765
|
-
const last = stroke.points.at(-1);
|
|
2766
|
-
if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
|
|
2767
|
-
if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
|
|
2768
|
-
stroke.points.push(point);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
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"));
|
|
2772
3367
|
}
|
|
2773
|
-
schedule(false);
|
|
2774
3368
|
};
|
|
2775
3369
|
const end = (event, cancel = false) => {
|
|
2776
3370
|
if (navigation.end(event)) return;
|
|
@@ -2793,7 +3387,7 @@ window.__ModuleLoader__.load({
|
|
|
2793
3387
|
layer: drawn.layer,
|
|
2794
3388
|
id: stroke.id
|
|
2795
3389
|
});
|
|
2796
|
-
|
|
3390
|
+
chooseTool("select");
|
|
2797
3391
|
}
|
|
2798
3392
|
}
|
|
2799
3393
|
active.current = null;
|
|
@@ -2819,7 +3413,7 @@ window.__ModuleLoader__.load({
|
|
|
2819
3413
|
available: () => enabled && agentEnabled,
|
|
2820
3414
|
previewEnabled: () => agentPreview,
|
|
2821
3415
|
open: () => onOpen(),
|
|
2822
|
-
busy: () => busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
|
|
3416
|
+
busy: () => operationGate.current.running || busy || Boolean(active.current) || Boolean(textEdit) || sizeGesture.current,
|
|
2823
3417
|
document: () => doc.current,
|
|
2824
3418
|
snapshot: () => ({
|
|
2825
3419
|
documentId: documentId.current,
|
|
@@ -2866,23 +3460,18 @@ window.__ModuleLoader__.load({
|
|
|
2866
3460
|
previewEnabled: () => agentAdapter.current.previewEnabled()
|
|
2867
3461
|
});
|
|
2868
3462
|
(0, react.useEffect)(() => {
|
|
2869
|
-
if (!enabled || !agentEnabled) return;
|
|
3463
|
+
if (!enabled || !agentEnabled || !hydrated) return;
|
|
2870
3464
|
const api = Object.freeze({
|
|
2871
3465
|
version: 2,
|
|
2872
3466
|
sessionId,
|
|
2873
3467
|
execute: (request) => agentRun.current.execute(request),
|
|
2874
|
-
export:
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
extension,
|
|
2882
|
-
mediaType: blob.type,
|
|
2883
|
-
base64: btoa(raw)
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
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
|
+
})
|
|
2886
3475
|
});
|
|
2887
3476
|
window.dshSketchAgent = api;
|
|
2888
3477
|
return () => {
|
|
@@ -2892,14 +3481,15 @@ window.__ModuleLoader__.load({
|
|
|
2892
3481
|
enabled,
|
|
2893
3482
|
agentEnabled,
|
|
2894
3483
|
rpc,
|
|
2895
|
-
sessionId
|
|
3484
|
+
sessionId,
|
|
3485
|
+
hydrated
|
|
2896
3486
|
]);
|
|
2897
3487
|
(0, react.useEffect)(() => {
|
|
2898
3488
|
if (!enabled || !agentEnabled) {
|
|
2899
3489
|
if (agentRun.current.locked) agentRun.current.stop();
|
|
2900
3490
|
return;
|
|
2901
3491
|
}
|
|
2902
|
-
if (!rpc || !sessionId) return;
|
|
3492
|
+
if (!rpc || !sessionId || !hydrated) return;
|
|
2903
3493
|
let live = true;
|
|
2904
3494
|
const disconnect = connectSketchAgent(rpc, sessionId, (request) => {
|
|
2905
3495
|
if (!live) throw Error("Sketch session disconnected");
|
|
@@ -2907,7 +3497,7 @@ window.__ModuleLoader__.load({
|
|
|
2907
3497
|
}, (message) => {
|
|
2908
3498
|
agentRun.current.fail();
|
|
2909
3499
|
setError(message);
|
|
2910
|
-
}, () => 350);
|
|
3500
|
+
}, () => agentRun.current.locked ? 350 : 2e3);
|
|
2911
3501
|
return () => {
|
|
2912
3502
|
live = false;
|
|
2913
3503
|
disconnect();
|
|
@@ -2916,23 +3506,18 @@ window.__ModuleLoader__.load({
|
|
|
2916
3506
|
enabled,
|
|
2917
3507
|
agentEnabled,
|
|
2918
3508
|
rpc,
|
|
2919
|
-
sessionId
|
|
3509
|
+
sessionId,
|
|
3510
|
+
hydrated
|
|
2920
3511
|
]);
|
|
2921
|
-
const attach =
|
|
2922
|
-
if (!enabled
|
|
2923
|
-
|
|
2924
|
-
setError("");
|
|
2925
|
-
try {
|
|
3512
|
+
const attach = () => {
|
|
3513
|
+
if (!enabled) return;
|
|
3514
|
+
return runFile(async () => {
|
|
2926
3515
|
paint();
|
|
2927
|
-
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"));
|
|
2928
3517
|
await saveChanges();
|
|
2929
3518
|
await attachSketch(blob);
|
|
2930
3519
|
onClose();
|
|
2931
|
-
}
|
|
2932
|
-
setError(t("sketchFailed"));
|
|
2933
|
-
} finally {
|
|
2934
|
-
setBusy(false);
|
|
2935
|
-
}
|
|
3520
|
+
});
|
|
2936
3521
|
};
|
|
2937
3522
|
const exportFile = async (format = "png") => {
|
|
2938
3523
|
paint();
|
|
@@ -2952,8 +3537,8 @@ window.__ModuleLoader__.load({
|
|
|
2952
3537
|
};
|
|
2953
3538
|
agentAdapter.current.export = exportFile;
|
|
2954
3539
|
const download = async (format) => {
|
|
3540
|
+
setError("");
|
|
2955
3541
|
const { blob, extension } = await exportFile(format);
|
|
2956
|
-
await saveChanges();
|
|
2957
3542
|
const url = URL.createObjectURL(blob), link = document.createElement("a");
|
|
2958
3543
|
link.href = url;
|
|
2959
3544
|
link.download = `${(saved.current?.name || "sketch").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").slice(0, 80)}.${extension}`;
|
|
@@ -2962,24 +3547,17 @@ window.__ModuleLoader__.load({
|
|
|
2962
3547
|
link.remove();
|
|
2963
3548
|
setTimeout(() => URL.revokeObjectURL(url), 1e4);
|
|
2964
3549
|
};
|
|
2965
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [enabled && agentEnabled && !open && !noticeHidden
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
children: "×"
|
|
2977
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2978
|
-
type: "button",
|
|
2979
|
-
"aria-label": t("sketchCancel"),
|
|
2980
|
-
onClick: () => setNoticeHidden(true),
|
|
2981
|
-
children: "×"
|
|
2982
|
-
})]
|
|
3550
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [enabled && agentEnabled && !open && !noticeHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchRunStatus, {
|
|
3551
|
+
state: agentState,
|
|
3552
|
+
floating: true,
|
|
3553
|
+
t,
|
|
3554
|
+
onOpen,
|
|
3555
|
+
onStop: () => agentRun.current.stop(),
|
|
3556
|
+
onResume: () => {
|
|
3557
|
+
setError("");
|
|
3558
|
+
agentRun.current.resume();
|
|
3559
|
+
},
|
|
3560
|
+
onDismiss: () => setNoticeHidden(true)
|
|
2983
3561
|
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
2984
3562
|
ref: dialog,
|
|
2985
3563
|
className: "codexSketchDialog codexSketchStudio codexLayerStudio",
|
|
@@ -3026,7 +3604,7 @@ window.__ModuleLoader__.load({
|
|
|
3026
3604
|
type: "button",
|
|
3027
3605
|
"aria-label": t("sketchCancel"),
|
|
3028
3606
|
title: t("sketchCancel"),
|
|
3029
|
-
disabled: busy && !agentLocked,
|
|
3607
|
+
disabled: busy && hydrated && !agentLocked,
|
|
3030
3608
|
onClick: close,
|
|
3031
3609
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, { name: "close" })
|
|
3032
3610
|
}),
|
|
@@ -3039,8 +3617,7 @@ window.__ModuleLoader__.load({
|
|
|
3039
3617
|
hasContent: doc.current.layers.some((l) => l.visible && (l.image || l.strokes.length)),
|
|
3040
3618
|
disabled: agentLocked || busy,
|
|
3041
3619
|
t,
|
|
3042
|
-
|
|
3043
|
-
onWorking: setBusy
|
|
3620
|
+
runOperation: runFile
|
|
3044
3621
|
}),
|
|
3045
3622
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3046
3623
|
className: "codexSketchHeading",
|
|
@@ -3121,24 +3698,28 @@ window.__ModuleLoader__.load({
|
|
|
3121
3698
|
})
|
|
3122
3699
|
]
|
|
3123
3700
|
}),
|
|
3124
|
-
enabled && agentEnabled &&
|
|
3701
|
+
enabled && agentEnabled && !noticeHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchRunStatus, {
|
|
3702
|
+
state: agentState,
|
|
3703
|
+
t,
|
|
3704
|
+
onStop: () => agentRun.current.stop(),
|
|
3705
|
+
onResume: () => {
|
|
3706
|
+
setError("");
|
|
3707
|
+
agentRun.current.resume();
|
|
3708
|
+
},
|
|
3709
|
+
onDismiss: () => setNoticeHidden(true)
|
|
3710
|
+
}) : null,
|
|
3711
|
+
recovered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3125
3712
|
className: "codexSketchAgentStatus",
|
|
3126
3713
|
role: "status",
|
|
3127
|
-
"
|
|
3128
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${agentState}`) }), agentLocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3714
|
+
children: [t("sketchRecovered"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3129
3715
|
type: "button",
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
onClick: () => agentRun.current.stop(),
|
|
3716
|
+
onClick: () => setRecovered(false),
|
|
3717
|
+
"aria-label": t("sketchDismissStatus"),
|
|
3133
3718
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3134
3719
|
name: "close",
|
|
3135
|
-
size:
|
|
3720
|
+
size: 14
|
|
3136
3721
|
})
|
|
3137
|
-
})
|
|
3138
|
-
type: "button",
|
|
3139
|
-
onClick: () => agentRun.current.resume(),
|
|
3140
|
-
children: t("sketchRunResume")
|
|
3141
|
-
}) : null]
|
|
3722
|
+
})]
|
|
3142
3723
|
}) : null,
|
|
3143
3724
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3144
3725
|
className: `codexLayerBody ${layersOpen ? "withLayers" : ""}`,
|
|
@@ -3254,6 +3835,7 @@ window.__ModuleLoader__.load({
|
|
|
3254
3835
|
width,
|
|
3255
3836
|
fill: fillShape && ["rectangle", "circle"].includes(tool),
|
|
3256
3837
|
brush: tool === "pen" ? brush : "pen",
|
|
3838
|
+
...tool === "pen" ? { brushVersion: 2 } : {},
|
|
3257
3839
|
pressure: event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1,
|
|
3258
3840
|
points: [start]
|
|
3259
3841
|
});
|
|
@@ -3329,13 +3911,10 @@ window.__ModuleLoader__.load({
|
|
|
3329
3911
|
sizeGesture.current = false;
|
|
3330
3912
|
},
|
|
3331
3913
|
onChange: (value) => {
|
|
3332
|
-
if (opacityMode) {
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
setWidth(value);
|
|
3337
|
-
if (selected) editObject({ width: value });
|
|
3338
|
-
}
|
|
3914
|
+
if (opacityMode) if (selected) editObject({ opacity: value / 100 });
|
|
3915
|
+
else setFlow(value);
|
|
3916
|
+
else if (selected) editObject({ width: value });
|
|
3917
|
+
else setWidth(value);
|
|
3339
3918
|
}
|
|
3340
3919
|
}),
|
|
3341
3920
|
textEdit ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
@@ -3371,7 +3950,7 @@ window.__ModuleLoader__.load({
|
|
|
3371
3950
|
schedule();
|
|
3372
3951
|
}
|
|
3373
3952
|
setTextEdit(null);
|
|
3374
|
-
|
|
3953
|
+
chooseTool("select");
|
|
3375
3954
|
} catch (e) {
|
|
3376
3955
|
setError(e.message);
|
|
3377
3956
|
}
|
|
@@ -3440,89 +4019,11 @@ window.__ModuleLoader__.load({
|
|
|
3440
4019
|
!doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
|
|
3441
4020
|
]
|
|
3442
4021
|
}) : null,
|
|
3443
|
-
layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
type: "button",
|
|
3449
|
-
title: t("sketchLayerAdd"),
|
|
3450
|
-
"aria-label": t("sketchLayerAdd"),
|
|
3451
|
-
disabled: agentLocked || busy || doc.current.layers.length >= 8,
|
|
3452
|
-
onClick: () => change("add"),
|
|
3453
|
-
children: "+"
|
|
3454
|
-
})] }),
|
|
3455
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3456
|
-
className: "codexLayerList",
|
|
3457
|
-
children: doc.current.layers.slice().reverse().map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3458
|
-
className: "codexLayerRow",
|
|
3459
|
-
"data-active": layer.id === doc.current.active,
|
|
3460
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3461
|
-
type: "button",
|
|
3462
|
-
disabled: agentLocked || busy,
|
|
3463
|
-
"aria-label": `${t("sketchLayerVisible")} ${layer.id}`,
|
|
3464
|
-
"aria-pressed": layer.visible,
|
|
3465
|
-
onClick: () => change("visible", layer.id),
|
|
3466
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3467
|
-
name: layer.visible ? "eye" : "eyeOff",
|
|
3468
|
-
size: 18
|
|
3469
|
-
})
|
|
3470
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3471
|
-
type: "button",
|
|
3472
|
-
disabled: agentLocked || busy,
|
|
3473
|
-
"aria-pressed": layer.id === doc.current.active,
|
|
3474
|
-
onClick: () => change("select", layer.id),
|
|
3475
|
-
children: layer.name || `${t("sketchLayer")} ${layer.id}`
|
|
3476
|
-
})]
|
|
3477
|
-
}, layer.id))
|
|
3478
|
-
}),
|
|
3479
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
3480
|
-
className: "codexSketchLayerLabel",
|
|
3481
|
-
children: t("sketchLayerName")
|
|
3482
|
-
}),
|
|
3483
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3484
|
-
disabled: agentLocked || busy,
|
|
3485
|
-
"aria-label": t("sketchLayerName"),
|
|
3486
|
-
defaultValue: current.name,
|
|
3487
|
-
placeholder: `${t("sketchLayer")} ${current.id}`,
|
|
3488
|
-
maxLength: 40,
|
|
3489
|
-
onBlur: (e) => {
|
|
3490
|
-
if (e.target.value !== current.name) change("rename", current.id, e.target.value);
|
|
3491
|
-
},
|
|
3492
|
-
onKeyDown: (e) => {
|
|
3493
|
-
if (e.key === "Enter") e.currentTarget.blur();
|
|
3494
|
-
}
|
|
3495
|
-
}, current.id + "-" + current.name),
|
|
3496
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3497
|
-
className: "codexLayerActions",
|
|
3498
|
-
children: [
|
|
3499
|
-
"duplicate",
|
|
3500
|
-
"up",
|
|
3501
|
-
"down",
|
|
3502
|
-
"delete"
|
|
3503
|
-
].map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3504
|
-
type: "button",
|
|
3505
|
-
title: t(`sketchLayer_${action}`),
|
|
3506
|
-
"aria-label": t(`sketchLayer_${action}`),
|
|
3507
|
-
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],
|
|
3508
|
-
onClick: () => change(action),
|
|
3509
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3510
|
-
name: action === "delete" ? "clear" : action,
|
|
3511
|
-
size: 17
|
|
3512
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchLayer_${action}`) })]
|
|
3513
|
-
}, action))
|
|
3514
|
-
}),
|
|
3515
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3516
|
-
type: "button",
|
|
3517
|
-
className: "codexSketchClearLayer",
|
|
3518
|
-
disabled: agentLocked || busy || !current.strokes.length && !current.image,
|
|
3519
|
-
onClick: () => change("clear"),
|
|
3520
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3521
|
-
name: "clear",
|
|
3522
|
-
size: 16
|
|
3523
|
-
}), t("sketchClearLayer")]
|
|
3524
|
-
})
|
|
3525
|
-
]
|
|
4022
|
+
layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
|
|
4023
|
+
document: doc.current,
|
|
4024
|
+
disabled: agentLocked || busy,
|
|
4025
|
+
change,
|
|
4026
|
+
t
|
|
3526
4027
|
}) : null
|
|
3527
4028
|
]
|
|
3528
4029
|
}),
|
|
@@ -3540,70 +4041,15 @@ window.__ModuleLoader__.load({
|
|
|
3540
4041
|
navigation,
|
|
3541
4042
|
t
|
|
3542
4043
|
}),
|
|
3543
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
"text",
|
|
3553
|
-
"eraser"
|
|
3554
|
-
].map((name) => {
|
|
3555
|
-
const drawing = [
|
|
3556
|
-
"pen",
|
|
3557
|
-
"pencil",
|
|
3558
|
-
"marker"
|
|
3559
|
-
].includes(name);
|
|
3560
|
-
const label = t(drawing ? `sketchBrush_${name}` : `sketchTool_${name}`);
|
|
3561
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3562
|
-
type: "button",
|
|
3563
|
-
"aria-label": label,
|
|
3564
|
-
title: label,
|
|
3565
|
-
"aria-pressed": drawing ? tool === "pen" && brush === name : tool === name,
|
|
3566
|
-
disabled: agentLocked || busy,
|
|
3567
|
-
onClick: () => {
|
|
3568
|
-
setTool(drawing ? "pen" : name);
|
|
3569
|
-
if (name !== "select") setSelection(null);
|
|
3570
|
-
if (drawing) setBrush(name);
|
|
3571
|
-
},
|
|
3572
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3573
|
-
name,
|
|
3574
|
-
size: 23
|
|
3575
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
3576
|
-
}, name);
|
|
3577
|
-
}),
|
|
3578
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3579
|
-
className: "codexSketchShapeToggle",
|
|
3580
|
-
type: "button",
|
|
3581
|
-
"aria-expanded": shapesOpen,
|
|
3582
|
-
disabled: agentLocked || busy,
|
|
3583
|
-
onClick: () => setShapesOpen((v) => !v),
|
|
3584
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
3585
|
-
name: "rectangle",
|
|
3586
|
-
size: 23
|
|
3587
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchShapes") })]
|
|
3588
|
-
}),
|
|
3589
|
-
shapesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3590
|
-
className: "codexSketchShapeMenu",
|
|
3591
|
-
children: [
|
|
3592
|
-
"line",
|
|
3593
|
-
"arrow",
|
|
3594
|
-
"rectangle",
|
|
3595
|
-
"circle"
|
|
3596
|
-
].map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3597
|
-
type: "button",
|
|
3598
|
-
onClick: () => {
|
|
3599
|
-
setTool(name);
|
|
3600
|
-
setSelection(null);
|
|
3601
|
-
setShapesOpen(false);
|
|
3602
|
-
},
|
|
3603
|
-
children: t(`sketchTool_${name}`)
|
|
3604
|
-
}, name))
|
|
3605
|
-
}) : null
|
|
3606
|
-
]
|
|
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
|
|
3607
4053
|
}),
|
|
3608
4054
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3609
4055
|
className: "codexLayerBrush",
|
|
@@ -3614,20 +4060,6 @@ window.__ModuleLoader__.load({
|
|
|
3614
4060
|
checked: fillShape,
|
|
3615
4061
|
onChange: (e) => setFillShape(e.target.checked)
|
|
3616
4062
|
}), t("sketchFill")] }) : null,
|
|
3617
|
-
tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
3618
|
-
"aria-label": t("sketchBrush"),
|
|
3619
|
-
value: brush,
|
|
3620
|
-
onChange: (e) => setBrush(e.target.value),
|
|
3621
|
-
disabled: agentLocked || busy,
|
|
3622
|
-
children: [
|
|
3623
|
-
"pen",
|
|
3624
|
-
"pencil",
|
|
3625
|
-
"marker"
|
|
3626
|
-
].map((b) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
3627
|
-
value: b,
|
|
3628
|
-
children: t(`sketchBrush_${b}`)
|
|
3629
|
-
}, b))
|
|
3630
|
-
}) : null,
|
|
3631
4063
|
selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3632
4064
|
className: "codexSketchObjectActions",
|
|
3633
4065
|
children: [
|
|
@@ -3709,10 +4141,14 @@ window.__ModuleLoader__.load({
|
|
|
3709
4141
|
})
|
|
3710
4142
|
]
|
|
3711
4143
|
}),
|
|
3712
|
-
error ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4144
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3713
4145
|
className: "codexSketchHint",
|
|
3714
4146
|
role: "alert",
|
|
3715
|
-
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]
|
|
3716
4152
|
}) : null
|
|
3717
4153
|
]
|
|
3718
4154
|
})] });
|
|
@@ -3734,7 +4170,7 @@ window.__ModuleLoader__.load({
|
|
|
3734
4170
|
.codexLayerBrush>select{background:var(--sketch-bg);color:inherit;border:1px solid var(--sketch-line);border-radius:8px;padding:6px}
|
|
3735
4171
|
|
|
3736
4172
|
.codexSketchBackgroundStatus{position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:1000;display:flex;gap:8px;align-items:center;padding:6px 10px;border-radius:12px;background:var(--dsw-alias-bg-layer-1,#f5f5f7);color:var(--dsw-alias-label-primary,#202124);border:1px solid #8883;box-shadow:0 4px 16px #0002;font:12px system-ui}.codexSketchBackgroundStatus button{color:inherit;background:none;border:0;cursor:pointer}
|
|
3737
|
-
.codexSketchAgentStatus{display:flex;align-items:center;justify-content:center;gap:8px;min-height:24px;color:var(--sketch-muted);font-size:12px}.codexSketchAgentStatus button{padding:
|
|
4173
|
+
.codexSketchAgentStatus{display:flex;align-items:center;justify-content:center;gap:8px;min-height:24px;color:var(--sketch-muted);font-size:12px}.codexSketchAgentStatus button{min-height:30px;padding:4px 10px;border-radius:16px;background:var(--sketch-line)}.codexSketchAgentStatus{align-self:center;max-width:100%;min-height:34px;padding:0 6px;flex-wrap:wrap}.codexSketchBackgroundStatus button{min-height:30px;display:inline-flex;align-items:center;gap:5px}.codexSketchAgentStatus .codexSketchStop,.codexSketchBackgroundStatus .codexSketchStop{font-weight:500;background:var(--sketch-line,#8882);border-radius:16px;padding:4px 10px}
|
|
3738
4174
|
.codexSketchDialog{--sketch-bg:var(--dsw-alias-bg-layer-1,#f5f5f7);--sketch-fg:var(--dsw-alias-label-primary,#202124);--sketch-muted:var(--dsw-alias-label-secondary,#727279);--sketch-line:var(--dsw-alias-border-l2,#8883);--sketch-glass:color-mix(in srgb,var(--sketch-bg) 90%,transparent);box-sizing:border-box;width:min(1280px,calc(100vw - 24px));height:94dvh;max-height:94dvh;margin:auto;padding:10px;border:1px solid var(--sketch-line);border-radius:18px;background:var(--sketch-bg);color:var(--sketch-fg);box-shadow:0 24px 90px #0004;overflow:hidden;font:13px/1.4 system-ui}
|
|
3739
4175
|
.codexSketchDialog[open]{display:flex;flex-direction:column;gap:8px}
|
|
3740
4176
|
.codexSketchDialog::backdrop{background:#0005;backdrop-filter:blur(12px)}
|
|
@@ -3915,9 +4351,27 @@ window.__ModuleLoader__.load({
|
|
|
3915
4351
|
//#endregion
|
|
3916
4352
|
//#region src/client-locales.js
|
|
3917
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: "草稿存储空间已满。请先导出,或删除不需要的草稿后保存。",
|
|
3918
4367
|
sketchSizeShort: "粗细",
|
|
3919
4368
|
sketchObjectDuplicate: "复制对象",
|
|
3920
4369
|
sketchObjectDelete: "删除对象",
|
|
4370
|
+
sketchBrushHint_pen: "实色圆头墨线",
|
|
4371
|
+
sketchBrushHint_pencil: "细腻颗粒,叠画加深",
|
|
4372
|
+
sketchBrushHint_marker: "半透明平头,适合高亮",
|
|
4373
|
+
sketchDismissStatus: "收起提示",
|
|
4374
|
+
sketchRunResumeHint: "允许 Agent 接收后续绘图请求,不会自动重发消息",
|
|
3921
4375
|
sketchTool_select: "选择",
|
|
3922
4376
|
sketchTool_text: "文字",
|
|
3923
4377
|
sketchTool_arrow: "箭头",
|
|
@@ -4321,9 +4775,27 @@ window.__ModuleLoader__.load({
|
|
|
4321
4775
|
imageRemoveAnnotation: "删除标注"
|
|
4322
4776
|
};
|
|
4323
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.",
|
|
4324
4791
|
sketchSizeShort: "Size",
|
|
4325
4792
|
sketchObjectDuplicate: "Duplicate object",
|
|
4326
4793
|
sketchObjectDelete: "Delete object",
|
|
4794
|
+
sketchBrushHint_pen: "Solid round ink",
|
|
4795
|
+
sketchBrushHint_pencil: "Grain builds with repeated strokes",
|
|
4796
|
+
sketchBrushHint_marker: "Translucent flat tip for highlights",
|
|
4797
|
+
sketchDismissStatus: "Dismiss status",
|
|
4798
|
+
sketchRunResumeHint: "Allow subsequent drawing requests; does not resend a message",
|
|
4327
4799
|
sketchTool_select: "Select",
|
|
4328
4800
|
sketchTool_text: "Text",
|
|
4329
4801
|
sketchTool_arrow: "Arrow",
|
|
@@ -4729,6 +5201,15 @@ window.__ModuleLoader__.load({
|
|
|
4729
5201
|
//#endregion
|
|
4730
5202
|
//#region src/client-styles.js
|
|
4731
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)}
|
|
4732
5213
|
.codexComposerQuota[data-warning=true]{color:var(--dsw-alias-state-error-primary)}
|
|
4733
5214
|
.codexComposerQuota[data-warning=true] progress{accent-color:var(--dsw-alias-state-error-primary)}
|
|
4734
5215
|
.codexComposerQuota[data-warning=true] progress::-webkit-progress-value{background:var(--dsw-alias-state-error-primary)}
|
|
@@ -4781,20 +5262,9 @@ window.__ModuleLoader__.load({
|
|
|
4781
5262
|
.codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
4782
5263
|
.codexSubscriptionContextInput{width:116px}
|
|
4783
5264
|
.codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}
|
|
4784
|
-
.codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}
|
|
4785
|
-
.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}
|
|
4786
|
-
.codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}
|
|
4787
|
-
.codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
|
|
4788
|
-
.codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
|
|
4789
|
-
.codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}
|
|
4790
|
-
.codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}
|
|
4791
|
-
.codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}
|
|
4792
|
-
.codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}
|
|
4793
|
-
.codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}
|
|
4794
|
-
.codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}
|
|
4795
5265
|
.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
|
|
4796
5266
|
.codexSubscriptionQuotaModes[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
|
|
4797
|
-
.codexSubscriptionSearchChoices[data-saving=true] .
|
|
5267
|
+
.codexSubscriptionSearchChoices[data-saving=true] .codexSubscriptionQuotaMode:has(input:disabled){cursor:wait;opacity:1}
|
|
4798
5268
|
.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
4799
5269
|
.codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
|
|
4800
5270
|
.codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
@@ -6082,6 +6552,7 @@ window.__ModuleLoader__.load({
|
|
|
6082
6552
|
let modelRefreshGeneration = 0;
|
|
6083
6553
|
let modelRefreshStarted = false;
|
|
6084
6554
|
let disposed = false;
|
|
6555
|
+
let subagentBackendAvailable = false;
|
|
6085
6556
|
const sameModels = (left, right) => left.length === right.length && left.every((model, index) => JSON.stringify(model) === JSON.stringify(right[index]));
|
|
6086
6557
|
const nativeSnapshot = () => scope.getSnapshot();
|
|
6087
6558
|
const read = () => {
|
|
@@ -6095,6 +6566,9 @@ window.__ModuleLoader__.load({
|
|
|
6095
6566
|
return Object.freeze({
|
|
6096
6567
|
status: current.status,
|
|
6097
6568
|
...capabilities,
|
|
6569
|
+
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6570
|
+
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
6571
|
+
subagentBackendAvailable,
|
|
6098
6572
|
quickQuotaMode: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
|
|
6099
6573
|
searchProvider: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
|
|
6100
6574
|
speedMode: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
|
|
@@ -6128,6 +6602,7 @@ window.__ModuleLoader__.load({
|
|
|
6128
6602
|
publish();
|
|
6129
6603
|
});
|
|
6130
6604
|
const acceptFallback = (value) => {
|
|
6605
|
+
subagentBackendAvailable = value?.subagentBackendAvailable === true;
|
|
6131
6606
|
if (!modelRefreshStarted) {
|
|
6132
6607
|
contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
|
|
6133
6608
|
verbosityModels = Array.isArray(value?.verbosityModels) ? value.verbosityModels : [];
|
|
@@ -6138,6 +6613,8 @@ window.__ModuleLoader__.load({
|
|
|
6138
6613
|
fallback = {
|
|
6139
6614
|
status: "ready",
|
|
6140
6615
|
value: {
|
|
6616
|
+
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6617
|
+
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
6141
6618
|
...readCapabilitySettings(value),
|
|
6142
6619
|
[QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
|
|
6143
6620
|
[SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
|
|
@@ -6161,6 +6638,7 @@ window.__ModuleLoader__.load({
|
|
|
6161
6638
|
try {
|
|
6162
6639
|
const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
|
|
6163
6640
|
if (current !== generation || disposed) return;
|
|
6641
|
+
subagentBackendAvailable = value?.subagentBackendAvailable === true;
|
|
6164
6642
|
if (nativeSnapshot().status === "ready") {
|
|
6165
6643
|
if (!modelRefreshStarted) {
|
|
6166
6644
|
contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
|
|
@@ -6221,7 +6699,7 @@ window.__ModuleLoader__.load({
|
|
|
6221
6699
|
failedPatch = void 0;
|
|
6222
6700
|
publish();
|
|
6223
6701
|
try {
|
|
6224
|
-
if (nativeSnapshot().status === "ready") {
|
|
6702
|
+
if (nativeSnapshot().status === "ready" && !Object.hasOwn(patch, "subagentBackend")) {
|
|
6225
6703
|
for (const [field, value] of entries) {
|
|
6226
6704
|
if (current !== generation) return;
|
|
6227
6705
|
await scope.set(field, value);
|
|
@@ -6547,7 +7025,7 @@ window.__ModuleLoader__.load({
|
|
|
6547
7025
|
}
|
|
6548
7026
|
//#endregion
|
|
6549
7027
|
//#region src/version.js
|
|
6550
|
-
const PACKAGE_VERSION = "2.1.
|
|
7028
|
+
const PACKAGE_VERSION = "2.1.1-beta.1";
|
|
6551
7029
|
//#endregion
|
|
6552
7030
|
//#region src/client-recovery.js
|
|
6553
7031
|
async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
|
|
@@ -7186,64 +7664,76 @@ window.__ModuleLoader__.load({
|
|
|
7186
7664
|
const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
|
|
7187
7665
|
const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
|
|
7188
7666
|
const active = snapshot.imageGeneration || snapshot.imageEditing;
|
|
7189
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7667
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
7190
7668
|
className: "codexSubscriptionCard codexImageSettings",
|
|
7191
7669
|
"aria-label": t("imageSettings"),
|
|
7192
|
-
children:
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
imageModel,
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
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
|
+
})
|
|
7247
7737
|
});
|
|
7248
7738
|
}
|
|
7249
7739
|
//#endregion
|
|
@@ -7458,10 +7948,9 @@ window.__ModuleLoader__.load({
|
|
|
7458
7948
|
function SearchProviderPreference({ preference, t }) {
|
|
7459
7949
|
const snapshot = usePreferenceSnapshot(preference);
|
|
7460
7950
|
const writable = snapshot.status === "ready" && snapshot.writable === true;
|
|
7461
|
-
const choice = (value, label
|
|
7462
|
-
className: "
|
|
7951
|
+
const choice = (value, label) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7952
|
+
className: "codexSubscriptionQuotaMode",
|
|
7463
7953
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7464
|
-
className: "codexSubscriptionSearchInput",
|
|
7465
7954
|
type: "radio",
|
|
7466
7955
|
name: "codex-subscription-search-provider",
|
|
7467
7956
|
checked: snapshot.searchProvider === value,
|
|
@@ -7469,39 +7958,39 @@ window.__ModuleLoader__.load({
|
|
|
7469
7958
|
onChange: () => {
|
|
7470
7959
|
preference.set({ [SEARCH_PROVIDER_FIELD]: value });
|
|
7471
7960
|
}
|
|
7472
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
7473
|
-
className: "codexSubscriptionSearchCopy",
|
|
7474
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hint })]
|
|
7475
|
-
})]
|
|
7961
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
7476
7962
|
});
|
|
7963
|
+
const hint = snapshot.searchProvider === "dsh" ? "searchDshHint" : snapshot.searchProvider === "codex" ? "searchCodexHint" : "searchAutoHint";
|
|
7477
7964
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7478
7965
|
className: "codexSubscriptionSearch",
|
|
7479
|
-
children: [
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
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)
|
|
7485
7976
|
})]
|
|
7486
|
-
}),
|
|
7487
|
-
|
|
7488
|
-
className: "codexSubscriptionSearchChoices",
|
|
7977
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7978
|
+
className: "codexSubscriptionSearchChoices codexSubscriptionQuotaModes",
|
|
7489
7979
|
"data-saving": snapshot.saving || void 0,
|
|
7490
7980
|
"aria-busy": snapshot.saving || void 0,
|
|
7491
7981
|
role: "radiogroup",
|
|
7492
7982
|
"aria-label": t("searchTitle"),
|
|
7493
7983
|
children: [
|
|
7494
|
-
choice(SEARCH_PROVIDER_AUTO, t("searchAuto")
|
|
7495
|
-
choice("dsh",
|
|
7496
|
-
choice(SEARCH_PROVIDER_CODEX,
|
|
7984
|
+
choice(SEARCH_PROVIDER_AUTO, t("searchAuto")),
|
|
7985
|
+
choice("dsh", "DSH"),
|
|
7986
|
+
choice(SEARCH_PROVIDER_CODEX, "Codex")
|
|
7497
7987
|
]
|
|
7498
|
-
})
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
]
|
|
7988
|
+
})]
|
|
7989
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
|
|
7990
|
+
preference,
|
|
7991
|
+
t,
|
|
7992
|
+
section: "search"
|
|
7993
|
+
})]
|
|
7505
7994
|
});
|
|
7506
7995
|
}
|
|
7507
7996
|
function ContextWindowPreference({ preference, t }) {
|
|
@@ -7667,16 +8156,99 @@ window.__ModuleLoader__.load({
|
|
|
7667
8156
|
function PreferencesCard({ preference, t, section = "display" }) {
|
|
7668
8157
|
const snapshot = usePreferenceSnapshot(preference);
|
|
7669
8158
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7670
|
-
className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
8159
|
+
className: section === "advanced" ? "codexSubscriptionAdvancedPreferences" : "codexSubscriptionCard codexSubscriptionPreferencesCard",
|
|
7671
8160
|
children: [section === "advanced" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7672
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
7673
|
-
|
|
7674
|
-
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
|
+
]
|
|
7675
8176
|
}),
|
|
7676
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
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
|
+
})
|
|
7680
8252
|
})
|
|
7681
8253
|
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
|
|
7682
8254
|
preference,
|