dsh-codex-subscription 2.2.1 → 2.2.3
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 +151 -150
- package/README.md +150 -150
- package/lib/client.js +2183 -1595
- package/lib/index.js +564 -60
- package/locale/en.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -877,6 +877,8 @@ window.__ModuleLoader__.load({
|
|
|
877
877
|
"runtime/install",
|
|
878
878
|
"runtime/remove",
|
|
879
879
|
"runtime/cancel",
|
|
880
|
+
"storage/status",
|
|
881
|
+
"storage/clear-forecast",
|
|
880
882
|
"reset-credit/inspect",
|
|
881
883
|
"reset-credit/prepare",
|
|
882
884
|
"reset-credit/consume",
|
|
@@ -1294,6 +1296,53 @@ window.__ModuleLoader__.load({
|
|
|
1294
1296
|
});
|
|
1295
1297
|
}
|
|
1296
1298
|
//#endregion
|
|
1299
|
+
//#region src/sketch-picture-panel.jsx
|
|
1300
|
+
function SketchPicturePanel({ document, disabled, pictureInput, importImage, runFile, change, t }) {
|
|
1301
|
+
const pictures = document.layers.filter((layer) => layer.image);
|
|
1302
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
1303
|
+
className: "codexSketchPictures",
|
|
1304
|
+
children: [
|
|
1305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchPictures") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1306
|
+
type: "button",
|
|
1307
|
+
disabled,
|
|
1308
|
+
onClick: () => pictureInput.current.click(),
|
|
1309
|
+
children: t("sketchPictureAdd")
|
|
1310
|
+
})] }),
|
|
1311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1312
|
+
ref: pictureInput,
|
|
1313
|
+
hidden: true,
|
|
1314
|
+
type: "file",
|
|
1315
|
+
accept: "image/png,image/jpeg,image/webp",
|
|
1316
|
+
onChange: (event) => {
|
|
1317
|
+
const file = event.target.files?.[0];
|
|
1318
|
+
event.target.value = "";
|
|
1319
|
+
if (file) runFile(() => importImage(file));
|
|
1320
|
+
}
|
|
1321
|
+
}),
|
|
1322
|
+
pictures.map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1323
|
+
"data-active": layer.id === document.active,
|
|
1324
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1325
|
+
type: "button",
|
|
1326
|
+
disabled,
|
|
1327
|
+
"aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
|
|
1328
|
+
onClick: () => change("select", layer.id),
|
|
1329
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
1330
|
+
src: layer.image.src,
|
|
1331
|
+
alt: layer.name
|
|
1332
|
+
})
|
|
1333
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1334
|
+
type: "button",
|
|
1335
|
+
disabled,
|
|
1336
|
+
"aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
|
|
1337
|
+
onClick: () => change(document.layers.length === 1 ? "clear" : "delete", layer.id),
|
|
1338
|
+
children: "×"
|
|
1339
|
+
})]
|
|
1340
|
+
}, layer.id)),
|
|
1341
|
+
pictures.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
|
|
1342
|
+
]
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
//#endregion
|
|
1297
1346
|
//#region src/sketch-tool-picker.jsx
|
|
1298
1347
|
function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
|
|
1299
1348
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1382,1460 +1431,1641 @@ window.__ModuleLoader__.load({
|
|
|
1382
1431
|
});
|
|
1383
1432
|
}
|
|
1384
1433
|
//#endregion
|
|
1385
|
-
//#region src/sketch-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1434
|
+
//#region src/sketch-interactions.js
|
|
1435
|
+
function useSketchDismiss(open, close, host, selectors) {
|
|
1436
|
+
const latest = (0, react.useRef)(close);
|
|
1437
|
+
latest.current = close;
|
|
1438
|
+
(0, react.useEffect)(() => {
|
|
1439
|
+
if (!open) return;
|
|
1440
|
+
const dialog = host.current?.closest("dialog") ?? host.current;
|
|
1441
|
+
if (!dialog) return;
|
|
1442
|
+
const pointer = (event) => {
|
|
1443
|
+
if (selectors.some((selector) => event.target.closest?.(selector))) return;
|
|
1444
|
+
latest.current(false);
|
|
1445
|
+
if (event.target.matches?.("canvas")) {
|
|
1446
|
+
event.preventDefault();
|
|
1447
|
+
event.stopPropagation();
|
|
1448
|
+
event.target.focus({ preventScroll: true });
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
const key = (event) => {
|
|
1452
|
+
if (event.key !== "Escape" || event.target.closest?.("dialog") !== dialog) return;
|
|
1453
|
+
event.preventDefault();
|
|
1454
|
+
event.stopPropagation();
|
|
1455
|
+
latest.current(false);
|
|
1456
|
+
dialog.querySelector("canvas")?.focus({ preventScroll: true });
|
|
1457
|
+
};
|
|
1458
|
+
const hidden = () => latest.current(false);
|
|
1459
|
+
document.addEventListener("pointerdown", pointer, true);
|
|
1460
|
+
document.addEventListener("keydown", key, true);
|
|
1461
|
+
dialog.addEventListener("close", hidden);
|
|
1462
|
+
return () => {
|
|
1463
|
+
document.removeEventListener("pointerdown", pointer, true);
|
|
1464
|
+
document.removeEventListener("keydown", key, true);
|
|
1465
|
+
dialog.removeEventListener("close", hidden);
|
|
1466
|
+
};
|
|
1467
|
+
}, [
|
|
1468
|
+
open,
|
|
1469
|
+
host,
|
|
1470
|
+
selectors.join("|")
|
|
1471
|
+
]);
|
|
1405
1472
|
}
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1473
|
+
function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
|
|
1474
|
+
const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
|
|
1475
|
+
const update = (event, bounds) => {
|
|
1476
|
+
if (event) last.current = event;
|
|
1477
|
+
const pointer = last.current, node = canvas.current, cursor = ring.current;
|
|
1478
|
+
if (!pointer || !node || !cursor) return;
|
|
1479
|
+
const rect = bounds ?? node.getBoundingClientRect();
|
|
1480
|
+
if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
|
|
1481
|
+
cursor.hidden = true;
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
|
|
1485
|
+
cursor.hidden = false;
|
|
1486
|
+
cursor.style.width = `${diameter}px`;
|
|
1487
|
+
cursor.style.height = `${diameter}px`;
|
|
1488
|
+
cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
|
|
1489
|
+
};
|
|
1490
|
+
(0, react.useEffect)(() => {
|
|
1491
|
+
update();
|
|
1492
|
+
const observer = new ResizeObserver(() => update());
|
|
1493
|
+
if (canvas.current) observer.observe(canvas.current);
|
|
1494
|
+
return () => observer.disconnect();
|
|
1495
|
+
}, [
|
|
1496
|
+
width,
|
|
1497
|
+
brush,
|
|
1498
|
+
zoom,
|
|
1499
|
+
hidden
|
|
1500
|
+
]);
|
|
1419
1501
|
return {
|
|
1420
|
-
|
|
1421
|
-
|
|
1502
|
+
down: (event) => {
|
|
1503
|
+
heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
|
|
1504
|
+
},
|
|
1505
|
+
move: (event, bounds) => update({
|
|
1506
|
+
clientX: event.clientX,
|
|
1507
|
+
clientY: event.clientY,
|
|
1508
|
+
pointerId: event.pointerId,
|
|
1509
|
+
pointerType: event.pointerType,
|
|
1510
|
+
pressure: event.pressure
|
|
1511
|
+
}, bounds),
|
|
1512
|
+
leave: () => {
|
|
1513
|
+
last.current = null;
|
|
1514
|
+
if (ring.current) ring.current.hidden = true;
|
|
1515
|
+
}
|
|
1422
1516
|
};
|
|
1423
1517
|
}
|
|
1424
|
-
function sketchObjectSummary(doc) {
|
|
1425
|
-
return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
|
|
1426
|
-
layer: layer.id,
|
|
1427
|
-
id: objectId(stroke, i),
|
|
1428
|
-
shape: stroke.shape,
|
|
1429
|
-
color: stroke.color,
|
|
1430
|
-
bounds: objectBounds(stroke),
|
|
1431
|
-
...stroke.text ? { text: stroke.text } : {}
|
|
1432
|
-
})));
|
|
1433
|
-
}
|
|
1434
1518
|
//#endregion
|
|
1435
|
-
//#region src/sketch-
|
|
1436
|
-
function
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
};
|
|
1519
|
+
//#region src/sketch-shortcuts.js
|
|
1520
|
+
function sketchShortcutAction(keys, key) {
|
|
1521
|
+
key = key.toLowerCase();
|
|
1522
|
+
return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
|
|
1523
|
+
v: "select",
|
|
1524
|
+
t: "text",
|
|
1525
|
+
"+": "zoomIn"
|
|
1526
|
+
}[key];
|
|
1443
1527
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1528
|
+
//#endregion
|
|
1529
|
+
//#region src/sketch-view.jsx
|
|
1530
|
+
const DEFAULT_KEYS = {
|
|
1531
|
+
pen: "b",
|
|
1532
|
+
eraser: "e",
|
|
1533
|
+
line: "l",
|
|
1534
|
+
rectangle: "r",
|
|
1535
|
+
circle: "o",
|
|
1536
|
+
pan: " ",
|
|
1537
|
+
zoomIn: "=",
|
|
1538
|
+
zoomOut: "-",
|
|
1539
|
+
fit: "0"
|
|
1540
|
+
};
|
|
1541
|
+
function useSketchView(canvas, open) {
|
|
1542
|
+
const [view, setView] = (0, react.useState)({
|
|
1543
|
+
scale: 1,
|
|
1544
|
+
x: 0,
|
|
1545
|
+
y: 0
|
|
1546
|
+
}), [keys, setKeys] = (0, react.useState)(() => {
|
|
1547
|
+
try {
|
|
1548
|
+
return {
|
|
1549
|
+
...DEFAULT_KEYS,
|
|
1550
|
+
...JSON.parse(localStorage.getItem("codex-sketch-keys"))
|
|
1551
|
+
};
|
|
1552
|
+
} catch {
|
|
1553
|
+
return DEFAULT_KEYS;
|
|
1455
1554
|
}
|
|
1456
|
-
return {
|
|
1457
|
-
...point,
|
|
1458
|
-
x: point.x + (x / weight - point.x) * amount,
|
|
1459
|
-
y: point.y + (y / weight - point.y) * amount
|
|
1460
|
-
};
|
|
1461
1555
|
});
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1556
|
+
const [shortcuts, setShortcuts] = (0, react.useState)(() => {
|
|
1557
|
+
try {
|
|
1558
|
+
return localStorage.getItem("codex-sketch-shortcuts") !== "off";
|
|
1559
|
+
} catch {
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
}), [space, setSpace] = (0, react.useState)(false);
|
|
1563
|
+
const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
|
|
1564
|
+
viewRef.current = view;
|
|
1565
|
+
const zoom = (factor) => setView((v) => ({
|
|
1566
|
+
...v,
|
|
1567
|
+
scale: Math.max(.25, Math.min(8, v.scale * factor))
|
|
1568
|
+
}));
|
|
1569
|
+
const reset = () => setView({
|
|
1570
|
+
scale: 1,
|
|
1571
|
+
x: 0,
|
|
1572
|
+
y: 0
|
|
1573
|
+
});
|
|
1574
|
+
(0, react.useEffect)(() => {
|
|
1575
|
+
const node = canvas.current;
|
|
1576
|
+
if (!node || !open) return;
|
|
1577
|
+
const wheel = (e) => {
|
|
1578
|
+
if (!shortcuts || !e.altKey) return;
|
|
1579
|
+
e.preventDefault();
|
|
1580
|
+
zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
|
|
1487
1581
|
};
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1582
|
+
node.addEventListener("wheel", wheel, { passive: false });
|
|
1583
|
+
return () => node.removeEventListener("wheel", wheel);
|
|
1584
|
+
}, [open, shortcuts]);
|
|
1585
|
+
(0, react.useEffect)(() => {
|
|
1586
|
+
const stop = () => {
|
|
1587
|
+
drag.current = null;
|
|
1588
|
+
setSpace(false);
|
|
1589
|
+
};
|
|
1590
|
+
window.addEventListener("blur", stop);
|
|
1591
|
+
return () => window.removeEventListener("blur", stop);
|
|
1592
|
+
}, []);
|
|
1593
|
+
(0, react.useEffect)(() => {
|
|
1594
|
+
if (!open || !shortcuts) {
|
|
1595
|
+
setSpace(false);
|
|
1596
|
+
drag.current = null;
|
|
1597
|
+
}
|
|
1598
|
+
}, [open, shortcuts]);
|
|
1599
|
+
const setKey = (action, key) => {
|
|
1600
|
+
key = key.toLowerCase();
|
|
1601
|
+
if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
|
|
1602
|
+
const next = {
|
|
1603
|
+
...keys,
|
|
1604
|
+
[action]: key
|
|
1605
|
+
};
|
|
1606
|
+
setKeys(next);
|
|
1607
|
+
try {
|
|
1608
|
+
localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
|
|
1609
|
+
} catch {}
|
|
1610
|
+
};
|
|
1611
|
+
return {
|
|
1612
|
+
view,
|
|
1613
|
+
keys,
|
|
1614
|
+
shortcuts,
|
|
1615
|
+
space,
|
|
1616
|
+
zoom,
|
|
1617
|
+
reset,
|
|
1618
|
+
setKey,
|
|
1619
|
+
toggle: () => setShortcuts((v) => {
|
|
1620
|
+
try {
|
|
1621
|
+
localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
|
|
1622
|
+
} catch {}
|
|
1623
|
+
return !v;
|
|
1624
|
+
}),
|
|
1625
|
+
keyDown: (e) => {
|
|
1626
|
+
if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
1627
|
+
const action = sketchShortcutAction(keys, e.key);
|
|
1628
|
+
if (action === "pan") {
|
|
1629
|
+
e.preventDefault();
|
|
1630
|
+
setSpace(true);
|
|
1503
1631
|
return true;
|
|
1504
|
-
}
|
|
1505
|
-
} else {
|
|
1506
|
-
const stroke = layer.strokes.at(-1);
|
|
1507
|
-
if (!stroke) return doc;
|
|
1632
|
+
}
|
|
1508
1633
|
if ([
|
|
1509
|
-
"
|
|
1510
|
-
"
|
|
1511
|
-
"
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
if (
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
x: a.x * w,
|
|
1518
|
-
y: a.y * h
|
|
1519
|
-
}, {
|
|
1520
|
-
x: point.x * w,
|
|
1521
|
-
y: point.y * h
|
|
1522
|
-
});
|
|
1523
|
-
point = {
|
|
1524
|
-
x: snapped.x / w,
|
|
1525
|
-
y: snapped.y / h
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
stroke.points = [stroke.points[0], point];
|
|
1529
|
-
} else {
|
|
1530
|
-
const last = stroke.points.at(-1);
|
|
1531
|
-
if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
|
|
1532
|
-
if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
|
|
1533
|
-
stroke.points.push(point);
|
|
1634
|
+
"zoomIn",
|
|
1635
|
+
"zoomOut",
|
|
1636
|
+
"fit"
|
|
1637
|
+
].includes(action)) {
|
|
1638
|
+
e.preventDefault();
|
|
1639
|
+
if (action === "fit") reset();
|
|
1640
|
+
else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
|
|
1641
|
+
return true;
|
|
1534
1642
|
}
|
|
1643
|
+
return false;
|
|
1644
|
+
},
|
|
1645
|
+
keyUp: (e) => {
|
|
1646
|
+
if (e.key.toLowerCase() === keys.pan) setSpace(false);
|
|
1647
|
+
},
|
|
1648
|
+
down: (e) => {
|
|
1649
|
+
if (e.button !== 1 && !space) return false;
|
|
1650
|
+
e.preventDefault();
|
|
1651
|
+
drag.current = {
|
|
1652
|
+
id: e.pointerId,
|
|
1653
|
+
x: e.clientX,
|
|
1654
|
+
y: e.clientY,
|
|
1655
|
+
view: viewRef.current
|
|
1656
|
+
};
|
|
1657
|
+
canvas.current.setPointerCapture(e.pointerId);
|
|
1658
|
+
return true;
|
|
1659
|
+
},
|
|
1660
|
+
move: (e) => {
|
|
1661
|
+
const d = drag.current;
|
|
1662
|
+
if (!d || d.id !== e.pointerId) return false;
|
|
1663
|
+
setView({
|
|
1664
|
+
...d.view,
|
|
1665
|
+
x: d.view.x + e.clientX - d.x,
|
|
1666
|
+
y: d.view.y + e.clientY - d.y
|
|
1667
|
+
});
|
|
1668
|
+
return true;
|
|
1669
|
+
},
|
|
1670
|
+
end: (e) => {
|
|
1671
|
+
if (drag.current?.id !== e.pointerId) return false;
|
|
1672
|
+
drag.current = null;
|
|
1673
|
+
if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
|
|
1674
|
+
return true;
|
|
1535
1675
|
}
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
return doc;
|
|
1676
|
+
};
|
|
1539
1677
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (blocked) {
|
|
1562
|
-
request.transaction.abort();
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
const db = request.result, tx = request.transaction;
|
|
1566
|
-
if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
|
|
1567
|
-
const meta = db.createObjectStore("metadata", { keyPath: "key" });
|
|
1568
|
-
db.createObjectStore("recovery", { keyPath: "id" });
|
|
1569
|
-
const cursor = tx.objectStore("drafts").openCursor();
|
|
1570
|
-
cursor.onsuccess = () => {
|
|
1571
|
-
const row = cursor.result;
|
|
1572
|
-
if (row) {
|
|
1573
|
-
meta.put(metadata("drafts", row.value));
|
|
1574
|
-
row.continue();
|
|
1678
|
+
function SketchViewControls({ navigation, t }) {
|
|
1679
|
+
const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
|
|
1680
|
+
const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
|
|
1681
|
+
const close = () => {
|
|
1682
|
+
setOpen(false);
|
|
1683
|
+
trigger.current?.focus({ preventScroll: true });
|
|
1684
|
+
};
|
|
1685
|
+
useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
|
|
1686
|
+
(0, react.useLayoutEffect)(() => {
|
|
1687
|
+
if (!open) return;
|
|
1688
|
+
const dialog = host.current.closest("dialog");
|
|
1689
|
+
const place = () => {
|
|
1690
|
+
const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
|
|
1691
|
+
const width = Math.min(360, box.width - 24);
|
|
1692
|
+
setPlacement({
|
|
1693
|
+
dialog,
|
|
1694
|
+
style: {
|
|
1695
|
+
width,
|
|
1696
|
+
left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
|
|
1697
|
+
bottom: box.bottom - anchor.top + 8,
|
|
1698
|
+
maxHeight: Math.max(80, anchor.top - box.top - 24)
|
|
1575
1699
|
}
|
|
1576
|
-
};
|
|
1700
|
+
});
|
|
1577
1701
|
};
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1702
|
+
place();
|
|
1703
|
+
const observer = new ResizeObserver(place);
|
|
1704
|
+
observer.observe(dialog);
|
|
1705
|
+
observer.observe(host.current);
|
|
1706
|
+
window.addEventListener("resize", place);
|
|
1707
|
+
return () => {
|
|
1708
|
+
observer.disconnect();
|
|
1709
|
+
window.removeEventListener("resize", place);
|
|
1585
1710
|
};
|
|
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
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
const image = new Image();
|
|
1663
|
-
image.src = src;
|
|
1664
|
-
await image.decode();
|
|
1665
|
-
images.set(src, image);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
async function importSketchImage(file) {
|
|
1669
|
-
if (![
|
|
1670
|
-
"image/png",
|
|
1671
|
-
"image/jpeg",
|
|
1672
|
-
"image/webp"
|
|
1673
|
-
].includes(file.type) || file.size > 20 * 1024 * 1024) throw Error("Image must be PNG, JPEG or WebP under 20 MB");
|
|
1674
|
-
const bitmap = await createImageBitmap(file);
|
|
1675
|
-
try {
|
|
1676
|
-
if (bitmap.width * bitmap.height > 32 * 1024 * 1024) throw Error("Image too large");
|
|
1677
|
-
const scale = Math.min(1, 1024 / Math.max(bitmap.width, bitmap.height));
|
|
1678
|
-
const canvas = document.createElement("canvas");
|
|
1679
|
-
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
1680
|
-
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
1681
|
-
canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
1682
|
-
return {
|
|
1683
|
-
src: canvas.toDataURL("image/png"),
|
|
1684
|
-
width: canvas.width,
|
|
1685
|
-
height: canvas.height
|
|
1686
|
-
};
|
|
1687
|
-
} finally {
|
|
1688
|
-
bitmap.close();
|
|
1689
|
-
}
|
|
1711
|
+
}, [open]);
|
|
1712
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1713
|
+
ref: host,
|
|
1714
|
+
className: "codexSketchViewControls",
|
|
1715
|
+
children: [
|
|
1716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1717
|
+
type: "button",
|
|
1718
|
+
"aria-label": t("sketchZoomOut"),
|
|
1719
|
+
onClick: () => navigation.zoom(1 / 1.2),
|
|
1720
|
+
children: "−"
|
|
1721
|
+
}),
|
|
1722
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1723
|
+
type: "button",
|
|
1724
|
+
title: t("sketchFit"),
|
|
1725
|
+
onClick: navigation.reset,
|
|
1726
|
+
children: [Math.round(navigation.view.scale * 100), "%"]
|
|
1727
|
+
}),
|
|
1728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1729
|
+
type: "button",
|
|
1730
|
+
"aria-label": t("sketchZoomIn"),
|
|
1731
|
+
onClick: () => navigation.zoom(1.2),
|
|
1732
|
+
children: "+"
|
|
1733
|
+
}),
|
|
1734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1735
|
+
ref: trigger,
|
|
1736
|
+
type: "button",
|
|
1737
|
+
"aria-expanded": open,
|
|
1738
|
+
onClick: () => setOpen(!open),
|
|
1739
|
+
children: t("sketchKeys")
|
|
1740
|
+
}),
|
|
1741
|
+
open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1742
|
+
className: "codexSketchKeyPanel",
|
|
1743
|
+
"aria-label": t("sketchKeys"),
|
|
1744
|
+
style: placement.style,
|
|
1745
|
+
onKeyDown: (e) => {
|
|
1746
|
+
if (e.key === "Escape") {
|
|
1747
|
+
e.preventDefault();
|
|
1748
|
+
e.stopPropagation();
|
|
1749
|
+
close();
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
children: [
|
|
1753
|
+
/* @__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", {
|
|
1754
|
+
type: "button",
|
|
1755
|
+
"aria-label": t("sketchFileClose"),
|
|
1756
|
+
onClick: close,
|
|
1757
|
+
children: "×"
|
|
1758
|
+
})] }),
|
|
1759
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1760
|
+
className: "codexSketchKeysEnabled",
|
|
1761
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1762
|
+
type: "checkbox",
|
|
1763
|
+
checked: navigation.shortcuts,
|
|
1764
|
+
onChange: navigation.toggle
|
|
1765
|
+
})]
|
|
1766
|
+
}),
|
|
1767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
|
|
1768
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1769
|
+
className: "codexSketchKeyGrid",
|
|
1770
|
+
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", {
|
|
1771
|
+
"aria-label": t(`sketchKey_${action}`),
|
|
1772
|
+
value: key === " " ? "Space" : key,
|
|
1773
|
+
readOnly: true,
|
|
1774
|
+
onKeyDown: (e) => {
|
|
1775
|
+
if (e.key === "Tab" || e.key === "Escape") return;
|
|
1776
|
+
e.preventDefault();
|
|
1777
|
+
e.stopPropagation();
|
|
1778
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
|
|
1779
|
+
}
|
|
1780
|
+
})] }, action))
|
|
1781
|
+
}),
|
|
1782
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
|
|
1783
|
+
]
|
|
1784
|
+
}), placement.dialog) : null
|
|
1785
|
+
]
|
|
1786
|
+
});
|
|
1690
1787
|
}
|
|
1691
1788
|
//#endregion
|
|
1692
|
-
//#region src/sketch-
|
|
1693
|
-
const
|
|
1694
|
-
"
|
|
1695
|
-
"
|
|
1696
|
-
"
|
|
1789
|
+
//#region src/sketch-controls.jsx
|
|
1790
|
+
const PALETTE = [
|
|
1791
|
+
"#18181b",
|
|
1792
|
+
"#929398",
|
|
1793
|
+
"#ff3936",
|
|
1794
|
+
"#ff9500",
|
|
1795
|
+
"#ffcc00",
|
|
1796
|
+
"#34c759",
|
|
1797
|
+
"#0088ff"
|
|
1697
1798
|
];
|
|
1698
|
-
function
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1799
|
+
function SketchControls({ t, navigation, picturesOpen, setPicturesOpen, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen, fillShape, setFillShape, selected, editObject, selection, setTextEdit, stability, setStability, eraser, setEraser, color, pickColor }) {
|
|
1800
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1801
|
+
className: "codexSketchControls",
|
|
1802
|
+
children: [
|
|
1803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1804
|
+
type: "button",
|
|
1805
|
+
className: "codexSketchPicturesToggle",
|
|
1806
|
+
"aria-expanded": picturesOpen,
|
|
1807
|
+
onClick: () => setPicturesOpen((value) => !value),
|
|
1808
|
+
children: t("sketchPictures")
|
|
1809
|
+
}),
|
|
1810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchViewControls, {
|
|
1811
|
+
navigation,
|
|
1812
|
+
t
|
|
1813
|
+
}),
|
|
1814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
|
|
1815
|
+
t,
|
|
1816
|
+
disabled,
|
|
1817
|
+
tool,
|
|
1818
|
+
brush,
|
|
1819
|
+
chooseBrush,
|
|
1820
|
+
chooseTool,
|
|
1821
|
+
shapesOpen,
|
|
1822
|
+
setShapesOpen
|
|
1823
|
+
}),
|
|
1824
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1825
|
+
className: "codexLayerBrush",
|
|
1826
|
+
children: [
|
|
1827
|
+
["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1828
|
+
type: "checkbox",
|
|
1829
|
+
disabled,
|
|
1830
|
+
checked: fillShape,
|
|
1831
|
+
onChange: (event) => setFillShape(event.target.checked)
|
|
1832
|
+
}), t("sketchFill")] }) : null,
|
|
1833
|
+
selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1834
|
+
className: "codexSketchObjectActions",
|
|
1835
|
+
children: [
|
|
1836
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1837
|
+
disabled,
|
|
1838
|
+
onClick: () => editObject({}, "duplicate"),
|
|
1839
|
+
children: t("sketchObjectDuplicate")
|
|
1840
|
+
}),
|
|
1841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1842
|
+
disabled,
|
|
1843
|
+
onClick: () => editObject({}, "delete"),
|
|
1844
|
+
children: t("sketchObjectDelete")
|
|
1845
|
+
}),
|
|
1846
|
+
selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1847
|
+
disabled,
|
|
1848
|
+
onClick: () => setTextEdit({
|
|
1849
|
+
selection,
|
|
1850
|
+
value: selected.text
|
|
1851
|
+
}),
|
|
1852
|
+
children: t("sketchText")
|
|
1853
|
+
}) : null
|
|
1854
|
+
]
|
|
1855
|
+
}) : null,
|
|
1856
|
+
tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1857
|
+
className: "codexSketchStability",
|
|
1858
|
+
children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1859
|
+
"aria-label": t("sketchStability"),
|
|
1860
|
+
value: stability,
|
|
1861
|
+
disabled,
|
|
1862
|
+
onChange: (event) => setStability(Number(event.target.value)),
|
|
1863
|
+
children: [
|
|
1864
|
+
0,
|
|
1865
|
+
25,
|
|
1866
|
+
50,
|
|
1867
|
+
75
|
|
1868
|
+
].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1869
|
+
value,
|
|
1870
|
+
children: t(`sketchStability${value}`)
|
|
1871
|
+
}, value))
|
|
1872
|
+
})]
|
|
1873
|
+
}) : null,
|
|
1874
|
+
tool === "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1875
|
+
className: "codexSketchSegment",
|
|
1876
|
+
role: "group",
|
|
1877
|
+
"aria-label": t("sketchEraserMode"),
|
|
1878
|
+
children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1879
|
+
type: "button",
|
|
1880
|
+
"aria-pressed": eraser === value,
|
|
1881
|
+
disabled,
|
|
1882
|
+
onClick: () => setEraser(value),
|
|
1883
|
+
children: t(`sketchErase_${value}`)
|
|
1884
|
+
}, value))
|
|
1885
|
+
}) : null
|
|
1886
|
+
]
|
|
1887
|
+
}),
|
|
1888
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1889
|
+
className: "codexSketchPalette",
|
|
1890
|
+
role: "group",
|
|
1891
|
+
"aria-label": t("sketchColor"),
|
|
1892
|
+
children: [PALETTE.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1893
|
+
type: "button",
|
|
1894
|
+
className: "codexSketchSwatch",
|
|
1895
|
+
style: { "--swatch": value },
|
|
1896
|
+
"aria-label": `${t("sketchColor")} ${value}`,
|
|
1897
|
+
"aria-pressed": (selected?.color ?? color) === value,
|
|
1898
|
+
disabled,
|
|
1899
|
+
onClick: () => pickColor(value)
|
|
1900
|
+
}, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1901
|
+
className: "codexSketchCustom",
|
|
1902
|
+
title: t("sketchColor"),
|
|
1903
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { background: color } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1904
|
+
type: "color",
|
|
1905
|
+
"aria-label": t("sketchColor"),
|
|
1906
|
+
value: color,
|
|
1907
|
+
disabled,
|
|
1908
|
+
onChange: (event) => pickColor(event.target.value)
|
|
1909
|
+
})]
|
|
1910
|
+
})]
|
|
1911
|
+
})
|
|
1912
|
+
]
|
|
1717
1913
|
});
|
|
1914
|
+
}
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/sketch-import-confirm.jsx
|
|
1917
|
+
function useSketchImportConfirm(t) {
|
|
1918
|
+
const [pending, setPending] = (0, react.useState)(false);
|
|
1919
|
+
const resolve = (0, react.useRef)(null);
|
|
1920
|
+
const dialog = (0, react.useRef)(null);
|
|
1921
|
+
const finish = (accepted) => {
|
|
1922
|
+
const done = resolve.current;
|
|
1923
|
+
resolve.current = null;
|
|
1924
|
+
setPending(false);
|
|
1925
|
+
done?.(accepted);
|
|
1926
|
+
};
|
|
1927
|
+
(0, react.useEffect)(() => {
|
|
1928
|
+
if (pending) dialog.current?.showModal();
|
|
1929
|
+
else dialog.current?.close();
|
|
1930
|
+
}, [pending]);
|
|
1931
|
+
(0, react.useEffect)(() => () => {
|
|
1932
|
+
resolve.current?.(false);
|
|
1933
|
+
resolve.current = null;
|
|
1934
|
+
}, []);
|
|
1718
1935
|
return {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1936
|
+
confirmPsd: () => new Promise((done) => {
|
|
1937
|
+
resolve.current?.(false);
|
|
1938
|
+
resolve.current = done;
|
|
1939
|
+
setPending(true);
|
|
1940
|
+
}),
|
|
1941
|
+
prompt: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
1942
|
+
ref: dialog,
|
|
1943
|
+
className: "codexSketchImportConfirm",
|
|
1944
|
+
"aria-label": t("sketchPsdConfirmTitle"),
|
|
1945
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
1946
|
+
onCancel: (event) => {
|
|
1947
|
+
event.preventDefault();
|
|
1948
|
+
event.stopPropagation();
|
|
1949
|
+
finish(false);
|
|
1950
|
+
},
|
|
1951
|
+
children: [
|
|
1952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("sketchPsdConfirmTitle") }),
|
|
1953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchPsdConfirmBody") }),
|
|
1954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1955
|
+
type: "button",
|
|
1956
|
+
autoFocus: true,
|
|
1957
|
+
onClick: () => finish(false),
|
|
1958
|
+
children: t("sketchCancel")
|
|
1959
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1960
|
+
type: "button",
|
|
1961
|
+
onClick: () => finish(true),
|
|
1962
|
+
children: t("sketchImport")
|
|
1963
|
+
})] })
|
|
1964
|
+
]
|
|
1965
|
+
})
|
|
1725
1966
|
};
|
|
1726
1967
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
layer
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
points: original.points.map((p) => ({ ...p }))
|
|
1799
|
-
});
|
|
1800
|
-
else if (command.action === "update") {
|
|
1801
|
-
const patch = command.patch ?? {};
|
|
1802
|
-
if (Object.keys(patch).some((k) => ![
|
|
1803
|
-
"color",
|
|
1804
|
-
"width",
|
|
1805
|
-
"opacity",
|
|
1806
|
-
"fill",
|
|
1807
|
-
"text",
|
|
1808
|
-
"points"
|
|
1809
|
-
].includes(k))) throw Error("Unsupported object property");
|
|
1810
|
-
const changed = command.transform ? transformObject({
|
|
1811
|
-
...original,
|
|
1812
|
-
...patch
|
|
1813
|
-
}, command.transform) : {
|
|
1814
|
-
...original,
|
|
1815
|
-
...patch
|
|
1816
|
-
};
|
|
1817
|
-
strokes[index] = {
|
|
1818
|
-
...applySketchCommands({
|
|
1819
|
-
...doc,
|
|
1820
|
-
layers: [{
|
|
1821
|
-
...layer,
|
|
1822
|
-
strokes: []
|
|
1823
|
-
}]
|
|
1824
|
-
}, [{
|
|
1825
|
-
...changed,
|
|
1826
|
-
op: "stroke",
|
|
1827
|
-
layer: layer.id
|
|
1828
|
-
}]).layers[0].strokes[0],
|
|
1829
|
-
brush: original.brush ?? "pen",
|
|
1830
|
-
...original.pressure !== void 0 ? { pressure: original.pressure } : {},
|
|
1831
|
-
...original.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
1832
|
-
};
|
|
1833
|
-
} else throw Error("Unknown object action");
|
|
1834
|
-
doc = {
|
|
1835
|
-
...doc,
|
|
1836
|
-
layers: doc.layers.map((l) => l === layer ? {
|
|
1837
|
-
...l,
|
|
1838
|
-
strokes
|
|
1839
|
-
} : l)
|
|
1840
|
-
};
|
|
1841
|
-
continue;
|
|
1842
|
-
}
|
|
1843
|
-
if (command.op !== "stroke") throw Error("Unknown command");
|
|
1844
|
-
const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
|
|
1845
|
-
const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
|
|
1846
|
-
let points = command.points;
|
|
1847
|
-
if (command.start !== void 0 || command.segments !== void 0) {
|
|
1848
|
-
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");
|
|
1849
|
-
points = [command.start, ...command.segments.flatMap((s) => [
|
|
1850
|
-
s?.control1,
|
|
1851
|
-
s?.control2,
|
|
1852
|
-
s?.end
|
|
1853
|
-
])];
|
|
1968
|
+
//#endregion
|
|
1969
|
+
//#region src/sketch-objects.js
|
|
1970
|
+
const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
|
|
1971
|
+
const identifyObjects = (doc) => ({
|
|
1972
|
+
...doc,
|
|
1973
|
+
layers: doc.layers.map((layer) => ({
|
|
1974
|
+
...layer,
|
|
1975
|
+
strokes: layer.strokes.map((s, i) => s.id ? s : {
|
|
1976
|
+
...s,
|
|
1977
|
+
id: objectId(s, i)
|
|
1978
|
+
})
|
|
1979
|
+
}))
|
|
1980
|
+
});
|
|
1981
|
+
function objectBounds(stroke) {
|
|
1982
|
+
const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
|
|
1983
|
+
return {
|
|
1984
|
+
x: Math.min(...xs),
|
|
1985
|
+
y: Math.min(...ys),
|
|
1986
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
1987
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
|
|
1991
|
+
if (![
|
|
1992
|
+
dx,
|
|
1993
|
+
dy,
|
|
1994
|
+
scaleX,
|
|
1995
|
+
scaleY
|
|
1996
|
+
].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
|
|
1997
|
+
const box = objectBounds(stroke);
|
|
1998
|
+
const points = stroke.points.map((p) => ({
|
|
1999
|
+
x: box.x + (p.x - box.x) * scaleX + dx,
|
|
2000
|
+
y: box.y + (p.y - box.y) * scaleY + dy
|
|
2001
|
+
}));
|
|
2002
|
+
if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
|
|
2003
|
+
return {
|
|
2004
|
+
...stroke,
|
|
2005
|
+
points
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function sketchObjectSummary(doc) {
|
|
2009
|
+
return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
|
|
2010
|
+
layer: layer.id,
|
|
2011
|
+
id: objectId(stroke, i),
|
|
2012
|
+
shape: stroke.shape,
|
|
2013
|
+
color: stroke.color,
|
|
2014
|
+
bounds: objectBounds(stroke),
|
|
2015
|
+
...stroke.text ? { text: stroke.text } : {}
|
|
2016
|
+
})));
|
|
2017
|
+
}
|
|
2018
|
+
//#endregion
|
|
2019
|
+
//#region src/sketch-input.js
|
|
2020
|
+
function snapLine(start, end) {
|
|
2021
|
+
const angle = Math.round(Math.atan2(end.y - start.y, end.x - start.x) / (Math.PI / 4)) * Math.PI / 4;
|
|
2022
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
2023
|
+
return {
|
|
2024
|
+
x: start.x + Math.cos(angle) * length,
|
|
2025
|
+
y: start.y + Math.sin(angle) * length
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function smoothStrokePoints(points, strength = 0) {
|
|
2029
|
+
if (!strength || points.length < 3) return points;
|
|
2030
|
+
const radius = Math.max(1, Math.round(strength / 25)), amount = Math.min(1, strength / 75);
|
|
2031
|
+
return points.map((point, i) => {
|
|
2032
|
+
if (i === 0 || i === points.length - 1) return point;
|
|
2033
|
+
let x = 0, y = 0, weight = 0;
|
|
2034
|
+
for (let j = Math.max(0, i - radius); j <= Math.min(points.length - 1, i + radius); j++) {
|
|
2035
|
+
const w = radius + 1 - Math.abs(j - i);
|
|
2036
|
+
x += points[j].x * w;
|
|
2037
|
+
y += points[j].y * w;
|
|
2038
|
+
weight += w;
|
|
1854
2039
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
"circle",
|
|
1860
|
-
"polygon",
|
|
1861
|
-
"bezier",
|
|
1862
|
-
"arrow",
|
|
1863
|
-
"text",
|
|
1864
|
-
"eraser"
|
|
1865
|
-
].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");
|
|
1866
|
-
if ([
|
|
1867
|
-
"line",
|
|
1868
|
-
"arrow",
|
|
1869
|
-
"text",
|
|
1870
|
-
"rectangle",
|
|
1871
|
-
"circle"
|
|
1872
|
-
].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
|
|
1873
|
-
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.`);
|
|
1874
|
-
if (fill && ![
|
|
1875
|
-
"rectangle",
|
|
1876
|
-
"circle",
|
|
1877
|
-
"polygon",
|
|
1878
|
-
"bezier"
|
|
1879
|
-
].includes(shape)) throw Error("Fill requires a closed shape");
|
|
1880
|
-
const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
|
|
1881
|
-
if (!layer?.visible) throw Error("Target layer is missing or hidden");
|
|
1882
|
-
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");
|
|
1883
|
-
const id = command.id ?? crypto.randomUUID();
|
|
1884
|
-
if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
|
|
1885
|
-
const stroke = {
|
|
1886
|
-
id,
|
|
1887
|
-
...shape === "text" ? { text: command.text } : {},
|
|
1888
|
-
shape,
|
|
1889
|
-
color,
|
|
1890
|
-
width,
|
|
1891
|
-
opacity,
|
|
1892
|
-
fill,
|
|
1893
|
-
brush: "pen",
|
|
1894
|
-
points: points.map((p) => ({
|
|
1895
|
-
x: p.x,
|
|
1896
|
-
y: p.y
|
|
1897
|
-
}))
|
|
2040
|
+
return {
|
|
2041
|
+
...point,
|
|
2042
|
+
x: point.x + (x / weight - point.x) * amount,
|
|
2043
|
+
y: point.y + (y / weight - point.y) * amount
|
|
1898
2044
|
};
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
//#endregion
|
|
2048
|
+
//#region src/sketch-gesture.js
|
|
2049
|
+
function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
|
|
2050
|
+
if (!samples.length) return doc;
|
|
2051
|
+
if (gesture.object) {
|
|
2052
|
+
const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
|
|
2053
|
+
if (!point) return doc;
|
|
2054
|
+
const box = objectBounds(gesture.object);
|
|
2055
|
+
const stroke = gesture.handle === "end" ? {
|
|
2056
|
+
...gesture.object,
|
|
2057
|
+
points: [gesture.object.points[0], point]
|
|
2058
|
+
} : gesture.handle === "size" ? transformObject(gesture.object, {
|
|
2059
|
+
scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
|
|
2060
|
+
scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
|
|
2061
|
+
}) : transformObject(gesture.object, {
|
|
2062
|
+
dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
|
|
2063
|
+
dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
|
|
2064
|
+
});
|
|
1899
2065
|
doc = {
|
|
1900
2066
|
...doc,
|
|
1901
|
-
layers: doc.layers.map((
|
|
1902
|
-
...
|
|
1903
|
-
strokes:
|
|
1904
|
-
} :
|
|
2067
|
+
layers: doc.layers.map((l) => l.id === gesture.layer ? {
|
|
2068
|
+
...l,
|
|
2069
|
+
strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
|
|
2070
|
+
} : l)
|
|
1905
2071
|
};
|
|
2072
|
+
return doc;
|
|
2073
|
+
}
|
|
2074
|
+
for (const sample of samples) {
|
|
2075
|
+
let point = sketchPoint(sample.clientX, sample.clientY, rect);
|
|
2076
|
+
if (!point) continue;
|
|
2077
|
+
const layer = doc.layers.find((layer) => layer.id === gesture.layer);
|
|
2078
|
+
if (!layer) return doc;
|
|
2079
|
+
if (gesture.eraseStroke) {
|
|
2080
|
+
const previous = gesture.last ?? point;
|
|
2081
|
+
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))));
|
|
2082
|
+
layer.strokes = layer.strokes.filter((stroke) => {
|
|
2083
|
+
for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
|
|
2084
|
+
x: previous.x + (point.x - previous.x) * i / steps,
|
|
2085
|
+
y: previous.y + (point.y - previous.y) * i / steps
|
|
2086
|
+
}, width / 2, doc.width, doc.height)) return false;
|
|
2087
|
+
return true;
|
|
2088
|
+
});
|
|
2089
|
+
} else {
|
|
2090
|
+
const stroke = layer.strokes.at(-1);
|
|
2091
|
+
if (!stroke) return doc;
|
|
2092
|
+
if ([
|
|
2093
|
+
"line",
|
|
2094
|
+
"arrow",
|
|
2095
|
+
"rectangle",
|
|
2096
|
+
"circle"
|
|
2097
|
+
].includes(stroke.shape)) {
|
|
2098
|
+
if (stroke.shape === "line" && shiftKey) {
|
|
2099
|
+
const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
|
|
2100
|
+
const snapped = snapLine({
|
|
2101
|
+
x: a.x * w,
|
|
2102
|
+
y: a.y * h
|
|
2103
|
+
}, {
|
|
2104
|
+
x: point.x * w,
|
|
2105
|
+
y: point.y * h
|
|
2106
|
+
});
|
|
2107
|
+
point = {
|
|
2108
|
+
x: snapped.x / w,
|
|
2109
|
+
y: snapped.y / h
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
stroke.points = [stroke.points[0], point];
|
|
2113
|
+
} else {
|
|
2114
|
+
const last = stroke.points.at(-1);
|
|
2115
|
+
if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
|
|
2116
|
+
if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
|
|
2117
|
+
stroke.points.push(point);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
gesture.last = point;
|
|
1906
2121
|
}
|
|
1907
|
-
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");
|
|
1908
2122
|
return doc;
|
|
1909
2123
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
|
|
1932
|
-
if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
|
|
1933
|
-
if (request.action === "preview") return {
|
|
1934
|
-
...current,
|
|
1935
|
-
png: await adapter.preview()
|
|
2124
|
+
//#endregion
|
|
2125
|
+
//#region src/sketch-drafts.js
|
|
2126
|
+
const DATABASE = "dsh-codex-sketches-v1";
|
|
2127
|
+
const MAX_STORAGE = 32 * 1024 * 1024;
|
|
2128
|
+
const metadata = (kind, row) => ({
|
|
2129
|
+
key: `${kind}:${row.id}`,
|
|
2130
|
+
kind,
|
|
2131
|
+
id: row.id,
|
|
2132
|
+
name: row.name,
|
|
2133
|
+
updated: row.updated,
|
|
2134
|
+
size: JSON.stringify(row).length
|
|
2135
|
+
});
|
|
2136
|
+
async function sketchDrafts(action, value, recoverySession) {
|
|
2137
|
+
const db = await new Promise((resolve, reject) => {
|
|
2138
|
+
let blocked = false;
|
|
2139
|
+
const request = indexedDB.open(DATABASE, 2);
|
|
2140
|
+
request.onblocked = () => {
|
|
2141
|
+
blocked = true;
|
|
2142
|
+
reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
|
|
1936
2143
|
};
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
runId: void 0
|
|
1942
|
-
});
|
|
1943
|
-
const cached = completed.get(key);
|
|
1944
|
-
if (cached) {
|
|
1945
|
-
if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
|
|
1946
|
-
return cached.result;
|
|
1947
|
-
}
|
|
1948
|
-
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
1949
|
-
let changedObjects;
|
|
1950
|
-
if (request.action === "apply") {
|
|
1951
|
-
const before = adapter.document();
|
|
1952
|
-
let next;
|
|
1953
|
-
try {
|
|
1954
|
-
next = applySketchCommands(before, request.commands);
|
|
1955
|
-
} catch (cause) {
|
|
1956
|
-
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
1957
|
-
error.code = "SKETCH_INVALID_BATCH";
|
|
1958
|
-
throw error;
|
|
1959
|
-
}
|
|
1960
|
-
adapter.commit(next);
|
|
1961
|
-
const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
|
|
1962
|
-
changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
|
|
1963
|
-
layer: l.id,
|
|
1964
|
-
id: s.id
|
|
1965
|
-
})));
|
|
1966
|
-
} else {
|
|
1967
|
-
if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
|
|
1968
|
-
pending = true;
|
|
1969
|
-
try {
|
|
1970
|
-
await adapter.save(request.name);
|
|
1971
|
-
} finally {
|
|
1972
|
-
pending = false;
|
|
2144
|
+
request.onupgradeneeded = () => {
|
|
2145
|
+
if (blocked) {
|
|
2146
|
+
request.transaction.abort();
|
|
2147
|
+
return;
|
|
1973
2148
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2149
|
+
const db = request.result, tx = request.transaction;
|
|
2150
|
+
if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
|
|
2151
|
+
const meta = db.createObjectStore("metadata", { keyPath: "key" });
|
|
2152
|
+
db.createObjectStore("recovery", { keyPath: "id" });
|
|
2153
|
+
const cursor = tx.objectStore("drafts").openCursor();
|
|
2154
|
+
cursor.onsuccess = () => {
|
|
2155
|
+
const row = cursor.result;
|
|
2156
|
+
if (row) {
|
|
2157
|
+
meta.put(metadata("drafts", row.value));
|
|
2158
|
+
row.continue();
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
1981
2161
|
};
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
requestId: request.requestId,
|
|
1987
|
-
action: request.action,
|
|
1988
|
-
revision: result.revision
|
|
2162
|
+
request.onsuccess = () => {
|
|
2163
|
+
if (blocked) {
|
|
2164
|
+
request.result.close();
|
|
2165
|
+
return;
|
|
1989
2166
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
|
|
1993
|
-
const oldest = completed.keys().next().value;
|
|
1994
|
-
cachedCharacters -= completed.get(oldest).fingerprint.length;
|
|
1995
|
-
completed.delete(oldest);
|
|
1996
|
-
}
|
|
1997
|
-
return result;
|
|
1998
|
-
};
|
|
1999
|
-
}
|
|
2000
|
-
//#endregion
|
|
2001
|
-
//#region src/sketch-formats.js
|
|
2002
|
-
const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
|
|
2003
|
-
const canvas = (w, h) => {
|
|
2004
|
-
const c = document.createElement("canvas");
|
|
2005
|
-
c.width = w;
|
|
2006
|
-
c.height = h;
|
|
2007
|
-
return c;
|
|
2008
|
-
};
|
|
2009
|
-
function runPsdCodec(action, payload) {
|
|
2010
|
-
return new Promise((resolve, reject) => {
|
|
2011
|
-
const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
|
|
2012
|
-
const finish = (callback, value) => {
|
|
2013
|
-
clearTimeout(timer);
|
|
2014
|
-
worker.terminate();
|
|
2015
|
-
callback(value);
|
|
2167
|
+
request.result.onversionchange = () => request.result.close();
|
|
2168
|
+
resolve(request.result);
|
|
2016
2169
|
};
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
if (i) doc = applySketchCommands(doc, [{
|
|
2048
|
-
op: "layer",
|
|
2049
|
-
action: "add"
|
|
2050
|
-
}]);
|
|
2051
|
-
if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
|
|
2052
|
-
for (let j = 0; j < layer.strokes.length; j += 256) {
|
|
2053
|
-
const strokes = layer.strokes.slice(j, j + 256);
|
|
2054
|
-
doc = applySketchCommands(doc, strokes.map((s) => ({
|
|
2055
|
-
...s,
|
|
2056
|
-
op: "stroke",
|
|
2057
|
-
layer: doc.active,
|
|
2058
|
-
fill: s.fill ?? false
|
|
2059
|
-
})));
|
|
2060
|
-
const added = doc.layers.at(-1).strokes;
|
|
2061
|
-
for (let k = 0; k < strokes.length; k++) {
|
|
2062
|
-
const s = strokes[k];
|
|
2063
|
-
if (s.brush !== void 0 && ![
|
|
2064
|
-
"pen",
|
|
2065
|
-
"pencil",
|
|
2066
|
-
"marker"
|
|
2067
|
-
].includes(s.brush)) throw Error("Invalid brush");
|
|
2068
|
-
if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
|
|
2069
|
-
if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
|
|
2070
|
-
Object.assign(added[added.length - strokes.length + k], {
|
|
2071
|
-
brush: s.brush ?? "pen",
|
|
2072
|
-
pressure: s.pressure ?? 1,
|
|
2073
|
-
...s.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
2074
|
-
});
|
|
2170
|
+
request.onerror = () => reject(request.error);
|
|
2171
|
+
});
|
|
2172
|
+
try {
|
|
2173
|
+
return await new Promise((resolve, reject) => {
|
|
2174
|
+
const write = [
|
|
2175
|
+
"save",
|
|
2176
|
+
"delete",
|
|
2177
|
+
"checkpoint",
|
|
2178
|
+
"clearRecovery"
|
|
2179
|
+
].includes(action);
|
|
2180
|
+
const tx = db.transaction([
|
|
2181
|
+
"drafts",
|
|
2182
|
+
"metadata",
|
|
2183
|
+
"recovery"
|
|
2184
|
+
], write ? "readwrite" : "readonly");
|
|
2185
|
+
const meta = tx.objectStore("metadata"), kind = [
|
|
2186
|
+
"checkpoint",
|
|
2187
|
+
"recover",
|
|
2188
|
+
"clearRecovery"
|
|
2189
|
+
].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
|
|
2190
|
+
let result, failure;
|
|
2191
|
+
tx.oncomplete = () => resolve(result);
|
|
2192
|
+
tx.onerror = () => reject(tx.error);
|
|
2193
|
+
tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
|
|
2194
|
+
if (action === "get" || action === "recover") {
|
|
2195
|
+
const req = store.get(value);
|
|
2196
|
+
req.onsuccess = () => {
|
|
2197
|
+
result = req.result;
|
|
2198
|
+
};
|
|
2199
|
+
return;
|
|
2075
2200
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
"
|
|
2084
|
-
"
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2201
|
+
if (action === "delete" || action === "clearRecovery") {
|
|
2202
|
+
store.delete(value);
|
|
2203
|
+
meta.delete(`${kind}:${value}`);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (![
|
|
2207
|
+
"list",
|
|
2208
|
+
"save",
|
|
2209
|
+
"checkpoint"
|
|
2210
|
+
].includes(action)) {
|
|
2211
|
+
tx.abort();
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const request = meta.getAll();
|
|
2215
|
+
request.onsuccess = () => {
|
|
2216
|
+
const rows = request.result;
|
|
2217
|
+
if (action === "list") {
|
|
2218
|
+
result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
|
|
2222
|
+
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;
|
|
2223
|
+
if (code) {
|
|
2224
|
+
failure = Object.assign(Error("Draft storage limit reached"), { code });
|
|
2225
|
+
tx.abort();
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
store.put(value);
|
|
2229
|
+
meta.put(next);
|
|
2230
|
+
if (action === "save" && recoverySession) {
|
|
2231
|
+
tx.objectStore("recovery").delete(recoverySession);
|
|
2232
|
+
meta.delete(`recovery:${recoverySession}`);
|
|
2233
|
+
}
|
|
2234
|
+
result = value;
|
|
2098
2235
|
};
|
|
2099
|
-
}
|
|
2236
|
+
});
|
|
2237
|
+
} finally {
|
|
2238
|
+
db.close();
|
|
2100
2239
|
}
|
|
2101
|
-
const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
|
|
2102
|
-
doc.active = doc.layers[Math.max(0, activeIndex)].id;
|
|
2103
|
-
return doc;
|
|
2104
2240
|
}
|
|
2105
|
-
async function
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
if (
|
|
2110
|
-
|
|
2241
|
+
async function decodeSketchImages(doc, images) {
|
|
2242
|
+
for (const layer of doc.layers) {
|
|
2243
|
+
const src = layer.image?.src;
|
|
2244
|
+
if (!src || images.has(src)) continue;
|
|
2245
|
+
if (!src.startsWith("data:image/png;base64,") || src.length > 8 * 1024 * 1024) throw Error("Invalid image");
|
|
2246
|
+
const image = new Image();
|
|
2247
|
+
image.src = src;
|
|
2248
|
+
await image.decode();
|
|
2249
|
+
images.set(src, image);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function importSketchImage(file) {
|
|
2253
|
+
if (![
|
|
2254
|
+
"image/png",
|
|
2255
|
+
"image/jpeg",
|
|
2256
|
+
"image/webp"
|
|
2257
|
+
].includes(file.type) || file.size > 20 * 1024 * 1024) throw Error("Image must be PNG, JPEG or WebP under 20 MB");
|
|
2258
|
+
const bitmap = await createImageBitmap(file);
|
|
2259
|
+
try {
|
|
2260
|
+
if (bitmap.width * bitmap.height > 32 * 1024 * 1024) throw Error("Image too large");
|
|
2261
|
+
const scale = Math.min(1, 1024 / Math.max(bitmap.width, bitmap.height));
|
|
2262
|
+
const canvas = document.createElement("canvas");
|
|
2263
|
+
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
2264
|
+
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
2265
|
+
canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
2111
2266
|
return {
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
blendMode: "normal",
|
|
2116
|
-
imageData: ctx.getImageData(0, 0, width, height)
|
|
2267
|
+
src: canvas.toDataURL("image/png"),
|
|
2268
|
+
width: canvas.width,
|
|
2269
|
+
height: canvas.height
|
|
2117
2270
|
};
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
context.fillStyle = "#fff";
|
|
2121
|
-
context.fillRect(0, 0, width, height);
|
|
2122
|
-
if (children[0] && !children[0].hidden) {
|
|
2123
|
-
const bottom = canvas(width, height);
|
|
2124
|
-
bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
|
|
2125
|
-
context.drawImage(bottom, 0, 0);
|
|
2126
|
-
children[0].imageData = context.getImageData(0, 0, width, height);
|
|
2127
|
-
} else {
|
|
2128
|
-
if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
|
|
2129
|
-
children.unshift({
|
|
2130
|
-
name: "Paper",
|
|
2131
|
-
opacity: 1,
|
|
2132
|
-
blendMode: "normal",
|
|
2133
|
-
imageData: context.getImageData(0, 0, width, height)
|
|
2134
|
-
});
|
|
2271
|
+
} finally {
|
|
2272
|
+
bitmap.close();
|
|
2135
2273
|
}
|
|
2136
|
-
return runPsdCodec("write", {
|
|
2137
|
-
width,
|
|
2138
|
-
height,
|
|
2139
|
-
children,
|
|
2140
|
-
imageData: composite.getContext("2d").getImageData(0, 0, width, height)
|
|
2141
|
-
});
|
|
2142
2274
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2275
|
+
//#endregion
|
|
2276
|
+
//#region src/sketch-presets.js
|
|
2277
|
+
const SKETCH_PRESETS = [
|
|
2278
|
+
"triangle",
|
|
2279
|
+
"diamond",
|
|
2280
|
+
"star"
|
|
2281
|
+
];
|
|
2282
|
+
function expandSketchPreset(command) {
|
|
2283
|
+
if (!SKETCH_PRESETS.includes(command.shape)) return command;
|
|
2284
|
+
const points = command.points;
|
|
2285
|
+
if (!Array.isArray(points) || points.length !== 2 || points.some((p) => !p || ![p.x, p.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))) throw Error("Shape preset requires two normalized bounding-box corners");
|
|
2286
|
+
const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
|
|
2287
|
+
const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
|
|
2288
|
+
if (!w || !h) throw Error("Shape preset requires a non-empty box");
|
|
2289
|
+
const vertices = command.shape === "triangle" ? [
|
|
2290
|
+
[.5, 0],
|
|
2291
|
+
[1, 1],
|
|
2292
|
+
[0, 1]
|
|
2293
|
+
] : command.shape === "diamond" ? [
|
|
2294
|
+
[.5, 0],
|
|
2295
|
+
[1, .5],
|
|
2296
|
+
[.5, 1],
|
|
2297
|
+
[0, .5]
|
|
2298
|
+
] : Array.from({ length: 10 }, (_, i) => {
|
|
2299
|
+
const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
|
|
2300
|
+
return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
|
|
2301
|
+
});
|
|
2302
|
+
return {
|
|
2303
|
+
...command,
|
|
2304
|
+
shape: "polygon",
|
|
2305
|
+
points: vertices.map(([u, v]) => ({
|
|
2306
|
+
x: x + u * w,
|
|
2307
|
+
y: y + v * h
|
|
2308
|
+
}))
|
|
2154
2309
|
};
|
|
2155
|
-
for (const [i, layer] of psd.layers.entries()) {
|
|
2156
|
-
const src = canvas(layer.imageData.width, layer.imageData.height);
|
|
2157
|
-
src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
|
|
2158
|
-
const out = canvas(width, height), ctx = out.getContext("2d");
|
|
2159
|
-
ctx.globalAlpha = layer.opacity;
|
|
2160
|
-
ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
|
|
2161
|
-
doc.layers.push({
|
|
2162
|
-
id: i + 1,
|
|
2163
|
-
name: layer.name,
|
|
2164
|
-
visible: !layer.hidden,
|
|
2165
|
-
strokes: [],
|
|
2166
|
-
image: {
|
|
2167
|
-
src: out.toDataURL("image/png"),
|
|
2168
|
-
x: 0,
|
|
2169
|
-
y: 0,
|
|
2170
|
-
width: 1,
|
|
2171
|
-
height: 1
|
|
2172
|
-
}
|
|
2173
|
-
});
|
|
2174
|
-
}
|
|
2175
|
-
return doc;
|
|
2176
2310
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2311
|
+
const SKETCH_COMMAND_HELP = {
|
|
2312
|
+
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.",
|
|
2313
|
+
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.",
|
|
2314
|
+
commands: {
|
|
2315
|
+
preset: "triangle/diamond/star: {op:\"stroke\",id:\"badge\",shape:\"star\",points:[{x:0.1,y:0.1},{x:0.3,y:0.3}],color:\"#ffcc00\",fill:true}. Two opposite box corners; stored as editable polygon. Inspect objectId for one object without repeating full help.",
|
|
2316
|
+
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},...]}",
|
|
2317
|
+
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.",
|
|
2318
|
+
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.",
|
|
2319
|
+
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.",
|
|
2320
|
+
resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
|
|
2321
|
+
},
|
|
2322
|
+
limits: {
|
|
2323
|
+
strokes: MAX_SKETCH_STROKES,
|
|
2324
|
+
pointsPerStroke: MAX_STROKE_POINTS,
|
|
2325
|
+
pointsTotal: 2e5,
|
|
2326
|
+
commandsPerBatch: 256
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
|
|
2330
|
+
function applySketchCommands(source, commands) {
|
|
2331
|
+
if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
|
|
2332
|
+
let doc = identifyObjects(source);
|
|
2333
|
+
for (let command of commands) {
|
|
2334
|
+
if (!command || typeof command !== "object") throw Error("Invalid command");
|
|
2335
|
+
if (command.op === "stroke") command = expandSketchPreset(command);
|
|
2336
|
+
if (command.op === "resize") {
|
|
2337
|
+
doc = resizeSketch(doc, command.ratio);
|
|
2338
|
+
continue;
|
|
2196
2339
|
}
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2340
|
+
if (command.op === "layer") {
|
|
2341
|
+
if (![
|
|
2342
|
+
"add",
|
|
2343
|
+
"select",
|
|
2344
|
+
"rename",
|
|
2345
|
+
"visible",
|
|
2346
|
+
"duplicate",
|
|
2347
|
+
"up",
|
|
2348
|
+
"down",
|
|
2349
|
+
"delete",
|
|
2350
|
+
"clear"
|
|
2351
|
+
].includes(command.action)) throw Error("Unknown layer action");
|
|
2352
|
+
if (command.action === "add") {
|
|
2353
|
+
const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
|
|
2354
|
+
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");
|
|
2355
|
+
const next = changeSketchLayer(doc, "add", after);
|
|
2356
|
+
if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
|
|
2357
|
+
doc = {
|
|
2358
|
+
...next,
|
|
2359
|
+
active: id,
|
|
2360
|
+
nextId: Math.max(next.nextId, id + 1),
|
|
2361
|
+
layers: next.layers.map((l) => l.id === next.active ? {
|
|
2362
|
+
...l,
|
|
2363
|
+
id,
|
|
2364
|
+
name: String(command.value ?? "").trim().slice(0, 40)
|
|
2365
|
+
} : l)
|
|
2366
|
+
};
|
|
2367
|
+
continue;
|
|
2205
2368
|
}
|
|
2369
|
+
const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
|
|
2370
|
+
if (next === doc) throw Error("Layer action unavailable; inspect the document first");
|
|
2371
|
+
doc = next;
|
|
2372
|
+
continue;
|
|
2206
2373
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2374
|
+
if (command.op === "object") {
|
|
2375
|
+
const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
|
|
2376
|
+
if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
|
|
2377
|
+
const strokes = layer.strokes.slice(), original = strokes[index];
|
|
2378
|
+
if (command.action === "delete") strokes.splice(index, 1);
|
|
2379
|
+
else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
|
|
2380
|
+
...original,
|
|
2381
|
+
id: crypto.randomUUID(),
|
|
2382
|
+
points: original.points.map((p) => ({ ...p }))
|
|
2383
|
+
});
|
|
2384
|
+
else if (command.action === "update") {
|
|
2385
|
+
const patch = command.patch ?? {};
|
|
2386
|
+
if (Object.keys(patch).some((k) => ![
|
|
2387
|
+
"color",
|
|
2388
|
+
"width",
|
|
2389
|
+
"opacity",
|
|
2390
|
+
"fill",
|
|
2391
|
+
"text",
|
|
2392
|
+
"points"
|
|
2393
|
+
].includes(k))) throw Error("Unsupported object property");
|
|
2394
|
+
const changed = command.transform ? transformObject({
|
|
2395
|
+
...original,
|
|
2396
|
+
...patch
|
|
2397
|
+
}, command.transform) : {
|
|
2398
|
+
...original,
|
|
2399
|
+
...patch
|
|
2400
|
+
};
|
|
2401
|
+
strokes[index] = {
|
|
2402
|
+
...applySketchCommands({
|
|
2403
|
+
...doc,
|
|
2404
|
+
layers: [{
|
|
2405
|
+
...layer,
|
|
2406
|
+
strokes: []
|
|
2407
|
+
}]
|
|
2408
|
+
}, [{
|
|
2409
|
+
...changed,
|
|
2410
|
+
op: "stroke",
|
|
2411
|
+
layer: layer.id
|
|
2412
|
+
}]).layers[0].strokes[0],
|
|
2413
|
+
brush: original.brush ?? "pen",
|
|
2414
|
+
...original.pressure !== void 0 ? { pressure: original.pressure } : {},
|
|
2415
|
+
...original.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
2416
|
+
};
|
|
2417
|
+
} else throw Error("Unknown object action");
|
|
2418
|
+
doc = {
|
|
2419
|
+
...doc,
|
|
2420
|
+
layers: doc.layers.map((l) => l === layer ? {
|
|
2421
|
+
...l,
|
|
2422
|
+
strokes
|
|
2423
|
+
} : l)
|
|
2424
|
+
};
|
|
2425
|
+
continue;
|
|
2216
2426
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
saved.current = identity;
|
|
2229
|
-
dirty.current = false;
|
|
2230
|
-
schedule();
|
|
2231
|
-
};
|
|
2232
|
-
const fresh = async () => {
|
|
2233
|
-
await saveChanges();
|
|
2234
|
-
replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
|
|
2235
|
-
};
|
|
2236
|
-
const load = async (row) => {
|
|
2237
|
-
if (row.id === saved.current?.id) return;
|
|
2238
|
-
row = await store("get", row.id);
|
|
2239
|
-
if (!row) throw Error("Draft no longer exists");
|
|
2240
|
-
await saveChanges();
|
|
2241
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2242
|
-
await decodeImages(row.doc, decoded);
|
|
2243
|
-
replace(row.doc, decoded, {
|
|
2244
|
-
id: row.id,
|
|
2245
|
-
name: row.name
|
|
2246
|
-
});
|
|
2247
|
-
};
|
|
2248
|
-
const importImage = async (file) => {
|
|
2249
|
-
if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
|
|
2250
|
-
if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
|
|
2251
|
-
const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
|
|
2252
|
-
const decoded = /* @__PURE__ */ new Map();
|
|
2253
|
-
await decodeImages(next, decoded);
|
|
2254
|
-
await saveChanges();
|
|
2255
|
-
replace(next, decoded, null);
|
|
2256
|
-
dirty.current = true;
|
|
2257
|
-
return;
|
|
2427
|
+
if (command.op !== "stroke") throw Error("Unknown command");
|
|
2428
|
+
const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
|
|
2429
|
+
const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
|
|
2430
|
+
let points = command.points;
|
|
2431
|
+
if (command.start !== void 0 || command.segments !== void 0) {
|
|
2432
|
+
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");
|
|
2433
|
+
points = [command.start, ...command.segments.flatMap((s) => [
|
|
2434
|
+
s?.control1,
|
|
2435
|
+
s?.control2,
|
|
2436
|
+
s?.end
|
|
2437
|
+
])];
|
|
2258
2438
|
}
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2439
|
+
if (![
|
|
2440
|
+
"pen",
|
|
2441
|
+
"line",
|
|
2442
|
+
"rectangle",
|
|
2443
|
+
"circle",
|
|
2444
|
+
"polygon",
|
|
2445
|
+
"bezier",
|
|
2446
|
+
"arrow",
|
|
2447
|
+
"text",
|
|
2448
|
+
"eraser"
|
|
2449
|
+
].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");
|
|
2450
|
+
if ([
|
|
2451
|
+
"line",
|
|
2452
|
+
"arrow",
|
|
2453
|
+
"text",
|
|
2454
|
+
"rectangle",
|
|
2455
|
+
"circle"
|
|
2456
|
+
].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
|
|
2457
|
+
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.`);
|
|
2458
|
+
if (fill && ![
|
|
2459
|
+
"rectangle",
|
|
2460
|
+
"circle",
|
|
2461
|
+
"polygon",
|
|
2462
|
+
"bezier"
|
|
2463
|
+
].includes(shape)) throw Error("Fill requires a closed shape");
|
|
2464
|
+
const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
|
|
2465
|
+
if (!layer?.visible) throw Error("Target layer is missing or hidden");
|
|
2466
|
+
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");
|
|
2467
|
+
const id = command.id ?? crypto.randomUUID();
|
|
2468
|
+
if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
|
|
2469
|
+
const stroke = {
|
|
2470
|
+
id,
|
|
2471
|
+
...shape === "text" ? { text: command.text } : {},
|
|
2472
|
+
shape,
|
|
2473
|
+
color,
|
|
2474
|
+
width,
|
|
2475
|
+
opacity,
|
|
2476
|
+
fill,
|
|
2477
|
+
brush: "pen",
|
|
2478
|
+
points: points.map((p) => ({
|
|
2479
|
+
x: p.x,
|
|
2480
|
+
y: p.y
|
|
2481
|
+
}))
|
|
2274
2482
|
};
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2483
|
+
doc = {
|
|
2484
|
+
...doc,
|
|
2485
|
+
layers: doc.layers.map((item) => item === layer ? {
|
|
2486
|
+
...item,
|
|
2487
|
+
strokes: [...item.strokes, stroke]
|
|
2488
|
+
} : item)
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
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");
|
|
2492
|
+
return doc;
|
|
2493
|
+
}
|
|
2494
|
+
function createSketchCommandSession(adapter) {
|
|
2495
|
+
const completed = /* @__PURE__ */ new Map();
|
|
2496
|
+
let pending = false, cachedCharacters = 0;
|
|
2497
|
+
return async (request) => {
|
|
2498
|
+
if (!request || typeof request !== "object") throw Error("Invalid sketch request");
|
|
2499
|
+
if (!adapter.available()) throw Error("Open the sketch board for this session first");
|
|
2500
|
+
const current = adapter.snapshot();
|
|
2501
|
+
if (request.action === "inspect") {
|
|
2502
|
+
const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
|
|
2503
|
+
if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
|
|
2504
|
+
return {
|
|
2505
|
+
...current,
|
|
2506
|
+
protocolVersion: 2,
|
|
2507
|
+
objects: request.objectId ? [] : objects.slice(offset, offset + 50),
|
|
2508
|
+
objectCount: objects.length,
|
|
2509
|
+
...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
|
|
2510
|
+
...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
|
|
2511
|
+
recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
|
|
2512
|
+
...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
|
|
2516
|
+
if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
|
|
2517
|
+
if (request.action === "preview") return {
|
|
2518
|
+
...current,
|
|
2519
|
+
png: await adapter.preview()
|
|
2282
2520
|
};
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
|
|
2294
|
-
if (row) {
|
|
2295
|
-
replace(row.doc, decoded, recovery ? null : {
|
|
2296
|
-
id: row.id,
|
|
2297
|
-
name: row.name
|
|
2298
|
-
});
|
|
2299
|
-
dirty.current = Boolean(recovery);
|
|
2300
|
-
setRecovered(Boolean(recovery));
|
|
2521
|
+
if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
|
|
2522
|
+
if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
|
|
2523
|
+
const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
|
|
2524
|
+
...request,
|
|
2525
|
+
runId: void 0
|
|
2526
|
+
});
|
|
2527
|
+
const cached = completed.get(key);
|
|
2528
|
+
if (cached) {
|
|
2529
|
+
if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
|
|
2530
|
+
return cached.result;
|
|
2301
2531
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
saveChanges,
|
|
2308
|
-
replace,
|
|
2309
|
-
fresh,
|
|
2310
|
-
load,
|
|
2311
|
-
importImage,
|
|
2312
|
-
restore
|
|
2313
|
-
};
|
|
2314
|
-
}
|
|
2315
|
-
//#endregion
|
|
2316
|
-
//#region src/sketch-operation-gate.js
|
|
2317
|
-
function createSketchOperationGate() {
|
|
2318
|
-
let running = false;
|
|
2319
|
-
return {
|
|
2320
|
-
get running() {
|
|
2321
|
-
return running;
|
|
2322
|
-
},
|
|
2323
|
-
async run(operation, { blocked = false, working, report, rethrow = false }) {
|
|
2324
|
-
if (blocked || running) return false;
|
|
2325
|
-
running = true;
|
|
2532
|
+
if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
|
|
2533
|
+
let changedObjects;
|
|
2534
|
+
if (request.action === "apply") {
|
|
2535
|
+
const before = adapter.document();
|
|
2536
|
+
let next;
|
|
2326
2537
|
try {
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2538
|
+
next = applySketchCommands(before, request.commands);
|
|
2539
|
+
} catch (cause) {
|
|
2540
|
+
const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
|
|
2541
|
+
error.code = "SKETCH_INVALID_BATCH";
|
|
2542
|
+
throw error;
|
|
2543
|
+
}
|
|
2544
|
+
adapter.commit(next);
|
|
2545
|
+
const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
|
|
2546
|
+
changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
|
|
2547
|
+
layer: l.id,
|
|
2548
|
+
id: s.id
|
|
2549
|
+
})));
|
|
2550
|
+
} else {
|
|
2551
|
+
if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
|
|
2552
|
+
pending = true;
|
|
2553
|
+
try {
|
|
2554
|
+
await adapter.save(request.name);
|
|
2335
2555
|
} finally {
|
|
2336
|
-
|
|
2337
|
-
working(false);
|
|
2556
|
+
pending = false;
|
|
2338
2557
|
}
|
|
2339
2558
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
if (!await gate.run(async () => {
|
|
2347
|
-
const { blob, extension } = await exportFile(format);
|
|
2348
|
-
const data = new Uint8Array(await blob.arrayBuffer());
|
|
2349
|
-
let raw = "";
|
|
2350
|
-
for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
|
|
2351
|
-
result = {
|
|
2352
|
-
extension,
|
|
2353
|
-
mediaType: blob.type,
|
|
2354
|
-
base64: btoa(raw)
|
|
2559
|
+
const result = {
|
|
2560
|
+
...adapter.snapshot(),
|
|
2561
|
+
...changedObjects ? {
|
|
2562
|
+
changedObjects: changedObjects.slice(0, 100),
|
|
2563
|
+
changedObjectCount: changedObjects.length
|
|
2564
|
+
} : {}
|
|
2355
2565
|
};
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
children: [
|
|
2374
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2375
|
-
name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
|
|
2376
|
-
size: 15
|
|
2377
|
-
}),
|
|
2378
|
-
floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2379
|
-
type: "button",
|
|
2380
|
-
onClick: onOpen,
|
|
2381
|
-
children: t(`sketchRun_${state}`)
|
|
2382
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
|
|
2383
|
-
drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2384
|
-
type: "button",
|
|
2385
|
-
className: "codexSketchStop",
|
|
2386
|
-
onClick: onStop,
|
|
2387
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2388
|
-
name: "stop",
|
|
2389
|
-
size: 12
|
|
2390
|
-
}), t("sketchRunStop")]
|
|
2391
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2392
|
-
type: "button",
|
|
2393
|
-
title: t("sketchRunResumeHint"),
|
|
2394
|
-
onClick: onResume,
|
|
2395
|
-
children: t("sketchRunResume")
|
|
2396
|
-
}) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2397
|
-
type: "button",
|
|
2398
|
-
"aria-label": t("sketchDismissStatus"),
|
|
2399
|
-
title: t("sketchDismissStatus"),
|
|
2400
|
-
onClick: onDismiss,
|
|
2401
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2402
|
-
name: "close",
|
|
2403
|
-
size: 14
|
|
2404
|
-
})
|
|
2405
|
-
}) : null] })
|
|
2406
|
-
]
|
|
2407
|
-
});
|
|
2408
|
-
}
|
|
2409
|
-
//#endregion
|
|
2410
|
-
//#region src/sketch-tool-widths.js
|
|
2411
|
-
const defaults = {
|
|
2412
|
-
pen: 12,
|
|
2413
|
-
pencil: 6,
|
|
2414
|
-
marker: 28,
|
|
2415
|
-
eraser: 24,
|
|
2416
|
-
text: 32,
|
|
2417
|
-
line: 12,
|
|
2418
|
-
arrow: 12,
|
|
2419
|
-
rectangle: 12,
|
|
2420
|
-
circle: 12
|
|
2421
|
-
};
|
|
2422
|
-
const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
|
|
2423
|
-
function switchSketchToolWidth(memory, current, next) {
|
|
2424
|
-
if (current.tool !== "select") memory[keyFor(current)] = current.width;
|
|
2425
|
-
if (next.tool === "select") return current.width;
|
|
2426
|
-
const key = keyFor(next);
|
|
2427
|
-
return memory[key] ?? defaults[key] ?? 12;
|
|
2428
|
-
}
|
|
2429
|
-
function stepSketchWidth(width, direction) {
|
|
2430
|
-
return Math.max(1, Math.min(256, width + direction * 2));
|
|
2431
|
-
}
|
|
2432
|
-
//#endregion
|
|
2433
|
-
//#region src/sketch-shortcuts.js
|
|
2434
|
-
function sketchShortcutAction(keys, key) {
|
|
2435
|
-
key = key.toLowerCase();
|
|
2436
|
-
return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
|
|
2437
|
-
v: "select",
|
|
2438
|
-
t: "text",
|
|
2439
|
-
"+": "zoomIn"
|
|
2440
|
-
}[key];
|
|
2566
|
+
completed.set(key, {
|
|
2567
|
+
fingerprint,
|
|
2568
|
+
result,
|
|
2569
|
+
receipt: {
|
|
2570
|
+
requestId: request.requestId,
|
|
2571
|
+
action: request.action,
|
|
2572
|
+
revision: result.revision
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
cachedCharacters += fingerprint.length;
|
|
2576
|
+
while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
|
|
2577
|
+
const oldest = completed.keys().next().value;
|
|
2578
|
+
cachedCharacters -= completed.get(oldest).fingerprint.length;
|
|
2579
|
+
completed.delete(oldest);
|
|
2580
|
+
}
|
|
2581
|
+
return result;
|
|
2582
|
+
};
|
|
2441
2583
|
}
|
|
2442
2584
|
//#endregion
|
|
2443
|
-
//#region src/sketch-
|
|
2444
|
-
const
|
|
2445
|
-
const
|
|
2585
|
+
//#region src/sketch-formats.js
|
|
2586
|
+
const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
|
|
2587
|
+
const canvas = (w, h) => {
|
|
2446
2588
|
const c = document.createElement("canvas");
|
|
2447
|
-
c.width =
|
|
2448
|
-
c.height =
|
|
2589
|
+
c.width = w;
|
|
2590
|
+
c.height = h;
|
|
2449
2591
|
return c;
|
|
2450
2592
|
};
|
|
2451
|
-
function
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2593
|
+
function runPsdCodec(action, payload) {
|
|
2594
|
+
return new Promise((resolve, reject) => {
|
|
2595
|
+
const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
|
|
2596
|
+
const finish = (callback, value) => {
|
|
2597
|
+
clearTimeout(timer);
|
|
2598
|
+
worker.terminate();
|
|
2599
|
+
callback(value);
|
|
2600
|
+
};
|
|
2601
|
+
const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
|
|
2602
|
+
worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
|
|
2603
|
+
worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
|
|
2604
|
+
worker.postMessage({
|
|
2605
|
+
action,
|
|
2606
|
+
payload
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
function encodeSketchDocument(doc) {
|
|
2611
|
+
return JSON.stringify({
|
|
2612
|
+
format: "dsh-sketch",
|
|
2613
|
+
version: 1,
|
|
2614
|
+
doc
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
function decodeSketchDocument(text) {
|
|
2618
|
+
if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
|
|
2619
|
+
const file = JSON.parse(text), source = file.doc;
|
|
2620
|
+
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");
|
|
2621
|
+
const w = source.width ?? 1024, h = source.height ?? 1024;
|
|
2622
|
+
if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
|
|
2623
|
+
let doc = {
|
|
2624
|
+
...createSketchLayers(),
|
|
2625
|
+
width: w,
|
|
2626
|
+
height: h,
|
|
2627
|
+
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
|
|
2628
|
+
};
|
|
2629
|
+
for (let i = 0; i < source.layers.length; i++) {
|
|
2630
|
+
const layer = source.layers[i];
|
|
2631
|
+
if (i) doc = applySketchCommands(doc, [{
|
|
2632
|
+
op: "layer",
|
|
2633
|
+
action: "add"
|
|
2634
|
+
}]);
|
|
2635
|
+
if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
|
|
2636
|
+
for (let j = 0; j < layer.strokes.length; j += 256) {
|
|
2637
|
+
const strokes = layer.strokes.slice(j, j + 256);
|
|
2638
|
+
doc = applySketchCommands(doc, strokes.map((s) => ({
|
|
2639
|
+
...s,
|
|
2640
|
+
op: "stroke",
|
|
2641
|
+
layer: doc.active,
|
|
2642
|
+
fill: s.fill ?? false
|
|
2643
|
+
})));
|
|
2644
|
+
const added = doc.layers.at(-1).strokes;
|
|
2645
|
+
for (let k = 0; k < strokes.length; k++) {
|
|
2646
|
+
const s = strokes[k];
|
|
2647
|
+
if (s.brush !== void 0 && ![
|
|
2648
|
+
"pen",
|
|
2649
|
+
"pencil",
|
|
2650
|
+
"marker"
|
|
2651
|
+
].includes(s.brush)) throw Error("Invalid brush");
|
|
2652
|
+
if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
|
|
2653
|
+
if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
|
|
2654
|
+
Object.assign(added[added.length - strokes.length + k], {
|
|
2655
|
+
brush: s.brush ?? "pen",
|
|
2656
|
+
pressure: s.pressure ?? 1,
|
|
2657
|
+
...s.brushVersion === 2 ? { brushVersion: 2 } : {}
|
|
2658
|
+
});
|
|
2477
2659
|
}
|
|
2478
|
-
paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
|
|
2479
|
-
entry.count = count;
|
|
2480
|
-
entry.prefix = prefix;
|
|
2481
|
-
entry.image = layer.image;
|
|
2482
|
-
entry.strokes = layer.strokes;
|
|
2483
2660
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2661
|
+
const target = doc.layers.at(-1);
|
|
2662
|
+
target.name = String(layer.name ?? "").slice(0, 40);
|
|
2663
|
+
target.visible = layer.visible !== false;
|
|
2664
|
+
if (layer.image) {
|
|
2665
|
+
const image = layer.image;
|
|
2666
|
+
if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
|
|
2667
|
+
"x",
|
|
2668
|
+
"y",
|
|
2669
|
+
"width",
|
|
2670
|
+
"height"
|
|
2671
|
+
].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
|
|
2672
|
+
const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
|
|
2673
|
+
if (bytes.length < 24) throw Error("Invalid draft image");
|
|
2674
|
+
const header = new DataView(bytes.buffer);
|
|
2675
|
+
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");
|
|
2676
|
+
target.image = {
|
|
2677
|
+
src: image.src,
|
|
2678
|
+
x: image.x,
|
|
2679
|
+
y: image.y,
|
|
2680
|
+
width: image.width,
|
|
2681
|
+
height: image.height
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2491
2684
|
}
|
|
2685
|
+
const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
|
|
2686
|
+
doc.active = doc.layers[Math.max(0, activeIndex)].id;
|
|
2687
|
+
return doc;
|
|
2492
2688
|
}
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
if (event.target.matches?.("canvas")) {
|
|
2506
|
-
event.preventDefault();
|
|
2507
|
-
event.stopPropagation();
|
|
2508
|
-
event.target.focus({ preventScroll: true });
|
|
2509
|
-
}
|
|
2510
|
-
};
|
|
2511
|
-
const key = (event) => {
|
|
2512
|
-
if (event.key !== "Escape") return;
|
|
2513
|
-
event.preventDefault();
|
|
2514
|
-
event.stopPropagation();
|
|
2515
|
-
latest.current(false);
|
|
2516
|
-
dialog.querySelector("canvas")?.focus({ preventScroll: true });
|
|
2517
|
-
};
|
|
2518
|
-
const hidden = () => latest.current(false);
|
|
2519
|
-
document.addEventListener("pointerdown", pointer, true);
|
|
2520
|
-
document.addEventListener("keydown", key, true);
|
|
2521
|
-
dialog.addEventListener("close", hidden);
|
|
2522
|
-
return () => {
|
|
2523
|
-
document.removeEventListener("pointerdown", pointer, true);
|
|
2524
|
-
document.removeEventListener("keydown", key, true);
|
|
2525
|
-
dialog.removeEventListener("close", hidden);
|
|
2689
|
+
async function exportSketchPsd(doc, images, composite) {
|
|
2690
|
+
const width = doc.width ?? 1024, height = doc.height ?? 1024;
|
|
2691
|
+
const children = doc.layers.map((layer, i) => {
|
|
2692
|
+
const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
|
|
2693
|
+
if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
|
|
2694
|
+
paintSketch(ctx, layer.strokes, width, true, height);
|
|
2695
|
+
return {
|
|
2696
|
+
name: layer.name || `Layer ${i + 1}`,
|
|
2697
|
+
hidden: !layer.visible,
|
|
2698
|
+
opacity: 1,
|
|
2699
|
+
blendMode: "normal",
|
|
2700
|
+
imageData: ctx.getImageData(0, 0, width, height)
|
|
2526
2701
|
};
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
])
|
|
2702
|
+
});
|
|
2703
|
+
const context = canvas(width, height).getContext("2d");
|
|
2704
|
+
context.fillStyle = "#fff";
|
|
2705
|
+
context.fillRect(0, 0, width, height);
|
|
2706
|
+
if (children[0] && !children[0].hidden) {
|
|
2707
|
+
const bottom = canvas(width, height);
|
|
2708
|
+
bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
|
|
2709
|
+
context.drawImage(bottom, 0, 0);
|
|
2710
|
+
children[0].imageData = context.getImageData(0, 0, width, height);
|
|
2711
|
+
} else {
|
|
2712
|
+
if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
|
|
2713
|
+
children.unshift({
|
|
2714
|
+
name: "Paper",
|
|
2715
|
+
opacity: 1,
|
|
2716
|
+
blendMode: "normal",
|
|
2717
|
+
imageData: context.getImageData(0, 0, width, height)
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
return runPsdCodec("write", {
|
|
2721
|
+
width,
|
|
2722
|
+
height,
|
|
2723
|
+
children,
|
|
2724
|
+
imageData: composite.getContext("2d").getImageData(0, 0, width, height)
|
|
2725
|
+
});
|
|
2532
2726
|
}
|
|
2533
|
-
function
|
|
2534
|
-
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const rect = bounds ?? node.getBoundingClientRect();
|
|
2540
|
-
if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
|
|
2541
|
-
cursor.hidden = true;
|
|
2542
|
-
return;
|
|
2543
|
-
}
|
|
2544
|
-
const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
|
|
2545
|
-
cursor.hidden = false;
|
|
2546
|
-
cursor.style.width = `${diameter}px`;
|
|
2547
|
-
cursor.style.height = `${diameter}px`;
|
|
2548
|
-
cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
|
|
2549
|
-
};
|
|
2550
|
-
(0, react.useEffect)(() => {
|
|
2551
|
-
update();
|
|
2552
|
-
const observer = new ResizeObserver(() => update());
|
|
2553
|
-
if (canvas.current) observer.observe(canvas.current);
|
|
2554
|
-
return () => observer.disconnect();
|
|
2555
|
-
}, [
|
|
2727
|
+
async function importSketchPsd(file) {
|
|
2728
|
+
if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
|
|
2729
|
+
const psd = await runPsdCodec("read", await file.arrayBuffer());
|
|
2730
|
+
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));
|
|
2731
|
+
const doc = {
|
|
2732
|
+
...createSketchLayers(),
|
|
2556
2733
|
width,
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
return {
|
|
2562
|
-
down: (event) => {
|
|
2563
|
-
heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
|
|
2564
|
-
},
|
|
2565
|
-
move: (event, bounds) => update({
|
|
2566
|
-
clientX: event.clientX,
|
|
2567
|
-
clientY: event.clientY,
|
|
2568
|
-
pointerId: event.pointerId,
|
|
2569
|
-
pointerType: event.pointerType,
|
|
2570
|
-
pressure: event.pressure
|
|
2571
|
-
}, bounds),
|
|
2572
|
-
leave: () => {
|
|
2573
|
-
last.current = null;
|
|
2574
|
-
if (ring.current) ring.current.hidden = true;
|
|
2575
|
-
}
|
|
2734
|
+
height,
|
|
2735
|
+
ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
|
|
2736
|
+
layers: [],
|
|
2737
|
+
nextId: psd.layers.length + 1
|
|
2576
2738
|
};
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
}
|
|
2739
|
+
for (const [i, layer] of psd.layers.entries()) {
|
|
2740
|
+
const src = canvas(layer.imageData.width, layer.imageData.height);
|
|
2741
|
+
src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
|
|
2742
|
+
const out = canvas(width, height), ctx = out.getContext("2d");
|
|
2743
|
+
ctx.globalAlpha = layer.opacity;
|
|
2744
|
+
ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
|
|
2745
|
+
doc.layers.push({
|
|
2746
|
+
id: i + 1,
|
|
2747
|
+
name: layer.name,
|
|
2748
|
+
visible: !layer.hidden,
|
|
2749
|
+
strokes: [],
|
|
2750
|
+
image: {
|
|
2751
|
+
src: out.toDataURL("image/png"),
|
|
2752
|
+
x: 0,
|
|
2753
|
+
y: 0,
|
|
2754
|
+
width: 1,
|
|
2755
|
+
height: 1
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
return doc;
|
|
2760
|
+
}
|
|
2761
|
+
//#endregion
|
|
2762
|
+
//#region src/sketch-document-lifecycle.js
|
|
2763
|
+
function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, confirmPsd = async () => false, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
|
|
2764
|
+
const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
|
|
2765
|
+
const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
|
|
2766
|
+
const save = async (name) => {
|
|
2767
|
+
const savingDocument = documentId.current, savingRevision = documentRevision.current;
|
|
2768
|
+
const row = {
|
|
2769
|
+
id: saved.current?.id ?? crypto.randomUUID(),
|
|
2770
|
+
name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
2771
|
+
updated: Date.now(),
|
|
2772
|
+
doc: structuredClone(doc.current)
|
|
2773
|
+
};
|
|
2597
2774
|
try {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2775
|
+
await store("save", row, sessionId);
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
|
|
2778
|
+
if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
|
|
2779
|
+
throw error;
|
|
2780
|
+
}
|
|
2781
|
+
if (documentId.current === savingDocument) {
|
|
2782
|
+
saved.current = {
|
|
2783
|
+
id: row.id,
|
|
2784
|
+
name: row.name
|
|
2601
2785
|
};
|
|
2602
|
-
|
|
2603
|
-
|
|
2786
|
+
if (documentRevision.current === savingRevision) {
|
|
2787
|
+
dirty.current = false;
|
|
2788
|
+
setRecovered(false);
|
|
2789
|
+
}
|
|
2604
2790
|
}
|
|
2605
|
-
}
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2791
|
+
};
|
|
2792
|
+
const saveChanges = async () => {
|
|
2793
|
+
if (!dirty.current) return;
|
|
2794
|
+
if (hasContent() || saved.current) return save();
|
|
2795
|
+
const id = documentId.current, revision = documentRevision.current;
|
|
2796
|
+
await store("clearRecovery", sessionId);
|
|
2797
|
+
if (documentId.current === id && documentRevision.current === revision) {
|
|
2798
|
+
dirty.current = false;
|
|
2799
|
+
setRecovered(false);
|
|
2611
2800
|
}
|
|
2612
|
-
}
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2801
|
+
};
|
|
2802
|
+
const replace = (next, decoded, identity) => {
|
|
2803
|
+
documentId.current = crypto.randomUUID();
|
|
2804
|
+
documentRevision.current++;
|
|
2805
|
+
doc.current = identifyObjects(structuredClone(next));
|
|
2806
|
+
setSelection(null);
|
|
2807
|
+
setTextEdit(null);
|
|
2808
|
+
images.current = decoded;
|
|
2809
|
+
cache.current.clear();
|
|
2810
|
+
undo.current = [];
|
|
2811
|
+
redo.current = [];
|
|
2812
|
+
saved.current = identity;
|
|
2813
|
+
dirty.current = false;
|
|
2814
|
+
schedule();
|
|
2815
|
+
};
|
|
2816
|
+
const fresh = async () => {
|
|
2817
|
+
await saveChanges();
|
|
2818
|
+
replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
|
|
2819
|
+
};
|
|
2820
|
+
const load = async (row) => {
|
|
2821
|
+
if (row.id === saved.current?.id) return;
|
|
2822
|
+
row = await store("get", row.id);
|
|
2823
|
+
if (!row) throw Error("Draft no longer exists");
|
|
2824
|
+
await saveChanges();
|
|
2825
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2826
|
+
await decodeImages(row.doc, decoded);
|
|
2827
|
+
replace(row.doc, decoded, {
|
|
2828
|
+
id: row.id,
|
|
2829
|
+
name: row.name
|
|
2830
|
+
});
|
|
2831
|
+
};
|
|
2832
|
+
const importImage = async (file) => {
|
|
2833
|
+
if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
|
|
2834
|
+
if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
|
|
2835
|
+
if (file.name.toLowerCase().endsWith(".psd") && !await confirmPsd()) return;
|
|
2836
|
+
const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
|
|
2837
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2838
|
+
await decodeImages(next, decoded);
|
|
2839
|
+
await saveChanges();
|
|
2840
|
+
replace(next, decoded, null);
|
|
2841
|
+
dirty.current = true;
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
if (doc.current.layers.length >= 8) throw Error("Layer limit");
|
|
2845
|
+
const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
|
|
2846
|
+
const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
|
|
2847
|
+
const layer = {
|
|
2848
|
+
id: doc.current.nextId,
|
|
2849
|
+
name: file.name?.slice(0, 40) || t("sketchImport"),
|
|
2850
|
+
visible: true,
|
|
2851
|
+
strokes: [],
|
|
2852
|
+
image: {
|
|
2853
|
+
src: image.src,
|
|
2854
|
+
x: (1 - width) / 2,
|
|
2855
|
+
y: (1 - height) / 2,
|
|
2856
|
+
width,
|
|
2857
|
+
height
|
|
2858
|
+
}
|
|
2631
2859
|
};
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2860
|
+
await decodeImages({ layers: [layer] }, images.current);
|
|
2861
|
+
checkpoint();
|
|
2862
|
+
doc.current = {
|
|
2863
|
+
...doc.current,
|
|
2864
|
+
nextId: layer.id + 1,
|
|
2865
|
+
active: layer.id,
|
|
2866
|
+
layers: [...doc.current.layers, layer]
|
|
2639
2867
|
};
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2868
|
+
schedule();
|
|
2869
|
+
};
|
|
2870
|
+
const restore = async (isCurrent = () => true) => {
|
|
2871
|
+
if (dirty.current || hasContent()) return;
|
|
2872
|
+
const id = documentId.current, revision = documentRevision.current;
|
|
2873
|
+
const recovery = await store("recover", sessionId);
|
|
2874
|
+
const archived = state.restoreId.current;
|
|
2875
|
+
const row = recovery ?? (archived ? await store("get", archived) : null);
|
|
2876
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
2877
|
+
if (row) await decodeImages(row.doc, decoded);
|
|
2878
|
+
if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
|
|
2879
|
+
if (row) {
|
|
2880
|
+
replace(row.doc, decoded, recovery ? null : {
|
|
2881
|
+
id: row.id,
|
|
2882
|
+
name: row.name
|
|
2883
|
+
});
|
|
2884
|
+
dirty.current = Boolean(recovery);
|
|
2885
|
+
setRecovered(Boolean(recovery));
|
|
2647
2886
|
}
|
|
2648
|
-
|
|
2649
|
-
const setKey = (action, key) => {
|
|
2650
|
-
key = key.toLowerCase();
|
|
2651
|
-
if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
|
|
2652
|
-
const next = {
|
|
2653
|
-
...keys,
|
|
2654
|
-
[action]: key
|
|
2655
|
-
};
|
|
2656
|
-
setKeys(next);
|
|
2657
|
-
try {
|
|
2658
|
-
localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
|
|
2659
|
-
} catch {}
|
|
2887
|
+
state.restoreId.current = null;
|
|
2660
2888
|
};
|
|
2661
2889
|
return {
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
e.preventDefault();
|
|
2680
|
-
setSpace(true);
|
|
2681
|
-
return true;
|
|
2682
|
-
}
|
|
2683
|
-
if ([
|
|
2684
|
-
"zoomIn",
|
|
2685
|
-
"zoomOut",
|
|
2686
|
-
"fit"
|
|
2687
|
-
].includes(action)) {
|
|
2688
|
-
e.preventDefault();
|
|
2689
|
-
if (action === "fit") reset();
|
|
2690
|
-
else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
|
|
2691
|
-
return true;
|
|
2692
|
-
}
|
|
2693
|
-
return false;
|
|
2694
|
-
},
|
|
2695
|
-
keyUp: (e) => {
|
|
2696
|
-
if (e.key.toLowerCase() === keys.pan) setSpace(false);
|
|
2697
|
-
},
|
|
2698
|
-
down: (e) => {
|
|
2699
|
-
if (e.button !== 1 && !space) return false;
|
|
2700
|
-
e.preventDefault();
|
|
2701
|
-
drag.current = {
|
|
2702
|
-
id: e.pointerId,
|
|
2703
|
-
x: e.clientX,
|
|
2704
|
-
y: e.clientY,
|
|
2705
|
-
view: viewRef.current
|
|
2706
|
-
};
|
|
2707
|
-
canvas.current.setPointerCapture(e.pointerId);
|
|
2708
|
-
return true;
|
|
2709
|
-
},
|
|
2710
|
-
move: (e) => {
|
|
2711
|
-
const d = drag.current;
|
|
2712
|
-
if (!d || d.id !== e.pointerId) return false;
|
|
2713
|
-
setView({
|
|
2714
|
-
...d.view,
|
|
2715
|
-
x: d.view.x + e.clientX - d.x,
|
|
2716
|
-
y: d.view.y + e.clientY - d.y
|
|
2717
|
-
});
|
|
2718
|
-
return true;
|
|
2890
|
+
hasContent,
|
|
2891
|
+
save,
|
|
2892
|
+
saveChanges,
|
|
2893
|
+
replace,
|
|
2894
|
+
fresh,
|
|
2895
|
+
load,
|
|
2896
|
+
importImage,
|
|
2897
|
+
restore
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
//#endregion
|
|
2901
|
+
//#region src/sketch-operation-gate.js
|
|
2902
|
+
function createSketchOperationGate() {
|
|
2903
|
+
let running = false;
|
|
2904
|
+
return {
|
|
2905
|
+
get running() {
|
|
2906
|
+
return running;
|
|
2719
2907
|
},
|
|
2720
|
-
|
|
2721
|
-
if (
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2908
|
+
async run(operation, { blocked = false, working, report, rethrow = false }) {
|
|
2909
|
+
if (blocked || running) return false;
|
|
2910
|
+
running = true;
|
|
2911
|
+
try {
|
|
2912
|
+
working(true);
|
|
2913
|
+
report(null);
|
|
2914
|
+
await operation();
|
|
2915
|
+
return true;
|
|
2916
|
+
} catch (error) {
|
|
2917
|
+
report(error);
|
|
2918
|
+
if (rethrow) throw error;
|
|
2919
|
+
return false;
|
|
2920
|
+
} finally {
|
|
2921
|
+
running = false;
|
|
2922
|
+
working(false);
|
|
2923
|
+
}
|
|
2725
2924
|
}
|
|
2726
2925
|
};
|
|
2727
2926
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
const width = Math.min(360, box.width - 24);
|
|
2742
|
-
setPlacement({
|
|
2743
|
-
dialog,
|
|
2744
|
-
style: {
|
|
2745
|
-
width,
|
|
2746
|
-
left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
|
|
2747
|
-
bottom: box.bottom - anchor.top + 8,
|
|
2748
|
-
maxHeight: Math.max(80, anchor.top - box.top - 24)
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2751
|
-
};
|
|
2752
|
-
place();
|
|
2753
|
-
const observer = new ResizeObserver(place);
|
|
2754
|
-
observer.observe(dialog);
|
|
2755
|
-
observer.observe(host.current);
|
|
2756
|
-
window.addEventListener("resize", place);
|
|
2757
|
-
return () => {
|
|
2758
|
-
observer.disconnect();
|
|
2759
|
-
window.removeEventListener("resize", place);
|
|
2927
|
+
//#endregion
|
|
2928
|
+
//#region src/sketch-agent-export.js
|
|
2929
|
+
async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
|
|
2930
|
+
let result;
|
|
2931
|
+
if (!await gate.run(async () => {
|
|
2932
|
+
const { blob, extension } = await exportFile(format);
|
|
2933
|
+
const data = new Uint8Array(await blob.arrayBuffer());
|
|
2934
|
+
let raw = "";
|
|
2935
|
+
for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
|
|
2936
|
+
result = {
|
|
2937
|
+
extension,
|
|
2938
|
+
mediaType: blob.type,
|
|
2939
|
+
base64: btoa(raw)
|
|
2760
2940
|
};
|
|
2761
|
-
},
|
|
2941
|
+
}, {
|
|
2942
|
+
blocked,
|
|
2943
|
+
working,
|
|
2944
|
+
report,
|
|
2945
|
+
rethrow: true
|
|
2946
|
+
})) throw Error("Sketch is being edited; retry after it settles");
|
|
2947
|
+
return result;
|
|
2948
|
+
}
|
|
2949
|
+
//#endregion
|
|
2950
|
+
//#region src/sketch-run-status.jsx
|
|
2951
|
+
function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
|
|
2952
|
+
if (state === "idle") return null;
|
|
2953
|
+
const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
|
|
2762
2954
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2763
|
-
|
|
2764
|
-
|
|
2955
|
+
className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
|
|
2956
|
+
role: "status",
|
|
2957
|
+
"aria-live": "polite",
|
|
2765
2958
|
children: [
|
|
2766
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
onClick: () => navigation.zoom(1 / 1.2),
|
|
2770
|
-
children: "−"
|
|
2959
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2960
|
+
name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
|
|
2961
|
+
size: 15
|
|
2771
2962
|
}),
|
|
2772
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2963
|
+
floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2773
2964
|
type: "button",
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2965
|
+
onClick: onOpen,
|
|
2966
|
+
children: t(`sketchRun_${state}`)
|
|
2967
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
|
|
2968
|
+
drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2779
2969
|
type: "button",
|
|
2780
|
-
|
|
2781
|
-
onClick:
|
|
2782
|
-
children:
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2970
|
+
className: "codexSketchStop",
|
|
2971
|
+
onClick: onStop,
|
|
2972
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2973
|
+
name: "stop",
|
|
2974
|
+
size: 12
|
|
2975
|
+
}), t("sketchRunStop")]
|
|
2976
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2786
2977
|
type: "button",
|
|
2787
|
-
"
|
|
2788
|
-
onClick:
|
|
2789
|
-
children: t("
|
|
2790
|
-
}),
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
}
|
|
2801
|
-
},
|
|
2802
|
-
children: [
|
|
2803
|
-
/* @__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", {
|
|
2804
|
-
type: "button",
|
|
2805
|
-
"aria-label": t("sketchFileClose"),
|
|
2806
|
-
onClick: close,
|
|
2807
|
-
children: "×"
|
|
2808
|
-
})] }),
|
|
2809
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2810
|
-
className: "codexSketchKeysEnabled",
|
|
2811
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2812
|
-
type: "checkbox",
|
|
2813
|
-
checked: navigation.shortcuts,
|
|
2814
|
-
onChange: navigation.toggle
|
|
2815
|
-
})]
|
|
2816
|
-
}),
|
|
2817
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
|
|
2818
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2819
|
-
className: "codexSketchKeyGrid",
|
|
2820
|
-
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", {
|
|
2821
|
-
"aria-label": t(`sketchKey_${action}`),
|
|
2822
|
-
value: key === " " ? "Space" : key,
|
|
2823
|
-
readOnly: true,
|
|
2824
|
-
onKeyDown: (e) => {
|
|
2825
|
-
if (e.key === "Tab" || e.key === "Escape") return;
|
|
2826
|
-
e.preventDefault();
|
|
2827
|
-
e.stopPropagation();
|
|
2828
|
-
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
|
|
2829
|
-
}
|
|
2830
|
-
})] }, action))
|
|
2831
|
-
}),
|
|
2832
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
|
|
2833
|
-
]
|
|
2834
|
-
}), placement.dialog) : null
|
|
2978
|
+
title: t("sketchRunResumeHint"),
|
|
2979
|
+
onClick: onResume,
|
|
2980
|
+
children: t("sketchRunResume")
|
|
2981
|
+
}) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2982
|
+
type: "button",
|
|
2983
|
+
"aria-label": t("sketchDismissStatus"),
|
|
2984
|
+
title: t("sketchDismissStatus"),
|
|
2985
|
+
onClick: onDismiss,
|
|
2986
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
|
|
2987
|
+
name: "close",
|
|
2988
|
+
size: 14
|
|
2989
|
+
})
|
|
2990
|
+
}) : null] })
|
|
2835
2991
|
]
|
|
2836
2992
|
});
|
|
2837
2993
|
}
|
|
2838
2994
|
//#endregion
|
|
2995
|
+
//#region src/sketch-tool-widths.js
|
|
2996
|
+
const defaults = {
|
|
2997
|
+
pen: 12,
|
|
2998
|
+
pencil: 6,
|
|
2999
|
+
marker: 28,
|
|
3000
|
+
eraser: 24,
|
|
3001
|
+
text: 32,
|
|
3002
|
+
line: 12,
|
|
3003
|
+
arrow: 12,
|
|
3004
|
+
rectangle: 12,
|
|
3005
|
+
circle: 12
|
|
3006
|
+
};
|
|
3007
|
+
const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
|
|
3008
|
+
function switchSketchToolWidth(memory, current, next) {
|
|
3009
|
+
if (current.tool !== "select") memory[keyFor(current)] = current.width;
|
|
3010
|
+
if (next.tool === "select") return current.width;
|
|
3011
|
+
const key = keyFor(next);
|
|
3012
|
+
return memory[key] ?? defaults[key] ?? 12;
|
|
3013
|
+
}
|
|
3014
|
+
function stepSketchWidth(width, direction) {
|
|
3015
|
+
return Math.max(1, Math.min(256, width + direction * 2));
|
|
3016
|
+
}
|
|
3017
|
+
//#endregion
|
|
3018
|
+
//#region src/sketch-layer-renderer.js
|
|
3019
|
+
const NO_IMAGES = /* @__PURE__ */ new Map();
|
|
3020
|
+
const surface = (width, height) => {
|
|
3021
|
+
const c = document.createElement("canvas");
|
|
3022
|
+
c.width = width;
|
|
3023
|
+
c.height = height;
|
|
3024
|
+
return c;
|
|
3025
|
+
};
|
|
3026
|
+
function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
|
|
3027
|
+
context.globalCompositeOperation = "source-over";
|
|
3028
|
+
context.globalAlpha = 1;
|
|
3029
|
+
context.fillStyle = "#fff";
|
|
3030
|
+
context.fillRect(0, 0, size, height);
|
|
3031
|
+
for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
|
|
3032
|
+
for (const layer of doc.layers) {
|
|
3033
|
+
if (!layer.visible) continue;
|
|
3034
|
+
let entry = cache.get(layer.id);
|
|
3035
|
+
if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
|
|
3036
|
+
entry = {
|
|
3037
|
+
surface: surface(size, height),
|
|
3038
|
+
base: surface(size, height)
|
|
3039
|
+
};
|
|
3040
|
+
cache.set(layer.id, entry);
|
|
3041
|
+
}
|
|
3042
|
+
const moving = layer.id === activeLayer;
|
|
3043
|
+
const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
|
|
3044
|
+
const prefix = layer.strokes[count - 1];
|
|
3045
|
+
if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
|
|
3046
|
+
const ctx = entry.base.getContext("2d");
|
|
3047
|
+
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]));
|
|
3048
|
+
if (!append) {
|
|
3049
|
+
ctx.clearRect(0, 0, size, height);
|
|
3050
|
+
const ref = layer.image, image = ref && images.get(ref.src);
|
|
3051
|
+
if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
|
|
3052
|
+
}
|
|
3053
|
+
paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
|
|
3054
|
+
entry.count = count;
|
|
3055
|
+
entry.prefix = prefix;
|
|
3056
|
+
entry.image = layer.image;
|
|
3057
|
+
entry.strokes = layer.strokes;
|
|
3058
|
+
}
|
|
3059
|
+
if (moving) {
|
|
3060
|
+
const ctx = entry.surface.getContext("2d");
|
|
3061
|
+
ctx.clearRect(0, 0, size, height);
|
|
3062
|
+
ctx.drawImage(entry.base, 0, 0);
|
|
3063
|
+
paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
|
|
3064
|
+
context.drawImage(entry.surface, 0, 0);
|
|
3065
|
+
} else context.drawImage(entry.base, 0, 0);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
//#endregion
|
|
2839
3069
|
//#region src/sketch-files.jsx
|
|
2840
3070
|
function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
|
|
2841
3071
|
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);
|
|
@@ -3248,17 +3478,9 @@ window.__ModuleLoader__.load({
|
|
|
3248
3478
|
}
|
|
3249
3479
|
//#endregion
|
|
3250
3480
|
//#region src/sketch-studio.jsx
|
|
3251
|
-
const PALETTE = [
|
|
3252
|
-
"#18181b",
|
|
3253
|
-
"#929398",
|
|
3254
|
-
"#ff3936",
|
|
3255
|
-
"#ff9500",
|
|
3256
|
-
"#ffcc00",
|
|
3257
|
-
"#34c759",
|
|
3258
|
-
"#0088ff"
|
|
3259
|
-
];
|
|
3260
3481
|
function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc, sessionState }) {
|
|
3261
3482
|
const localSession = (0, react.useRef)(null);
|
|
3483
|
+
const psdImport = useSketchImportConfirm(t);
|
|
3262
3484
|
localSession.current ??= sessionState ?? createSketchSessionState();
|
|
3263
3485
|
const { doc, undo, redo, images, saved, dirty, documentId, documentRevision, agentAdapter, agentSession, agentRun } = localSession.current;
|
|
3264
3486
|
const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
@@ -3377,6 +3599,7 @@ window.__ModuleLoader__.load({
|
|
|
3377
3599
|
cache,
|
|
3378
3600
|
setSelection,
|
|
3379
3601
|
setTextEdit,
|
|
3602
|
+
confirmPsd: psdImport.confirmPsd,
|
|
3380
3603
|
setRecovered
|
|
3381
3604
|
});
|
|
3382
3605
|
(0, react.useEffect)(() => localSession.current.retain?.(), []);
|
|
@@ -4133,47 +4356,14 @@ window.__ModuleLoader__.load({
|
|
|
4133
4356
|
})
|
|
4134
4357
|
]
|
|
4135
4358
|
}) : null,
|
|
4136
|
-
picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
})] }),
|
|
4145
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4146
|
-
ref: pictureInput,
|
|
4147
|
-
hidden: true,
|
|
4148
|
-
type: "file",
|
|
4149
|
-
accept: "image/png,image/jpeg,image/webp",
|
|
4150
|
-
onChange: (e) => {
|
|
4151
|
-
const file = e.target.files?.[0];
|
|
4152
|
-
e.target.value = "";
|
|
4153
|
-
if (file) runFile(() => importImage(file));
|
|
4154
|
-
}
|
|
4155
|
-
}),
|
|
4156
|
-
doc.current.layers.filter((layer) => layer.image).map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4157
|
-
"data-active": layer.id === doc.current.active,
|
|
4158
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4159
|
-
type: "button",
|
|
4160
|
-
disabled: agentLocked || busy,
|
|
4161
|
-
"aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
|
|
4162
|
-
onClick: () => change("select", layer.id),
|
|
4163
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4164
|
-
src: layer.image.src,
|
|
4165
|
-
alt: layer.name
|
|
4166
|
-
})
|
|
4167
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4168
|
-
type: "button",
|
|
4169
|
-
disabled: agentLocked || busy,
|
|
4170
|
-
"aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
|
|
4171
|
-
onClick: () => change(doc.current.layers.length === 1 ? "clear" : "delete", layer.id),
|
|
4172
|
-
children: "×"
|
|
4173
|
-
})]
|
|
4174
|
-
}, layer.id)),
|
|
4175
|
-
!doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
|
|
4176
|
-
]
|
|
4359
|
+
picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchPicturePanel, {
|
|
4360
|
+
document: doc.current,
|
|
4361
|
+
disabled: agentLocked || busy,
|
|
4362
|
+
pictureInput,
|
|
4363
|
+
importImage,
|
|
4364
|
+
runFile,
|
|
4365
|
+
change,
|
|
4366
|
+
t
|
|
4177
4367
|
}) : null,
|
|
4178
4368
|
layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
|
|
4179
4369
|
document: doc.current,
|
|
@@ -4183,119 +4373,30 @@ window.__ModuleLoader__.load({
|
|
|
4183
4373
|
}) : null
|
|
4184
4374
|
]
|
|
4185
4375
|
}),
|
|
4186
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4211
|
-
className: "codexLayerBrush",
|
|
4212
|
-
children: [
|
|
4213
|
-
["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4214
|
-
type: "checkbox",
|
|
4215
|
-
disabled: agentLocked || busy,
|
|
4216
|
-
checked: fillShape,
|
|
4217
|
-
onChange: (e) => setFillShape(e.target.checked)
|
|
4218
|
-
}), t("sketchFill")] }) : null,
|
|
4219
|
-
selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4220
|
-
className: "codexSketchObjectActions",
|
|
4221
|
-
children: [
|
|
4222
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4223
|
-
disabled: agentLocked || busy,
|
|
4224
|
-
onClick: () => editObject({}, "duplicate"),
|
|
4225
|
-
children: t("sketchObjectDuplicate")
|
|
4226
|
-
}),
|
|
4227
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4228
|
-
disabled: agentLocked || busy,
|
|
4229
|
-
onClick: () => editObject({}, "delete"),
|
|
4230
|
-
children: t("sketchObjectDelete")
|
|
4231
|
-
}),
|
|
4232
|
-
selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4233
|
-
disabled: agentLocked || busy,
|
|
4234
|
-
onClick: () => setTextEdit({
|
|
4235
|
-
selection,
|
|
4236
|
-
value: selected.text
|
|
4237
|
-
}),
|
|
4238
|
-
children: t("sketchText")
|
|
4239
|
-
}) : null
|
|
4240
|
-
]
|
|
4241
|
-
}) : null,
|
|
4242
|
-
tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4243
|
-
className: "codexSketchStability",
|
|
4244
|
-
children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
4245
|
-
"aria-label": t("sketchStability"),
|
|
4246
|
-
value: stability,
|
|
4247
|
-
disabled: agentLocked || busy,
|
|
4248
|
-
onChange: (e) => setStability(Number(e.target.value)),
|
|
4249
|
-
children: [
|
|
4250
|
-
0,
|
|
4251
|
-
25,
|
|
4252
|
-
50,
|
|
4253
|
-
75
|
|
4254
|
-
].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4255
|
-
value,
|
|
4256
|
-
children: t(`sketchStability${value}`)
|
|
4257
|
-
}, value))
|
|
4258
|
-
})]
|
|
4259
|
-
}) : null,
|
|
4260
|
-
tool === "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4261
|
-
className: "codexSketchSegment",
|
|
4262
|
-
role: "group",
|
|
4263
|
-
"aria-label": t("sketchEraserMode"),
|
|
4264
|
-
children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4265
|
-
type: "button",
|
|
4266
|
-
"aria-pressed": eraser === value,
|
|
4267
|
-
disabled: agentLocked || busy,
|
|
4268
|
-
onClick: () => setEraser(value),
|
|
4269
|
-
children: t(`sketchErase_${value}`)
|
|
4270
|
-
}, value))
|
|
4271
|
-
}) : null
|
|
4272
|
-
]
|
|
4273
|
-
}),
|
|
4274
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4275
|
-
className: "codexSketchPalette",
|
|
4276
|
-
role: "group",
|
|
4277
|
-
"aria-label": t("sketchColor"),
|
|
4278
|
-
children: [PALETTE.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4279
|
-
type: "button",
|
|
4280
|
-
className: "codexSketchSwatch",
|
|
4281
|
-
style: { "--swatch": value },
|
|
4282
|
-
"aria-label": `${t("sketchColor")} ${value}`,
|
|
4283
|
-
"aria-pressed": (selected?.color ?? color) === value,
|
|
4284
|
-
disabled: agentLocked || busy,
|
|
4285
|
-
onClick: () => pickColor(value)
|
|
4286
|
-
}, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4287
|
-
className: "codexSketchCustom",
|
|
4288
|
-
title: t("sketchColor"),
|
|
4289
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { background: color } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4290
|
-
type: "color",
|
|
4291
|
-
"aria-label": t("sketchColor"),
|
|
4292
|
-
value: color,
|
|
4293
|
-
disabled: agentLocked || busy,
|
|
4294
|
-
onChange: (e) => pickColor(e.target.value)
|
|
4295
|
-
})]
|
|
4296
|
-
})]
|
|
4297
|
-
})
|
|
4298
|
-
]
|
|
4376
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchControls, {
|
|
4377
|
+
t,
|
|
4378
|
+
navigation,
|
|
4379
|
+
picturesOpen,
|
|
4380
|
+
setPicturesOpen,
|
|
4381
|
+
disabled: agentLocked || busy,
|
|
4382
|
+
tool,
|
|
4383
|
+
brush,
|
|
4384
|
+
chooseBrush,
|
|
4385
|
+
chooseTool,
|
|
4386
|
+
shapesOpen,
|
|
4387
|
+
setShapesOpen,
|
|
4388
|
+
fillShape,
|
|
4389
|
+
setFillShape,
|
|
4390
|
+
selected,
|
|
4391
|
+
editObject,
|
|
4392
|
+
selection,
|
|
4393
|
+
setTextEdit,
|
|
4394
|
+
stability,
|
|
4395
|
+
setStability,
|
|
4396
|
+
eraser,
|
|
4397
|
+
setEraser,
|
|
4398
|
+
color,
|
|
4399
|
+
pickColor
|
|
4299
4400
|
}),
|
|
4300
4401
|
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4301
4402
|
className: "codexSketchHint",
|
|
@@ -4305,13 +4406,15 @@ window.__ModuleLoader__.load({
|
|
|
4305
4406
|
onClick: () => retryRestore((value) => value + 1),
|
|
4306
4407
|
children: t("accountRetry")
|
|
4307
4408
|
}) : null]
|
|
4308
|
-
}) : null
|
|
4409
|
+
}) : null,
|
|
4410
|
+
psdImport.prompt
|
|
4309
4411
|
]
|
|
4310
4412
|
})] });
|
|
4311
4413
|
}
|
|
4312
4414
|
//#endregion
|
|
4313
4415
|
//#region src/sketch-styles.js
|
|
4314
4416
|
const SKETCH_CSS = `
|
|
4417
|
+
.codexSketchImportConfirm{width:min(440px,calc(100vw - 32px));box-sizing:border-box;padding:24px;border:1px solid var(--sketch-line);border-radius:20px;background:var(--sketch-bg);color:var(--sketch-fg);box-shadow:0 12px 60px #0005}.codexSketchImportConfirm::backdrop{background:#0007}.codexSketchImportConfirm h3{margin:0 0 12px;font-size:16px}.codexSketchImportConfirm p{font-size:14px;line-height:1.7;color:var(--sketch-muted)}.codexSketchImportConfirm>div{display:flex;justify-content:flex-end;gap:12px}.codexSketchImportConfirm button{padding:8px 16px;border-radius:12px;border:1px solid var(--sketch-line);background:var(--sketch-line);color:var(--sketch-fg)}
|
|
4315
4418
|
.codexSketchSizeModes{display:flex;flex-direction:column;gap:3px}.codexSketchSizeModes button{font-size:11px;padding:4px 6px;border-radius:10px;color:var(--sketch-muted)}.codexSketchSizeModes button[aria-pressed=true]{background:var(--sketch-line);color:var(--sketch-fg)}
|
|
4316
4419
|
|
|
4317
4420
|
.codexSketchSizeControl{position:absolute;left:10px;top:50%;transform:translateY(-50%);z-index:2;display:flex;flex-direction:column;align-items:center;gap:10px;padding:12px 6px;border:1px solid var(--sketch-line);border-radius:24px;background:var(--sketch-glass);backdrop-filter:blur(18px);box-shadow:0 3px 14px #0001}
|
|
@@ -4524,6 +4627,59 @@ window.__ModuleLoader__.load({
|
|
|
4524
4627
|
//#endregion
|
|
4525
4628
|
//#region src/client-locales.js
|
|
4526
4629
|
const zh = {
|
|
4630
|
+
"diagnosticReason_catalog-online": "在线目录已载入",
|
|
4631
|
+
"diagnosticReason_catalog-fallback": "使用备用目录",
|
|
4632
|
+
"diagnosticReason_storage-readable": "预测缓存可读",
|
|
4633
|
+
"diagnosticReason_tool-registered": "全局工具已注册",
|
|
4634
|
+
"diagnosticReason_tool-missing": "未找到全局工具",
|
|
4635
|
+
"diagnosticsCoverageHint": "分别展示已检查的前置条件与最近 15 分钟的操作结果。请求完成不代表整个功能通过验收;未验证不等于故障。",
|
|
4636
|
+
"diagnosticsVersionMismatch": "前后端插件版本不同,请重载后重试。",
|
|
4637
|
+
"diagnosticsPartial": "部分信息未能采集,其余检查结果已保留。请在详情中查看失败或超时项。",
|
|
4638
|
+
"diagnosticsCapability": "功能",
|
|
4639
|
+
"diagnosticsReadiness": "前置条件",
|
|
4640
|
+
"diagnosticsEvidence": "近期操作证据",
|
|
4641
|
+
"diagnosticsUnverified": "尚未验证",
|
|
4642
|
+
"diagnosticsHostCollected": "已采集宿主注册与选中结果(详情内)。这不证明界面可见或功能执行正常。",
|
|
4643
|
+
"diagnosticsHostUnknown": "宿主界面检查不可用或未挂载,不能据此判断插件干扰。",
|
|
4644
|
+
"diagnosticsDetails": "展开脱敏详情",
|
|
4645
|
+
"diagnosticsCopyFailed": "复制失败,可展开详情手动复制。",
|
|
4646
|
+
"diagnosticSource_rpc": "接口响应",
|
|
4647
|
+
"diagnosticSource_tool": "工具",
|
|
4648
|
+
"diagnosticOutcome_completed": "完成",
|
|
4649
|
+
"diagnosticOutcome_failed": "失败",
|
|
4650
|
+
"diagnosticOutcome_cancelled": "取消",
|
|
4651
|
+
"diagnosticCapability_account": "登录",
|
|
4652
|
+
"diagnosticCapability_accounts": "多账号",
|
|
4653
|
+
"diagnosticCapability_settings": "设置",
|
|
4654
|
+
"diagnosticCapability_models": "模型",
|
|
4655
|
+
"diagnosticCapability_quota": "额度",
|
|
4656
|
+
"diagnosticCapability_search": "搜索",
|
|
4657
|
+
"diagnosticCapability_images": "生图与编辑",
|
|
4658
|
+
"diagnosticCapability_sketch": "草图",
|
|
4659
|
+
"diagnosticCapability_subagents": "子任务",
|
|
4660
|
+
"diagnosticCapability_compaction": "压缩",
|
|
4661
|
+
"diagnosticCapability_transport": "连接",
|
|
4662
|
+
"diagnosticCapability_storage": "存储",
|
|
4663
|
+
"diagnosticCapability_host-ui": "宿主界面",
|
|
4664
|
+
"diagnosticReason_not-instrumented": "证据不足",
|
|
4665
|
+
"diagnosticReason_disabled-or-host-owned": "未启用或由宿主管理",
|
|
4666
|
+
"diagnosticReason_sign-in-required": "依赖登录",
|
|
4667
|
+
"diagnosticReason_local-sign-in-present": "本地登录存在",
|
|
4668
|
+
"diagnosticReason_sign-in-unverified": "登录未确认",
|
|
4669
|
+
"diagnosticReason_settings-writable": "可写",
|
|
4670
|
+
"diagnosticReason_settings-readonly": "不可写",
|
|
4671
|
+
"diagnosticReason_restart-required": "需要重启",
|
|
4672
|
+
"diagnosticReason_runtime-present": "组件已安装",
|
|
4673
|
+
"diagnosticReason_runtime-missing": "组件未安装",
|
|
4674
|
+
storageLoading: "正在读取缓存占用…",
|
|
4675
|
+
storageTitle: "存储与缓存",
|
|
4676
|
+
storageForecast: "额度预测缓存",
|
|
4677
|
+
storageClear: "清除预测历史",
|
|
4678
|
+
storageHint: "仅清理本地预测历史,保留原图、草稿、登录信息和共享依赖。子任务组件可在“模型与运行”中管理。",
|
|
4679
|
+
storageConfirm: "清除所有已登录账号的本地预测历史后,需要重新积累采样。服务端额度不变,后续查询会重新生成缓存。",
|
|
4680
|
+
storageFailed: "未能读取或清除预测缓存,请重试。",
|
|
4681
|
+
sketchPsdConfirmTitle: "导入 PSD 副本",
|
|
4682
|
+
sketchPsdConfirmBody: "支持最多 8 层、8 位 RGB 的简单 PSD(32 MB 内,边长不超过 4096)。导入后各层转为图片,最长边缩至 1024;不支持蒙版、调整层和特殊混合效果。原文件不会修改。需要保留草图对象的可编辑性,请使用 DSH 草图格式。继续导入?",
|
|
4527
4683
|
runtimeIncompatible: "检测到不兼容的 Codex 子任务组件,请卸载后重启,再安装受支持版本。",
|
|
4528
4684
|
runtimeManagedElsewhere: "此组件由宿主或其他依赖管理,请在 DSH 插件管理中查看;这里不能直接卸载。",
|
|
4529
4685
|
runtimeLoading: "正在检查组件…",
|
|
@@ -4891,9 +5047,16 @@ window.__ModuleLoader__.load({
|
|
|
4891
5047
|
contextTokens: "Token 上限",
|
|
4892
5048
|
contextFixed: "固定 {value}",
|
|
4893
5049
|
contextMaximum: "范围 {minimum}–{value}",
|
|
4894
|
-
settingsTab_account: "
|
|
5050
|
+
settingsTab_account: "账号与额度",
|
|
4895
5051
|
settingsTab_display: "使用偏好",
|
|
4896
|
-
settingsTab_advanced: "
|
|
5052
|
+
settingsTab_advanced: "模型与运行",
|
|
5053
|
+
settingsTab_creative: "图片与草图",
|
|
5054
|
+
settingsTab_maintenance: "维护",
|
|
5055
|
+
creativeSketch: "草图画板",
|
|
5056
|
+
diagnosticsHint: "遇到问题时生成报告,可复制后用于反馈。不含登录凭据和账号标识。",
|
|
5057
|
+
sketchCanvasSetting: "画板入口",
|
|
5058
|
+
sketchAgentSetting: "Agent 绘图",
|
|
5059
|
+
sketchAgentPreviewSetting: "完成后返回预览",
|
|
4897
5060
|
quotaShowIndicator: "显示额度",
|
|
4898
5061
|
quotaIndicatorHint: "输入框只显示图标,点击查看详情。关闭时暂停续航采样。",
|
|
4899
5062
|
quotaForecastOptional: "续航预测 · 实验",
|
|
@@ -4980,6 +5143,59 @@ window.__ModuleLoader__.load({
|
|
|
4980
5143
|
imageRemoveAnnotation: "删除标注"
|
|
4981
5144
|
};
|
|
4982
5145
|
const en = {
|
|
5146
|
+
"diagnosticReason_catalog-online": "Online catalog loaded",
|
|
5147
|
+
"diagnosticReason_catalog-fallback": "Fallback catalog",
|
|
5148
|
+
"diagnosticReason_storage-readable": "Forecast cache readable",
|
|
5149
|
+
"diagnosticReason_tool-registered": "Global tool registered",
|
|
5150
|
+
"diagnosticReason_tool-missing": "Global tool not found",
|
|
5151
|
+
"diagnosticsCoverageHint": "Shows inspected prerequisites and operations observed in the last 15 minutes. Request completion is not end-to-end validation; unverified does not mean broken.",
|
|
5152
|
+
"diagnosticsVersionMismatch": "Client and server versions differ. Reload and retry.",
|
|
5153
|
+
"diagnosticsPartial": "Some inspections failed or timed out. Other results are preserved; see details.",
|
|
5154
|
+
"diagnosticsCapability": "Capability",
|
|
5155
|
+
"diagnosticsReadiness": "Prerequisites",
|
|
5156
|
+
"diagnosticsEvidence": "Recent evidence",
|
|
5157
|
+
"diagnosticsUnverified": "Unverified",
|
|
5158
|
+
"diagnosticsHostCollected": "Host registrations and selection are included in details, not proof of visibility or execution.",
|
|
5159
|
+
"diagnosticsHostUnknown": "Host inspection unavailable or unmounted; interference cannot be determined.",
|
|
5160
|
+
"diagnosticsDetails": "Redacted details",
|
|
5161
|
+
"diagnosticsCopyFailed": "Copy failed. Expand the details to copy manually.",
|
|
5162
|
+
"diagnosticSource_rpc": "RPC response",
|
|
5163
|
+
"diagnosticSource_tool": "Tool",
|
|
5164
|
+
"diagnosticOutcome_completed": "Completed",
|
|
5165
|
+
"diagnosticOutcome_failed": "Failed",
|
|
5166
|
+
"diagnosticOutcome_cancelled": "Cancelled",
|
|
5167
|
+
"diagnosticCapability_account": "Sign-in",
|
|
5168
|
+
"diagnosticCapability_accounts": "Accounts",
|
|
5169
|
+
"diagnosticCapability_settings": "Settings",
|
|
5170
|
+
"diagnosticCapability_models": "Models",
|
|
5171
|
+
"diagnosticCapability_quota": "Quota",
|
|
5172
|
+
"diagnosticCapability_search": "Search",
|
|
5173
|
+
"diagnosticCapability_images": "Images",
|
|
5174
|
+
"diagnosticCapability_sketch": "Sketch",
|
|
5175
|
+
"diagnosticCapability_subagents": "Subtasks",
|
|
5176
|
+
"diagnosticCapability_compaction": "Compaction",
|
|
5177
|
+
"diagnosticCapability_transport": "Connection",
|
|
5178
|
+
"diagnosticCapability_storage": "Storage",
|
|
5179
|
+
"diagnosticCapability_host-ui": "Host UI",
|
|
5180
|
+
"diagnosticReason_not-instrumented": "Insufficient evidence",
|
|
5181
|
+
"diagnosticReason_disabled-or-host-owned": "Disabled or host-managed",
|
|
5182
|
+
"diagnosticReason_sign-in-required": "Sign-in required",
|
|
5183
|
+
"diagnosticReason_local-sign-in-present": "Local sign-in present",
|
|
5184
|
+
"diagnosticReason_sign-in-unverified": "Sign-in unverified",
|
|
5185
|
+
"diagnosticReason_settings-writable": "Writable",
|
|
5186
|
+
"diagnosticReason_settings-readonly": "Read-only",
|
|
5187
|
+
"diagnosticReason_restart-required": "Restart required",
|
|
5188
|
+
"diagnosticReason_runtime-present": "Runtime installed",
|
|
5189
|
+
"diagnosticReason_runtime-missing": "Runtime missing",
|
|
5190
|
+
storageLoading: "Reading cache size…",
|
|
5191
|
+
storageTitle: "Storage and cache",
|
|
5192
|
+
storageForecast: "Quota forecast cache",
|
|
5193
|
+
storageClear: "Clear forecast history",
|
|
5194
|
+
storageHint: "Clears local forecast history only. Original images, drafts, sign-in data and shared dependencies are preserved. Manage subtask components under Models & runtime.",
|
|
5195
|
+
storageConfirm: "This clears local forecast samples for all signed-in accounts. Forecasts will need new samples. Server quotas stay unchanged; subsequent checks recreate the cache.",
|
|
5196
|
+
storageFailed: "Could not read or clear the forecast cache. Try again.",
|
|
5197
|
+
sketchPsdConfirmTitle: "Import a PSD copy",
|
|
5198
|
+
sketchPsdConfirmBody: "Supports simple 8-bit RGB PSDs with up to 8 layers, 32 MB and 4096 pixels per side. Layers become images and the longest side is reduced to 1024 pixels. Masks, adjustments and special blending effects are unsupported. The original file stays unchanged. Use the DSH sketch format to preserve editable sketch objects. Continue?",
|
|
4983
5199
|
runtimeIncompatible: "An incompatible Codex subtask component was found. Uninstall it, restart DSH, then install the supported version.",
|
|
4984
5200
|
runtimeManagedElsewhere: "This component is managed by the host or another dependency. Check DSH plugin management; it cannot be removed here.",
|
|
4985
5201
|
runtimeLoading: "Checking component…",
|
|
@@ -5347,9 +5563,16 @@ window.__ModuleLoader__.load({
|
|
|
5347
5563
|
contextTokens: "Token limit",
|
|
5348
5564
|
contextFixed: "Fixed {value}",
|
|
5349
5565
|
contextMaximum: "{minimum}–{value}",
|
|
5350
|
-
settingsTab_account: "Account
|
|
5566
|
+
settingsTab_account: "Account",
|
|
5351
5567
|
settingsTab_display: "Preferences",
|
|
5352
|
-
settingsTab_advanced: "
|
|
5568
|
+
settingsTab_advanced: "Models & runtime",
|
|
5569
|
+
settingsTab_creative: "Images & sketch",
|
|
5570
|
+
settingsTab_maintenance: "Maintenance",
|
|
5571
|
+
creativeSketch: "Sketch board",
|
|
5572
|
+
diagnosticsHint: "Generate a report to copy into a support request. Sign-in credentials and account identifiers are excluded.",
|
|
5573
|
+
sketchCanvasSetting: "Board entry",
|
|
5574
|
+
sketchAgentSetting: "Agent drawing",
|
|
5575
|
+
sketchAgentPreviewSetting: "Return preview on completion",
|
|
5353
5576
|
quotaShowIndicator: "Show quota",
|
|
5354
5577
|
quotaIndicatorHint: "Show an icon in the composer; click for details. Hiding it pauses forecast sampling.",
|
|
5355
5578
|
quotaForecastOptional: "Runway forecast · Experimental",
|
|
@@ -5530,7 +5753,7 @@ window.__ModuleLoader__.load({
|
|
|
5530
5753
|
.codexSubscriptionRecover{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
5531
5754
|
.codexSubscriptionRecover .codexSubscriptionError{flex:1}
|
|
5532
5755
|
.codexSubscriptionRecover button{flex:0 0 auto}
|
|
5533
|
-
.codexSubscriptionDiagnostics{
|
|
5756
|
+
.codexSubscriptionDiagnostics{color:var(--dsw-alias-label-secondary)}
|
|
5534
5757
|
.codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}
|
|
5535
5758
|
.codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}
|
|
5536
5759
|
.codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}
|
|
@@ -5588,8 +5811,9 @@ window.__ModuleLoader__.load({
|
|
|
5588
5811
|
.codexSubscriptionSpendLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}
|
|
5589
5812
|
.codexSubscriptionSpendLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
|
|
5590
5813
|
.codexSubscriptionSpendLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
|
|
5591
|
-
.codexSettingsTabs{display:
|
|
5592
|
-
.codexSettingsTabs button{
|
|
5814
|
+
.codexSettingsTabs{position:sticky;top:0;z-index:2;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;padding:4px;background:var(--dsw-alias-bg-module-platform);border-radius:12px}
|
|
5815
|
+
.codexSettingsTabs button{min-width:0;min-height:36px;border:0;border-radius:9px;padding:7px 6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}
|
|
5816
|
+
.codexSettingsTabs button:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}
|
|
5593
5817
|
.codexSettingsTabs button[aria-selected=true]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px #0001}
|
|
5594
5818
|
.codexSubscription>[role=tabpanel]:not([hidden]){display:flex;flex-direction:column;gap:10px}
|
|
5595
5819
|
.codexComposerQuota:focus-visible{outline:1px solid var(--dsw-alias-border-l3);outline-offset:2px}
|
|
@@ -5616,7 +5840,7 @@ window.__ModuleLoader__.load({
|
|
|
5616
5840
|
.codexModelSelectEffort{flex:none;color:var(--dsw-alias-label-caption)}
|
|
5617
5841
|
.codexModelSelectChevron{flex:none;color:var(--dsw-alias-label-caption);transition:transform 120ms}
|
|
5618
5842
|
.codexModelSelectTrigger[aria-expanded=true] .codexModelSelectChevron{transform:rotate(180deg)}
|
|
5619
|
-
.codexModelSelectMenu,.codexModelSelectSubmenu{position:absolute;z-index:30;box-sizing:border-box;width:max-content;min-width:min(240px,calc(100vw - 32px));max-width:min(420px,calc(100vw - 32px));max-height:min(360px,calc(100vh - 96px));padding:4px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-
|
|
5843
|
+
.codexModelSelectMenu,.codexModelSelectSubmenu{position:absolute;z-index:30;box-sizing:border-box;width:max-content;min-width:min(240px,calc(100vw - 32px));max-width:min(420px,calc(100vw - 32px));max-height:min(360px,calc(100vh - 96px));padding:4px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#2c2c2e);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);overflow:hidden}
|
|
5620
5844
|
.codexModelSelectMenu{right:0;bottom:calc(100% + 8px)}
|
|
5621
5845
|
.codexModelSelectSubmenu{right:calc(100% + 8px);bottom:0;min-width:min(230px,calc(100vw - 32px))}
|
|
5622
5846
|
.codexModelSelectCell{display:flex;align-items:center;gap:8px;width:100%;min-width:100%;height:40px;box-sizing:border-box;padding:0 10px;border:0;border-radius:10px;background:transparent;color:inherit;font-size:14px;line-height:22px;text-align:left;cursor:pointer}
|
|
@@ -5627,7 +5851,7 @@ window.__ModuleLoader__.load({
|
|
|
5627
5851
|
.codexModelSelectCellChevron{flex:none;color:var(--dsw-alias-label-tertiary)}
|
|
5628
5852
|
.codexModelSelectGroups{min-height:0;max-height:352px;overflow-y:auto}
|
|
5629
5853
|
.codexModelSelectGroup+.codexModelSelectGroup{margin-top:4px}
|
|
5630
|
-
.codexModelSelectGroupTitle{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-
|
|
5854
|
+
.codexModelSelectGroupTitle{position:sticky;top:0;z-index:1;padding:5px 8px 3px;background:var(--dsw-alias-bg-layer-2,#2c2c2e);color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;line-height:18px}
|
|
5631
5855
|
.codexModelSelectOption{display:flex;align-items:center;gap:8px;width:100%;min-width:100%;min-height:38px;box-sizing:border-box;padding:6px 8px;border:0;border-radius:10px;outline:0;background:transparent;color:inherit;text-align:left;cursor:pointer}
|
|
5632
5856
|
.codexModelSelectOption:hover:not(:disabled),.codexModelSelectOption:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}
|
|
5633
5857
|
.codexModelSelectOption:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}
|
|
@@ -5669,6 +5893,25 @@ window.__ModuleLoader__.load({
|
|
|
5669
5893
|
.codexSubscriptionRuntime button{flex-shrink:0;white-space:nowrap}
|
|
5670
5894
|
.codexSubscriptionRuntime code{display:block;overflow-wrap:anywhere;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;user-select:text}
|
|
5671
5895
|
@media(max-width:640px){.codexSubscriptionCard{padding:14px}}
|
|
5896
|
+
.codexSubscriptionHelp{font-size:12px;line-height:1.65;color:var(--dsw-alias-label-tertiary)}
|
|
5897
|
+
.codexSubscriptionBadge{flex:none;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-module-platform);font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
|
|
5898
|
+
.codexImageSettings>.codexSubscriptionSectionHead{flex-direction:row;align-items:center}
|
|
5899
|
+
.codexSubscriptionSettingRows>.codexImagePreference+.codexImagePreference{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
5900
|
+
.codexSubscriptionDiagnostics .codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column;gap:12px}
|
|
5901
|
+
.codexSubscriptionDiagnostics .codexSubscriptionSectionTitle{display:flex;flex-direction:column;gap:5px}
|
|
5902
|
+
.codexSubscriptionStorageMeter{display:flex;flex-direction:column;gap:9px;padding:12px;border-radius:10px;background:var(--dsw-alias-bg-module-platform)}
|
|
5903
|
+
.codexSubscriptionStorageMeter>div{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:12px;line-height:18px}
|
|
5904
|
+
.codexSubscriptionStorageMeter output{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
|
|
5905
|
+
.codexSubscriptionStorageMeter progress{width:100%;height:4px;border:0;border-radius:4px;overflow:hidden;accent-color:var(--dsw-alias-label-secondary)}
|
|
5906
|
+
.codexSubscriptionStorageMeter progress::-webkit-progress-bar{background:var(--dsw-alias-border-l2)}
|
|
5907
|
+
.codexSubscriptionStorageMeter progress::-webkit-progress-value{background:var(--dsw-alias-label-secondary)}
|
|
5908
|
+
.codexSubscriptionConfirm{padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;font-size:12px;line-height:1.65}
|
|
5909
|
+
.codexSubscriptionConfirm p{margin-bottom:12px}
|
|
5910
|
+
.codexSubscriptionRuntime{padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);margin-top:2px}
|
|
5911
|
+
.codexSubscriptionRuntime details{margin-top:8px}.codexSubscriptionRuntime details[open]>summary{margin-bottom:10px}
|
|
5912
|
+
@container (max-width:480px){.codexSubscriptionPreference,.codexSubscriptionContextHead{flex-wrap:wrap}.codexSubscriptionPreferenceHint{max-width:none}.codexSubscriptionPreference>.codexSubscriptionPreferenceCopy{flex:1 1 220px}.codexSubscriptionQuotaModes{max-width:100%;flex-wrap:wrap}.codexSubscriptionQuotaMode{min-height:30px}.codexSubscriptionActions{flex-wrap:wrap}}
|
|
5913
|
+
@container (max-width:340px){.codexSettingsTabs{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
5914
|
+
@media(prefers-reduced-motion:reduce){.codexSubscription *{transition:none!important}}
|
|
5672
5915
|
`;
|
|
5673
5916
|
//#endregion
|
|
5674
5917
|
//#region src/image-edit-reference.js
|
|
@@ -7290,7 +7533,7 @@ window.__ModuleLoader__.load({
|
|
|
7290
7533
|
}
|
|
7291
7534
|
//#endregion
|
|
7292
7535
|
//#region src/version.js
|
|
7293
|
-
const PACKAGE_VERSION = "2.2.
|
|
7536
|
+
const PACKAGE_VERSION = "2.2.3";
|
|
7294
7537
|
//#endregion
|
|
7295
7538
|
//#region src/client-recovery.js
|
|
7296
7539
|
async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
|
|
@@ -7925,29 +8168,30 @@ window.__ModuleLoader__.load({
|
|
|
7925
8168
|
});
|
|
7926
8169
|
}
|
|
7927
8170
|
const modelLabel = (model) => model.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
|
|
7928
|
-
function ImagePreferences({ preference, t }) {
|
|
8171
|
+
function ImagePreferences({ preference, t, section = "image" }) {
|
|
7929
8172
|
const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
|
|
7930
8173
|
const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
|
|
7931
8174
|
const active = snapshot.imageGeneration || snapshot.imageEditing;
|
|
7932
|
-
|
|
8175
|
+
const sketch = section === "sketch";
|
|
8176
|
+
const title = t(sketch ? "creativeSketch" : "imageSettings");
|
|
8177
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
7933
8178
|
className: "codexSubscriptionCard codexImageSettings",
|
|
7934
|
-
"aria-label":
|
|
7935
|
-
children:
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
className: "codexSubscriptionSettingsDisclosureBody",
|
|
8179
|
+
"aria-label": title,
|
|
8180
|
+
children: [
|
|
8181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8182
|
+
className: "codexSubscriptionSectionHead",
|
|
8183
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8184
|
+
className: "codexSubscriptionBadge",
|
|
8185
|
+
children: sketch ? "Beta" : active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
|
|
8186
|
+
})]
|
|
8187
|
+
}),
|
|
8188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8189
|
+
className: "codexSubscriptionSettingRows",
|
|
7946
8190
|
children: [
|
|
7947
|
-
Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
|
|
8191
|
+
Object.keys(IMAGE_SETTING_GROUPS).filter((group) => group.startsWith("sketch") === sketch).map((group) => {
|
|
7948
8192
|
const value = imageGroupValue(snapshot, group);
|
|
7949
8193
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7950
|
-
label: t(group),
|
|
8194
|
+
label: t(sketch ? `${group}Setting` : group),
|
|
7951
8195
|
hint: t(`${group}Hint`),
|
|
7952
8196
|
value,
|
|
7953
8197
|
text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
|
|
@@ -7961,7 +8205,7 @@ window.__ModuleLoader__.load({
|
|
|
7961
8205
|
}
|
|
7962
8206
|
}, group);
|
|
7963
8207
|
}),
|
|
7964
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8208
|
+
!sketch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7965
8209
|
className: "codexImageDefaultsGroup",
|
|
7966
8210
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
|
|
7967
8211
|
label: t("imageModel"),
|
|
@@ -7991,14 +8235,29 @@ window.__ModuleLoader__.load({
|
|
|
7991
8235
|
preference.set({ imageQuality });
|
|
7992
8236
|
}
|
|
7993
8237
|
})]
|
|
7994
|
-
}),
|
|
7995
|
-
snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8238
|
+
}) : null,
|
|
8239
|
+
!sketch && snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7996
8240
|
className: "codexSubscriptionPreferenceHint",
|
|
7997
8241
|
children: t("imageModelHint")
|
|
7998
8242
|
}) : null
|
|
7999
8243
|
]
|
|
8000
|
-
})
|
|
8001
|
-
|
|
8244
|
+
}),
|
|
8245
|
+
snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8246
|
+
className: "codexSubscriptionRecover",
|
|
8247
|
+
role: "alert",
|
|
8248
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8249
|
+
className: "codexSubscriptionError",
|
|
8250
|
+
children: t("preferenceFailed")
|
|
8251
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
8252
|
+
type: "button",
|
|
8253
|
+
variant: "outline",
|
|
8254
|
+
onClick: () => {
|
|
8255
|
+
preference.retry();
|
|
8256
|
+
},
|
|
8257
|
+
children: t("preferenceRetry")
|
|
8258
|
+
})]
|
|
8259
|
+
}) : null
|
|
8260
|
+
]
|
|
8002
8261
|
});
|
|
8003
8262
|
}
|
|
8004
8263
|
//#endregion
|
|
@@ -8225,16 +8484,15 @@ window.__ModuleLoader__.load({
|
|
|
8225
8484
|
className: "codexSubscriptionPreference",
|
|
8226
8485
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8227
8486
|
role: "status",
|
|
8228
|
-
children: t(!state ? "runtimeLoading" : state.restartRequired ? "runtimeRestart" : busy ? `runtime_${state.phase}` : state.installed ? "subagentRuntimeInstalled" : state.present ? "runtimeIncompatible" : "subagentRuntimeMissing")
|
|
8229
|
-
}), state?.available && (state.present
|
|
8487
|
+
children: t(!state ? "runtimeLoading" : state.restartRequired ? "runtimeRestart" : busy ? `runtime_${state.phase}` : state.installed ? "subagentRuntimeInstalled" : !state.componentVersion ? "runtimeHostUnsupported" : state.present ? state.removable ? "runtimeIncompatible" : "runtimeManagedElsewhere" : "subagentRuntimeMissing")
|
|
8488
|
+
}), state?.available && (state.present ? state.removable : state.installable) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
8230
8489
|
type: "button",
|
|
8231
8490
|
variant: "outline",
|
|
8232
|
-
disabled: locked
|
|
8491
|
+
disabled: locked,
|
|
8233
8492
|
onClick: () => state.present ? setConfirm(true) : void act("install"),
|
|
8234
8493
|
children: t(state.present ? "runtimeRemove" : "runtimeInstall")
|
|
8235
8494
|
}) : null]
|
|
8236
8495
|
}),
|
|
8237
|
-
state?.present && state.available && !state.removable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeManagedElsewhere") }) : null,
|
|
8238
8496
|
state?.active > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeActive") }) : null,
|
|
8239
8497
|
busy && state.phase === "installing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
8240
8498
|
type: "button",
|
|
@@ -8276,21 +8534,22 @@ window.__ModuleLoader__.load({
|
|
|
8276
8534
|
role: "status",
|
|
8277
8535
|
children: t("runtimeCancelled")
|
|
8278
8536
|
}) : null,
|
|
8279
|
-
!busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
8280
|
-
type: "button",
|
|
8281
|
-
variant: "outline",
|
|
8282
|
-
disabled: sending,
|
|
8283
|
-
onClick: () => {
|
|
8284
|
-
read();
|
|
8285
|
-
preference.load();
|
|
8286
|
-
},
|
|
8287
|
-
children: t("runtimeRefresh")
|
|
8288
|
-
}) : null,
|
|
8289
8537
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
|
|
8290
8538
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("subagentRuntimeManage") }),
|
|
8539
|
+
state?.present && state.available && !state.removable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeManagedElsewhere") }) : null,
|
|
8540
|
+
!busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
8541
|
+
type: "button",
|
|
8542
|
+
variant: "outline",
|
|
8543
|
+
disabled: sending,
|
|
8544
|
+
onClick: () => {
|
|
8545
|
+
read();
|
|
8546
|
+
preference.load();
|
|
8547
|
+
},
|
|
8548
|
+
children: t("runtimeRefresh")
|
|
8549
|
+
}) : null,
|
|
8291
8550
|
state && !state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeUnavailable") }) : null,
|
|
8292
8551
|
state?.available && !state.installable && !state.present ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeHostUnsupported") }) : null,
|
|
8293
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeInstallHint") }),
|
|
8552
|
+
state?.installable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeInstallHint") }) : null,
|
|
8294
8553
|
state?.componentVersion ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", { children: ["@deepseek-ai/dsh-subagent-codex@", state.componentVersion] }) : null,
|
|
8295
8554
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("subagentRuntimeCacheHint") }),
|
|
8296
8555
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
@@ -9255,32 +9514,41 @@ window.__ModuleLoader__.load({
|
|
|
9255
9514
|
}
|
|
9256
9515
|
//#endregion
|
|
9257
9516
|
//#region src/client-diagnostics.jsx
|
|
9258
|
-
function DiagnosticsCard({ rpc, t }) {
|
|
9517
|
+
function DiagnosticsCard({ rpc, t, diagnostics }) {
|
|
9259
9518
|
const [report, setReport] = (0, react.useState)();
|
|
9260
9519
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
9261
9520
|
const [copied, setCopied] = (0, react.useState)(false);
|
|
9262
9521
|
const [error, setError] = (0, react.useState)(false);
|
|
9522
|
+
const [copyError, setCopyError] = (0, react.useState)(false);
|
|
9263
9523
|
const load = () => {
|
|
9264
9524
|
setBusy(true);
|
|
9265
9525
|
setError(false);
|
|
9266
9526
|
setCopied(false);
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
setError(
|
|
9527
|
+
setCopyError(false);
|
|
9528
|
+
Promise.allSettled([recoveryCall(rpc, "diagnostics"), Promise.resolve().then(() => diagnostics?.collect())]).then(([server, client]) => {
|
|
9529
|
+
setError(server.status === "rejected");
|
|
9530
|
+
setReport({
|
|
9531
|
+
...server.status === "fulfilled" ? server.value : clientDiagnostic(server.reason),
|
|
9532
|
+
client: client.status === "fulfilled" && client.value ? client.value : { status: "unknown" }
|
|
9533
|
+
});
|
|
9270
9534
|
}).finally(() => setBusy(false));
|
|
9271
9535
|
};
|
|
9272
9536
|
const copy = () => {
|
|
9273
9537
|
if (report === void 0) return;
|
|
9274
|
-
|
|
9538
|
+
setCopyError(false);
|
|
9539
|
+
Promise.resolve().then(() => navigator.clipboard.writeText(JSON.stringify(report, null, 2))).then(() => setCopied(true)).catch(() => setCopyError(true));
|
|
9275
9540
|
};
|
|
9276
9541
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9277
9542
|
className: "codexSubscriptionCard codexSubscriptionDiagnostics",
|
|
9278
9543
|
children: [
|
|
9279
9544
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9280
9545
|
className: "codexSubscriptionSectionHead",
|
|
9281
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
9546
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9282
9547
|
className: "codexSubscriptionSectionTitle",
|
|
9283
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") })
|
|
9548
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9549
|
+
className: "codexSubscriptionHelp",
|
|
9550
|
+
children: t("diagnosticsHint")
|
|
9551
|
+
})]
|
|
9284
9552
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9285
9553
|
className: "codexSubscriptionActions",
|
|
9286
9554
|
children: [
|
|
@@ -9307,11 +9575,151 @@ window.__ModuleLoader__.load({
|
|
|
9307
9575
|
]
|
|
9308
9576
|
})]
|
|
9309
9577
|
}),
|
|
9310
|
-
report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.
|
|
9578
|
+
report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
9579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9580
|
+
className: "codexSubscriptionHelp",
|
|
9581
|
+
children: t("diagnosticsCoverageHint")
|
|
9582
|
+
}),
|
|
9583
|
+
report.version && report.client?.version && report.version !== report.client.version ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9584
|
+
role: "status",
|
|
9585
|
+
children: t("diagnosticsVersionMismatch")
|
|
9586
|
+
}) : null,
|
|
9587
|
+
(report.inspection?.checks ?? []).some((item) => ["inspection-failed", "inspection-timeout"].includes(item.reason)) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9588
|
+
role: "status",
|
|
9589
|
+
children: t("diagnosticsPartial")
|
|
9590
|
+
}) : null,
|
|
9591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
9592
|
+
style: {
|
|
9593
|
+
width: "100%",
|
|
9594
|
+
fontSize: 12,
|
|
9595
|
+
textAlign: "left",
|
|
9596
|
+
borderSpacing: "0 8px"
|
|
9597
|
+
},
|
|
9598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
9599
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsCapability") }),
|
|
9600
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsReadiness") }),
|
|
9601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsEvidence") })
|
|
9602
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
9603
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
|
|
9604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
|
|
9605
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString()}` : t("diagnosticsUnverified") })
|
|
9606
|
+
] }, item.id)) })]
|
|
9607
|
+
}),
|
|
9608
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9609
|
+
className: "codexSubscriptionHelp",
|
|
9610
|
+
children: report.client?.topology?.length ? t("diagnosticsHostCollected") : t("diagnosticsHostUnknown")
|
|
9611
|
+
}),
|
|
9612
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("diagnosticsDetails") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(report, null, 2) })] })
|
|
9613
|
+
] }),
|
|
9311
9614
|
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9312
9615
|
className: "codexSubscriptionError",
|
|
9313
9616
|
role: "alert",
|
|
9314
9617
|
children: t("diagnosticsFailed")
|
|
9618
|
+
}) : null,
|
|
9619
|
+
copyError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9620
|
+
className: "codexSubscriptionError",
|
|
9621
|
+
role: "alert",
|
|
9622
|
+
children: t("diagnosticsCopyFailed")
|
|
9623
|
+
}) : null
|
|
9624
|
+
]
|
|
9625
|
+
});
|
|
9626
|
+
}
|
|
9627
|
+
//#endregion
|
|
9628
|
+
//#region src/client-storage.jsx
|
|
9629
|
+
function StorageCard({ rpc, t }) {
|
|
9630
|
+
const [usage, setUsage] = (0, react.useState)();
|
|
9631
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
9632
|
+
const [confirm, setConfirm] = (0, react.useState)(false);
|
|
9633
|
+
const [error, setError] = (0, react.useState)(false);
|
|
9634
|
+
const mounted = (0, react.useRef)(false);
|
|
9635
|
+
const pending = (0, react.useRef)(false);
|
|
9636
|
+
const read = async (clear) => {
|
|
9637
|
+
if (pending.current) return;
|
|
9638
|
+
pending.current = true;
|
|
9639
|
+
setBusy(true);
|
|
9640
|
+
setError(false);
|
|
9641
|
+
try {
|
|
9642
|
+
const result = unwrap(await rpc.call(CHANNEL, clear ? "storage/clear-forecast" : "storage/status", {}));
|
|
9643
|
+
if (mounted.current) {
|
|
9644
|
+
setUsage(result);
|
|
9645
|
+
setConfirm(false);
|
|
9646
|
+
}
|
|
9647
|
+
} catch {
|
|
9648
|
+
if (mounted.current) setError(true);
|
|
9649
|
+
} finally {
|
|
9650
|
+
pending.current = false;
|
|
9651
|
+
if (mounted.current) setBusy(false);
|
|
9652
|
+
}
|
|
9653
|
+
};
|
|
9654
|
+
(0, react.useEffect)(() => {
|
|
9655
|
+
mounted.current = true;
|
|
9656
|
+
read(false);
|
|
9657
|
+
return () => {
|
|
9658
|
+
mounted.current = false;
|
|
9659
|
+
};
|
|
9660
|
+
}, [rpc]);
|
|
9661
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
9662
|
+
className: "codexSubscriptionCard codexSubscriptionStorage",
|
|
9663
|
+
"aria-label": t("storageTitle"),
|
|
9664
|
+
children: [
|
|
9665
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("storageTitle") }),
|
|
9666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9667
|
+
className: "codexSubscriptionStorageMeter",
|
|
9668
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("storageForecast") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
|
|
9669
|
+
role: "status",
|
|
9670
|
+
children: usage ? `${(usage.bytes / 1024).toFixed(1)} / ${(usage.limit / 1024).toFixed(0)} KiB` : t("storageLoading")
|
|
9671
|
+
})] }), usage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
|
|
9672
|
+
"aria-label": t("storageForecast"),
|
|
9673
|
+
value: usage.bytes,
|
|
9674
|
+
max: usage.limit
|
|
9675
|
+
}) : null]
|
|
9676
|
+
}),
|
|
9677
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9678
|
+
className: "codexSubscriptionHelp",
|
|
9679
|
+
children: t("storageHint")
|
|
9680
|
+
}),
|
|
9681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9682
|
+
className: "codexSubscriptionActions",
|
|
9683
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
9684
|
+
type: "button",
|
|
9685
|
+
variant: "outline",
|
|
9686
|
+
disabled: busy,
|
|
9687
|
+
onClick: () => void read(false),
|
|
9688
|
+
children: t("runtimeRefresh")
|
|
9689
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
9690
|
+
type: "button",
|
|
9691
|
+
variant: "outline",
|
|
9692
|
+
disabled: busy || !usage?.bytes,
|
|
9693
|
+
onClick: () => setConfirm(true),
|
|
9694
|
+
children: t("storageClear")
|
|
9695
|
+
})]
|
|
9696
|
+
}),
|
|
9697
|
+
confirm ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9698
|
+
className: "codexSubscriptionConfirm",
|
|
9699
|
+
role: "group",
|
|
9700
|
+
"aria-label": t("storageClear"),
|
|
9701
|
+
children: [
|
|
9702
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("storageConfirm") }),
|
|
9703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
9704
|
+
type: "button",
|
|
9705
|
+
variant: "outline",
|
|
9706
|
+
disabled: busy,
|
|
9707
|
+
onClick: () => setConfirm(false),
|
|
9708
|
+
children: t("sketchCancel")
|
|
9709
|
+
}),
|
|
9710
|
+
" ",
|
|
9711
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
9712
|
+
type: "button",
|
|
9713
|
+
variant: "outline",
|
|
9714
|
+
disabled: busy,
|
|
9715
|
+
onClick: () => void read(true),
|
|
9716
|
+
children: t("storageClear")
|
|
9717
|
+
})
|
|
9718
|
+
]
|
|
9719
|
+
}) : null,
|
|
9720
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9721
|
+
role: "alert",
|
|
9722
|
+
children: t("storageFailed")
|
|
9315
9723
|
}) : null
|
|
9316
9724
|
]
|
|
9317
9725
|
});
|
|
@@ -9722,10 +10130,15 @@ window.__ModuleLoader__.load({
|
|
|
9722
10130
|
}
|
|
9723
10131
|
//#endregion
|
|
9724
10132
|
//#region src/client-section.jsx
|
|
9725
|
-
function CodexSection({ preference, rpc, accountStatus, t }) {
|
|
10133
|
+
function CodexSection({ preference, rpc, accountStatus, t, diagnostics }) {
|
|
9726
10134
|
const [tab, setTab] = (0, react.useState)("account");
|
|
9727
10135
|
const id = (0, react.useId)();
|
|
9728
|
-
const tabs = [
|
|
10136
|
+
const tabs = [
|
|
10137
|
+
"account",
|
|
10138
|
+
"advanced",
|
|
10139
|
+
"creative",
|
|
10140
|
+
"maintenance"
|
|
10141
|
+
];
|
|
9729
10142
|
const accountSnapshot = useAccountStatusSnapshot(accountStatus);
|
|
9730
10143
|
const account = accountSnapshot.account;
|
|
9731
10144
|
const [resetKey, setResetKey] = (0, react.useState)(0);
|
|
@@ -9800,32 +10213,203 @@ window.__ModuleLoader__.load({
|
|
|
9800
10213
|
})
|
|
9801
10214
|
]
|
|
9802
10215
|
}),
|
|
9803
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
10216
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9804
10217
|
role: "tabpanel",
|
|
9805
10218
|
id: `${id}-advanced`,
|
|
9806
10219
|
"aria-labelledby": `${id}-advanced-tab`,
|
|
9807
10220
|
hidden: tab !== "advanced",
|
|
9808
|
-
children:
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
10221
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreferencesCard, {
|
|
10222
|
+
rpc,
|
|
10223
|
+
preference,
|
|
10224
|
+
t,
|
|
10225
|
+
section: "advanced"
|
|
10226
|
+
})
|
|
10227
|
+
}),
|
|
10228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10229
|
+
role: "tabpanel",
|
|
10230
|
+
id: `${id}-creative`,
|
|
10231
|
+
"aria-labelledby": `${id}-creative-tab`,
|
|
10232
|
+
hidden: tab !== "creative",
|
|
10233
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
|
|
10234
|
+
preference,
|
|
10235
|
+
t
|
|
10236
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
|
|
10237
|
+
preference,
|
|
10238
|
+
t,
|
|
10239
|
+
section: "sketch"
|
|
10240
|
+
})]
|
|
10241
|
+
}),
|
|
10242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10243
|
+
role: "tabpanel",
|
|
10244
|
+
id: `${id}-maintenance`,
|
|
10245
|
+
"aria-labelledby": `${id}-maintenance-tab`,
|
|
10246
|
+
hidden: tab !== "maintenance",
|
|
10247
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StorageCard, {
|
|
10248
|
+
rpc,
|
|
10249
|
+
t
|
|
10250
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsCard, {
|
|
10251
|
+
rpc,
|
|
10252
|
+
t,
|
|
10253
|
+
diagnostics
|
|
10254
|
+
})]
|
|
9824
10255
|
})
|
|
9825
10256
|
]
|
|
9826
10257
|
});
|
|
9827
10258
|
}
|
|
9828
10259
|
//#endregion
|
|
10260
|
+
//#region src/diagnostic-checks.js
|
|
10261
|
+
const states = /* @__PURE__ */ new Set([
|
|
10262
|
+
"pass",
|
|
10263
|
+
"fail",
|
|
10264
|
+
"warn",
|
|
10265
|
+
"unknown",
|
|
10266
|
+
"not-applicable"
|
|
10267
|
+
]);
|
|
10268
|
+
/** Adapters return bounded reason codes, never exceptions or raw service data. */
|
|
10269
|
+
async function collectChecks(definitions, { timeoutMs = 1500, now = Date.now, source = "server", scope = "plugin-process" } = {}) {
|
|
10270
|
+
return Promise.all(definitions.map(async ({ id, capability, run }) => {
|
|
10271
|
+
const startedAt = now();
|
|
10272
|
+
let timer;
|
|
10273
|
+
const controller = new AbortController();
|
|
10274
|
+
const base = {
|
|
10275
|
+
id,
|
|
10276
|
+
capability,
|
|
10277
|
+
source,
|
|
10278
|
+
scope
|
|
10279
|
+
};
|
|
10280
|
+
try {
|
|
10281
|
+
if (!run) return {
|
|
10282
|
+
...base,
|
|
10283
|
+
status: "unknown",
|
|
10284
|
+
reason: "not-instrumented",
|
|
10285
|
+
observedAt: now()
|
|
10286
|
+
};
|
|
10287
|
+
const result = await Promise.race([Promise.resolve().then(() => run(controller.signal)), new Promise((resolve) => {
|
|
10288
|
+
timer = setTimeout(() => {
|
|
10289
|
+
controller.abort();
|
|
10290
|
+
resolve({
|
|
10291
|
+
status: "unknown",
|
|
10292
|
+
reason: "inspection-timeout"
|
|
10293
|
+
});
|
|
10294
|
+
}, timeoutMs);
|
|
10295
|
+
})]);
|
|
10296
|
+
const valid = states.has(result?.status) && /^[a-z][a-z0-9-]{0,63}$/u.test(result?.reason ?? "");
|
|
10297
|
+
return {
|
|
10298
|
+
...base,
|
|
10299
|
+
status: valid ? result.status : "unknown",
|
|
10300
|
+
reason: valid ? result.reason : "invalid-inspection-result",
|
|
10301
|
+
observedAt: now(),
|
|
10302
|
+
elapsedMs: Math.max(0, now() - startedAt)
|
|
10303
|
+
};
|
|
10304
|
+
} catch {
|
|
10305
|
+
return {
|
|
10306
|
+
...base,
|
|
10307
|
+
status: "unknown",
|
|
10308
|
+
reason: "inspection-failed",
|
|
10309
|
+
observedAt: now(),
|
|
10310
|
+
elapsedMs: Math.max(0, now() - startedAt)
|
|
10311
|
+
};
|
|
10312
|
+
} finally {
|
|
10313
|
+
clearTimeout(timer);
|
|
10314
|
+
}
|
|
10315
|
+
}));
|
|
10316
|
+
}
|
|
10317
|
+
Object.freeze([
|
|
10318
|
+
"account",
|
|
10319
|
+
"accounts",
|
|
10320
|
+
"settings",
|
|
10321
|
+
"models",
|
|
10322
|
+
"quota",
|
|
10323
|
+
"search",
|
|
10324
|
+
"images",
|
|
10325
|
+
"sketch",
|
|
10326
|
+
"subagents",
|
|
10327
|
+
"compaction",
|
|
10328
|
+
"transport",
|
|
10329
|
+
"storage",
|
|
10330
|
+
"host-ui"
|
|
10331
|
+
]);
|
|
10332
|
+
//#endregion
|
|
10333
|
+
//#region src/diagnostic-host.js
|
|
10334
|
+
const roots = [
|
|
10335
|
+
"settings.section",
|
|
10336
|
+
"conversation.input.right",
|
|
10337
|
+
"conversation.input.model",
|
|
10338
|
+
"conversation.input.attachments",
|
|
10339
|
+
"conversation.message.images",
|
|
10340
|
+
"conversation.trajectory.images",
|
|
10341
|
+
"shell.overlay"
|
|
10342
|
+
];
|
|
10343
|
+
const packageName = /^(?:@[a-z0-9_.-]+\/)?[a-z0-9][a-z0-9_.-]{0,99}$/u;
|
|
10344
|
+
function projectHostSlots(slots) {
|
|
10345
|
+
if (typeof slots?.snapshot !== "function") return void 0;
|
|
10346
|
+
return roots.map((name) => {
|
|
10347
|
+
const nodes = slots.snapshot(name);
|
|
10348
|
+
const node = Array.isArray(nodes) ? nodes.find((item) => item.type === "slot" && item.name === name) : void 0;
|
|
10349
|
+
const occupants = Array.isArray(node?.occupants) ? node.occupants : [];
|
|
10350
|
+
return {
|
|
10351
|
+
name,
|
|
10352
|
+
declared: !!node,
|
|
10353
|
+
truncated: occupants.length > 16,
|
|
10354
|
+
occupants: occupants.slice(0, 16).map((value) => ({
|
|
10355
|
+
owner: typeof value.registrant === "string" && packageName.test(value.registrant) ? value.registrant : "unknown",
|
|
10356
|
+
active: value.active === true
|
|
10357
|
+
}))
|
|
10358
|
+
};
|
|
10359
|
+
});
|
|
10360
|
+
}
|
|
10361
|
+
function createHostDiagnostics() {
|
|
10362
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
10363
|
+
let sequence = 0;
|
|
10364
|
+
return {
|
|
10365
|
+
register(slots) {
|
|
10366
|
+
const item = scopes.get(slots) ?? {
|
|
10367
|
+
alias: `scope-${++sequence}`,
|
|
10368
|
+
count: 0
|
|
10369
|
+
};
|
|
10370
|
+
item.count++;
|
|
10371
|
+
scopes.set(slots, item);
|
|
10372
|
+
return () => {
|
|
10373
|
+
if (--item.count === 0) scopes.delete(slots);
|
|
10374
|
+
};
|
|
10375
|
+
},
|
|
10376
|
+
async collect() {
|
|
10377
|
+
const topology = [];
|
|
10378
|
+
const active = [...scopes.entries()].slice(0, 8);
|
|
10379
|
+
const checks = await collectChecks(active.map(([slots, item]) => ({
|
|
10380
|
+
id: `host-slots-${item.alias}`,
|
|
10381
|
+
capability: "host-ui",
|
|
10382
|
+
run: () => {
|
|
10383
|
+
const value = projectHostSlots(slots);
|
|
10384
|
+
if (!value) return {
|
|
10385
|
+
status: "unknown",
|
|
10386
|
+
reason: "host-inspection-unavailable"
|
|
10387
|
+
};
|
|
10388
|
+
topology.push({
|
|
10389
|
+
scope: item.alias,
|
|
10390
|
+
slots: value
|
|
10391
|
+
});
|
|
10392
|
+
return {
|
|
10393
|
+
status: "pass",
|
|
10394
|
+
reason: "inspection-completed"
|
|
10395
|
+
};
|
|
10396
|
+
}
|
|
10397
|
+
})), {
|
|
10398
|
+
source: "client",
|
|
10399
|
+
scope: "mounted-host-scopes"
|
|
10400
|
+
});
|
|
10401
|
+
return {
|
|
10402
|
+
version: PACKAGE_VERSION,
|
|
10403
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10404
|
+
checks,
|
|
10405
|
+
topology,
|
|
10406
|
+
truncated: scopes.size > active.length,
|
|
10407
|
+
interpretation: "host-selection-not-visual-or-execution-proof"
|
|
10408
|
+
};
|
|
10409
|
+
}
|
|
10410
|
+
};
|
|
10411
|
+
}
|
|
10412
|
+
//#endregion
|
|
9829
10413
|
//#region src/client.jsx
|
|
9830
10414
|
const inject = [
|
|
9831
10415
|
"slots",
|
|
@@ -9838,6 +10422,8 @@ window.__ModuleLoader__.load({
|
|
|
9838
10422
|
"sessions"
|
|
9839
10423
|
];
|
|
9840
10424
|
function apply(ctx) {
|
|
10425
|
+
const diagnostics = createHostDiagnostics();
|
|
10426
|
+
ctx.effect(() => diagnostics.register(ctx.slots), "codex-subscription: host inspection");
|
|
9841
10427
|
const imageViewer = new SubscriptionImageViewerService();
|
|
9842
10428
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
9843
10429
|
zh,
|
|
@@ -9898,11 +10484,13 @@ window.__ModuleLoader__.load({
|
|
|
9898
10484
|
preference,
|
|
9899
10485
|
rpc,
|
|
9900
10486
|
accountStatus,
|
|
9901
|
-
t
|
|
10487
|
+
t,
|
|
10488
|
+
diagnostics
|
|
9902
10489
|
})
|
|
9903
10490
|
}, CodexSection));
|
|
9904
10491
|
const sessions = ctx.get("sessions");
|
|
9905
10492
|
const installDirectorySlots = (scope) => {
|
|
10493
|
+
scope.effect(() => diagnostics.register(scope.slots), "codex-subscription: scoped host inspection");
|
|
9906
10494
|
const modelDirectories = scope.get("modelDirectories");
|
|
9907
10495
|
scope.slots.inject("conversation.input.right", () => scope.slots.register({
|
|
9908
10496
|
name: "conversation.input.right",
|