@xom11/whiteboard 0.6.2 → 0.6.5
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/dist/{ExcalidrawWithMenus-KBLDWPM2.mjs → ExcalidrawWithMenus-EAVPOPJZ.mjs} +3 -2
- package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +11 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/index.css +249 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +2910 -427
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2906 -432
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -1
- package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,12 @@ var dynamic__default = /*#__PURE__*/_interopDefault(dynamic);
|
|
|
15
15
|
|
|
16
16
|
var __defProp = Object.defineProperty;
|
|
17
17
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
18
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
19
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
20
|
+
}) : x)(function(x) {
|
|
21
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
22
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
23
|
+
});
|
|
18
24
|
var __esm = (fn, res) => function __init() {
|
|
19
25
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
20
26
|
};
|
|
@@ -292,6 +298,22 @@ var GROUP_LABELS = {
|
|
|
292
298
|
edit: "Ch\u1EC9nh s\u1EEDa",
|
|
293
299
|
transform: "Ph\xE9p bi\u1EBFn h\xECnh"
|
|
294
300
|
};
|
|
301
|
+
var GROUP_ORDER = [
|
|
302
|
+
"move",
|
|
303
|
+
"point",
|
|
304
|
+
"line",
|
|
305
|
+
"construct",
|
|
306
|
+
"polygon",
|
|
307
|
+
"circle",
|
|
308
|
+
"measure",
|
|
309
|
+
"edit",
|
|
310
|
+
"transform"
|
|
311
|
+
];
|
|
312
|
+
var A_CODE = "A".charCodeAt(0);
|
|
313
|
+
function letterForGroup(g) {
|
|
314
|
+
const idx = GROUP_ORDER.indexOf(g);
|
|
315
|
+
return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
|
|
316
|
+
}
|
|
295
317
|
function objKind(obj) {
|
|
296
318
|
if (!obj) return "other";
|
|
297
319
|
const e = (obj.elType || obj.type || "").toString().toLowerCase();
|
|
@@ -1684,7 +1706,24 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1684
1706
|
});
|
|
1685
1707
|
onReady({
|
|
1686
1708
|
getContainer: () => containerRef.current,
|
|
1687
|
-
|
|
1709
|
+
// Sync toạ độ live của free point về log trước khi trả ra. JSXGraph
|
|
1710
|
+
// cho phép drag free point (args=[x,y] không có ref), việc drag chỉ
|
|
1711
|
+
// cập nhật obj.X()/Y() trên board chứ không đụng log → re-edit + Chèn
|
|
1712
|
+
// sẽ serialize toạ độ cũ → SVG không đổi → fileId trùng → user thấy
|
|
1713
|
+
// "k thay đổi". Line/segment/circle/polygon tham chiếu point qua id
|
|
1714
|
+
// nên auto-update theo.
|
|
1715
|
+
getCreationLog: () => creationLogRef.current.map((e) => {
|
|
1716
|
+
if (e.type !== "point") return { ...e };
|
|
1717
|
+
const args = e.args;
|
|
1718
|
+
if (!Array.isArray(args) || args.length !== 2) return { ...e };
|
|
1719
|
+
if (typeof args[0] !== "number" || typeof args[1] !== "number") return { ...e };
|
|
1720
|
+
const obj = objMapRef.current.get(e.id);
|
|
1721
|
+
if (!obj || typeof obj.X !== "function" || typeof obj.Y !== "function") return { ...e };
|
|
1722
|
+
const x = obj.X();
|
|
1723
|
+
const y = obj.Y();
|
|
1724
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { ...e };
|
|
1725
|
+
return { ...e, args: [x, y] };
|
|
1726
|
+
}),
|
|
1688
1727
|
getBbox: () => boardRef.current ? boardRef.current.getBoundingBox() : [-10, 10, 10, -10],
|
|
1689
1728
|
getShowAxis: () => showAxisRef.current,
|
|
1690
1729
|
getShowGrid: () => showGridRef.current,
|
|
@@ -1882,8 +1921,140 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1882
1921
|
}
|
|
1883
1922
|
);
|
|
1884
1923
|
};
|
|
1924
|
+
function MobileToolDrawer({
|
|
1925
|
+
title,
|
|
1926
|
+
headerIcon,
|
|
1927
|
+
chips,
|
|
1928
|
+
actions,
|
|
1929
|
+
groups,
|
|
1930
|
+
activeTool,
|
|
1931
|
+
onToolSelect,
|
|
1932
|
+
drawerOpen,
|
|
1933
|
+
onDrawerClose,
|
|
1934
|
+
isDark,
|
|
1935
|
+
testId
|
|
1936
|
+
}) {
|
|
1937
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1938
|
+
drawerOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1939
|
+
"div",
|
|
1940
|
+
{
|
|
1941
|
+
className: "stamp-drawer-backdrop",
|
|
1942
|
+
onPointerDown: onDrawerClose,
|
|
1943
|
+
"aria-hidden": "true"
|
|
1944
|
+
}
|
|
1945
|
+
),
|
|
1946
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1947
|
+
"aside",
|
|
1948
|
+
{
|
|
1949
|
+
role: "complementary",
|
|
1950
|
+
"aria-label": title,
|
|
1951
|
+
"aria-hidden": !drawerOpen ? "true" : void 0,
|
|
1952
|
+
"data-testid": testId,
|
|
1953
|
+
"data-stamp-area": "true",
|
|
1954
|
+
"data-mobile-drawer": "true",
|
|
1955
|
+
"data-geo-mobile": "true",
|
|
1956
|
+
"data-drawer-state": drawerOpen ? "open" : "closed",
|
|
1957
|
+
className: [
|
|
1958
|
+
isDark ? "theme--dark " : "",
|
|
1959
|
+
"stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
|
|
1960
|
+
].join(""),
|
|
1961
|
+
children: [
|
|
1962
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3", children: [
|
|
1963
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
|
|
1964
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
|
|
1965
|
+
title
|
|
1966
|
+
] }),
|
|
1967
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1968
|
+
"button",
|
|
1969
|
+
{
|
|
1970
|
+
type: "button",
|
|
1971
|
+
onClick: onDrawerClose,
|
|
1972
|
+
"aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
|
|
1973
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
1974
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1975
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1976
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1977
|
+
] })
|
|
1978
|
+
}
|
|
1979
|
+
)
|
|
1980
|
+
] }),
|
|
1981
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur", children: [
|
|
1982
|
+
chips.map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1983
|
+
"button",
|
|
1984
|
+
{
|
|
1985
|
+
type: "button",
|
|
1986
|
+
role: "switch",
|
|
1987
|
+
"aria-pressed": c.pressed,
|
|
1988
|
+
"aria-label": c.label,
|
|
1989
|
+
"data-testid": c.testId,
|
|
1990
|
+
onClick: () => c.onToggle(!c.pressed),
|
|
1991
|
+
className: "geo-mobile-chip",
|
|
1992
|
+
children: [
|
|
1993
|
+
c.icon,
|
|
1994
|
+
c.label
|
|
1995
|
+
]
|
|
1996
|
+
},
|
|
1997
|
+
c.label
|
|
1998
|
+
)),
|
|
1999
|
+
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2000
|
+
"button",
|
|
2001
|
+
{
|
|
2002
|
+
type: "button",
|
|
2003
|
+
onClick: a.onClick,
|
|
2004
|
+
disabled: a.disabled,
|
|
2005
|
+
"aria-label": a.label,
|
|
2006
|
+
title: a.title ?? a.label,
|
|
2007
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
2008
|
+
children: a.icon
|
|
2009
|
+
},
|
|
2010
|
+
a.label
|
|
2011
|
+
)) })
|
|
2012
|
+
] }),
|
|
2013
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2014
|
+
"div",
|
|
2015
|
+
{
|
|
2016
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
2017
|
+
style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
|
|
2018
|
+
children: groups.map((g) => /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "px-3 pt-3 pb-1", children: [
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
2020
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
|
|
2021
|
+
g.groupLabel
|
|
2022
|
+
] }),
|
|
2023
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
|
|
2024
|
+
const active = activeTool === t.key;
|
|
2025
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2026
|
+
"button",
|
|
2027
|
+
{
|
|
2028
|
+
type: "button",
|
|
2029
|
+
"aria-label": t.label,
|
|
2030
|
+
"aria-pressed": active,
|
|
2031
|
+
"data-tool": t.key,
|
|
2032
|
+
onClick: () => {
|
|
2033
|
+
onToolSelect(t.key);
|
|
2034
|
+
onDrawerClose();
|
|
2035
|
+
},
|
|
2036
|
+
className: [
|
|
2037
|
+
"flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
|
|
2038
|
+
active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
|
|
2039
|
+
].join(" "),
|
|
2040
|
+
children: [
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 items-center justify-center", children: t.icon }),
|
|
2042
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
|
|
2043
|
+
]
|
|
2044
|
+
},
|
|
2045
|
+
t.key
|
|
2046
|
+
);
|
|
2047
|
+
}) })
|
|
2048
|
+
] }, g.group))
|
|
2049
|
+
}
|
|
2050
|
+
)
|
|
2051
|
+
]
|
|
2052
|
+
}
|
|
2053
|
+
)
|
|
2054
|
+
] });
|
|
2055
|
+
}
|
|
1885
2056
|
var TOOLTIP_DELAY_MS = 400;
|
|
1886
|
-
function Shell({ title, icon, onClose, children, isDark }) {
|
|
2057
|
+
function Shell({ title, icon, onClose, children, isDark, closeLabel = "\u0110\xF3ng" }) {
|
|
1887
2058
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1888
2059
|
"aside",
|
|
1889
2060
|
{
|
|
@@ -1891,7 +2062,10 @@ function Shell({ title, icon, onClose, children, isDark }) {
|
|
|
1891
2062
|
"aria-label": title,
|
|
1892
2063
|
"data-testid": "stamp-left-panel",
|
|
1893
2064
|
"data-stamp-area": "true",
|
|
1894
|
-
className:
|
|
2065
|
+
className: [
|
|
2066
|
+
isDark ? "theme--dark " : "",
|
|
2067
|
+
"absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
|
|
2068
|
+
].join(""),
|
|
1895
2069
|
children: [
|
|
1896
2070
|
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
|
|
1897
2071
|
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -1902,12 +2076,9 @@ function Shell({ title, icon, onClose, children, isDark }) {
|
|
|
1902
2076
|
"button",
|
|
1903
2077
|
{
|
|
1904
2078
|
onClick: onClose,
|
|
1905
|
-
"aria-label":
|
|
2079
|
+
"aria-label": closeLabel,
|
|
1906
2080
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
1907
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
1908
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1909
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1910
|
-
] })
|
|
2081
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
|
|
1911
2082
|
}
|
|
1912
2083
|
)
|
|
1913
2084
|
] }),
|
|
@@ -1928,24 +2099,36 @@ var GeometryIconHeader = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", h
|
|
|
1928
2099
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
|
|
1929
2100
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" })
|
|
1930
2101
|
] });
|
|
1931
|
-
function
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2102
|
+
function CloseIcon() {
|
|
2103
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2104
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2105
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2106
|
+
] });
|
|
2107
|
+
}
|
|
2108
|
+
function UndoIcon() {
|
|
2109
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2110
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
2111
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
2112
|
+
] });
|
|
2113
|
+
}
|
|
2114
|
+
function AxisIcon() {
|
|
2115
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2116
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
|
|
2117
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "4", y2: "4" }),
|
|
2118
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "2 6 4 4 6 6" }),
|
|
2119
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "18 18 20 20 18 22" })
|
|
2120
|
+
] });
|
|
2121
|
+
}
|
|
2122
|
+
function GridIcon() {
|
|
2123
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2124
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "1" }),
|
|
2125
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "10", x2: "20", y2: "10" }),
|
|
2126
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "16", x2: "20", y2: "16" }),
|
|
2127
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "4", x2: "10", y2: "20" }),
|
|
2128
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "16", y1: "4", x2: "16", y2: "20" })
|
|
2129
|
+
] });
|
|
2130
|
+
}
|
|
2131
|
+
function useToolHoverTooltip() {
|
|
1949
2132
|
const [hover, setHover] = react.useState(null);
|
|
1950
2133
|
const [portalReady, setPortalReady] = react.useState(false);
|
|
1951
2134
|
const hoverTimerRef = react.useRef(null);
|
|
@@ -1969,6 +2152,23 @@ function LeftPanel({
|
|
|
1969
2152
|
}
|
|
1970
2153
|
setHover(null);
|
|
1971
2154
|
}, []);
|
|
2155
|
+
return { hover, portalReady, showHover, hideHover };
|
|
2156
|
+
}
|
|
2157
|
+
function DesktopGeometryPanel(props) {
|
|
2158
|
+
const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onClose, isDark, chordGroup } = props;
|
|
2159
|
+
const grouped = react.useMemo(() => {
|
|
2160
|
+
return TOOLS.reduce((acc, t) => {
|
|
2161
|
+
var _a;
|
|
2162
|
+
(acc[_a = t.group] ?? (acc[_a] = [])).push(t);
|
|
2163
|
+
return acc;
|
|
2164
|
+
}, {});
|
|
2165
|
+
}, []);
|
|
2166
|
+
const groupKeys = react.useMemo(
|
|
2167
|
+
() => GROUP_ORDER.filter((g) => grouped[g]),
|
|
2168
|
+
[grouped]
|
|
2169
|
+
);
|
|
2170
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
2171
|
+
const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
|
|
1972
2172
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1973
2173
|
/* @__PURE__ */ jsxRuntime.jsxs(Shell, { title: "H\xECnh h\u1ECDc", icon: GeometryIconHeader, onClose, isDark, children: [
|
|
1974
2174
|
/* @__PURE__ */ jsxRuntime.jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
|
|
@@ -2005,36 +2205,95 @@ function LeftPanel({
|
|
|
2005
2205
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
2006
2206
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
2007
2207
|
className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
2008
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
2009
|
-
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
2010
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
2011
|
-
] })
|
|
2208
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
|
|
2012
2209
|
}
|
|
2013
2210
|
)
|
|
2014
2211
|
] }) }),
|
|
2015
|
-
groupKeys.map((group) =>
|
|
2016
|
-
const
|
|
2017
|
-
|
|
2018
|
-
|
|
2212
|
+
groupKeys.map((group) => {
|
|
2213
|
+
const isChordActive = chordGroup === group;
|
|
2214
|
+
const dimmed = chordGroup !== null && !isChordActive;
|
|
2215
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2216
|
+
"section",
|
|
2019
2217
|
{
|
|
2020
|
-
|
|
2021
|
-
"
|
|
2022
|
-
"aria-pressed": active,
|
|
2023
|
-
"data-tool": t.key,
|
|
2024
|
-
onClick: () => onToolChange(t.key),
|
|
2025
|
-
onMouseEnter: (e) => showHover(e.currentTarget, t),
|
|
2026
|
-
onMouseLeave: hideHover,
|
|
2027
|
-
onFocus: (e) => showHover(e.currentTarget, t),
|
|
2028
|
-
onBlur: hideHover,
|
|
2218
|
+
"data-chord-group": group,
|
|
2219
|
+
"data-chord-active": isChordActive ? "true" : "false",
|
|
2029
2220
|
className: [
|
|
2030
|
-
"
|
|
2031
|
-
|
|
2221
|
+
"rounded-md transition",
|
|
2222
|
+
isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
|
|
2223
|
+
dimmed ? "opacity-55" : "opacity-100"
|
|
2032
2224
|
].join(" "),
|
|
2033
|
-
children:
|
|
2225
|
+
children: [
|
|
2226
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
2227
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: GROUP_LABELS[group] }),
|
|
2228
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2229
|
+
"span",
|
|
2230
|
+
{
|
|
2231
|
+
"data-testid": `chord-letter-${group}`,
|
|
2232
|
+
className: [
|
|
2233
|
+
"font-mono text-[10px] leading-none transition",
|
|
2234
|
+
isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
|
|
2235
|
+
].join(" "),
|
|
2236
|
+
children: letterForGroup(group)
|
|
2237
|
+
}
|
|
2238
|
+
)
|
|
2239
|
+
] }),
|
|
2240
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
|
|
2241
|
+
const active = activeTool === t.key;
|
|
2242
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2243
|
+
"button",
|
|
2244
|
+
{
|
|
2245
|
+
type: "button",
|
|
2246
|
+
"aria-label": t.label,
|
|
2247
|
+
"aria-pressed": active,
|
|
2248
|
+
"data-tool": t.key,
|
|
2249
|
+
onClick: () => onToolChange(t.key),
|
|
2250
|
+
onMouseEnter: (e) => showHover(e.currentTarget, t),
|
|
2251
|
+
onMouseLeave: hideHover,
|
|
2252
|
+
onFocus: (e) => showHover(e.currentTarget, t),
|
|
2253
|
+
onBlur: hideHover,
|
|
2254
|
+
className: [
|
|
2255
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
2256
|
+
active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
2257
|
+
].join(" "),
|
|
2258
|
+
children: [
|
|
2259
|
+
t.icon,
|
|
2260
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2261
|
+
"span",
|
|
2262
|
+
{
|
|
2263
|
+
"data-testid": `chord-num-${t.key}`,
|
|
2264
|
+
className: [
|
|
2265
|
+
"pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
|
|
2266
|
+
active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
|
|
2267
|
+
].join(" "),
|
|
2268
|
+
children: i + 1
|
|
2269
|
+
}
|
|
2270
|
+
)
|
|
2271
|
+
]
|
|
2272
|
+
},
|
|
2273
|
+
t.key
|
|
2274
|
+
);
|
|
2275
|
+
}) })
|
|
2276
|
+
]
|
|
2034
2277
|
},
|
|
2035
|
-
|
|
2278
|
+
group
|
|
2036
2279
|
);
|
|
2037
|
-
})
|
|
2280
|
+
}),
|
|
2281
|
+
chordGroup && activeGroupTools && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2282
|
+
"div",
|
|
2283
|
+
{
|
|
2284
|
+
"data-testid": "chord-hint",
|
|
2285
|
+
className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
|
|
2286
|
+
children: [
|
|
2287
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: letterForGroup(chordGroup) }),
|
|
2288
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
|
|
2289
|
+
activeGroupTools.map((t, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2 inline-block", children: [
|
|
2290
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
|
|
2291
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1", children: t.label })
|
|
2292
|
+
] }, t.key)),
|
|
2293
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
|
|
2294
|
+
]
|
|
2295
|
+
}
|
|
2296
|
+
)
|
|
2038
2297
|
] }),
|
|
2039
2298
|
portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
|
|
2040
2299
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -2058,6 +2317,78 @@ function LeftPanel({
|
|
|
2058
2317
|
) : null
|
|
2059
2318
|
] });
|
|
2060
2319
|
}
|
|
2320
|
+
function MobileGeometryPanel(props) {
|
|
2321
|
+
const {
|
|
2322
|
+
activeTool,
|
|
2323
|
+
onToolChange,
|
|
2324
|
+
showAxis,
|
|
2325
|
+
showGrid,
|
|
2326
|
+
onShowAxisChange,
|
|
2327
|
+
onShowGridChange,
|
|
2328
|
+
onUndo,
|
|
2329
|
+
canUndo,
|
|
2330
|
+
isDark,
|
|
2331
|
+
drawerOpen,
|
|
2332
|
+
onDrawerClose
|
|
2333
|
+
} = props;
|
|
2334
|
+
const groups = react.useMemo(() => {
|
|
2335
|
+
const acc = /* @__PURE__ */ new Map();
|
|
2336
|
+
for (const t of TOOLS) {
|
|
2337
|
+
if (!acc.has(t.group)) acc.set(t.group, []);
|
|
2338
|
+
acc.get(t.group).push(t);
|
|
2339
|
+
}
|
|
2340
|
+
return Array.from(acc.entries()).map(([group, tools]) => ({
|
|
2341
|
+
group,
|
|
2342
|
+
groupLabel: GROUP_LABELS[group],
|
|
2343
|
+
tools: tools.map((t) => ({ key: t.key, label: t.label, icon: t.icon }))
|
|
2344
|
+
}));
|
|
2345
|
+
}, []);
|
|
2346
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2347
|
+
MobileToolDrawer,
|
|
2348
|
+
{
|
|
2349
|
+
title: "H\xECnh h\u1ECDc",
|
|
2350
|
+
headerIcon: GeometryIconHeader,
|
|
2351
|
+
testId: "stamp-left-panel",
|
|
2352
|
+
isDark,
|
|
2353
|
+
drawerOpen: !!drawerOpen,
|
|
2354
|
+
onDrawerClose: () => onDrawerClose?.(),
|
|
2355
|
+
chips: [
|
|
2356
|
+
{
|
|
2357
|
+
label: "Tr\u1EE5c",
|
|
2358
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(AxisIcon, {}),
|
|
2359
|
+
pressed: showAxis,
|
|
2360
|
+
onToggle: onShowAxisChange,
|
|
2361
|
+
testId: "toggle-axis"
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
label: "L\u01B0\u1EDBi",
|
|
2365
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(GridIcon, {}),
|
|
2366
|
+
pressed: showGrid,
|
|
2367
|
+
onToggle: onShowGridChange,
|
|
2368
|
+
testId: "toggle-grid"
|
|
2369
|
+
}
|
|
2370
|
+
],
|
|
2371
|
+
actions: [
|
|
2372
|
+
{
|
|
2373
|
+
label: "Ho\xE0n t\xE1c",
|
|
2374
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
2375
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {}),
|
|
2376
|
+
onClick: onUndo,
|
|
2377
|
+
disabled: !canUndo
|
|
2378
|
+
}
|
|
2379
|
+
],
|
|
2380
|
+
groups,
|
|
2381
|
+
activeTool,
|
|
2382
|
+
onToolSelect: onToolChange
|
|
2383
|
+
}
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
function LeftPanel(props) {
|
|
2387
|
+
if (props.isMobile) {
|
|
2388
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MobileGeometryPanel, { ...props });
|
|
2389
|
+
}
|
|
2390
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DesktopGeometryPanel, { ...props });
|
|
2391
|
+
}
|
|
2061
2392
|
|
|
2062
2393
|
// src/stamps/geometry-2d/serialize.ts
|
|
2063
2394
|
function serializeBoard(board, log, options = {}) {
|
|
@@ -2202,6 +2533,38 @@ var STROKE_PALETTE = [
|
|
|
2202
2533
|
"#868e96"
|
|
2203
2534
|
// gray
|
|
2204
2535
|
];
|
|
2536
|
+
var MOBILE_QUERY = "(max-width: 768px)";
|
|
2537
|
+
var NO_HOVER_QUERY = "(hover: none)";
|
|
2538
|
+
function readMatch(query) {
|
|
2539
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
2540
|
+
try {
|
|
2541
|
+
return window.matchMedia(query).matches;
|
|
2542
|
+
} catch {
|
|
2543
|
+
return false;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
function useIsMobile() {
|
|
2547
|
+
const [state, setState] = react.useState(() => ({
|
|
2548
|
+
isMobile: readMatch(MOBILE_QUERY),
|
|
2549
|
+
isTouchOnly: readMatch(NO_HOVER_QUERY)
|
|
2550
|
+
}));
|
|
2551
|
+
react.useEffect(() => {
|
|
2552
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
2553
|
+
const mql = window.matchMedia(MOBILE_QUERY);
|
|
2554
|
+
const tql = window.matchMedia(NO_HOVER_QUERY);
|
|
2555
|
+
const update = () => {
|
|
2556
|
+
setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
|
|
2557
|
+
};
|
|
2558
|
+
update();
|
|
2559
|
+
mql.addEventListener("change", update);
|
|
2560
|
+
tql.addEventListener("change", update);
|
|
2561
|
+
return () => {
|
|
2562
|
+
mql.removeEventListener("change", update);
|
|
2563
|
+
tql.removeEventListener("change", update);
|
|
2564
|
+
};
|
|
2565
|
+
}, []);
|
|
2566
|
+
return state;
|
|
2567
|
+
}
|
|
2205
2568
|
var DASH_OPTIONS = [
|
|
2206
2569
|
{ value: 0, label: "N\xE9t li\u1EC1n" },
|
|
2207
2570
|
{ value: 2, label: "N\xE9t \u0111\u1EE9t" },
|
|
@@ -2260,6 +2623,26 @@ var PropertiesPopover = (props) => {
|
|
|
2260
2623
|
const { anchor, onClose, onMutate, isDark, getAllNames } = props;
|
|
2261
2624
|
const rootRef = react.useRef(null);
|
|
2262
2625
|
const [section, setSection] = react.useState(null);
|
|
2626
|
+
const { isMobile } = useIsMobile();
|
|
2627
|
+
const [clamped, setClamped] = react.useState(null);
|
|
2628
|
+
react.useLayoutEffect(() => {
|
|
2629
|
+
if (typeof window === "undefined") return;
|
|
2630
|
+
const margin = 8;
|
|
2631
|
+
if (isMobile) {
|
|
2632
|
+
const rect2 = rootRef.current?.getBoundingClientRect();
|
|
2633
|
+
const w2 = rect2?.width ?? 280;
|
|
2634
|
+
const left2 = Math.max(margin, (window.innerWidth - w2) / 2);
|
|
2635
|
+
const top2 = window.innerHeight - (rect2?.height ?? 80) - margin - 12;
|
|
2636
|
+
setClamped({ left: left2, top: Math.max(margin, top2) });
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
2640
|
+
const w = rect?.width ?? 280;
|
|
2641
|
+
const h = rect?.height ?? 80;
|
|
2642
|
+
const left = Math.max(margin, Math.min(anchor.x, window.innerWidth - w - margin));
|
|
2643
|
+
const top = Math.max(margin, Math.min(anchor.y, window.innerHeight - h - margin));
|
|
2644
|
+
setClamped({ left, top });
|
|
2645
|
+
}, [anchor.x, anchor.y, isMobile, section]);
|
|
2263
2646
|
const initialName = props.kind === "point" ? props.currentName : props.kind === "line" || props.kind === "circle" ? props.currentName : "";
|
|
2264
2647
|
const [name, setName] = react.useState(initialName);
|
|
2265
2648
|
react.useEffect(() => {
|
|
@@ -2272,14 +2655,14 @@ var PropertiesPopover = (props) => {
|
|
|
2272
2655
|
onClose();
|
|
2273
2656
|
}
|
|
2274
2657
|
};
|
|
2275
|
-
const
|
|
2658
|
+
const onPointerDown = (e) => {
|
|
2276
2659
|
if (!rootRef.current?.contains(e.target)) onClose();
|
|
2277
2660
|
};
|
|
2278
2661
|
document.addEventListener("keydown", onKey);
|
|
2279
|
-
document.addEventListener("
|
|
2662
|
+
document.addEventListener("pointerdown", onPointerDown, { capture: true });
|
|
2280
2663
|
return () => {
|
|
2281
2664
|
document.removeEventListener("keydown", onKey);
|
|
2282
|
-
document.removeEventListener("
|
|
2665
|
+
document.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
2283
2666
|
};
|
|
2284
2667
|
}, [onClose]);
|
|
2285
2668
|
const pickColor = (c) => {
|
|
@@ -2316,6 +2699,7 @@ var PropertiesPopover = (props) => {
|
|
|
2316
2699
|
{
|
|
2317
2700
|
type: "button",
|
|
2318
2701
|
"data-section": id,
|
|
2702
|
+
"data-pill-btn": id,
|
|
2319
2703
|
"aria-label": label,
|
|
2320
2704
|
"aria-pressed": !!active,
|
|
2321
2705
|
onClick,
|
|
@@ -2334,13 +2718,14 @@ var PropertiesPopover = (props) => {
|
|
|
2334
2718
|
}
|
|
2335
2719
|
);
|
|
2336
2720
|
const colorIndicatorTint = react.useMemo(() => currentColor, [currentColor]);
|
|
2721
|
+
const pos = clamped ?? { left: anchor.x, top: anchor.y };
|
|
2337
2722
|
const node = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2338
2723
|
"div",
|
|
2339
2724
|
{
|
|
2340
2725
|
ref: rootRef,
|
|
2341
2726
|
"data-stamp-area": "true",
|
|
2342
2727
|
className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-1.5`,
|
|
2343
|
-
style: { left:
|
|
2728
|
+
style: { left: pos.left, top: pos.top },
|
|
2344
2729
|
role: "dialog",
|
|
2345
2730
|
"aria-label": "Thu\u1ED9c t\xEDnh \u0111\u1ED1i t\u01B0\u1EE3ng",
|
|
2346
2731
|
children: [
|
|
@@ -2530,7 +2915,7 @@ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel,
|
|
|
2530
2915
|
return reactDom.createPortal(node, document.body);
|
|
2531
2916
|
};
|
|
2532
2917
|
var GeometryEditorPanel = react.forwardRef(
|
|
2533
|
-
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark }, ref) {
|
|
2918
|
+
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer }, ref) {
|
|
2534
2919
|
const handleRef = react.useRef(null);
|
|
2535
2920
|
const [ready, setReady] = react.useState(false);
|
|
2536
2921
|
const [propsPopover, setPropsPopover] = react.useState(null);
|
|
@@ -2592,7 +2977,7 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2592
2977
|
insert: performInsert,
|
|
2593
2978
|
hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
|
|
2594
2979
|
}), [performInsert]);
|
|
2595
|
-
const wrapperStyle = {
|
|
2980
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
2596
2981
|
position: "absolute",
|
|
2597
2982
|
top: "50%",
|
|
2598
2983
|
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
@@ -2606,11 +2991,30 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2606
2991
|
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
|
|
2607
2992
|
"data-testid": "geometry-editor-panel",
|
|
2608
2993
|
"data-stamp-area": "true",
|
|
2994
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
2609
2995
|
style: wrapperStyle,
|
|
2610
|
-
className:
|
|
2996
|
+
className: [
|
|
2997
|
+
isDark ? "theme--dark " : "",
|
|
2998
|
+
"flex flex-col overflow-hidden bg-white",
|
|
2999
|
+
isMobile ? "h-full w-full" : "h-[540px] max-h-[85vh] w-[640px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
|
|
3000
|
+
].join(" "),
|
|
2611
3001
|
children: [
|
|
2612
|
-
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center
|
|
2613
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3002
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
|
|
3003
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3004
|
+
"button",
|
|
3005
|
+
{
|
|
3006
|
+
type: "button",
|
|
3007
|
+
onClick: onOpenDrawer,
|
|
3008
|
+
"aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
|
|
3009
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
3010
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3011
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
3012
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
3013
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
3014
|
+
] })
|
|
3015
|
+
}
|
|
3016
|
+
),
|
|
3017
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
2614
3018
|
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2615
3019
|
/* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "3,18 12,3 21,18" }),
|
|
2616
3020
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
|
|
@@ -2619,12 +3023,23 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2619
3023
|
] }),
|
|
2620
3024
|
"D\u1EF1ng h\xECnh h\u1ECDc"
|
|
2621
3025
|
] }),
|
|
2622
|
-
|
|
3026
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3027
|
+
"button",
|
|
3028
|
+
{
|
|
3029
|
+
type: "button",
|
|
3030
|
+
onClick: handleInsert,
|
|
3031
|
+
disabled: !ready,
|
|
3032
|
+
"data-testid": "geometry-insert-btn-mobile",
|
|
3033
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
3034
|
+
children: "Ch\xE8n"
|
|
3035
|
+
}
|
|
3036
|
+
),
|
|
3037
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2623
3038
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2624
3039
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2625
3040
|
] }) })
|
|
2626
3041
|
] }),
|
|
2627
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", style: { height: "420px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3042
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", style: isMobile ? void 0 : { height: "420px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2628
3043
|
JSXGraphMiniBoard,
|
|
2629
3044
|
{
|
|
2630
3045
|
onReady: handleReady,
|
|
@@ -2697,7 +3112,7 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2697
3112
|
}
|
|
2698
3113
|
}
|
|
2699
3114
|
),
|
|
2700
|
-
/* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
3115
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
2701
3116
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
|
|
2702
3117
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
2703
3118
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2725,6 +3140,76 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2725
3140
|
);
|
|
2726
3141
|
}
|
|
2727
3142
|
);
|
|
3143
|
+
var A_CODE2 = "a".charCodeAt(0);
|
|
3144
|
+
function isFieldFocused() {
|
|
3145
|
+
const ae = typeof document !== "undefined" ? document.activeElement : null;
|
|
3146
|
+
return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
|
|
3147
|
+
}
|
|
3148
|
+
function useChordShortcut(args) {
|
|
3149
|
+
const { groupOrder, tools, onSelect, enabled } = args;
|
|
3150
|
+
const [chordGroup, setChordGroup] = react.useState(null);
|
|
3151
|
+
const groupOrderRef = react.useRef(groupOrder);
|
|
3152
|
+
const toolsRef = react.useRef(tools);
|
|
3153
|
+
const onSelectRef = react.useRef(onSelect);
|
|
3154
|
+
const chordGroupRef = react.useRef(null);
|
|
3155
|
+
groupOrderRef.current = groupOrder;
|
|
3156
|
+
toolsRef.current = tools;
|
|
3157
|
+
onSelectRef.current = onSelect;
|
|
3158
|
+
const cancel = react.useCallback(() => {
|
|
3159
|
+
chordGroupRef.current = null;
|
|
3160
|
+
setChordGroup(null);
|
|
3161
|
+
}, []);
|
|
3162
|
+
react.useEffect(() => {
|
|
3163
|
+
if (!enabled) return;
|
|
3164
|
+
const setChord = (next) => {
|
|
3165
|
+
chordGroupRef.current = next;
|
|
3166
|
+
setChordGroup(next);
|
|
3167
|
+
};
|
|
3168
|
+
const onKey = (e) => {
|
|
3169
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
3170
|
+
if (isFieldFocused()) return;
|
|
3171
|
+
const key = e.key;
|
|
3172
|
+
const lower = key.length === 1 ? key.toLowerCase() : key;
|
|
3173
|
+
if (key === "Escape") {
|
|
3174
|
+
if (chordGroupRef.current !== null) {
|
|
3175
|
+
e.preventDefault();
|
|
3176
|
+
e.stopPropagation();
|
|
3177
|
+
setChord(null);
|
|
3178
|
+
}
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
if (lower.length === 1 && lower >= "a" && lower <= "z") {
|
|
3182
|
+
const idx = lower.charCodeAt(0) - A_CODE2;
|
|
3183
|
+
if (idx < groupOrderRef.current.length) {
|
|
3184
|
+
e.preventDefault();
|
|
3185
|
+
e.stopPropagation();
|
|
3186
|
+
setChord(groupOrderRef.current[idx]);
|
|
3187
|
+
}
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
if (key >= "1" && key <= "9") {
|
|
3191
|
+
const active = chordGroupRef.current;
|
|
3192
|
+
if (active === null) return;
|
|
3193
|
+
const n = key.charCodeAt(0) - "1".charCodeAt(0);
|
|
3194
|
+
const toolsInGroup = toolsRef.current.filter(
|
|
3195
|
+
(t) => t.group === active
|
|
3196
|
+
);
|
|
3197
|
+
e.preventDefault();
|
|
3198
|
+
e.stopPropagation();
|
|
3199
|
+
if (n < toolsInGroup.length) {
|
|
3200
|
+
onSelectRef.current(toolsInGroup[n].key);
|
|
3201
|
+
}
|
|
3202
|
+
setChord(null);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
3207
|
+
return () => {
|
|
3208
|
+
window.removeEventListener("keydown", onKey, { capture: true });
|
|
3209
|
+
};
|
|
3210
|
+
}, [enabled]);
|
|
3211
|
+
return { chordGroup, cancel };
|
|
3212
|
+
}
|
|
2728
3213
|
|
|
2729
3214
|
// src/stamps/shared/svgToImage.ts
|
|
2730
3215
|
async function hashString(input) {
|
|
@@ -2847,6 +3332,14 @@ var GeometryStampHost = react.forwardRef(
|
|
|
2847
3332
|
function GeometryStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
2848
3333
|
const panelRef = react.useRef(null);
|
|
2849
3334
|
const [geomState, setGeomState] = react.useState(INITIAL_GEOM_STATE);
|
|
3335
|
+
const { isMobile } = useIsMobile();
|
|
3336
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
3337
|
+
const { chordGroup } = useChordShortcut({
|
|
3338
|
+
groupOrder: GROUP_ORDER,
|
|
3339
|
+
tools: TOOLS,
|
|
3340
|
+
onSelect: (key) => panelRef.current?.setTool(key),
|
|
3341
|
+
enabled: !isMobile
|
|
3342
|
+
});
|
|
2850
3343
|
const initialState = react.useMemo(() => {
|
|
2851
3344
|
if (!editingElement) return null;
|
|
2852
3345
|
if (!isGeometryCustomData(editingElement.customData)) return null;
|
|
@@ -2900,7 +3393,11 @@ var GeometryStampHost = react.forwardRef(
|
|
|
2900
3393
|
onUndo: () => panelRef.current?.undo(),
|
|
2901
3394
|
canUndo: geomState.canUndo,
|
|
2902
3395
|
onClose,
|
|
2903
|
-
isDark
|
|
3396
|
+
isDark,
|
|
3397
|
+
isMobile,
|
|
3398
|
+
drawerOpen,
|
|
3399
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
3400
|
+
chordGroup
|
|
2904
3401
|
}
|
|
2905
3402
|
),
|
|
2906
3403
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2911,8 +3408,10 @@ var GeometryStampHost = react.forwardRef(
|
|
|
2911
3408
|
onInsert: handleInsert,
|
|
2912
3409
|
onClose,
|
|
2913
3410
|
onStateChange: setGeomState,
|
|
2914
|
-
withLeftPanel:
|
|
2915
|
-
isDark
|
|
3411
|
+
withLeftPanel: !isMobile,
|
|
3412
|
+
isDark,
|
|
3413
|
+
isMobile,
|
|
3414
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
2916
3415
|
}
|
|
2917
3416
|
)
|
|
2918
3417
|
] });
|
|
@@ -2950,38 +3449,58 @@ var geometryStamp = {
|
|
|
2950
3449
|
},
|
|
2951
3450
|
Host: GeometryStampHost
|
|
2952
3451
|
};
|
|
2953
|
-
function Shell2({ title, icon, onClose, children }) {
|
|
2954
|
-
|
|
2955
|
-
"
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3452
|
+
function Shell2({ title, icon, onClose, children, isMobile, drawerOpen, onDrawerClose }) {
|
|
3453
|
+
const mobileAttrs = isMobile ? {
|
|
3454
|
+
"data-mobile-drawer": "true",
|
|
3455
|
+
"data-drawer-state": drawerOpen ? "open" : "closed"
|
|
3456
|
+
} : {};
|
|
3457
|
+
const handleHeaderClose = () => {
|
|
3458
|
+
if (isMobile) onDrawerClose?.();
|
|
3459
|
+
else onClose();
|
|
3460
|
+
};
|
|
3461
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3462
|
+
isMobile && drawerOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3463
|
+
"div",
|
|
3464
|
+
{
|
|
3465
|
+
className: "stamp-drawer-backdrop",
|
|
3466
|
+
onPointerDown: onDrawerClose,
|
|
3467
|
+
"aria-hidden": "true"
|
|
3468
|
+
}
|
|
3469
|
+
),
|
|
3470
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3471
|
+
"aside",
|
|
3472
|
+
{
|
|
3473
|
+
role: "complementary",
|
|
3474
|
+
"aria-label": title,
|
|
3475
|
+
"aria-hidden": isMobile && !drawerOpen ? "true" : void 0,
|
|
3476
|
+
"data-testid": "stamp-left-panel",
|
|
3477
|
+
"data-stamp-area": "true",
|
|
3478
|
+
...mobileAttrs,
|
|
3479
|
+
className: isMobile ? "stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md" : "absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200",
|
|
3480
|
+
children: [
|
|
3481
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
|
|
3482
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
3483
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: icon }),
|
|
3484
|
+
title
|
|
3485
|
+
] }),
|
|
3486
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3487
|
+
"button",
|
|
3488
|
+
{
|
|
3489
|
+
onClick: handleHeaderClose,
|
|
3490
|
+
"aria-label": isMobile ? "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5" : "\u0110\xF3ng",
|
|
3491
|
+
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
3492
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3493
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
3494
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
3495
|
+
] })
|
|
3496
|
+
}
|
|
3497
|
+
)
|
|
2967
3498
|
] }),
|
|
2968
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
2974
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2975
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2976
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2977
|
-
] })
|
|
2978
|
-
}
|
|
2979
|
-
)
|
|
2980
|
-
] }),
|
|
2981
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
2982
|
-
]
|
|
2983
|
-
}
|
|
2984
|
-
);
|
|
3499
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
3500
|
+
]
|
|
3501
|
+
}
|
|
3502
|
+
)
|
|
3503
|
+
] });
|
|
2985
3504
|
}
|
|
2986
3505
|
function Section2({ label, children }) {
|
|
2987
3506
|
return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
|
|
@@ -3028,65 +3547,80 @@ function LeftPanel2({
|
|
|
3028
3547
|
displayMode,
|
|
3029
3548
|
onDisplayModeChange,
|
|
3030
3549
|
onInsertSnippet,
|
|
3031
|
-
onClose
|
|
3550
|
+
onClose,
|
|
3551
|
+
isMobile,
|
|
3552
|
+
drawerOpen,
|
|
3553
|
+
onDrawerClose
|
|
3032
3554
|
}) {
|
|
3033
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3555
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3556
|
+
Shell2,
|
|
3557
|
+
{
|
|
3558
|
+
title: "C\xF4ng th\u1EE9c LaTeX",
|
|
3559
|
+
icon: "\u2211",
|
|
3560
|
+
onClose,
|
|
3561
|
+
isMobile,
|
|
3562
|
+
drawerOpen,
|
|
3563
|
+
onDrawerClose,
|
|
3564
|
+
children: [
|
|
3565
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section2, { label: "Ch\u1EBF \u0111\u1ED9 hi\u1EC3n th\u1ECB", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [
|
|
3566
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3567
|
+
"button",
|
|
3568
|
+
{
|
|
3569
|
+
type: "button",
|
|
3570
|
+
onClick: () => onDisplayModeChange(false),
|
|
3571
|
+
"aria-pressed": !displayMode,
|
|
3572
|
+
className: [
|
|
3573
|
+
"rounded-md border px-2 py-1.5 text-xs transition",
|
|
3574
|
+
!displayMode ? "border-emerald-500 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-300" : "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"
|
|
3575
|
+
].join(" "),
|
|
3576
|
+
children: [
|
|
3577
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: "Inline" }),
|
|
3578
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[10px] text-slate-500", children: "$ ... $" })
|
|
3579
|
+
]
|
|
3580
|
+
}
|
|
3581
|
+
),
|
|
3582
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3583
|
+
"button",
|
|
3584
|
+
{
|
|
3585
|
+
type: "button",
|
|
3586
|
+
onClick: () => onDisplayModeChange(true),
|
|
3587
|
+
"aria-pressed": displayMode,
|
|
3588
|
+
className: [
|
|
3589
|
+
"rounded-md border px-2 py-1.5 text-xs transition",
|
|
3590
|
+
displayMode ? "border-emerald-500 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-300" : "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"
|
|
3591
|
+
].join(" "),
|
|
3592
|
+
children: [
|
|
3593
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: "Block" }),
|
|
3594
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[10px] text-slate-500", children: "$$ ... $$" })
|
|
3595
|
+
]
|
|
3596
|
+
}
|
|
3597
|
+
)
|
|
3598
|
+
] }) }),
|
|
3599
|
+
SNIPPETS.map((group) => /* @__PURE__ */ jsxRuntime.jsx(Section2, { label: group.group, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: group.items.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3600
|
+
"button",
|
|
3601
|
+
{
|
|
3602
|
+
type: "button",
|
|
3603
|
+
"data-snippet": s.snippet,
|
|
3604
|
+
onClick: () => onInsertSnippet(s.snippet),
|
|
3605
|
+
title: s.snippet,
|
|
3606
|
+
className: "rounded border border-slate-200 bg-white px-2 py-1 text-xs text-slate-700 transition hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700",
|
|
3607
|
+
children: s.preview
|
|
3608
|
+
},
|
|
3609
|
+
s.snippet
|
|
3610
|
+
)) }) }, group.group)),
|
|
3611
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section2, { label: "Ph\xEDm t\u1EAFt", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2 text-[11px] text-slate-600", children: [
|
|
3612
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
3613
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Enter" }),
|
|
3614
|
+
"ch\xE8n"
|
|
3615
|
+
] }),
|
|
3616
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
3617
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Esc" }),
|
|
3618
|
+
"\u0111\xF3ng"
|
|
3619
|
+
] })
|
|
3620
|
+
] }) })
|
|
3621
|
+
]
|
|
3622
|
+
}
|
|
3623
|
+
);
|
|
3090
3624
|
}
|
|
3091
3625
|
|
|
3092
3626
|
// src/stamps/latex/render.ts
|
|
@@ -3138,7 +3672,9 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3138
3672
|
onClose,
|
|
3139
3673
|
displayMode: controlledDisplayMode,
|
|
3140
3674
|
onDisplayModeChange,
|
|
3141
|
-
withLeftPanel = false
|
|
3675
|
+
withLeftPanel = false,
|
|
3676
|
+
isMobile = false,
|
|
3677
|
+
onOpenDrawer
|
|
3142
3678
|
}, ref) {
|
|
3143
3679
|
const [value, setValue] = react.useState(initialValue);
|
|
3144
3680
|
const [internalDisplayMode] = react.useState(false);
|
|
@@ -3209,7 +3745,7 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3209
3745
|
[value, previewSvg, error, displayMode, onInsert]
|
|
3210
3746
|
);
|
|
3211
3747
|
const isLegacyPosition = x > 0 || y > 0;
|
|
3212
|
-
const wrapperStyle = isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
|
|
3748
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 50 } : isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
|
|
3213
3749
|
position: "absolute",
|
|
3214
3750
|
top: "50%",
|
|
3215
3751
|
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
@@ -3221,29 +3757,55 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3221
3757
|
{
|
|
3222
3758
|
style: wrapperStyle,
|
|
3223
3759
|
"data-stamp-area": "true",
|
|
3224
|
-
|
|
3760
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
3761
|
+
className: isMobile ? "flex h-full w-full flex-col bg-white" : "w-[420px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 bg-white shadow-2xl ring-1 ring-black/5",
|
|
3225
3762
|
role: "dialog",
|
|
3226
3763
|
"aria-label": "Nh\u1EADp c\xF4ng th\u1EE9c LaTeX",
|
|
3227
3764
|
children: [
|
|
3228
|
-
/* @__PURE__ */ jsxRuntime.jsxs("header", { className:
|
|
3229
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3765
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: `flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-indigo-600 to-purple-600 px-3 py-2 text-white${isMobile ? "" : " rounded-t-lg"}`, children: [
|
|
3766
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3767
|
+
"button",
|
|
3768
|
+
{
|
|
3769
|
+
type: "button",
|
|
3770
|
+
onClick: onOpenDrawer,
|
|
3771
|
+
"aria-label": "M\u1EDF ng\u0103n snippet",
|
|
3772
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
3773
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3774
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
3775
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
3776
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
3777
|
+
] })
|
|
3778
|
+
}
|
|
3779
|
+
),
|
|
3780
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
3230
3781
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: "\u2211" }),
|
|
3231
3782
|
"C\xF4ng th\u1EE9c LaTeX"
|
|
3232
3783
|
] }),
|
|
3784
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3785
|
+
"button",
|
|
3786
|
+
{
|
|
3787
|
+
type: "button",
|
|
3788
|
+
onClick: handleInsert,
|
|
3789
|
+
disabled: !previewSvg || !!error,
|
|
3790
|
+
"data-testid": "latex-insert-btn-mobile",
|
|
3791
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
3792
|
+
children: "Ch\xE8n"
|
|
3793
|
+
}
|
|
3794
|
+
),
|
|
3233
3795
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3234
3796
|
"button",
|
|
3235
3797
|
{
|
|
3236
3798
|
onClick: onClose,
|
|
3237
3799
|
"aria-label": "\u0110\xF3ng",
|
|
3238
|
-
className: "
|
|
3239
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "
|
|
3800
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
3801
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3240
3802
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
3241
3803
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
3242
3804
|
] })
|
|
3243
3805
|
}
|
|
3244
3806
|
)
|
|
3245
3807
|
] }),
|
|
3246
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className:
|
|
3808
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `space-y-2 p-3${isMobile ? " flex min-h-0 flex-1 flex-col" : ""}`, children: [
|
|
3247
3809
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3248
3810
|
"input",
|
|
3249
3811
|
{
|
|
@@ -3254,7 +3816,7 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3254
3816
|
onChange: (e) => setValue(e.target.value),
|
|
3255
3817
|
onKeyDown: handleKeyDown,
|
|
3256
3818
|
placeholder: "Vd: \\frac{a^2+b^2}{c}",
|
|
3257
|
-
className:
|
|
3819
|
+
className: `w-full rounded border border-slate-300 px-2 py-1.5 font-mono outline-none focus:border-indigo-400 focus:ring-2 focus:ring-indigo-200${isMobile ? " min-h-[44px] text-base" : " text-sm"}`,
|
|
3258
3820
|
autoFocus: true
|
|
3259
3821
|
}
|
|
3260
3822
|
),
|
|
@@ -3262,7 +3824,8 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3262
3824
|
"div",
|
|
3263
3825
|
{
|
|
3264
3826
|
className: [
|
|
3265
|
-
"flex
|
|
3827
|
+
"flex items-center justify-center rounded border p-3 text-center",
|
|
3828
|
+
isMobile ? "min-h-0 flex-1 overflow-auto" : "min-h-[64px]",
|
|
3266
3829
|
error ? "border-rose-300 bg-rose-50 text-rose-700" : "border-slate-200 bg-slate-50"
|
|
3267
3830
|
].join(" "),
|
|
3268
3831
|
children: error ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs", children: [
|
|
@@ -3271,7 +3834,7 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3271
3834
|
] }) : previewSvg ? /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: previewSvg } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-400", children: "(xem tr\u01B0\u1EDBc)" })
|
|
3272
3835
|
}
|
|
3273
3836
|
),
|
|
3274
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3837
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3275
3838
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-slate-500", children: [
|
|
3276
3839
|
displayMode ? "Block" : "Inline",
|
|
3277
3840
|
" \xB7 Enter \u0111\u1EC3 ch\xE8n"
|
|
@@ -3295,6 +3858,10 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3295
3858
|
}
|
|
3296
3859
|
)
|
|
3297
3860
|
] })
|
|
3861
|
+
] }),
|
|
3862
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center text-[11px] text-slate-500", children: [
|
|
3863
|
+
displayMode ? "Block" : "Inline",
|
|
3864
|
+
" \xB7 B\u1EA5m Ch\xE8n \u1EDF thanh tr\xEAn"
|
|
3298
3865
|
] })
|
|
3299
3866
|
] })
|
|
3300
3867
|
]
|
|
@@ -3309,6 +3876,8 @@ function isLatexCustomData(data) {
|
|
|
3309
3876
|
var LatexStampHost = react.forwardRef(
|
|
3310
3877
|
function LatexStampHost2({ api, editingElement, onClose }, ref) {
|
|
3311
3878
|
const editorRef = react.useRef(null);
|
|
3879
|
+
const { isMobile } = useIsMobile();
|
|
3880
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
3312
3881
|
const initial = react.useMemo(() => {
|
|
3313
3882
|
if (editingElement && isLatexCustomData(editingElement.customData)) {
|
|
3314
3883
|
return {
|
|
@@ -3355,7 +3924,10 @@ var LatexStampHost = react.forwardRef(
|
|
|
3355
3924
|
displayMode,
|
|
3356
3925
|
onDisplayModeChange: setDisplayMode,
|
|
3357
3926
|
onInsertSnippet: (s) => editorRef.current?.insertAtCursor(s),
|
|
3358
|
-
onClose
|
|
3927
|
+
onClose,
|
|
3928
|
+
isMobile,
|
|
3929
|
+
drawerOpen,
|
|
3930
|
+
onDrawerClose: () => setDrawerOpen(false)
|
|
3359
3931
|
}
|
|
3360
3932
|
),
|
|
3361
3933
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3369,7 +3941,9 @@ var LatexStampHost = react.forwardRef(
|
|
|
3369
3941
|
onDisplayModeChange: setDisplayMode,
|
|
3370
3942
|
onInsert: handleInsert,
|
|
3371
3943
|
onClose,
|
|
3372
|
-
withLeftPanel:
|
|
3944
|
+
withLeftPanel: !isMobile,
|
|
3945
|
+
isMobile,
|
|
3946
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
3373
3947
|
}
|
|
3374
3948
|
)
|
|
3375
3949
|
] });
|
|
@@ -4001,7 +4575,25 @@ var MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState
|
|
|
4001
4575
|
toolRef.current = t;
|
|
4002
4576
|
notify();
|
|
4003
4577
|
},
|
|
4004
|
-
|
|
4578
|
+
// Sync toạ độ live của free point3d về log trước khi trả ra. JSXGraph
|
|
4579
|
+
// cho phép drag point3d (parents=[x,y,z] không có ref), việc drag chỉ
|
|
4580
|
+
// cập nhật obj.X()/Y()/Z() chứ không đụng log → re-edit + Chèn sẽ
|
|
4581
|
+
// serialize toạ độ cũ → SVG không đổi → fileId trùng → user thấy
|
|
4582
|
+
// "k thay đổi". Line/plane/polygon/sphere tham chiếu point qua @id nên
|
|
4583
|
+
// auto-update theo.
|
|
4584
|
+
getCreationLog: () => logRef.current.map((e) => {
|
|
4585
|
+
if (e.type !== "point3d") return { ...e };
|
|
4586
|
+
const parents = e.parents;
|
|
4587
|
+
if (!Array.isArray(parents) || parents.length !== 3) return { ...e };
|
|
4588
|
+
if (typeof parents[0] !== "number" || typeof parents[1] !== "number" || typeof parents[2] !== "number") return { ...e };
|
|
4589
|
+
const obj = objMapRef.current.get(e.id);
|
|
4590
|
+
if (!obj || typeof obj.X !== "function" || typeof obj.Y !== "function" || typeof obj.Z !== "function") return { ...e };
|
|
4591
|
+
const x = obj.X();
|
|
4592
|
+
const y = obj.Y();
|
|
4593
|
+
const z = obj.Z();
|
|
4594
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return { ...e };
|
|
4595
|
+
return { ...e, parents: [x, y, z] };
|
|
4596
|
+
}),
|
|
4005
4597
|
pushLog: (e) => {
|
|
4006
4598
|
logRef.current.push(e);
|
|
4007
4599
|
notify();
|
|
@@ -4077,15 +4669,159 @@ var MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState
|
|
|
4077
4669
|
}
|
|
4078
4670
|
);
|
|
4079
4671
|
});
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4672
|
+
var EditorPanel = react.forwardRef(function EditorPanel2({ isDark, initial, onInsert, onClose, isMobile = false, withLeftPanel = false, onBoardReady, onOpenDrawer }, ref) {
|
|
4673
|
+
const boardRef = react.useRef(null);
|
|
4674
|
+
const [ready, setReady] = react.useState(false);
|
|
4675
|
+
const onBoardReadyRef = react.useRef(onBoardReady);
|
|
4676
|
+
onBoardReadyRef.current = onBoardReady;
|
|
4677
|
+
const setBoard = react.useCallback((h) => {
|
|
4678
|
+
boardRef.current = h;
|
|
4679
|
+
setReady(!!h);
|
|
4680
|
+
onBoardReadyRef.current?.(h);
|
|
4681
|
+
}, []);
|
|
4682
|
+
const performInsert = react.useCallback(() => {
|
|
4683
|
+
const board = boardRef.current;
|
|
4684
|
+
if (!board) return false;
|
|
4685
|
+
const log = board.getCreationLog();
|
|
4686
|
+
if (log.length === 0) return false;
|
|
4687
|
+
const view = board.getViewState();
|
|
4688
|
+
const state = {
|
|
4689
|
+
version: 1,
|
|
4690
|
+
bbox: board.getBbox(),
|
|
4691
|
+
view,
|
|
4692
|
+
showAxes: board.getShowAxes(),
|
|
4693
|
+
showMesh: board.getShowMesh(),
|
|
4694
|
+
elements: log
|
|
4695
|
+
};
|
|
4696
|
+
const snap = board.snapshotSVG();
|
|
4697
|
+
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
4698
|
+
return true;
|
|
4699
|
+
}, [onInsert]);
|
|
4700
|
+
react.useImperativeHandle(
|
|
4701
|
+
ref,
|
|
4702
|
+
() => ({
|
|
4703
|
+
tryInsert: performInsert,
|
|
4704
|
+
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
4705
|
+
}),
|
|
4706
|
+
[performInsert]
|
|
4707
|
+
);
|
|
4708
|
+
const handleInsert = react.useCallback(() => {
|
|
4709
|
+
performInsert();
|
|
4710
|
+
}, [performInsert]);
|
|
4711
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
4712
|
+
position: "absolute",
|
|
4713
|
+
top: "50%",
|
|
4714
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
4715
|
+
transform: "translate(-50%, -50%)",
|
|
4716
|
+
zIndex: 40
|
|
4717
|
+
};
|
|
4718
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4719
|
+
"div",
|
|
4720
|
+
{
|
|
4721
|
+
role: "dialog",
|
|
4722
|
+
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc 3D",
|
|
4723
|
+
"data-testid": "geom3d-editor-panel",
|
|
4724
|
+
"data-stamp-area": "true",
|
|
4725
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
4726
|
+
style: wrapperStyle,
|
|
4727
|
+
className: [
|
|
4728
|
+
isDark ? "theme--dark " : "",
|
|
4729
|
+
"flex flex-col overflow-hidden bg-white",
|
|
4730
|
+
isMobile ? "h-full w-full" : "h-[600px] max-h-[85vh] w-[760px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
|
|
4731
|
+
].join(" "),
|
|
4732
|
+
children: [
|
|
4733
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-blue-600 to-cyan-600 px-3 py-2 text-white", children: [
|
|
4734
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4735
|
+
"button",
|
|
4736
|
+
{
|
|
4737
|
+
type: "button",
|
|
4738
|
+
onClick: onOpenDrawer,
|
|
4739
|
+
"aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
|
|
4740
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
4741
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4742
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
4743
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
4744
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
4745
|
+
] })
|
|
4746
|
+
}
|
|
4747
|
+
),
|
|
4748
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
4749
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7 L14 4 L20 7 L14 10 Z M4 7 L4 17 L14 20 L14 10 M14 20 L20 17 L20 7" }) }),
|
|
4750
|
+
"H\xECnh h\u1ECDc kh\xF4ng gian (3D)"
|
|
4751
|
+
] }),
|
|
4752
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4753
|
+
"button",
|
|
4754
|
+
{
|
|
4755
|
+
type: "button",
|
|
4756
|
+
onClick: handleInsert,
|
|
4757
|
+
disabled: !ready,
|
|
4758
|
+
"data-testid": "geom3d-insert-btn-mobile",
|
|
4759
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
4760
|
+
children: "Ch\xE8n"
|
|
4761
|
+
}
|
|
4762
|
+
),
|
|
4763
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4764
|
+
"button",
|
|
4765
|
+
{
|
|
4766
|
+
onClick: onClose,
|
|
4767
|
+
"aria-label": "\u0110\xF3ng",
|
|
4768
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
4769
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4770
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4771
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4772
|
+
] })
|
|
4773
|
+
}
|
|
4774
|
+
)
|
|
4775
|
+
] }),
|
|
4776
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial }) }),
|
|
4777
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
4778
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
|
|
4779
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
4780
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4781
|
+
"button",
|
|
4782
|
+
{
|
|
4783
|
+
onClick: onClose,
|
|
4784
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
4785
|
+
children: "Hu\u1EF7"
|
|
4786
|
+
}
|
|
4787
|
+
),
|
|
4788
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4789
|
+
"button",
|
|
4790
|
+
{
|
|
4791
|
+
onClick: handleInsert,
|
|
4792
|
+
disabled: !ready,
|
|
4793
|
+
"data-testid": "geom3d-insert-btn",
|
|
4794
|
+
className: "rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-blue-700 disabled:opacity-50",
|
|
4795
|
+
children: "Ch\xE8n"
|
|
4796
|
+
}
|
|
4797
|
+
)
|
|
4798
|
+
] })
|
|
4799
|
+
] })
|
|
4800
|
+
]
|
|
4801
|
+
}
|
|
4802
|
+
);
|
|
4803
|
+
});
|
|
4804
|
+
|
|
4805
|
+
// src/stamps/geometry-3d/editor/tools.ts
|
|
4806
|
+
var GROUP_LABELS_3D = {
|
|
4807
|
+
view: "Xem",
|
|
4808
|
+
primitive: "C\u01A1 b\u1EA3n",
|
|
4809
|
+
solid: "Kh\u1ED1i \u0111a di\u1EC7n",
|
|
4810
|
+
curved: "Kh\u1ED1i cong",
|
|
4811
|
+
meta: "Kh\xE1c"
|
|
4812
|
+
};
|
|
4813
|
+
var GROUP_ORDER_3D = [
|
|
4814
|
+
"view",
|
|
4815
|
+
"primitive",
|
|
4816
|
+
"solid",
|
|
4817
|
+
"curved",
|
|
4818
|
+
"meta"
|
|
4819
|
+
];
|
|
4820
|
+
var A_CODE_3D = "A".charCodeAt(0);
|
|
4821
|
+
function letterForGroup3D(g) {
|
|
4822
|
+
const idx = GROUP_ORDER_3D.indexOf(g);
|
|
4823
|
+
return idx >= 0 ? String.fromCharCode(A_CODE_3D + idx) : "";
|
|
4824
|
+
}
|
|
4089
4825
|
var TOOLS_3D = [
|
|
4090
4826
|
{ key: "move", label: "Di chuy\u1EC3n", group: "view", stepsRequired: 0 },
|
|
4091
4827
|
{ key: "point", label: "\u0110i\u1EC3m", group: "primitive", stepsRequired: 1, hint: "Nh\u1EADp (x, y, z)" },
|
|
@@ -4139,8 +4875,8 @@ var TOOLS_3D = [
|
|
|
4139
4875
|
},
|
|
4140
4876
|
{ key: "label", label: "Nh\xE3n", group: "meta", stepsRequired: 1, hint: "G\u1EAFn v\xE0o \u0111i\u1EC3m" }
|
|
4141
4877
|
];
|
|
4142
|
-
function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
4143
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4878
|
+
function ToolButton({ toolKey, label, hint, active, onClick, icon, badge }) {
|
|
4879
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4144
4880
|
"button",
|
|
4145
4881
|
{
|
|
4146
4882
|
type: "button",
|
|
@@ -4151,10 +4887,13 @@ function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
|
4151
4887
|
"data-active": active || void 0,
|
|
4152
4888
|
"data-tool": toolKey,
|
|
4153
4889
|
className: [
|
|
4154
|
-
"flex h-8 items-center justify-center rounded-md transition",
|
|
4890
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
4155
4891
|
active ? "bg-blue-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
4156
4892
|
].join(" "),
|
|
4157
|
-
children:
|
|
4893
|
+
children: [
|
|
4894
|
+
icon,
|
|
4895
|
+
badge
|
|
4896
|
+
]
|
|
4158
4897
|
}
|
|
4159
4898
|
);
|
|
4160
4899
|
}
|
|
@@ -4206,7 +4945,10 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4206
4945
|
"aria-label": title,
|
|
4207
4946
|
"data-testid": "geom3d-left-panel",
|
|
4208
4947
|
"data-stamp-area": "true",
|
|
4209
|
-
className:
|
|
4948
|
+
className: [
|
|
4949
|
+
isDark ? "theme--dark " : "",
|
|
4950
|
+
"absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
|
|
4951
|
+
].join(""),
|
|
4210
4952
|
children: [
|
|
4211
4953
|
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
|
|
4212
4954
|
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -4219,10 +4961,7 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4219
4961
|
onClick: onClose,
|
|
4220
4962
|
"aria-label": "\u0110\xF3ng",
|
|
4221
4963
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
4222
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4223
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4224
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4225
|
-
] })
|
|
4964
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon2, {})
|
|
4226
4965
|
}
|
|
4227
4966
|
)
|
|
4228
4967
|
] }),
|
|
@@ -4238,11 +4977,39 @@ function Section3({ label, children }) {
|
|
|
4238
4977
|
] });
|
|
4239
4978
|
}
|
|
4240
4979
|
var Geom3DIconHeader = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7 L14 4 L20 7 L14 10 Z M4 7 L4 17 L14 20 L14 10 M14 20 L20 17 L20 7" }) });
|
|
4241
|
-
function
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4980
|
+
function CloseIcon2() {
|
|
4981
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4982
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4983
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4984
|
+
] });
|
|
4985
|
+
}
|
|
4986
|
+
function UndoIcon2() {
|
|
4987
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4988
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
4989
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
4990
|
+
] });
|
|
4991
|
+
}
|
|
4992
|
+
function ResetViewIcon() {
|
|
4993
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4994
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
4995
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
|
|
4996
|
+
] });
|
|
4997
|
+
}
|
|
4998
|
+
function AxisIcon3D() {
|
|
4999
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5000
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "20", x2: "12", y2: "4" }),
|
|
5001
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "12", x2: "22", y2: "6" }),
|
|
5002
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "12", x2: "2", y2: "18" })
|
|
5003
|
+
] });
|
|
5004
|
+
}
|
|
5005
|
+
function MeshIcon() {
|
|
5006
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5007
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L12 4 L20 8 L12 12 Z" }),
|
|
5008
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L4 16 L12 20 L12 12" }),
|
|
5009
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 20 L20 16 L20 8" })
|
|
5010
|
+
] });
|
|
5011
|
+
}
|
|
5012
|
+
function useToolHoverTooltip2() {
|
|
4246
5013
|
const [hover, setHover] = react.useState(null);
|
|
4247
5014
|
const [portalReady, setPortalReady] = react.useState(false);
|
|
4248
5015
|
const hoverTimerRef = react.useRef(null);
|
|
@@ -4252,17 +5019,6 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4252
5019
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4253
5020
|
};
|
|
4254
5021
|
}, []);
|
|
4255
|
-
react.useEffect(() => {
|
|
4256
|
-
if (!handle) return;
|
|
4257
|
-
const sync = () => {
|
|
4258
|
-
setTool(handle.getTool());
|
|
4259
|
-
setShowAxes(handle.getShowAxes());
|
|
4260
|
-
setShowMesh(handle.getShowMesh());
|
|
4261
|
-
setCanUndo(handle.canUndo());
|
|
4262
|
-
};
|
|
4263
|
-
sync();
|
|
4264
|
-
return handle.subscribe(sync);
|
|
4265
|
-
}, [handle]);
|
|
4266
5022
|
const showHover = react.useCallback((el, t) => {
|
|
4267
5023
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4268
5024
|
hoverTimerRef.current = setTimeout(() => {
|
|
@@ -4277,14 +5033,45 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4277
5033
|
}
|
|
4278
5034
|
setHover(null);
|
|
4279
5035
|
}, []);
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
5036
|
+
return { hover, portalReady, showHover, hideHover };
|
|
5037
|
+
}
|
|
5038
|
+
function useHandleState(handle) {
|
|
5039
|
+
const [tool, setTool] = react.useState("move");
|
|
5040
|
+
const [showAxes, setShowAxes] = react.useState(true);
|
|
5041
|
+
const [showMesh, setShowMesh] = react.useState(false);
|
|
5042
|
+
const [canUndo, setCanUndo] = react.useState(false);
|
|
5043
|
+
react.useEffect(() => {
|
|
5044
|
+
if (!handle) return;
|
|
5045
|
+
const sync = () => {
|
|
5046
|
+
setTool(handle.getTool());
|
|
5047
|
+
setShowAxes(handle.getShowAxes());
|
|
5048
|
+
setShowMesh(handle.getShowMesh());
|
|
5049
|
+
setCanUndo(handle.canUndo());
|
|
5050
|
+
};
|
|
5051
|
+
sync();
|
|
5052
|
+
return handle.subscribe(sync);
|
|
5053
|
+
}, [handle]);
|
|
5054
|
+
return { tool, showAxes, showMesh, canUndo };
|
|
5055
|
+
}
|
|
5056
|
+
function DesktopPanel(props) {
|
|
5057
|
+
const { handle, onResetView, onClose, isDark, chordGroup } = props;
|
|
5058
|
+
const { tool, showAxes, showMesh, canUndo } = useHandleState(handle);
|
|
5059
|
+
const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip2();
|
|
5060
|
+
const grouped = react.useMemo(() => {
|
|
5061
|
+
return TOOLS_3D.reduce(
|
|
5062
|
+
(acc, t) => {
|
|
5063
|
+
var _a;
|
|
5064
|
+
(acc[_a = t.group] ?? (acc[_a] = [])).push(t);
|
|
5065
|
+
return acc;
|
|
5066
|
+
},
|
|
5067
|
+
{}
|
|
5068
|
+
);
|
|
5069
|
+
}, []);
|
|
5070
|
+
const orderedGroups = react.useMemo(
|
|
5071
|
+
() => GROUP_ORDER_3D.filter((g) => grouped[g]),
|
|
5072
|
+
[grouped]
|
|
4287
5073
|
);
|
|
5074
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
4288
5075
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4289
5076
|
/* @__PURE__ */ jsxRuntime.jsxs(Shell3, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
|
|
4290
5077
|
/* @__PURE__ */ jsxRuntime.jsx(Section3, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
|
|
@@ -4320,10 +5107,7 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4320
5107
|
title: "Reset g\xF3c nh\xECn",
|
|
4321
5108
|
"aria-label": "Reset view",
|
|
4322
5109
|
className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
|
|
4323
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4324
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
4325
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
|
|
4326
|
-
] })
|
|
5110
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon, {})
|
|
4327
5111
|
}
|
|
4328
5112
|
),
|
|
4329
5113
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4335,34 +5119,95 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4335
5119
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
4336
5120
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
4337
5121
|
className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
4338
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4339
|
-
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
4340
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
4341
|
-
] })
|
|
5122
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon2, {})
|
|
4342
5123
|
}
|
|
4343
5124
|
)
|
|
4344
5125
|
] }) }),
|
|
4345
|
-
|
|
4346
|
-
|
|
5126
|
+
orderedGroups.map((group) => {
|
|
5127
|
+
const tools = grouped[group];
|
|
5128
|
+
const isChordActive = chordGroup === group;
|
|
5129
|
+
const dimmed = chordGroup !== null && !isChordActive;
|
|
5130
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5131
|
+
"section",
|
|
5132
|
+
{
|
|
5133
|
+
"data-chord-group": group,
|
|
5134
|
+
"data-chord-active": isChordActive ? "true" : "false",
|
|
5135
|
+
className: [
|
|
5136
|
+
"rounded-md transition",
|
|
5137
|
+
isChordActive ? "bg-blue-50 ring-1 ring-blue-400 p-1" : "p-0",
|
|
5138
|
+
dimmed ? "opacity-55" : "opacity-100"
|
|
5139
|
+
].join(" "),
|
|
5140
|
+
children: [
|
|
5141
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
5142
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: GROUP_LABELS_3D[group] }),
|
|
5143
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5144
|
+
"span",
|
|
5145
|
+
{
|
|
5146
|
+
"data-testid": `chord-letter-${group}`,
|
|
5147
|
+
className: [
|
|
5148
|
+
"font-mono text-[10px] leading-none transition",
|
|
5149
|
+
isChordActive ? "text-blue-700 font-bold" : "text-slate-400"
|
|
5150
|
+
].join(" "),
|
|
5151
|
+
children: letterForGroup3D(group)
|
|
5152
|
+
}
|
|
5153
|
+
)
|
|
5154
|
+
] }),
|
|
5155
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: tools.map((t, i) => {
|
|
5156
|
+
const isActive = tool === t.key;
|
|
5157
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5158
|
+
ToolButton,
|
|
5159
|
+
{
|
|
5160
|
+
toolKey: t.key,
|
|
5161
|
+
label: t.label,
|
|
5162
|
+
hint: t.hint,
|
|
5163
|
+
active: isActive,
|
|
5164
|
+
onClick: () => handle?.setTool(t.key),
|
|
5165
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5166
|
+
"span",
|
|
5167
|
+
{
|
|
5168
|
+
onMouseEnter: (e) => showHover(e.currentTarget.closest("button"), t),
|
|
5169
|
+
onMouseLeave: hideHover,
|
|
5170
|
+
onFocus: (e) => showHover(e.currentTarget.closest("button"), t),
|
|
5171
|
+
onBlur: hideHover,
|
|
5172
|
+
children: ICONS_3D[t.key]
|
|
5173
|
+
}
|
|
5174
|
+
),
|
|
5175
|
+
badge: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5176
|
+
"span",
|
|
5177
|
+
{
|
|
5178
|
+
"data-testid": `chord-num-${t.key}`,
|
|
5179
|
+
className: [
|
|
5180
|
+
"pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
|
|
5181
|
+
isActive ? "text-white/70" : isChordActive ? "text-blue-700 font-bold" : "text-slate-400"
|
|
5182
|
+
].join(" "),
|
|
5183
|
+
children: i + 1
|
|
5184
|
+
}
|
|
5185
|
+
)
|
|
5186
|
+
},
|
|
5187
|
+
t.key
|
|
5188
|
+
);
|
|
5189
|
+
}) })
|
|
5190
|
+
]
|
|
5191
|
+
},
|
|
5192
|
+
group
|
|
5193
|
+
);
|
|
5194
|
+
}),
|
|
5195
|
+
chordGroup && activeGroupTools && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5196
|
+
"div",
|
|
4347
5197
|
{
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
}
|
|
4362
|
-
)
|
|
4363
|
-
},
|
|
4364
|
-
t.key
|
|
4365
|
-
)) }) }, group))
|
|
5198
|
+
"data-testid": "chord-hint",
|
|
5199
|
+
className: "mt-1 rounded border border-blue-200 bg-blue-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
|
|
5200
|
+
children: [
|
|
5201
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-blue-700", children: letterForGroup3D(chordGroup) }),
|
|
5202
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
|
|
5203
|
+
activeGroupTools.map((t, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2 inline-block", children: [
|
|
5204
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-blue-700", children: i + 1 }),
|
|
5205
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1", children: t.label })
|
|
5206
|
+
] }, t.key)),
|
|
5207
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
|
|
5208
|
+
]
|
|
5209
|
+
}
|
|
5210
|
+
)
|
|
4366
5211
|
] }),
|
|
4367
5212
|
portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
|
|
4368
5213
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -4386,107 +5231,71 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4386
5231
|
) : null
|
|
4387
5232
|
] });
|
|
4388
5233
|
}
|
|
4389
|
-
|
|
4390
|
-
const
|
|
4391
|
-
const
|
|
4392
|
-
const
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
() => ({
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
if (log.length === 0) return false;
|
|
4404
|
-
const view = board.getViewState();
|
|
4405
|
-
const state = {
|
|
4406
|
-
version: 1,
|
|
4407
|
-
bbox: board.getBbox(),
|
|
4408
|
-
view,
|
|
4409
|
-
showAxes: board.getShowAxes(),
|
|
4410
|
-
showMesh: board.getShowMesh(),
|
|
4411
|
-
elements: log
|
|
4412
|
-
};
|
|
4413
|
-
const snap = board.snapshotSVG();
|
|
4414
|
-
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
4415
|
-
return true;
|
|
4416
|
-
},
|
|
4417
|
-
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
4418
|
-
}),
|
|
4419
|
-
[onInsert]
|
|
4420
|
-
);
|
|
4421
|
-
const handleResetView = react.useCallback(() => {
|
|
4422
|
-
boardRef.current?.resetView();
|
|
5234
|
+
function MobilePanel(props) {
|
|
5235
|
+
const { handle, onResetView, isDark, drawerOpen, onDrawerClose } = props;
|
|
5236
|
+
const { tool, showAxes, showMesh, canUndo } = useHandleState(handle);
|
|
5237
|
+
const groups = react.useMemo(() => {
|
|
5238
|
+
const acc = /* @__PURE__ */ new Map();
|
|
5239
|
+
for (const t of TOOLS_3D) {
|
|
5240
|
+
if (!acc.has(t.group)) acc.set(t.group, []);
|
|
5241
|
+
acc.get(t.group).push(t);
|
|
5242
|
+
}
|
|
5243
|
+
return Array.from(acc.entries()).map(([group, tools]) => ({
|
|
5244
|
+
group,
|
|
5245
|
+
groupLabel: GROUP_LABELS_3D[group],
|
|
5246
|
+
tools: tools.map((t) => ({ key: t.key, label: t.label, icon: ICONS_3D[t.key] }))
|
|
5247
|
+
}));
|
|
4423
5248
|
}, []);
|
|
4424
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4425
|
-
|
|
5249
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5250
|
+
MobileToolDrawer,
|
|
4426
5251
|
{
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
"
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
}
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
onClose,
|
|
4468
|
-
isDark
|
|
4469
|
-
}
|
|
4470
|
-
),
|
|
4471
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4472
|
-
"div",
|
|
4473
|
-
{
|
|
4474
|
-
style: {
|
|
4475
|
-
position: "absolute",
|
|
4476
|
-
left: 120,
|
|
4477
|
-
top: 0,
|
|
4478
|
-
right: 0,
|
|
4479
|
-
bottom: 0,
|
|
4480
|
-
overflow: "hidden"
|
|
4481
|
-
},
|
|
4482
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial })
|
|
4483
|
-
}
|
|
4484
|
-
)
|
|
4485
|
-
] })
|
|
4486
|
-
]
|
|
5252
|
+
title: "H\xECnh h\u1ECDc 3D",
|
|
5253
|
+
headerIcon: Geom3DIconHeader,
|
|
5254
|
+
testId: "geom3d-left-panel",
|
|
5255
|
+
isDark,
|
|
5256
|
+
drawerOpen: !!drawerOpen,
|
|
5257
|
+
onDrawerClose: () => onDrawerClose?.(),
|
|
5258
|
+
chips: [
|
|
5259
|
+
{
|
|
5260
|
+
label: "Tr\u1EE5c",
|
|
5261
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(AxisIcon3D, {}),
|
|
5262
|
+
pressed: showAxes,
|
|
5263
|
+
onToggle: (b) => handle?.setShowAxes(b),
|
|
5264
|
+
testId: "toggle-axes"
|
|
5265
|
+
},
|
|
5266
|
+
{
|
|
5267
|
+
label: "L\u01B0\u1EDBi",
|
|
5268
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(MeshIcon, {}),
|
|
5269
|
+
pressed: showMesh,
|
|
5270
|
+
onToggle: (b) => handle?.setShowMesh(b),
|
|
5271
|
+
testId: "toggle-mesh"
|
|
5272
|
+
}
|
|
5273
|
+
],
|
|
5274
|
+
actions: [
|
|
5275
|
+
{
|
|
5276
|
+
label: "Reset view",
|
|
5277
|
+
title: "Reset g\xF3c nh\xECn",
|
|
5278
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon, {}),
|
|
5279
|
+
onClick: onResetView
|
|
5280
|
+
},
|
|
5281
|
+
{
|
|
5282
|
+
label: "Ho\xE0n t\xE1c",
|
|
5283
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
5284
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon2, {}),
|
|
5285
|
+
onClick: () => handle?.undo(),
|
|
5286
|
+
disabled: !canUndo
|
|
5287
|
+
}
|
|
5288
|
+
],
|
|
5289
|
+
groups,
|
|
5290
|
+
activeTool: tool,
|
|
5291
|
+
onToolSelect: (k) => handle?.setTool(k)
|
|
4487
5292
|
}
|
|
4488
5293
|
);
|
|
4489
|
-
}
|
|
5294
|
+
}
|
|
5295
|
+
function LeftPanel3(props) {
|
|
5296
|
+
if (props.isMobile) return /* @__PURE__ */ jsxRuntime.jsx(MobilePanel, { ...props });
|
|
5297
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DesktopPanel, { ...props });
|
|
5298
|
+
}
|
|
4490
5299
|
|
|
4491
5300
|
// src/stamps/geometry-3d/serialize.ts
|
|
4492
5301
|
function isGeometry3DCustomData(data) {
|
|
@@ -4571,66 +5380,1528 @@ async function renderGeometry3DSvgFromState(jsonState) {
|
|
|
4571
5380
|
JXG.JSXGraph.freeBoard(board);
|
|
4572
5381
|
} catch {
|
|
4573
5382
|
}
|
|
4574
|
-
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
4575
|
-
} finally {
|
|
4576
|
-
document.body.removeChild(div);
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
|
-
function parseInitial(editingElement) {
|
|
4580
|
-
if (!editingElement) return null;
|
|
4581
|
-
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
4582
|
-
try {
|
|
4583
|
-
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
4584
|
-
} catch {
|
|
4585
|
-
return null;
|
|
4586
|
-
}
|
|
5383
|
+
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
5384
|
+
} finally {
|
|
5385
|
+
document.body.removeChild(div);
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
function parseInitial(editingElement) {
|
|
5389
|
+
if (!editingElement) return null;
|
|
5390
|
+
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
5391
|
+
try {
|
|
5392
|
+
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
5393
|
+
} catch {
|
|
5394
|
+
return null;
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
var Geometry3DStampHost = react.forwardRef(
|
|
5398
|
+
function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
5399
|
+
const editorRef = react.useRef(null);
|
|
5400
|
+
const { isMobile } = useIsMobile();
|
|
5401
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
5402
|
+
const [boardHandle, setBoardHandle] = react.useState(null);
|
|
5403
|
+
const initial = react.useMemo(
|
|
5404
|
+
() => parseInitial(editingElement),
|
|
5405
|
+
[editingElement]
|
|
5406
|
+
);
|
|
5407
|
+
const handleBoardReady = react.useCallback((h) => {
|
|
5408
|
+
setBoardHandle((prev) => prev === h ? prev : h);
|
|
5409
|
+
}, []);
|
|
5410
|
+
const { chordGroup } = useChordShortcut({
|
|
5411
|
+
groupOrder: GROUP_ORDER_3D,
|
|
5412
|
+
tools: TOOLS_3D,
|
|
5413
|
+
onSelect: (key) => boardHandle?.setTool(key),
|
|
5414
|
+
enabled: !isMobile
|
|
5415
|
+
});
|
|
5416
|
+
const handleResetView = react.useCallback(() => {
|
|
5417
|
+
boardHandle?.resetView();
|
|
5418
|
+
}, [boardHandle]);
|
|
5419
|
+
const handleInsert = react.useCallback(
|
|
5420
|
+
async (jsonState, svgString, width, height) => {
|
|
5421
|
+
if (!api) return;
|
|
5422
|
+
await insertStampImage(api, {
|
|
5423
|
+
svgString,
|
|
5424
|
+
makeCustomData: () => ({
|
|
5425
|
+
kind: "geometry3d",
|
|
5426
|
+
version: 1,
|
|
5427
|
+
jsonState,
|
|
5428
|
+
svgWidth: width,
|
|
5429
|
+
svgHeight: height
|
|
5430
|
+
}),
|
|
5431
|
+
editingElementId: editingElement?.id ?? null
|
|
5432
|
+
});
|
|
5433
|
+
onClose();
|
|
5434
|
+
},
|
|
5435
|
+
[api, editingElement, onClose]
|
|
5436
|
+
);
|
|
5437
|
+
react.useImperativeHandle(
|
|
5438
|
+
ref,
|
|
5439
|
+
() => ({
|
|
5440
|
+
tryInsert: () => editorRef.current?.tryInsert() ?? false,
|
|
5441
|
+
hasContent: () => editorRef.current?.hasContent() ?? false
|
|
5442
|
+
}),
|
|
5443
|
+
[]
|
|
5444
|
+
);
|
|
5445
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
5446
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5447
|
+
LeftPanel3,
|
|
5448
|
+
{
|
|
5449
|
+
handle: boardHandle,
|
|
5450
|
+
onResetView: handleResetView,
|
|
5451
|
+
onClose,
|
|
5452
|
+
isDark,
|
|
5453
|
+
isMobile,
|
|
5454
|
+
drawerOpen,
|
|
5455
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
5456
|
+
chordGroup
|
|
5457
|
+
}
|
|
5458
|
+
),
|
|
5459
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5460
|
+
EditorPanel,
|
|
5461
|
+
{
|
|
5462
|
+
ref: editorRef,
|
|
5463
|
+
isDark,
|
|
5464
|
+
initial,
|
|
5465
|
+
onInsert: handleInsert,
|
|
5466
|
+
onClose,
|
|
5467
|
+
isMobile,
|
|
5468
|
+
withLeftPanel: !isMobile,
|
|
5469
|
+
onBoardReady: handleBoardReady,
|
|
5470
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
5471
|
+
}
|
|
5472
|
+
)
|
|
5473
|
+
] });
|
|
5474
|
+
}
|
|
5475
|
+
);
|
|
5476
|
+
var Geometry3DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5477
|
+
"svg",
|
|
5478
|
+
{
|
|
5479
|
+
width: "20",
|
|
5480
|
+
height: "20",
|
|
5481
|
+
viewBox: "0 0 24 24",
|
|
5482
|
+
fill: "none",
|
|
5483
|
+
stroke: "currentColor",
|
|
5484
|
+
strokeWidth: "1.6",
|
|
5485
|
+
strokeLinecap: "round",
|
|
5486
|
+
strokeLinejoin: "round",
|
|
5487
|
+
"aria-hidden": "true",
|
|
5488
|
+
children: [
|
|
5489
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3 L20 8 L20 16 L12 21 L4 16 L4 8 Z" }),
|
|
5490
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3 L12 21 M4 8 L12 12 L20 8 M4 16 L12 12 L20 16" })
|
|
5491
|
+
]
|
|
5492
|
+
}
|
|
5493
|
+
);
|
|
5494
|
+
var geometry3dStamp = {
|
|
5495
|
+
kind: "geometry3d",
|
|
5496
|
+
shortcutKey: "d",
|
|
5497
|
+
toolbarLabel: "D",
|
|
5498
|
+
toolbarTitle: "H\xECnh 3D (D)",
|
|
5499
|
+
toolbarIcon: Geometry3DIcon,
|
|
5500
|
+
toolbarTestId: "stamp-toolbar-geometry3d",
|
|
5501
|
+
matchesCustomData: isGeometry3DCustomData,
|
|
5502
|
+
async renderSvgFromCustomData(data) {
|
|
5503
|
+
if (!isGeometry3DCustomData(data)) {
|
|
5504
|
+
throw new Error("geometry3dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i geometry3d");
|
|
5505
|
+
}
|
|
5506
|
+
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
5507
|
+
return svgString;
|
|
5508
|
+
},
|
|
5509
|
+
restoreFileFromCustomData: async (element) => {
|
|
5510
|
+
const data = element.customData;
|
|
5511
|
+
const fileId = element.fileId;
|
|
5512
|
+
if (!data || !fileId) return null;
|
|
5513
|
+
if (!isGeometry3DCustomData(data)) return null;
|
|
5514
|
+
try {
|
|
5515
|
+
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
5516
|
+
const dataURL = `data:image/svg+xml;base64,${typeof btoa !== "undefined" ? btoa(unescape(encodeURIComponent(svgString))) : Buffer.from(svgString).toString("base64")}`;
|
|
5517
|
+
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
5518
|
+
} catch {
|
|
5519
|
+
return null;
|
|
5520
|
+
}
|
|
5521
|
+
},
|
|
5522
|
+
Host: Geometry3DStampHost
|
|
5523
|
+
};
|
|
5524
|
+
|
|
5525
|
+
// src/stamps/graph-2d/editor/tools.ts
|
|
5526
|
+
var GRAPH_TOOLS = [
|
|
5527
|
+
{ id: "move", label: "Di chuy\u1EC3n", title: "Di chuy\u1EC3n / ch\u1ECDn" },
|
|
5528
|
+
{ id: "point-on-curve", label: "\u0110i\u1EC3m tr\xEAn curve", title: "T\u1EA1o \u0111i\u1EC3m c\u1ED1 \u0111\u1ECBnh tr\xEAn \u0111\u1ED3 th\u1ECB" },
|
|
5529
|
+
{ id: "intersect", label: "Giao \u0111i\u1EC3m", title: "\u0110\xE1nh d\u1EA5u giao \u0111i\u1EC3m 2 \u0111\u1ED3 th\u1ECB" },
|
|
5530
|
+
{ id: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", title: "V\u1EBD ti\u1EBFp tuy\u1EBFn t\u1EA1i \u0111i\u1EC3m tr\xEAn \u0111\u1ED3 th\u1ECB" }
|
|
5531
|
+
];
|
|
5532
|
+
function FunctionRow(props) {
|
|
5533
|
+
const { id, name, expression, color, visible, error } = props;
|
|
5534
|
+
const [draft, setDraft] = react.useState(expression);
|
|
5535
|
+
react.useEffect(() => {
|
|
5536
|
+
setDraft(expression);
|
|
5537
|
+
}, [expression]);
|
|
5538
|
+
const commit = () => {
|
|
5539
|
+
if (draft !== expression) props.onExpressionCommit(draft);
|
|
5540
|
+
};
|
|
5541
|
+
const handleKeyDown = (e) => {
|
|
5542
|
+
if (e.key === "Enter") {
|
|
5543
|
+
e.preventDefault();
|
|
5544
|
+
commit();
|
|
5545
|
+
e.target.blur();
|
|
5546
|
+
} else if (e.key === "Escape") {
|
|
5547
|
+
setDraft(expression);
|
|
5548
|
+
e.target.blur();
|
|
5549
|
+
}
|
|
5550
|
+
};
|
|
5551
|
+
const handleBlur = (_) => commit();
|
|
5552
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `graph-function-row${error ? " is-error" : ""}`, "data-testid": `graph-function-row-${id}`, children: [
|
|
5553
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5554
|
+
"span",
|
|
5555
|
+
{
|
|
5556
|
+
className: "graph-function-color",
|
|
5557
|
+
style: { backgroundColor: color },
|
|
5558
|
+
"aria-hidden": "true"
|
|
5559
|
+
}
|
|
5560
|
+
),
|
|
5561
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "graph-function-name", "data-testid": `graph-function-name-${id}`, children: [
|
|
5562
|
+
name,
|
|
5563
|
+
"(x) ="
|
|
5564
|
+
] }),
|
|
5565
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5566
|
+
"input",
|
|
5567
|
+
{
|
|
5568
|
+
"aria-label": "Bi\u1EC3u th\u1EE9c",
|
|
5569
|
+
className: "graph-function-input",
|
|
5570
|
+
type: "text",
|
|
5571
|
+
value: draft,
|
|
5572
|
+
onChange: (e) => setDraft(e.target.value),
|
|
5573
|
+
onKeyDown: handleKeyDown,
|
|
5574
|
+
onBlur: handleBlur,
|
|
5575
|
+
spellCheck: false,
|
|
5576
|
+
autoCorrect: "off",
|
|
5577
|
+
autoCapitalize: "off"
|
|
5578
|
+
}
|
|
5579
|
+
),
|
|
5580
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5581
|
+
"button",
|
|
5582
|
+
{
|
|
5583
|
+
type: "button",
|
|
5584
|
+
"aria-label": "\u1EA8n/hi\u1EC7n \u0111\u1ED3 th\u1ECB",
|
|
5585
|
+
className: `graph-function-eye${visible ? "" : " is-hidden"}`,
|
|
5586
|
+
onClick: props.onToggleVisible,
|
|
5587
|
+
children: visible ? "\u{1F441}" : "\u2298"
|
|
5588
|
+
}
|
|
5589
|
+
),
|
|
5590
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5591
|
+
"button",
|
|
5592
|
+
{
|
|
5593
|
+
type: "button",
|
|
5594
|
+
"aria-label": "Xo\xE1 \u0111\u1ED3 th\u1ECB",
|
|
5595
|
+
className: "graph-function-remove",
|
|
5596
|
+
onClick: props.onRemove,
|
|
5597
|
+
children: "\u2715"
|
|
5598
|
+
}
|
|
5599
|
+
),
|
|
5600
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "graph-function-error", children: error }) : null
|
|
5601
|
+
] });
|
|
5602
|
+
}
|
|
5603
|
+
function SliderRow(props) {
|
|
5604
|
+
const { name, value, min, max, step } = props;
|
|
5605
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-slider-row", "data-testid": `graph-slider-row-${name}`, children: [
|
|
5606
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-slider-header", children: [
|
|
5607
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "graph-slider-name", children: name }),
|
|
5608
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "graph-slider-value", children: [
|
|
5609
|
+
"= ",
|
|
5610
|
+
value.toFixed(2)
|
|
5611
|
+
] }),
|
|
5612
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5613
|
+
"button",
|
|
5614
|
+
{
|
|
5615
|
+
type: "button",
|
|
5616
|
+
"aria-label": `Xo\xE1 tham s\u1ED1 ${name}`,
|
|
5617
|
+
className: "graph-slider-remove",
|
|
5618
|
+
onClick: props.onRemove,
|
|
5619
|
+
children: "\u2715"
|
|
5620
|
+
}
|
|
5621
|
+
)
|
|
5622
|
+
] }),
|
|
5623
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5624
|
+
"input",
|
|
5625
|
+
{
|
|
5626
|
+
type: "range",
|
|
5627
|
+
"aria-label": `Slider ${name}`,
|
|
5628
|
+
min,
|
|
5629
|
+
max,
|
|
5630
|
+
step,
|
|
5631
|
+
value,
|
|
5632
|
+
onChange: (e) => props.onChange(parseFloat(e.target.value)),
|
|
5633
|
+
className: "graph-slider-input"
|
|
5634
|
+
}
|
|
5635
|
+
),
|
|
5636
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-slider-range", children: [
|
|
5637
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: min }),
|
|
5638
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: max })
|
|
5639
|
+
] })
|
|
5640
|
+
] });
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5643
|
+
// src/stamps/graph-2d/colors.ts
|
|
5644
|
+
var GRAPH_PALETTE = [
|
|
5645
|
+
"#2563eb",
|
|
5646
|
+
// blue
|
|
5647
|
+
"#dc2626",
|
|
5648
|
+
// red
|
|
5649
|
+
"#16a34a",
|
|
5650
|
+
// green
|
|
5651
|
+
"#9333ea",
|
|
5652
|
+
// purple
|
|
5653
|
+
"#ea580c",
|
|
5654
|
+
// orange
|
|
5655
|
+
"#0891b2",
|
|
5656
|
+
// cyan
|
|
5657
|
+
"#db2777",
|
|
5658
|
+
// pink
|
|
5659
|
+
"#65a30d"
|
|
5660
|
+
// lime
|
|
5661
|
+
];
|
|
5662
|
+
var FUNCTION_NAMES = ["f", "g", "h", "i", "j", "k", "l", "m"];
|
|
5663
|
+
var MAX_FUNCTIONS = 8;
|
|
5664
|
+
var MAX_PARAMETERS = 8;
|
|
5665
|
+
function nextColor(usedColors) {
|
|
5666
|
+
for (const c of GRAPH_PALETTE) {
|
|
5667
|
+
if (!usedColors.includes(c)) return c;
|
|
5668
|
+
}
|
|
5669
|
+
return GRAPH_PALETTE[usedColors.length % GRAPH_PALETTE.length];
|
|
5670
|
+
}
|
|
5671
|
+
function nextFunctionName(usedNames) {
|
|
5672
|
+
for (const n of FUNCTION_NAMES) {
|
|
5673
|
+
if (!usedNames.includes(n)) return n;
|
|
5674
|
+
}
|
|
5675
|
+
return FUNCTION_NAMES[usedNames.length % FUNCTION_NAMES.length];
|
|
5676
|
+
}
|
|
5677
|
+
function AlgebraView(props) {
|
|
5678
|
+
const { graph, errors } = props;
|
|
5679
|
+
const atMax = graph.functions.length >= MAX_FUNCTIONS;
|
|
5680
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-algebra-view", children: [
|
|
5681
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "graph-algebra-section", children: [
|
|
5682
|
+
graph.functions.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
5683
|
+
FunctionRow,
|
|
5684
|
+
{
|
|
5685
|
+
id: f.id,
|
|
5686
|
+
name: f.name,
|
|
5687
|
+
expression: f.expression,
|
|
5688
|
+
color: f.color,
|
|
5689
|
+
visible: f.visible,
|
|
5690
|
+
error: errors[f.id] ?? null,
|
|
5691
|
+
onExpressionCommit: (expr) => props.onCommitFunctionExpr(f.id, expr),
|
|
5692
|
+
onToggleVisible: () => props.onToggleFunctionVisible(f.id),
|
|
5693
|
+
onRemove: () => props.onRemoveFunction(f.id)
|
|
5694
|
+
},
|
|
5695
|
+
f.id
|
|
5696
|
+
)),
|
|
5697
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5698
|
+
"button",
|
|
5699
|
+
{
|
|
5700
|
+
type: "button",
|
|
5701
|
+
"aria-label": "Th\xEAm h\xE0m s\u1ED1",
|
|
5702
|
+
className: "graph-algebra-add",
|
|
5703
|
+
onClick: props.onAddFunctionDraft,
|
|
5704
|
+
disabled: atMax,
|
|
5705
|
+
children: "+ Th\xEAm h\xE0m"
|
|
5706
|
+
}
|
|
5707
|
+
)
|
|
5708
|
+
] }),
|
|
5709
|
+
graph.parameters.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "graph-algebra-section graph-algebra-parameters", children: graph.parameters.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
5710
|
+
SliderRow,
|
|
5711
|
+
{
|
|
5712
|
+
name: p.name,
|
|
5713
|
+
value: p.value,
|
|
5714
|
+
min: p.min,
|
|
5715
|
+
max: p.max,
|
|
5716
|
+
step: p.step,
|
|
5717
|
+
onChange: (v) => props.onParameterChange(p.name, v),
|
|
5718
|
+
onRangeChange: (min, max, step) => props.onParameterRangeChange(p.name, min, max, step),
|
|
5719
|
+
onRemove: () => props.onRemoveParameter(p.name)
|
|
5720
|
+
},
|
|
5721
|
+
p.name
|
|
5722
|
+
)) }) : null
|
|
5723
|
+
] });
|
|
5724
|
+
}
|
|
5725
|
+
var GraphIconHeader = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5726
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 V3" }),
|
|
5727
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 H21" }),
|
|
5728
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
|
|
5729
|
+
] });
|
|
5730
|
+
function CloseIcon3() {
|
|
5731
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5732
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
5733
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
5734
|
+
] });
|
|
5735
|
+
}
|
|
5736
|
+
function UndoIcon3() {
|
|
5737
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5738
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
5739
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
5740
|
+
] });
|
|
5741
|
+
}
|
|
5742
|
+
function ResetViewIcon2() {
|
|
5743
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5744
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
|
|
5745
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "21" }),
|
|
5746
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" })
|
|
5747
|
+
] });
|
|
5748
|
+
}
|
|
5749
|
+
function MoveIcon() {
|
|
5750
|
+
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 4 L9 4 L9 9 L4 9 Z" }) });
|
|
5751
|
+
}
|
|
5752
|
+
function PointOnCurveIcon() {
|
|
5753
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5754
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17 C7 8, 14 8, 21 14" }),
|
|
5755
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11", r: "2.2", fill: "currentColor", stroke: "none" })
|
|
5756
|
+
] });
|
|
5757
|
+
}
|
|
5758
|
+
function IntersectIcon() {
|
|
5759
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5760
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17 C8 5, 14 5, 21 17" }),
|
|
5761
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 5 C8 17, 14 17, 21 5" }),
|
|
5762
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "11", r: "1.6", fill: "currentColor", stroke: "none" })
|
|
5763
|
+
] });
|
|
5764
|
+
}
|
|
5765
|
+
function TangentIcon() {
|
|
5766
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5767
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17 C8 7, 14 7, 21 16" }),
|
|
5768
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "14", x2: "20", y2: "6" }),
|
|
5769
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "10", r: "1.8", fill: "currentColor", stroke: "none" })
|
|
5770
|
+
] });
|
|
5771
|
+
}
|
|
5772
|
+
var TOOL_ICONS = {
|
|
5773
|
+
move: /* @__PURE__ */ jsxRuntime.jsx(MoveIcon, {}),
|
|
5774
|
+
"point-on-curve": /* @__PURE__ */ jsxRuntime.jsx(PointOnCurveIcon, {}),
|
|
5775
|
+
intersect: /* @__PURE__ */ jsxRuntime.jsx(IntersectIcon, {}),
|
|
5776
|
+
tangent: /* @__PURE__ */ jsxRuntime.jsx(TangentIcon, {})
|
|
5777
|
+
};
|
|
5778
|
+
function Section4({ label, children }) {
|
|
5779
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
|
|
5780
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
|
|
5781
|
+
children
|
|
5782
|
+
] });
|
|
5783
|
+
}
|
|
5784
|
+
function PanelBody(props) {
|
|
5785
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
5786
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section4, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
|
|
5787
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
|
|
5788
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5789
|
+
"input",
|
|
5790
|
+
{
|
|
5791
|
+
type: "checkbox",
|
|
5792
|
+
checked: props.showAxis,
|
|
5793
|
+
onChange: (e) => props.onShowAxisChange(e.target.checked),
|
|
5794
|
+
"data-testid": "toggle-axis"
|
|
5795
|
+
}
|
|
5796
|
+
),
|
|
5797
|
+
"Tr\u1EE5c"
|
|
5798
|
+
] }),
|
|
5799
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
|
|
5800
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5801
|
+
"input",
|
|
5802
|
+
{
|
|
5803
|
+
type: "checkbox",
|
|
5804
|
+
checked: props.showGrid,
|
|
5805
|
+
onChange: (e) => props.onShowGridChange(e.target.checked),
|
|
5806
|
+
"data-testid": "toggle-grid"
|
|
5807
|
+
}
|
|
5808
|
+
),
|
|
5809
|
+
"L\u01B0\u1EDBi"
|
|
5810
|
+
] }),
|
|
5811
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5812
|
+
"button",
|
|
5813
|
+
{
|
|
5814
|
+
type: "button",
|
|
5815
|
+
onClick: props.onResetView,
|
|
5816
|
+
title: "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
|
|
5817
|
+
"aria-label": "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
|
|
5818
|
+
className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
|
|
5819
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon2, {})
|
|
5820
|
+
}
|
|
5821
|
+
),
|
|
5822
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5823
|
+
"button",
|
|
5824
|
+
{
|
|
5825
|
+
type: "button",
|
|
5826
|
+
onClick: props.onUndo,
|
|
5827
|
+
disabled: !props.canUndo,
|
|
5828
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
5829
|
+
"aria-label": "Ho\xE0n t\xE1c",
|
|
5830
|
+
className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
5831
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon3, {})
|
|
5832
|
+
}
|
|
5833
|
+
)
|
|
5834
|
+
] }) }),
|
|
5835
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section4, { label: "C\xF4ng c\u1EE5", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: GRAPH_TOOLS.map((t) => {
|
|
5836
|
+
const isActive = props.activeTool === t.id;
|
|
5837
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5838
|
+
"button",
|
|
5839
|
+
{
|
|
5840
|
+
type: "button",
|
|
5841
|
+
"aria-label": t.title,
|
|
5842
|
+
title: t.title,
|
|
5843
|
+
"aria-pressed": isActive,
|
|
5844
|
+
onClick: () => props.onToolChange(t.id),
|
|
5845
|
+
"data-testid": `graph-tool-${t.id}`,
|
|
5846
|
+
className: [
|
|
5847
|
+
"flex h-8 items-center justify-center rounded-md transition",
|
|
5848
|
+
isActive ? "bg-orange-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
5849
|
+
].join(" "),
|
|
5850
|
+
children: TOOL_ICONS[t.id]
|
|
5851
|
+
},
|
|
5852
|
+
t.id
|
|
5853
|
+
);
|
|
5854
|
+
}) }) }),
|
|
5855
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section4, { label: "H\xE0m s\u1ED1", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5856
|
+
AlgebraView,
|
|
5857
|
+
{
|
|
5858
|
+
graph: props.graph,
|
|
5859
|
+
errors: props.errors,
|
|
5860
|
+
onAddFunctionDraft: props.onAddFunctionDraft,
|
|
5861
|
+
onCommitFunctionExpr: props.onCommitFunctionExpr,
|
|
5862
|
+
onToggleFunctionVisible: props.onToggleFunctionVisible,
|
|
5863
|
+
onRemoveFunction: props.onRemoveFunction,
|
|
5864
|
+
onParameterChange: props.onParameterChange,
|
|
5865
|
+
onParameterRangeChange: props.onParameterRangeChange,
|
|
5866
|
+
onRemoveParameter: props.onRemoveParameter
|
|
5867
|
+
}
|
|
5868
|
+
) })
|
|
5869
|
+
] });
|
|
5870
|
+
}
|
|
5871
|
+
function GraphLeftPanel(props) {
|
|
5872
|
+
const { isMobile, drawerOpen, isDark, onClose, onDrawerClose } = props;
|
|
5873
|
+
if (isMobile && !drawerOpen) return null;
|
|
5874
|
+
const handleClose = isMobile ? onDrawerClose : onClose;
|
|
5875
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5876
|
+
"aside",
|
|
5877
|
+
{
|
|
5878
|
+
role: "complementary",
|
|
5879
|
+
"aria-label": "\u0110\u1ED3 th\u1ECB 2D",
|
|
5880
|
+
"data-testid": "graph-left-panel",
|
|
5881
|
+
"data-stamp-area": "true",
|
|
5882
|
+
className: [
|
|
5883
|
+
isDark ? "theme--dark " : "",
|
|
5884
|
+
isMobile ? "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[85vw] flex-col bg-white shadow-2xl animate-in slide-in-from-left duration-200" : "absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
|
|
5885
|
+
].join(" "),
|
|
5886
|
+
children: [
|
|
5887
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
|
|
5888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
5889
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: GraphIconHeader }),
|
|
5890
|
+
"\u0110\u1ED3 th\u1ECB 2D"
|
|
5891
|
+
] }),
|
|
5892
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5893
|
+
"button",
|
|
5894
|
+
{
|
|
5895
|
+
onClick: handleClose,
|
|
5896
|
+
"aria-label": "\u0110\xF3ng",
|
|
5897
|
+
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
5898
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon3, {})
|
|
5899
|
+
}
|
|
5900
|
+
)
|
|
5901
|
+
] }),
|
|
5902
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children: /* @__PURE__ */ jsxRuntime.jsx(PanelBody, { ...props }) })
|
|
5903
|
+
]
|
|
5904
|
+
}
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
|
|
5908
|
+
// src/stamps/graph-2d/parser.ts
|
|
5909
|
+
var ALLOWED_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
5910
|
+
"sin",
|
|
5911
|
+
"cos",
|
|
5912
|
+
"tan",
|
|
5913
|
+
"asin",
|
|
5914
|
+
"acos",
|
|
5915
|
+
"atan",
|
|
5916
|
+
"log",
|
|
5917
|
+
"ln",
|
|
5918
|
+
"exp",
|
|
5919
|
+
"sqrt",
|
|
5920
|
+
"abs",
|
|
5921
|
+
"floor",
|
|
5922
|
+
"ceil",
|
|
5923
|
+
"round"
|
|
5924
|
+
]);
|
|
5925
|
+
var ALLOWED_CHARS = /^[a-zA-Z0-9_.+\-*/^()\s,]+$/;
|
|
5926
|
+
var IDENTIFIER_RE = /[a-zA-Z][a-zA-Z0-9_]*/g;
|
|
5927
|
+
var SUGGESTIONS = {
|
|
5928
|
+
tg: "tan",
|
|
5929
|
+
arcsin: "asin",
|
|
5930
|
+
arccos: "acos",
|
|
5931
|
+
arctan: "atan"
|
|
5932
|
+
};
|
|
5933
|
+
function errResult(message) {
|
|
5934
|
+
return { ok: false, error: message, freeVars: /* @__PURE__ */ new Set() };
|
|
5935
|
+
}
|
|
5936
|
+
function validate(expr) {
|
|
5937
|
+
const trimmed = expr.trim();
|
|
5938
|
+
if (!trimmed) return errResult("Bi\u1EC3u th\u1EE9c r\u1ED7ng");
|
|
5939
|
+
if (!ALLOWED_CHARS.test(trimmed)) return errResult("K\xFD t\u1EF1 kh\xF4ng h\u1EE3p l\u1EC7");
|
|
5940
|
+
const ids = trimmed.match(IDENTIFIER_RE) ?? [];
|
|
5941
|
+
const freeVars = /* @__PURE__ */ new Set();
|
|
5942
|
+
for (const id of ids) {
|
|
5943
|
+
if (id === "x" || id === "pi" || id === "e") continue;
|
|
5944
|
+
if (ALLOWED_FUNCTIONS.has(id)) continue;
|
|
5945
|
+
if (id.length === 1) {
|
|
5946
|
+
freeVars.add(id);
|
|
5947
|
+
continue;
|
|
5948
|
+
}
|
|
5949
|
+
const hint = SUGGESTIONS[id];
|
|
5950
|
+
return errResult(
|
|
5951
|
+
hint ? `T\xEAn h\xE0m kh\xF4ng h\u1EE3p l\u1EC7: "${id}". B\u1EA1n c\xF3 \xFD l\xE0 "${hint}" kh\xF4ng?` : `T\xEAn kh\xF4ng h\u1EE3p l\u1EC7: "${id}"`
|
|
5952
|
+
);
|
|
5953
|
+
}
|
|
5954
|
+
try {
|
|
5955
|
+
const paramSubs = Object.fromEntries([...freeVars].map((v) => [v, 1]));
|
|
5956
|
+
const rewritten = rewriteToJs(trimmed, paramSubs);
|
|
5957
|
+
new Function("x", `return (${rewritten})`);
|
|
5958
|
+
} catch {
|
|
5959
|
+
return errResult("L\u1ED7i c\xFA ph\xE1p");
|
|
5960
|
+
}
|
|
5961
|
+
return { ok: true, freeVars };
|
|
5962
|
+
}
|
|
5963
|
+
var FUNCTION_REPLACEMENTS = [
|
|
5964
|
+
// longest first để tránh substring conflict (asin trước sin)
|
|
5965
|
+
["asin", "Math.asin"],
|
|
5966
|
+
["acos", "Math.acos"],
|
|
5967
|
+
["atan", "Math.atan"],
|
|
5968
|
+
["sqrt", "Math.sqrt"],
|
|
5969
|
+
["floor", "Math.floor"],
|
|
5970
|
+
["round", "Math.round"],
|
|
5971
|
+
["ceil", "Math.ceil"],
|
|
5972
|
+
["sin", "Math.sin"],
|
|
5973
|
+
["cos", "Math.cos"],
|
|
5974
|
+
["tan", "Math.tan"],
|
|
5975
|
+
["abs", "Math.abs"],
|
|
5976
|
+
["exp", "Math.exp"],
|
|
5977
|
+
["log", "Math.log10"],
|
|
5978
|
+
["ln", "Math.log"]
|
|
5979
|
+
];
|
|
5980
|
+
function rewriteToJs(expr, params) {
|
|
5981
|
+
let s = expr.replace(/\^/g, "**");
|
|
5982
|
+
s = s.replace(/\bpi\b/g, "Math.PI");
|
|
5983
|
+
s = s.replace(/\be\b/g, "Math.E");
|
|
5984
|
+
for (const [from, to] of FUNCTION_REPLACEMENTS) {
|
|
5985
|
+
s = s.replace(new RegExp(`\\b${from}\\b`, "g"), to);
|
|
5986
|
+
}
|
|
5987
|
+
for (const [name, value] of Object.entries(params)) {
|
|
5988
|
+
if (name.length !== 1) continue;
|
|
5989
|
+
s = s.replace(new RegExp(`\\b${name}\\b`, "g"), `(${value})`);
|
|
5990
|
+
}
|
|
5991
|
+
return s;
|
|
5992
|
+
}
|
|
5993
|
+
function compile(expr, paramValues) {
|
|
5994
|
+
const v = validate(expr);
|
|
5995
|
+
if (!v.ok) return { error: v.error ?? "Invalid" };
|
|
5996
|
+
try {
|
|
5997
|
+
const rewritten = rewriteToJs(expr, paramValues);
|
|
5998
|
+
const raw = new Function("x", `return (${rewritten})`);
|
|
5999
|
+
return (x) => {
|
|
6000
|
+
try {
|
|
6001
|
+
const y = raw(x);
|
|
6002
|
+
return typeof y === "number" ? y : NaN;
|
|
6003
|
+
} catch {
|
|
6004
|
+
return NaN;
|
|
6005
|
+
}
|
|
6006
|
+
};
|
|
6007
|
+
} catch (err) {
|
|
6008
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
|
|
6012
|
+
// src/stamps/graph-2d/editor/handlers.ts
|
|
6013
|
+
function addPointOnCurve(graph, ctx, idFactory) {
|
|
6014
|
+
if (!ctx.functionId) return graph;
|
|
6015
|
+
const point = {
|
|
6016
|
+
id: idFactory(),
|
|
6017
|
+
functionId: ctx.functionId,
|
|
6018
|
+
x: ctx.x
|
|
6019
|
+
};
|
|
6020
|
+
return { ...graph, points: [...graph.points, point] };
|
|
6021
|
+
}
|
|
6022
|
+
function addIntersection(graph, functionIdA, functionIdB, idFactory) {
|
|
6023
|
+
if (functionIdA === functionIdB) return graph;
|
|
6024
|
+
const exists = graph.intersections.some(
|
|
6025
|
+
(i) => i.functionIdA === functionIdA && i.functionIdB === functionIdB || i.functionIdA === functionIdB && i.functionIdB === functionIdA
|
|
6026
|
+
);
|
|
6027
|
+
if (exists) return graph;
|
|
6028
|
+
const intersection = {
|
|
6029
|
+
id: idFactory(),
|
|
6030
|
+
functionIdA,
|
|
6031
|
+
functionIdB
|
|
6032
|
+
};
|
|
6033
|
+
return { ...graph, intersections: [...graph.intersections, intersection] };
|
|
6034
|
+
}
|
|
6035
|
+
function numericalDerivative(expression, paramValues, x, h = 1e-4) {
|
|
6036
|
+
const fn = compile(expression, paramValues);
|
|
6037
|
+
if (typeof fn !== "function") return NaN;
|
|
6038
|
+
const y1 = fn(x - h);
|
|
6039
|
+
const y2 = fn(x + h);
|
|
6040
|
+
return (y2 - y1) / (2 * h);
|
|
6041
|
+
}
|
|
6042
|
+
function MiniBoard({ graph, activeTool, isDark, onBoardEvent }) {
|
|
6043
|
+
const containerRef = react.useRef(null);
|
|
6044
|
+
const boardRef = react.useRef(null);
|
|
6045
|
+
const curvesRef = react.useRef(/* @__PURE__ */ new Map());
|
|
6046
|
+
react.useEffect(() => {
|
|
6047
|
+
let cancelled = false;
|
|
6048
|
+
let createdBoard = null;
|
|
6049
|
+
const containerEl = containerRef.current;
|
|
6050
|
+
if (!containerEl) return;
|
|
6051
|
+
const containerId = `jxg_graph2d_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6052
|
+
containerEl.id = containerId;
|
|
6053
|
+
(async () => {
|
|
6054
|
+
const JXG = (await import('jsxgraph')).default;
|
|
6055
|
+
if (cancelled) return;
|
|
6056
|
+
const opts = JXG.Options;
|
|
6057
|
+
if (opts) {
|
|
6058
|
+
opts.text = opts.text || {};
|
|
6059
|
+
opts.text.display = "internal";
|
|
6060
|
+
opts.label = opts.label || {};
|
|
6061
|
+
opts.label.display = "internal";
|
|
6062
|
+
}
|
|
6063
|
+
const board = JXG.JSXGraph.initBoard(containerId, {
|
|
6064
|
+
boundingbox: [graph.view.xMin, graph.view.yMax, graph.view.xMax, graph.view.yMin],
|
|
6065
|
+
axis: graph.view.showAxis,
|
|
6066
|
+
grid: graph.view.showGrid,
|
|
6067
|
+
showCopyright: false,
|
|
6068
|
+
showNavigation: true,
|
|
6069
|
+
pan: { enabled: true, needShift: false },
|
|
6070
|
+
zoom: { wheel: true, needShift: false },
|
|
6071
|
+
keepAspectRatio: false
|
|
6072
|
+
});
|
|
6073
|
+
boardRef.current = board;
|
|
6074
|
+
createdBoard = board;
|
|
6075
|
+
syncObjects(board, graph, curvesRef.current);
|
|
6076
|
+
board.on("boundingbox", () => {
|
|
6077
|
+
const bb = board.getBoundingBox();
|
|
6078
|
+
onBoardEvent({
|
|
6079
|
+
type: "view-change",
|
|
6080
|
+
view: {
|
|
6081
|
+
xMin: bb[0],
|
|
6082
|
+
xMax: bb[2],
|
|
6083
|
+
yMax: bb[1],
|
|
6084
|
+
yMin: bb[3],
|
|
6085
|
+
showAxis: graph.view.showAxis,
|
|
6086
|
+
showGrid: graph.view.showGrid
|
|
6087
|
+
}
|
|
6088
|
+
});
|
|
6089
|
+
});
|
|
6090
|
+
board.on("down", (ev) => {
|
|
6091
|
+
const usrCoords = board.getUsrCoordsOfMouse?.(ev);
|
|
6092
|
+
const x = usrCoords?.[0] ?? 0;
|
|
6093
|
+
const y = usrCoords?.[1] ?? 0;
|
|
6094
|
+
let functionId;
|
|
6095
|
+
for (const [id, ref] of curvesRef.current) {
|
|
6096
|
+
const obj = ref.obj;
|
|
6097
|
+
if (obj?.hasPoint && obj.hasPoint(ev.clientX ?? 0, ev.clientY ?? 0)) {
|
|
6098
|
+
functionId = id;
|
|
6099
|
+
break;
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
if (functionId) onBoardEvent({ type: "click-curve", functionId, x, y });
|
|
6103
|
+
else onBoardEvent({ type: "click-empty", x, y });
|
|
6104
|
+
});
|
|
6105
|
+
})().catch((err) => console.error("MiniBoard init failed:", err));
|
|
6106
|
+
return () => {
|
|
6107
|
+
cancelled = true;
|
|
6108
|
+
try {
|
|
6109
|
+
if (createdBoard) __require("jsxgraph").default.JSXGraph.freeBoard(createdBoard);
|
|
6110
|
+
} catch {
|
|
6111
|
+
}
|
|
6112
|
+
boardRef.current = null;
|
|
6113
|
+
curvesRef.current.clear();
|
|
6114
|
+
};
|
|
6115
|
+
}, []);
|
|
6116
|
+
react.useEffect(() => {
|
|
6117
|
+
if (!boardRef.current) return;
|
|
6118
|
+
syncObjects(boardRef.current, graph, curvesRef.current);
|
|
6119
|
+
}, [graph]);
|
|
6120
|
+
react.useEffect(() => {
|
|
6121
|
+
const el = containerRef.current;
|
|
6122
|
+
if (!el) return;
|
|
6123
|
+
el.style.cursor = activeTool === "move" ? "" : "crosshair";
|
|
6124
|
+
}, [activeTool]);
|
|
6125
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6126
|
+
"div",
|
|
6127
|
+
{
|
|
6128
|
+
ref: containerRef,
|
|
6129
|
+
className: "graph-miniboard",
|
|
6130
|
+
style: { width: "100%", height: "100%", minHeight: "300px" },
|
|
6131
|
+
"data-testid": "graph-miniboard"
|
|
6132
|
+
}
|
|
6133
|
+
);
|
|
6134
|
+
}
|
|
6135
|
+
function paramSig(graph) {
|
|
6136
|
+
return graph.parameters.map((p) => `${p.name}=${p.value}`).join(",");
|
|
6137
|
+
}
|
|
6138
|
+
function syncObjects(board, graph, curves) {
|
|
6139
|
+
const sig = paramSig(graph);
|
|
6140
|
+
const paramMap = {};
|
|
6141
|
+
for (const p of graph.parameters) paramMap[p.name] = p.value;
|
|
6142
|
+
const wantedIds = new Set(graph.functions.map((f) => f.id));
|
|
6143
|
+
for (const [id, ref] of curves) {
|
|
6144
|
+
if (!wantedIds.has(id)) {
|
|
6145
|
+
try {
|
|
6146
|
+
board.removeObject(ref.obj);
|
|
6147
|
+
} catch {
|
|
6148
|
+
}
|
|
6149
|
+
curves.delete(id);
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
for (const f of graph.functions) {
|
|
6153
|
+
const existing = curves.get(f.id);
|
|
6154
|
+
const needsRecreate = !existing || existing.expression !== f.expression || existing.color !== f.color || existing.visible !== f.visible || existing.paramSignature !== sig;
|
|
6155
|
+
if (!needsRecreate) continue;
|
|
6156
|
+
if (existing) {
|
|
6157
|
+
try {
|
|
6158
|
+
board.removeObject(existing.obj);
|
|
6159
|
+
} catch {
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
if (!f.visible) {
|
|
6163
|
+
curves.delete(f.id);
|
|
6164
|
+
continue;
|
|
6165
|
+
}
|
|
6166
|
+
const compiled = compile(f.expression, paramMap);
|
|
6167
|
+
if (typeof compiled !== "function") continue;
|
|
6168
|
+
const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
|
|
6169
|
+
const obj = board.create("functiongraph", [compiled, domain.min, domain.max], {
|
|
6170
|
+
strokeColor: f.color,
|
|
6171
|
+
strokeWidth: 2,
|
|
6172
|
+
name: f.name,
|
|
6173
|
+
withLabel: false,
|
|
6174
|
+
highlight: false
|
|
6175
|
+
});
|
|
6176
|
+
curves.set(f.id, {
|
|
6177
|
+
obj,
|
|
6178
|
+
expression: f.expression,
|
|
6179
|
+
color: f.color,
|
|
6180
|
+
visible: f.visible,
|
|
6181
|
+
paramSignature: sig
|
|
6182
|
+
});
|
|
6183
|
+
}
|
|
6184
|
+
for (const point of graph.points) {
|
|
6185
|
+
const fn = graph.functions.find((f) => f.id === point.functionId);
|
|
6186
|
+
if (!fn || !fn.visible) continue;
|
|
6187
|
+
const compiled = compile(fn.expression, paramMap);
|
|
6188
|
+
if (typeof compiled !== "function") continue;
|
|
6189
|
+
const y = compiled(point.x);
|
|
6190
|
+
board.create("point", [point.x, y], {
|
|
6191
|
+
name: point.label ?? "",
|
|
6192
|
+
size: 3,
|
|
6193
|
+
fillColor: fn.color,
|
|
6194
|
+
strokeColor: fn.color,
|
|
6195
|
+
withLabel: !!point.label
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
6198
|
+
for (const inter of graph.intersections) {
|
|
6199
|
+
const fa = graph.functions.find((f) => f.id === inter.functionIdA);
|
|
6200
|
+
const fb = graph.functions.find((f) => f.id === inter.functionIdB);
|
|
6201
|
+
if (!fa || !fb || !fa.visible || !fb.visible) continue;
|
|
6202
|
+
const cfa = compile(fa.expression, paramMap);
|
|
6203
|
+
const cfb = compile(fb.expression, paramMap);
|
|
6204
|
+
if (typeof cfa !== "function" || typeof cfb !== "function") continue;
|
|
6205
|
+
const roots = scanRoots((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
|
|
6206
|
+
for (const x of roots) {
|
|
6207
|
+
board.create("point", [x, cfa(x)], {
|
|
6208
|
+
size: 3,
|
|
6209
|
+
fillColor: "#000",
|
|
6210
|
+
strokeColor: "#000"
|
|
6211
|
+
});
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
for (const tan of graph.tangents) {
|
|
6215
|
+
const pt = graph.points.find((p) => p.id === tan.pointId);
|
|
6216
|
+
if (!pt) continue;
|
|
6217
|
+
const fn = graph.functions.find((f) => f.id === pt.functionId);
|
|
6218
|
+
if (!fn || !fn.visible) continue;
|
|
6219
|
+
const slope = numericalDerivative(fn.expression, paramMap, pt.x);
|
|
6220
|
+
const cfn = compile(fn.expression, paramMap);
|
|
6221
|
+
if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
|
|
6222
|
+
const y0 = cfn(pt.x);
|
|
6223
|
+
const x1 = graph.view.xMin;
|
|
6224
|
+
const x2 = graph.view.xMax;
|
|
6225
|
+
board.create(
|
|
6226
|
+
"line",
|
|
6227
|
+
[
|
|
6228
|
+
[x1, slope * (x1 - pt.x) + y0],
|
|
6229
|
+
[x2, slope * (x2 - pt.x) + y0]
|
|
6230
|
+
],
|
|
6231
|
+
{
|
|
6232
|
+
strokeColor: fn.color,
|
|
6233
|
+
strokeWidth: 1,
|
|
6234
|
+
dash: 2,
|
|
6235
|
+
straightFirst: false,
|
|
6236
|
+
straightLast: false
|
|
6237
|
+
}
|
|
6238
|
+
);
|
|
6239
|
+
}
|
|
6240
|
+
board.update();
|
|
6241
|
+
}
|
|
6242
|
+
function scanRoots(fn, xMin, xMax, samples = 200) {
|
|
6243
|
+
const roots = [];
|
|
6244
|
+
const step = (xMax - xMin) / samples;
|
|
6245
|
+
let prevX = xMin;
|
|
6246
|
+
let prevY = fn(prevX);
|
|
6247
|
+
for (let i = 1; i <= samples; i++) {
|
|
6248
|
+
const x = xMin + i * step;
|
|
6249
|
+
const y = fn(x);
|
|
6250
|
+
if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
|
|
6251
|
+
let a = prevX;
|
|
6252
|
+
let b = x;
|
|
6253
|
+
let ya = prevY;
|
|
6254
|
+
for (let j = 0; j < 30; j++) {
|
|
6255
|
+
const m = (a + b) / 2;
|
|
6256
|
+
const ym = fn(m);
|
|
6257
|
+
if (Math.abs(ym) < 1e-6) {
|
|
6258
|
+
a = b = m;
|
|
6259
|
+
break;
|
|
6260
|
+
}
|
|
6261
|
+
if (ya * ym < 0) {
|
|
6262
|
+
b = m;
|
|
6263
|
+
} else {
|
|
6264
|
+
a = m;
|
|
6265
|
+
ya = ym;
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
roots.push((a + b) / 2);
|
|
6269
|
+
}
|
|
6270
|
+
prevX = x;
|
|
6271
|
+
prevY = y;
|
|
6272
|
+
}
|
|
6273
|
+
return roots;
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
// src/stamps/graph-2d/serialize.ts
|
|
6277
|
+
var EMPTY_GRAPH = {
|
|
6278
|
+
version: 1,
|
|
6279
|
+
view: { xMin: -10, xMax: 10, yMin: -10, yMax: 10, showAxis: true, showGrid: true },
|
|
6280
|
+
functions: [],
|
|
6281
|
+
parameters: [],
|
|
6282
|
+
points: [],
|
|
6283
|
+
intersections: [],
|
|
6284
|
+
tangents: []
|
|
6285
|
+
};
|
|
6286
|
+
function stringifySerializedGraph(graph) {
|
|
6287
|
+
return JSON.stringify(graph);
|
|
6288
|
+
}
|
|
6289
|
+
function parseSerializedGraph(jsonState) {
|
|
6290
|
+
let raw;
|
|
6291
|
+
try {
|
|
6292
|
+
raw = JSON.parse(jsonState);
|
|
6293
|
+
} catch {
|
|
6294
|
+
return null;
|
|
6295
|
+
}
|
|
6296
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
6297
|
+
const r = raw;
|
|
6298
|
+
if (r.version !== 1) return null;
|
|
6299
|
+
if (!r.view || typeof r.view !== "object") return null;
|
|
6300
|
+
const v = r.view;
|
|
6301
|
+
if (typeof v.xMin !== "number" || typeof v.xMax !== "number" || typeof v.yMin !== "number" || typeof v.yMax !== "number" || typeof v.showAxis !== "boolean" || typeof v.showGrid !== "boolean") {
|
|
6302
|
+
return null;
|
|
6303
|
+
}
|
|
6304
|
+
for (const key of ["functions", "parameters", "points", "intersections", "tangents"]) {
|
|
6305
|
+
if (!Array.isArray(r[key])) return null;
|
|
6306
|
+
}
|
|
6307
|
+
return raw;
|
|
6308
|
+
}
|
|
6309
|
+
|
|
6310
|
+
// src/stamps/graph-2d/renderObjects.ts
|
|
6311
|
+
function renderGraphObjects(board, graph) {
|
|
6312
|
+
const paramMap = {};
|
|
6313
|
+
for (const p of graph.parameters) paramMap[p.name] = p.value;
|
|
6314
|
+
for (const f of graph.functions) {
|
|
6315
|
+
if (!f.visible) continue;
|
|
6316
|
+
const compiled = compile(f.expression, paramMap);
|
|
6317
|
+
if (typeof compiled !== "function") continue;
|
|
6318
|
+
const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
|
|
6319
|
+
board.create("functiongraph", [compiled, domain.min, domain.max], {
|
|
6320
|
+
strokeColor: f.color,
|
|
6321
|
+
strokeWidth: 2,
|
|
6322
|
+
name: f.name,
|
|
6323
|
+
withLabel: false,
|
|
6324
|
+
highlight: false
|
|
6325
|
+
});
|
|
6326
|
+
}
|
|
6327
|
+
for (const point of graph.points) {
|
|
6328
|
+
const fn = graph.functions.find((f) => f.id === point.functionId);
|
|
6329
|
+
if (!fn || !fn.visible) continue;
|
|
6330
|
+
const compiled = compile(fn.expression, paramMap);
|
|
6331
|
+
if (typeof compiled !== "function") continue;
|
|
6332
|
+
const y = compiled(point.x);
|
|
6333
|
+
board.create("point", [point.x, y], {
|
|
6334
|
+
name: point.label ?? "",
|
|
6335
|
+
size: 3,
|
|
6336
|
+
fillColor: fn.color,
|
|
6337
|
+
strokeColor: fn.color,
|
|
6338
|
+
withLabel: !!point.label
|
|
6339
|
+
});
|
|
6340
|
+
}
|
|
6341
|
+
for (const inter of graph.intersections) {
|
|
6342
|
+
const fa = graph.functions.find((f) => f.id === inter.functionIdA);
|
|
6343
|
+
const fb = graph.functions.find((f) => f.id === inter.functionIdB);
|
|
6344
|
+
if (!fa || !fb || !fa.visible || !fb.visible) continue;
|
|
6345
|
+
const cfa = compile(fa.expression, paramMap);
|
|
6346
|
+
const cfb = compile(fb.expression, paramMap);
|
|
6347
|
+
if (typeof cfa !== "function" || typeof cfb !== "function") continue;
|
|
6348
|
+
const roots = scanRoots2((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
|
|
6349
|
+
for (const x of roots) {
|
|
6350
|
+
board.create("point", [x, cfa(x)], {
|
|
6351
|
+
size: 3,
|
|
6352
|
+
fillColor: "#000",
|
|
6353
|
+
strokeColor: "#000"
|
|
6354
|
+
});
|
|
6355
|
+
}
|
|
6356
|
+
}
|
|
6357
|
+
for (const tan of graph.tangents) {
|
|
6358
|
+
const pt = graph.points.find((p) => p.id === tan.pointId);
|
|
6359
|
+
if (!pt) continue;
|
|
6360
|
+
const fn = graph.functions.find((f) => f.id === pt.functionId);
|
|
6361
|
+
if (!fn || !fn.visible) continue;
|
|
6362
|
+
const slope = numericalDerivative(fn.expression, paramMap, pt.x);
|
|
6363
|
+
const cfn = compile(fn.expression, paramMap);
|
|
6364
|
+
if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
|
|
6365
|
+
const y0 = cfn(pt.x);
|
|
6366
|
+
const x1 = graph.view.xMin;
|
|
6367
|
+
const x2 = graph.view.xMax;
|
|
6368
|
+
board.create(
|
|
6369
|
+
"line",
|
|
6370
|
+
[
|
|
6371
|
+
[x1, slope * (x1 - pt.x) + y0],
|
|
6372
|
+
[x2, slope * (x2 - pt.x) + y0]
|
|
6373
|
+
],
|
|
6374
|
+
{
|
|
6375
|
+
strokeColor: fn.color,
|
|
6376
|
+
strokeWidth: 1,
|
|
6377
|
+
dash: 2,
|
|
6378
|
+
straightFirst: false,
|
|
6379
|
+
straightLast: false
|
|
6380
|
+
}
|
|
6381
|
+
);
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
function scanRoots2(fn, xMin, xMax, samples = 200) {
|
|
6385
|
+
const roots = [];
|
|
6386
|
+
const step = (xMax - xMin) / samples;
|
|
6387
|
+
let prevX = xMin;
|
|
6388
|
+
let prevY = fn(prevX);
|
|
6389
|
+
for (let i = 1; i <= samples; i++) {
|
|
6390
|
+
const x = xMin + i * step;
|
|
6391
|
+
const y = fn(x);
|
|
6392
|
+
if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
|
|
6393
|
+
let a = prevX;
|
|
6394
|
+
let b = x;
|
|
6395
|
+
let ya = prevY;
|
|
6396
|
+
for (let j = 0; j < 30; j++) {
|
|
6397
|
+
const m = (a + b) / 2;
|
|
6398
|
+
const ym = fn(m);
|
|
6399
|
+
if (Math.abs(ym) < 1e-6) {
|
|
6400
|
+
a = b = m;
|
|
6401
|
+
break;
|
|
6402
|
+
}
|
|
6403
|
+
if (ya * ym < 0) {
|
|
6404
|
+
b = m;
|
|
6405
|
+
} else {
|
|
6406
|
+
a = m;
|
|
6407
|
+
ya = ym;
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
roots.push((a + b) / 2);
|
|
6411
|
+
}
|
|
6412
|
+
prevX = x;
|
|
6413
|
+
prevY = y;
|
|
6414
|
+
}
|
|
6415
|
+
return roots;
|
|
6416
|
+
}
|
|
6417
|
+
|
|
6418
|
+
// src/stamps/graph-2d/render.ts
|
|
6419
|
+
async function renderGraph2dSvgFromState(jsonState) {
|
|
6420
|
+
const parsed = parseSerializedGraph(jsonState);
|
|
6421
|
+
if (!parsed) throw new Error("renderGraph2dSvgFromState: jsonState corrupt");
|
|
6422
|
+
const JXG = (await import('jsxgraph')).default;
|
|
6423
|
+
const opts = JXG.Options;
|
|
6424
|
+
if (opts) {
|
|
6425
|
+
opts.text = opts.text || {};
|
|
6426
|
+
opts.text.display = "internal";
|
|
6427
|
+
opts.text.useASCIIMathML = false;
|
|
6428
|
+
opts.text.useMathJax = false;
|
|
6429
|
+
opts.text.useKatex = false;
|
|
6430
|
+
opts.label = opts.label || {};
|
|
6431
|
+
opts.label.display = "internal";
|
|
6432
|
+
}
|
|
6433
|
+
const container = document.createElement("div");
|
|
6434
|
+
container.id = `jxg_graph2d_off_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6435
|
+
container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:600px;height:400px;visibility:hidden;pointer-events:none;";
|
|
6436
|
+
document.body.appendChild(container);
|
|
6437
|
+
let board = null;
|
|
6438
|
+
try {
|
|
6439
|
+
board = JXG.JSXGraph.initBoard(container.id, {
|
|
6440
|
+
boundingbox: [parsed.view.xMin, parsed.view.yMax, parsed.view.xMax, parsed.view.yMin],
|
|
6441
|
+
axis: parsed.view.showAxis,
|
|
6442
|
+
grid: parsed.view.showGrid,
|
|
6443
|
+
showCopyright: false,
|
|
6444
|
+
showNavigation: false,
|
|
6445
|
+
keepAspectRatio: false
|
|
6446
|
+
});
|
|
6447
|
+
renderGraphObjects(board, parsed);
|
|
6448
|
+
board.update();
|
|
6449
|
+
const svgEl = container.querySelector("svg");
|
|
6450
|
+
if (!svgEl) throw new Error("renderGraph2dSvgFromState: no svg generated");
|
|
6451
|
+
return svgEl.outerHTML;
|
|
6452
|
+
} finally {
|
|
6453
|
+
try {
|
|
6454
|
+
if (board) JXG.JSXGraph.freeBoard(board);
|
|
6455
|
+
} catch {
|
|
6456
|
+
}
|
|
6457
|
+
if (container.parentNode) container.parentNode.removeChild(container);
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
var GraphEditorPanel = react.forwardRef(function GraphEditorPanel2(props, ref) {
|
|
6461
|
+
const initialGraph = props.initialState ?? EMPTY_GRAPH;
|
|
6462
|
+
const graphRef = react.useRef(initialGraph);
|
|
6463
|
+
const [, forceUpdate] = react.useState(0);
|
|
6464
|
+
const [errors, setErrors] = react.useState({});
|
|
6465
|
+
const [tool, setToolState] = react.useState("move");
|
|
6466
|
+
const undoStackRef = react.useRef([]);
|
|
6467
|
+
const idCounterRef = react.useRef(1);
|
|
6468
|
+
const toolRef = react.useRef(tool);
|
|
6469
|
+
toolRef.current = tool;
|
|
6470
|
+
const intersectFirstRef = react.useRef(null);
|
|
6471
|
+
const propsRef = react.useRef(props);
|
|
6472
|
+
propsRef.current = props;
|
|
6473
|
+
const initialGraphNotifiedRef = react.useRef(false);
|
|
6474
|
+
const pushUndo = react.useCallback((g) => {
|
|
6475
|
+
undoStackRef.current.push(g);
|
|
6476
|
+
if (undoStackRef.current.length > 30) undoStackRef.current.shift();
|
|
6477
|
+
}, []);
|
|
6478
|
+
const setErrorsWithNotify = react.useCallback(
|
|
6479
|
+
(updater) => {
|
|
6480
|
+
setErrors((prev) => {
|
|
6481
|
+
const next = updater(prev);
|
|
6482
|
+
propsRef.current.onErrorsChange?.(next);
|
|
6483
|
+
return next;
|
|
6484
|
+
});
|
|
6485
|
+
},
|
|
6486
|
+
[]
|
|
6487
|
+
);
|
|
6488
|
+
const notifyStateChange = react.useCallback((g, t) => {
|
|
6489
|
+
propsRef.current.onStateChange({
|
|
6490
|
+
tool: t,
|
|
6491
|
+
showAxis: g.view.showAxis,
|
|
6492
|
+
showGrid: g.view.showGrid,
|
|
6493
|
+
canUndo: undoStackRef.current.length > 0
|
|
6494
|
+
});
|
|
6495
|
+
}, []);
|
|
6496
|
+
const updateGraph = react.useCallback(
|
|
6497
|
+
(mutator) => {
|
|
6498
|
+
const prev = graphRef.current;
|
|
6499
|
+
pushUndo(prev);
|
|
6500
|
+
const next = mutator(prev);
|
|
6501
|
+
graphRef.current = next;
|
|
6502
|
+
notifyStateChange(next, toolRef.current);
|
|
6503
|
+
forceUpdate((n) => n + 1);
|
|
6504
|
+
propsRef.current.onGraphChange?.(next);
|
|
6505
|
+
},
|
|
6506
|
+
[pushUndo, notifyStateChange]
|
|
6507
|
+
);
|
|
6508
|
+
const onBoardEvent = react.useCallback((ev) => {
|
|
6509
|
+
const currentTool = toolRef.current;
|
|
6510
|
+
if (currentTool === "point-on-curve" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
|
|
6511
|
+
updateGraph(
|
|
6512
|
+
(g) => addPointOnCurve(
|
|
6513
|
+
g,
|
|
6514
|
+
{ x: ev.x, y: ev.y ?? 0, functionId: ev.functionId },
|
|
6515
|
+
() => `p${idCounterRef.current++}`
|
|
6516
|
+
)
|
|
6517
|
+
);
|
|
6518
|
+
setToolState("move");
|
|
6519
|
+
} else if (currentTool === "intersect" && ev.type === "click-curve" && ev.functionId) {
|
|
6520
|
+
if (!intersectFirstRef.current) {
|
|
6521
|
+
intersectFirstRef.current = ev.functionId;
|
|
6522
|
+
} else {
|
|
6523
|
+
const a = intersectFirstRef.current;
|
|
6524
|
+
const b = ev.functionId;
|
|
6525
|
+
intersectFirstRef.current = null;
|
|
6526
|
+
updateGraph(
|
|
6527
|
+
(g) => addIntersection(g, a, b, () => `i${idCounterRef.current++}`)
|
|
6528
|
+
);
|
|
6529
|
+
setToolState("move");
|
|
6530
|
+
}
|
|
6531
|
+
} else if (currentTool === "tangent" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
|
|
6532
|
+
const pointId = `p${idCounterRef.current++}`;
|
|
6533
|
+
const tangentId = `t${idCounterRef.current++}`;
|
|
6534
|
+
updateGraph((g) => ({
|
|
6535
|
+
...g,
|
|
6536
|
+
points: [...g.points, { id: pointId, functionId: ev.functionId, x: ev.x }],
|
|
6537
|
+
tangents: [...g.tangents, { id: tangentId, pointId }]
|
|
6538
|
+
}));
|
|
6539
|
+
setToolState("move");
|
|
6540
|
+
}
|
|
6541
|
+
}, [updateGraph]);
|
|
6542
|
+
react.useImperativeHandle(
|
|
6543
|
+
ref,
|
|
6544
|
+
() => ({
|
|
6545
|
+
insert: () => {
|
|
6546
|
+
const g = graphRef.current;
|
|
6547
|
+
if (g.functions.length === 0) return false;
|
|
6548
|
+
const jsonState = stringifySerializedGraph(g);
|
|
6549
|
+
renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
|
|
6550
|
+
return true;
|
|
6551
|
+
},
|
|
6552
|
+
hasContent: () => graphRef.current.functions.length > 0,
|
|
6553
|
+
setTool: (t) => {
|
|
6554
|
+
setToolState(t);
|
|
6555
|
+
const g = graphRef.current;
|
|
6556
|
+
propsRef.current.onStateChange({
|
|
6557
|
+
tool: t,
|
|
6558
|
+
showAxis: g.view.showAxis,
|
|
6559
|
+
showGrid: g.view.showGrid,
|
|
6560
|
+
canUndo: undoStackRef.current.length > 0
|
|
6561
|
+
});
|
|
6562
|
+
},
|
|
6563
|
+
setShowAxis: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showAxis: b } })),
|
|
6564
|
+
setShowGrid: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showGrid: b } })),
|
|
6565
|
+
resetView: () => updateGraph((g) => ({
|
|
6566
|
+
...g,
|
|
6567
|
+
view: { ...g.view, xMin: -10, xMax: 10, yMin: -10, yMax: 10 }
|
|
6568
|
+
})),
|
|
6569
|
+
undo: () => {
|
|
6570
|
+
const prev = undoStackRef.current.pop();
|
|
6571
|
+
if (!prev) return;
|
|
6572
|
+
graphRef.current = prev;
|
|
6573
|
+
forceUpdate((n) => n + 1);
|
|
6574
|
+
propsRef.current.onStateChange({
|
|
6575
|
+
tool: toolRef.current,
|
|
6576
|
+
showAxis: prev.view.showAxis,
|
|
6577
|
+
showGrid: prev.view.showGrid,
|
|
6578
|
+
canUndo: undoStackRef.current.length > 0
|
|
6579
|
+
});
|
|
6580
|
+
propsRef.current.onGraphChange?.(prev);
|
|
6581
|
+
},
|
|
6582
|
+
addFunction: (expr) => {
|
|
6583
|
+
const g = graphRef.current;
|
|
6584
|
+
if (g.functions.length >= MAX_FUNCTIONS) {
|
|
6585
|
+
return { ok: false, error: `T\u1ED1i \u0111a ${MAX_FUNCTIONS} h\xE0m` };
|
|
6586
|
+
}
|
|
6587
|
+
const v = validate(expr);
|
|
6588
|
+
if (!v.ok) return { ok: false, error: v.error ?? "Invalid" };
|
|
6589
|
+
const id = `f${idCounterRef.current++}`;
|
|
6590
|
+
const usedNames = g.functions.map((f) => f.name);
|
|
6591
|
+
const usedColors = g.functions.map((f) => f.color);
|
|
6592
|
+
const newFn = {
|
|
6593
|
+
id,
|
|
6594
|
+
name: nextFunctionName(usedNames),
|
|
6595
|
+
expression: expr,
|
|
6596
|
+
color: nextColor(usedColors),
|
|
6597
|
+
visible: true
|
|
6598
|
+
};
|
|
6599
|
+
const usedParamNames = new Set(g.parameters.map((p) => p.name));
|
|
6600
|
+
const newParams = [];
|
|
6601
|
+
for (const varName of v.freeVars) {
|
|
6602
|
+
if (usedParamNames.has(varName)) continue;
|
|
6603
|
+
if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
|
|
6604
|
+
newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
|
|
6605
|
+
}
|
|
6606
|
+
updateGraph((prev) => ({
|
|
6607
|
+
...prev,
|
|
6608
|
+
functions: [...prev.functions, newFn],
|
|
6609
|
+
parameters: [...prev.parameters, ...newParams]
|
|
6610
|
+
}));
|
|
6611
|
+
setErrorsWithNotify((e) => ({ ...e, [id]: null }));
|
|
6612
|
+
return { ok: true, id };
|
|
6613
|
+
},
|
|
6614
|
+
commitFunctionExpression: (id, expr) => {
|
|
6615
|
+
const g = graphRef.current;
|
|
6616
|
+
const v = validate(expr);
|
|
6617
|
+
if (!v.ok) {
|
|
6618
|
+
setErrorsWithNotify((e) => ({ ...e, [id]: v.error ?? "Invalid" }));
|
|
6619
|
+
return;
|
|
6620
|
+
}
|
|
6621
|
+
const usedParamNames = new Set(g.parameters.map((p) => p.name));
|
|
6622
|
+
const newParams = [];
|
|
6623
|
+
for (const varName of v.freeVars) {
|
|
6624
|
+
if (usedParamNames.has(varName)) continue;
|
|
6625
|
+
if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
|
|
6626
|
+
newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
|
|
6627
|
+
}
|
|
6628
|
+
updateGraph((prev) => ({
|
|
6629
|
+
...prev,
|
|
6630
|
+
functions: prev.functions.map(
|
|
6631
|
+
(f) => f.id === id ? { ...f, expression: expr } : f
|
|
6632
|
+
),
|
|
6633
|
+
parameters: [...prev.parameters, ...newParams]
|
|
6634
|
+
}));
|
|
6635
|
+
setErrorsWithNotify((e) => ({ ...e, [id]: null }));
|
|
6636
|
+
},
|
|
6637
|
+
toggleFunctionVisible: (id) => updateGraph((g) => ({
|
|
6638
|
+
...g,
|
|
6639
|
+
functions: g.functions.map(
|
|
6640
|
+
(f) => f.id === id ? { ...f, visible: !f.visible } : f
|
|
6641
|
+
)
|
|
6642
|
+
})),
|
|
6643
|
+
removeFunction: (id) => updateGraph((g) => ({
|
|
6644
|
+
...g,
|
|
6645
|
+
functions: g.functions.filter((f) => f.id !== id)
|
|
6646
|
+
})),
|
|
6647
|
+
// setParameter does NOT push undo — would flood the stack (slider drag)
|
|
6648
|
+
setParameter: (name, value) => {
|
|
6649
|
+
const next = {
|
|
6650
|
+
...graphRef.current,
|
|
6651
|
+
parameters: graphRef.current.parameters.map(
|
|
6652
|
+
(p) => p.name === name ? { ...p, value } : p
|
|
6653
|
+
)
|
|
6654
|
+
};
|
|
6655
|
+
graphRef.current = next;
|
|
6656
|
+
forceUpdate((n) => n + 1);
|
|
6657
|
+
propsRef.current.onGraphChange?.(next);
|
|
6658
|
+
},
|
|
6659
|
+
setParameterRange: (name, min, max, step) => updateGraph((g) => ({
|
|
6660
|
+
...g,
|
|
6661
|
+
parameters: g.parameters.map(
|
|
6662
|
+
(p) => p.name === name ? { ...p, min, max, step, value: Math.min(max, Math.max(min, p.value)) } : p
|
|
6663
|
+
)
|
|
6664
|
+
})),
|
|
6665
|
+
removeParameter: (name) => updateGraph((g) => ({
|
|
6666
|
+
...g,
|
|
6667
|
+
parameters: g.parameters.filter((p) => p.name !== name)
|
|
6668
|
+
})),
|
|
6669
|
+
getGraph: () => graphRef.current,
|
|
6670
|
+
getErrors: () => errors
|
|
6671
|
+
}),
|
|
6672
|
+
// deps: updateGraph stable; errors changes when function errors change; setErrorsWithNotify stable
|
|
6673
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
6674
|
+
[updateGraph, errors, setErrorsWithNotify]
|
|
6675
|
+
);
|
|
6676
|
+
react.useEffect(() => {
|
|
6677
|
+
if (!initialGraphNotifiedRef.current) {
|
|
6678
|
+
initialGraphNotifiedRef.current = true;
|
|
6679
|
+
propsRef.current.onGraphChange?.(graphRef.current);
|
|
6680
|
+
}
|
|
6681
|
+
}, []);
|
|
6682
|
+
const graph = graphRef.current;
|
|
6683
|
+
const hasContent = graph.functions.length > 0;
|
|
6684
|
+
const handleInsert = () => {
|
|
6685
|
+
const g = graphRef.current;
|
|
6686
|
+
if (g.functions.length === 0) return;
|
|
6687
|
+
const jsonState = stringifySerializedGraph(g);
|
|
6688
|
+
renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
|
|
6689
|
+
};
|
|
6690
|
+
const { isMobile, isDark, withLeftPanel } = props;
|
|
6691
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
6692
|
+
position: "absolute",
|
|
6693
|
+
top: "50%",
|
|
6694
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
6695
|
+
transform: "translate(-50%, -50%)",
|
|
6696
|
+
zIndex: 40
|
|
6697
|
+
};
|
|
6698
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6699
|
+
"div",
|
|
6700
|
+
{
|
|
6701
|
+
role: "dialog",
|
|
6702
|
+
"aria-label": "\u0110\u1ED3 th\u1ECB 2D",
|
|
6703
|
+
"data-testid": "graph-editor-panel",
|
|
6704
|
+
"data-stamp-area": "true",
|
|
6705
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
6706
|
+
style: wrapperStyle,
|
|
6707
|
+
className: [
|
|
6708
|
+
isDark ? "theme--dark " : "",
|
|
6709
|
+
"flex flex-col overflow-hidden bg-white",
|
|
6710
|
+
isMobile ? "h-full w-full" : "h-[540px] max-h-[85vh] w-[640px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
|
|
6711
|
+
].join(" "),
|
|
6712
|
+
children: [
|
|
6713
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-orange-500 to-amber-600 px-3 py-2 text-white", children: [
|
|
6714
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6715
|
+
"button",
|
|
6716
|
+
{
|
|
6717
|
+
type: "button",
|
|
6718
|
+
onClick: props.onOpenDrawer,
|
|
6719
|
+
"aria-label": "M\u1EDF b\u1EA3ng \u0111\u1EA1i s\u1ED1",
|
|
6720
|
+
"data-testid": "graph-drawer-toggle",
|
|
6721
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
6722
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6723
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
6724
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
6725
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
6726
|
+
] })
|
|
6727
|
+
}
|
|
6728
|
+
),
|
|
6729
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
6730
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6731
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 V3" }),
|
|
6732
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 H21" }),
|
|
6733
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
|
|
6734
|
+
] }),
|
|
6735
|
+
"\u0110\u1ED3 th\u1ECB 2D"
|
|
6736
|
+
] }),
|
|
6737
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6738
|
+
"button",
|
|
6739
|
+
{
|
|
6740
|
+
type: "button",
|
|
6741
|
+
onClick: handleInsert,
|
|
6742
|
+
disabled: !hasContent,
|
|
6743
|
+
"data-testid": "graph-insert-btn-mobile",
|
|
6744
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
6745
|
+
children: "Ch\xE8n"
|
|
6746
|
+
}
|
|
6747
|
+
),
|
|
6748
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6749
|
+
"button",
|
|
6750
|
+
{
|
|
6751
|
+
onClick: props.onClose,
|
|
6752
|
+
"aria-label": "\u0110\xF3ng",
|
|
6753
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
6754
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6755
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
6756
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
6757
|
+
] })
|
|
6758
|
+
}
|
|
6759
|
+
)
|
|
6760
|
+
] }),
|
|
6761
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6762
|
+
MiniBoard,
|
|
6763
|
+
{
|
|
6764
|
+
graph,
|
|
6765
|
+
activeTool: tool,
|
|
6766
|
+
isDark,
|
|
6767
|
+
onBoardEvent
|
|
6768
|
+
}
|
|
6769
|
+
) }),
|
|
6770
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
6771
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Nh\u1EADp bi\u1EC3u th\u1EE9c trong b\u1EA3ng \u0111\u1EA1i s\u1ED1 b\xEAn tr\xE1i." }),
|
|
6772
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
6773
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6774
|
+
"button",
|
|
6775
|
+
{
|
|
6776
|
+
onClick: props.onClose,
|
|
6777
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
6778
|
+
children: "Hu\u1EF7"
|
|
6779
|
+
}
|
|
6780
|
+
),
|
|
6781
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6782
|
+
"button",
|
|
6783
|
+
{
|
|
6784
|
+
onClick: handleInsert,
|
|
6785
|
+
disabled: !hasContent,
|
|
6786
|
+
"data-testid": "graph-insert-btn",
|
|
6787
|
+
className: "rounded bg-orange-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-orange-700 disabled:opacity-50",
|
|
6788
|
+
children: "Ch\xE8n"
|
|
6789
|
+
}
|
|
6790
|
+
)
|
|
6791
|
+
] })
|
|
6792
|
+
] })
|
|
6793
|
+
]
|
|
6794
|
+
}
|
|
6795
|
+
);
|
|
6796
|
+
});
|
|
6797
|
+
function isGraph2DCustomData(data) {
|
|
6798
|
+
if (!data || typeof data !== "object") return false;
|
|
6799
|
+
const d = data;
|
|
6800
|
+
return d.kind === "graph2d" && d.version === 1 && typeof d.jsonState === "string";
|
|
4587
6801
|
}
|
|
4588
|
-
var
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
6802
|
+
var INITIAL_GRAPH_STATE = {
|
|
6803
|
+
tool: "move",
|
|
6804
|
+
showAxis: true,
|
|
6805
|
+
showGrid: true,
|
|
6806
|
+
canUndo: false
|
|
6807
|
+
};
|
|
6808
|
+
var Graph2DStampHost = react.forwardRef(
|
|
6809
|
+
function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
6810
|
+
const panelRef = react.useRef(null);
|
|
6811
|
+
const [graphUIState, setGraphUIState] = react.useState(INITIAL_GRAPH_STATE);
|
|
6812
|
+
const { isMobile } = useIsMobile();
|
|
6813
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
6814
|
+
const initialState = react.useMemo(() => {
|
|
6815
|
+
if (!editingElement) return null;
|
|
6816
|
+
if (!isGraph2DCustomData(editingElement.customData)) return null;
|
|
6817
|
+
return parseSerializedGraph(editingElement.customData.jsonState);
|
|
6818
|
+
}, [editingElement]);
|
|
6819
|
+
const [graphSnapshot, setGraphSnapshot] = react.useState(
|
|
6820
|
+
initialState ?? EMPTY_GRAPH
|
|
4594
6821
|
);
|
|
6822
|
+
const [errorsSnapshot, setErrorsSnapshot] = react.useState({});
|
|
4595
6823
|
const handleInsert = react.useCallback(
|
|
4596
|
-
async (jsonState, svgString
|
|
6824
|
+
async (jsonState, svgString) => {
|
|
4597
6825
|
if (!api) return;
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
6826
|
+
try {
|
|
6827
|
+
await insertStampImage(api, {
|
|
6828
|
+
svgString,
|
|
6829
|
+
makeCustomData: (width, height) => ({
|
|
6830
|
+
kind: "graph2d",
|
|
6831
|
+
version: 1,
|
|
6832
|
+
jsonState,
|
|
6833
|
+
svgWidth: width,
|
|
6834
|
+
svgHeight: height
|
|
6835
|
+
}),
|
|
6836
|
+
editingElementId: editingElement?.id ?? null
|
|
6837
|
+
});
|
|
6838
|
+
} catch (err) {
|
|
6839
|
+
console.error("Graph2D insert failed:", err);
|
|
6840
|
+
}
|
|
4609
6841
|
onClose();
|
|
4610
6842
|
},
|
|
4611
|
-
[api, editingElement, onClose]
|
|
6843
|
+
[api, editingElement?.id, onClose]
|
|
4612
6844
|
);
|
|
4613
6845
|
react.useImperativeHandle(
|
|
4614
6846
|
ref,
|
|
4615
6847
|
() => ({
|
|
4616
|
-
tryInsert: () =>
|
|
4617
|
-
hasContent: () =>
|
|
6848
|
+
tryInsert: () => panelRef.current?.insert() ?? false,
|
|
6849
|
+
hasContent: () => panelRef.current?.hasContent() ?? false
|
|
4618
6850
|
}),
|
|
4619
6851
|
[]
|
|
4620
6852
|
);
|
|
4621
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
6853
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
6854
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6855
|
+
GraphLeftPanel,
|
|
6856
|
+
{
|
|
6857
|
+
activeTool: graphUIState.tool,
|
|
6858
|
+
onToolChange: (t) => panelRef.current?.setTool(t),
|
|
6859
|
+
showAxis: graphUIState.showAxis,
|
|
6860
|
+
showGrid: graphUIState.showGrid,
|
|
6861
|
+
onShowAxisChange: (b) => panelRef.current?.setShowAxis(b),
|
|
6862
|
+
onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
|
|
6863
|
+
onResetView: () => panelRef.current?.resetView(),
|
|
6864
|
+
onUndo: () => panelRef.current?.undo(),
|
|
6865
|
+
canUndo: graphUIState.canUndo,
|
|
6866
|
+
onClose,
|
|
6867
|
+
isDark,
|
|
6868
|
+
isMobile,
|
|
6869
|
+
drawerOpen,
|
|
6870
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
6871
|
+
graph: graphSnapshot,
|
|
6872
|
+
errors: errorsSnapshot,
|
|
6873
|
+
onAddFunctionDraft: () => {
|
|
6874
|
+
const result = panelRef.current?.addFunction("x");
|
|
6875
|
+
if (result && !result.ok) console.warn("addFunction failed:", result.error);
|
|
6876
|
+
},
|
|
6877
|
+
onCommitFunctionExpr: (id, expr) => panelRef.current?.commitFunctionExpression(id, expr),
|
|
6878
|
+
onToggleFunctionVisible: (id) => panelRef.current?.toggleFunctionVisible(id),
|
|
6879
|
+
onRemoveFunction: (id) => panelRef.current?.removeFunction(id),
|
|
6880
|
+
onParameterChange: (name, v) => panelRef.current?.setParameter(name, v),
|
|
6881
|
+
onParameterRangeChange: (name, min, max, step) => panelRef.current?.setParameterRange(name, min, max, step),
|
|
6882
|
+
onRemoveParameter: (name) => panelRef.current?.removeParameter(name)
|
|
6883
|
+
}
|
|
6884
|
+
),
|
|
6885
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6886
|
+
GraphEditorPanel,
|
|
6887
|
+
{
|
|
6888
|
+
ref: panelRef,
|
|
6889
|
+
initialState,
|
|
6890
|
+
onInsert: handleInsert,
|
|
6891
|
+
onClose,
|
|
6892
|
+
onStateChange: setGraphUIState,
|
|
6893
|
+
onGraphChange: setGraphSnapshot,
|
|
6894
|
+
onErrorsChange: setErrorsSnapshot,
|
|
6895
|
+
withLeftPanel: !isMobile,
|
|
6896
|
+
isDark,
|
|
6897
|
+
isMobile,
|
|
6898
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
6899
|
+
}
|
|
6900
|
+
)
|
|
6901
|
+
] });
|
|
4631
6902
|
}
|
|
4632
6903
|
);
|
|
4633
|
-
var
|
|
6904
|
+
var Graph2DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4634
6905
|
"svg",
|
|
4635
6906
|
{
|
|
4636
6907
|
width: "20",
|
|
@@ -4643,47 +6914,45 @@ var Geometry3DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
|
4643
6914
|
strokeLinejoin: "round",
|
|
4644
6915
|
"aria-hidden": "true",
|
|
4645
6916
|
children: [
|
|
4646
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "
|
|
4647
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "
|
|
6917
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 V3" }),
|
|
6918
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21 H21" }),
|
|
6919
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
|
|
4648
6920
|
]
|
|
4649
6921
|
}
|
|
4650
6922
|
);
|
|
4651
|
-
var
|
|
4652
|
-
kind: "
|
|
4653
|
-
shortcutKey: "
|
|
4654
|
-
toolbarLabel: "
|
|
4655
|
-
toolbarTitle: "
|
|
4656
|
-
toolbarIcon:
|
|
4657
|
-
toolbarTestId: "stamp-toolbar-
|
|
4658
|
-
matchesCustomData:
|
|
6923
|
+
var graph2dStamp = {
|
|
6924
|
+
kind: "graph2d",
|
|
6925
|
+
shortcutKey: "h",
|
|
6926
|
+
toolbarLabel: "H",
|
|
6927
|
+
toolbarTitle: "Ch\xE8n \u0111\u1ED3 th\u1ECB 2D (H)",
|
|
6928
|
+
toolbarIcon: Graph2DIcon,
|
|
6929
|
+
toolbarTestId: "stamp-toolbar-graph2d",
|
|
6930
|
+
matchesCustomData: isGraph2DCustomData,
|
|
4659
6931
|
async renderSvgFromCustomData(data) {
|
|
4660
|
-
if (!
|
|
4661
|
-
throw new Error("
|
|
6932
|
+
if (!isGraph2DCustomData(data)) {
|
|
6933
|
+
throw new Error("graph2dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i graph2d");
|
|
4662
6934
|
}
|
|
4663
|
-
|
|
4664
|
-
return svgString;
|
|
6935
|
+
return renderGraph2dSvgFromState(data.jsonState);
|
|
4665
6936
|
},
|
|
4666
|
-
|
|
6937
|
+
async restoreFileFromCustomData(element) {
|
|
4667
6938
|
const data = element.customData;
|
|
4668
6939
|
const fileId = element.fileId;
|
|
4669
6940
|
if (!data || !fileId) return null;
|
|
4670
|
-
if (!
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
} catch {
|
|
4676
|
-
return null;
|
|
4677
|
-
}
|
|
6941
|
+
if (!isGraph2DCustomData(data)) return null;
|
|
6942
|
+
const svgString = await renderGraph2dSvgFromState(data.jsonState);
|
|
6943
|
+
const utf8 = unescape(encodeURIComponent(svgString));
|
|
6944
|
+
const dataURL = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8).toString("base64"));
|
|
6945
|
+
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
4678
6946
|
},
|
|
4679
|
-
Host:
|
|
6947
|
+
Host: Graph2DStampHost
|
|
4680
6948
|
};
|
|
4681
6949
|
|
|
4682
6950
|
// src/stamps/shared/registry.ts
|
|
4683
6951
|
var DEFAULT_STAMPS = Object.freeze([
|
|
4684
6952
|
geometryStamp,
|
|
4685
6953
|
latexStamp,
|
|
4686
|
-
geometry3dStamp
|
|
6954
|
+
geometry3dStamp,
|
|
6955
|
+
graph2dStamp
|
|
4687
6956
|
]);
|
|
4688
6957
|
function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
4689
6958
|
for (const s of stamps) {
|
|
@@ -4694,36 +6963,90 @@ function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
|
4694
6963
|
function isStampElement(element, stamps = DEFAULT_STAMPS) {
|
|
4695
6964
|
return findStampForCustomData(element.customData, stamps) !== null;
|
|
4696
6965
|
}
|
|
4697
|
-
var
|
|
6966
|
+
var TOOLBAR_WRAPPER_ID = "stamp-toolbar-portal-wrapper";
|
|
6967
|
+
var MENU_WRAPPER_ID = "stamp-menu-portal-wrapper";
|
|
4698
6968
|
function ToolbarInjector({
|
|
4699
6969
|
enabled,
|
|
4700
6970
|
activeStampKind,
|
|
4701
6971
|
onToggle,
|
|
4702
6972
|
stamps = DEFAULT_STAMPS
|
|
4703
6973
|
}) {
|
|
4704
|
-
const [
|
|
6974
|
+
const [isMobile, setIsMobile] = react.useState(false);
|
|
6975
|
+
const [toolbarMount, setToolbarMount] = react.useState(null);
|
|
6976
|
+
const [menuMount, setMenuMount] = react.useState(null);
|
|
6977
|
+
const isMobileRef = react.useRef(false);
|
|
6978
|
+
const toolbarMountRef = react.useRef(null);
|
|
6979
|
+
const menuMountRef = react.useRef(null);
|
|
4705
6980
|
react.useEffect(() => {
|
|
4706
6981
|
if (!enabled) {
|
|
4707
|
-
|
|
6982
|
+
if (isMobileRef.current !== false) {
|
|
6983
|
+
isMobileRef.current = false;
|
|
6984
|
+
setIsMobile(false);
|
|
6985
|
+
}
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
let cancelled = false;
|
|
6989
|
+
let observer = null;
|
|
6990
|
+
let timer = null;
|
|
6991
|
+
let attempts = 0;
|
|
6992
|
+
const apply = (next) => {
|
|
6993
|
+
if (cancelled || isMobileRef.current === next) return;
|
|
6994
|
+
isMobileRef.current = next;
|
|
6995
|
+
queueMicrotask(() => {
|
|
6996
|
+
if (!cancelled) setIsMobile(next);
|
|
6997
|
+
});
|
|
6998
|
+
};
|
|
6999
|
+
const attach = () => {
|
|
7000
|
+
if (cancelled) return;
|
|
7001
|
+
const root = document.querySelector(".excalidraw");
|
|
7002
|
+
if (!root) {
|
|
7003
|
+
if (attempts++ < 20) timer = setTimeout(attach, 100);
|
|
7004
|
+
return;
|
|
7005
|
+
}
|
|
7006
|
+
apply(root.classList.contains("excalidraw--mobile"));
|
|
7007
|
+
observer = new MutationObserver(() => {
|
|
7008
|
+
apply(root.classList.contains("excalidraw--mobile"));
|
|
7009
|
+
});
|
|
7010
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
7011
|
+
};
|
|
7012
|
+
attach();
|
|
7013
|
+
return () => {
|
|
7014
|
+
cancelled = true;
|
|
7015
|
+
if (timer) clearTimeout(timer);
|
|
7016
|
+
observer?.disconnect();
|
|
7017
|
+
};
|
|
7018
|
+
}, [enabled]);
|
|
7019
|
+
react.useEffect(() => {
|
|
7020
|
+
if (!enabled || isMobile) {
|
|
7021
|
+
if (toolbarMountRef.current !== null) {
|
|
7022
|
+
toolbarMountRef.current = null;
|
|
7023
|
+
setToolbarMount(null);
|
|
7024
|
+
}
|
|
7025
|
+
document.getElementById(TOOLBAR_WRAPPER_ID)?.remove();
|
|
4708
7026
|
return;
|
|
4709
7027
|
}
|
|
4710
7028
|
let cancelled = false;
|
|
4711
7029
|
let attempts = 0;
|
|
4712
7030
|
let observer = null;
|
|
4713
7031
|
let timer = null;
|
|
7032
|
+
const apply = (next) => {
|
|
7033
|
+
if (cancelled || toolbarMountRef.current === next) return;
|
|
7034
|
+
toolbarMountRef.current = next;
|
|
7035
|
+
queueMicrotask(() => {
|
|
7036
|
+
if (!cancelled) setToolbarMount(next);
|
|
7037
|
+
});
|
|
7038
|
+
};
|
|
4714
7039
|
const tryMount = () => {
|
|
4715
7040
|
if (cancelled) return;
|
|
4716
7041
|
const container = document.querySelector(".excalidraw .App-toolbar .Stack_horizontal") ?? document.querySelector(".App-toolbar .Stack_horizontal");
|
|
4717
7042
|
if (!container) {
|
|
4718
|
-
if (attempts++ < 20)
|
|
4719
|
-
timer = setTimeout(tryMount, 100);
|
|
4720
|
-
}
|
|
7043
|
+
if (attempts++ < 20) timer = setTimeout(tryMount, 100);
|
|
4721
7044
|
return;
|
|
4722
7045
|
}
|
|
4723
|
-
let wrapper = container.querySelector("#" +
|
|
7046
|
+
let wrapper = container.querySelector("#" + TOOLBAR_WRAPPER_ID);
|
|
4724
7047
|
if (!wrapper) {
|
|
4725
7048
|
wrapper = document.createElement("div");
|
|
4726
|
-
wrapper.id =
|
|
7049
|
+
wrapper.id = TOOLBAR_WRAPPER_ID;
|
|
4727
7050
|
wrapper.className = "Stamp-toolbar-injector";
|
|
4728
7051
|
wrapper.setAttribute("data-stamp-area", "true");
|
|
4729
7052
|
wrapper.style.display = "inline-flex";
|
|
@@ -4739,13 +7062,13 @@ function ToolbarInjector({
|
|
|
4739
7062
|
container.appendChild(wrapper);
|
|
4740
7063
|
}
|
|
4741
7064
|
}
|
|
4742
|
-
|
|
7065
|
+
apply(wrapper);
|
|
4743
7066
|
};
|
|
4744
7067
|
tryMount();
|
|
4745
7068
|
const root = document.querySelector(".excalidraw") ?? document.body;
|
|
4746
7069
|
observer = new MutationObserver(() => {
|
|
4747
7070
|
if (cancelled) return;
|
|
4748
|
-
const stillThere = document.getElementById(
|
|
7071
|
+
const stillThere = document.getElementById(TOOLBAR_WRAPPER_ID);
|
|
4749
7072
|
if (!stillThere) {
|
|
4750
7073
|
attempts = 0;
|
|
4751
7074
|
tryMount();
|
|
@@ -4756,11 +7079,73 @@ function ToolbarInjector({
|
|
|
4756
7079
|
cancelled = true;
|
|
4757
7080
|
if (timer) clearTimeout(timer);
|
|
4758
7081
|
observer?.disconnect();
|
|
4759
|
-
document.getElementById(
|
|
7082
|
+
document.getElementById(TOOLBAR_WRAPPER_ID)?.remove();
|
|
4760
7083
|
};
|
|
4761
|
-
}, [enabled]);
|
|
4762
|
-
|
|
4763
|
-
|
|
7084
|
+
}, [enabled, isMobile]);
|
|
7085
|
+
react.useEffect(() => {
|
|
7086
|
+
if (!enabled || !isMobile) {
|
|
7087
|
+
if (menuMountRef.current !== null) {
|
|
7088
|
+
menuMountRef.current = null;
|
|
7089
|
+
setMenuMount(null);
|
|
7090
|
+
}
|
|
7091
|
+
document.getElementById(MENU_WRAPPER_ID)?.remove();
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
let cancelled = false;
|
|
7095
|
+
let observer = null;
|
|
7096
|
+
let rafId = null;
|
|
7097
|
+
const apply = (next) => {
|
|
7098
|
+
if (cancelled || menuMountRef.current === next) return;
|
|
7099
|
+
menuMountRef.current = next;
|
|
7100
|
+
queueMicrotask(() => {
|
|
7101
|
+
if (!cancelled) setMenuMount(next);
|
|
7102
|
+
});
|
|
7103
|
+
};
|
|
7104
|
+
const findMenu = () => {
|
|
7105
|
+
if (cancelled) return;
|
|
7106
|
+
const container = document.querySelector(
|
|
7107
|
+
".dropdown-menu--mobile .dropdown-menu-container"
|
|
7108
|
+
);
|
|
7109
|
+
if (!container) {
|
|
7110
|
+
apply(null);
|
|
7111
|
+
return;
|
|
7112
|
+
}
|
|
7113
|
+
let wrapper = container.querySelector("#" + MENU_WRAPPER_ID);
|
|
7114
|
+
if (!wrapper) {
|
|
7115
|
+
wrapper = document.createElement("div");
|
|
7116
|
+
wrapper.id = MENU_WRAPPER_ID;
|
|
7117
|
+
wrapper.setAttribute("data-stamp-menu", "true");
|
|
7118
|
+
wrapper.style.display = "contents";
|
|
7119
|
+
container.insertBefore(wrapper, container.firstChild);
|
|
7120
|
+
}
|
|
7121
|
+
apply(wrapper);
|
|
7122
|
+
};
|
|
7123
|
+
const schedule = () => {
|
|
7124
|
+
if (rafId != null) return;
|
|
7125
|
+
rafId = requestAnimationFrame(() => {
|
|
7126
|
+
rafId = null;
|
|
7127
|
+
findMenu();
|
|
7128
|
+
});
|
|
7129
|
+
};
|
|
7130
|
+
findMenu();
|
|
7131
|
+
const root = document.querySelector(".excalidraw") ?? document.body;
|
|
7132
|
+
observer = new MutationObserver(schedule);
|
|
7133
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
7134
|
+
return () => {
|
|
7135
|
+
cancelled = true;
|
|
7136
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
7137
|
+
observer?.disconnect();
|
|
7138
|
+
document.getElementById(MENU_WRAPPER_ID)?.remove();
|
|
7139
|
+
};
|
|
7140
|
+
}, [enabled, isMobile]);
|
|
7141
|
+
if (!enabled) return null;
|
|
7142
|
+
const closeMobileMenu = () => {
|
|
7143
|
+
const trigger = document.querySelector(
|
|
7144
|
+
".App-toolbar__extra-tools-trigger"
|
|
7145
|
+
);
|
|
7146
|
+
trigger?.click();
|
|
7147
|
+
};
|
|
7148
|
+
const desktopButtons = !isMobile && toolbarMount ? reactDom.createPortal(
|
|
4764
7149
|
/* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: stamps.map((stamp) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4765
7150
|
StampToolButton,
|
|
4766
7151
|
{
|
|
@@ -4773,8 +7158,42 @@ function ToolbarInjector({
|
|
|
4773
7158
|
},
|
|
4774
7159
|
stamp.kind
|
|
4775
7160
|
)) }),
|
|
4776
|
-
|
|
4777
|
-
);
|
|
7161
|
+
toolbarMount
|
|
7162
|
+
) : null;
|
|
7163
|
+
const mobileMenuItems = isMobile && menuMount ? reactDom.createPortal(
|
|
7164
|
+
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
7165
|
+
stamps.map((stamp) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
7166
|
+
StampMenuItem,
|
|
7167
|
+
{
|
|
7168
|
+
icon: stamp.toolbarIcon,
|
|
7169
|
+
label: stamp.toolbarTitle,
|
|
7170
|
+
active: activeStampKind === stamp.kind,
|
|
7171
|
+
onClick: () => {
|
|
7172
|
+
onToggle(stamp.kind);
|
|
7173
|
+
closeMobileMenu();
|
|
7174
|
+
},
|
|
7175
|
+
dataTestId: stamp.toolbarTestId
|
|
7176
|
+
},
|
|
7177
|
+
stamp.kind
|
|
7178
|
+
)),
|
|
7179
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7180
|
+
"div",
|
|
7181
|
+
{
|
|
7182
|
+
"aria-hidden": "true",
|
|
7183
|
+
style: {
|
|
7184
|
+
height: 1,
|
|
7185
|
+
background: "var(--default-border-color, rgba(0,0,0,0.08))",
|
|
7186
|
+
margin: "6px 4px"
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
)
|
|
7190
|
+
] }),
|
|
7191
|
+
menuMount
|
|
7192
|
+
) : null;
|
|
7193
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
7194
|
+
desktopButtons,
|
|
7195
|
+
mobileMenuItems
|
|
7196
|
+
] });
|
|
4778
7197
|
}
|
|
4779
7198
|
function StampToolButton({ icon, keybind, label, active, onClick, dataTestId }) {
|
|
4780
7199
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -4839,6 +7258,54 @@ function StampToolButton({ icon, keybind, label, active, onClick, dataTestId })
|
|
|
4839
7258
|
}
|
|
4840
7259
|
);
|
|
4841
7260
|
}
|
|
7261
|
+
function StampMenuItem({ icon, label, active, onClick, dataTestId }) {
|
|
7262
|
+
const className = [
|
|
7263
|
+
"dropdown-menu-item",
|
|
7264
|
+
"dropdown-menu-item-base",
|
|
7265
|
+
active ? "dropdown-menu-item--selected" : ""
|
|
7266
|
+
].filter(Boolean).join(" ");
|
|
7267
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7268
|
+
"button",
|
|
7269
|
+
{
|
|
7270
|
+
type: "button",
|
|
7271
|
+
onClick,
|
|
7272
|
+
"aria-label": label,
|
|
7273
|
+
"aria-pressed": active,
|
|
7274
|
+
"data-testid": dataTestId,
|
|
7275
|
+
className,
|
|
7276
|
+
style: {
|
|
7277
|
+
display: "flex",
|
|
7278
|
+
alignItems: "center",
|
|
7279
|
+
columnGap: "0.625rem",
|
|
7280
|
+
width: "100%",
|
|
7281
|
+
boxSizing: "border-box",
|
|
7282
|
+
background: "transparent",
|
|
7283
|
+
border: "1px solid transparent",
|
|
7284
|
+
cursor: "pointer",
|
|
7285
|
+
fontFamily: "inherit",
|
|
7286
|
+
fontSize: "0.875rem",
|
|
7287
|
+
color: "var(--color-on-surface)"
|
|
7288
|
+
},
|
|
7289
|
+
children: [
|
|
7290
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7291
|
+
"span",
|
|
7292
|
+
{
|
|
7293
|
+
"aria-hidden": "true",
|
|
7294
|
+
style: {
|
|
7295
|
+
display: "inline-flex",
|
|
7296
|
+
alignItems: "center",
|
|
7297
|
+
justifyContent: "center",
|
|
7298
|
+
width: "1rem",
|
|
7299
|
+
height: "1rem"
|
|
7300
|
+
},
|
|
7301
|
+
children: icon
|
|
7302
|
+
}
|
|
7303
|
+
),
|
|
7304
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: label })
|
|
7305
|
+
]
|
|
7306
|
+
}
|
|
7307
|
+
);
|
|
7308
|
+
}
|
|
4842
7309
|
function isEditableTarget(t) {
|
|
4843
7310
|
if (!t || !(t instanceof HTMLElement)) return false;
|
|
4844
7311
|
if (t.isContentEditable) return true;
|
|
@@ -5156,7 +7623,9 @@ function Whiteboard({
|
|
|
5156
7623
|
stamps = DEFAULT_STAMPS
|
|
5157
7624
|
}) {
|
|
5158
7625
|
const [api, setApi] = react.useState(null);
|
|
7626
|
+
const apiRef = react.useRef(null);
|
|
5159
7627
|
const [isDarkTheme, setIsDarkTheme] = react.useState(false);
|
|
7628
|
+
const isDarkThemeRef = react.useRef(false);
|
|
5160
7629
|
const knownFileIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
5161
7630
|
const lastSceneHashRef = react.useRef("");
|
|
5162
7631
|
const sceneThrottleRef = react.useRef(null);
|
|
@@ -5217,7 +7686,10 @@ function Whiteboard({
|
|
|
5217
7686
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5218
7687
|
(elements, appState, files) => {
|
|
5219
7688
|
const nextDark = appState?.theme === "dark";
|
|
5220
|
-
|
|
7689
|
+
if (isDarkThemeRef.current !== nextDark) {
|
|
7690
|
+
isDarkThemeRef.current = nextDark;
|
|
7691
|
+
queueMicrotask(() => setIsDarkTheme(nextDark));
|
|
7692
|
+
}
|
|
5221
7693
|
if (readOnly) return;
|
|
5222
7694
|
latestSceneRef.current = { elements, appState };
|
|
5223
7695
|
const cropId = appState?.croppingElementId;
|
|
@@ -5227,12 +7699,17 @@ function Whiteboard({
|
|
|
5227
7699
|
const stamp = findStampForCustomData(el.customData, stamps);
|
|
5228
7700
|
if (stamp) {
|
|
5229
7701
|
handledCropIdRef.current = cropId;
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
7702
|
+
const elId = el.id;
|
|
7703
|
+
const elCustom = el.customData;
|
|
7704
|
+
const stampKind = stamp.kind;
|
|
7705
|
+
queueMicrotask(() => {
|
|
7706
|
+
try {
|
|
7707
|
+
api.updateScene({
|
|
7708
|
+
appState: { croppingElementId: null, selectedElementIds: {} }
|
|
7709
|
+
});
|
|
7710
|
+
} catch {
|
|
7711
|
+
}
|
|
7712
|
+
openStamp(stampKind, { id: elId, customData: elCustom });
|
|
5236
7713
|
});
|
|
5237
7714
|
return;
|
|
5238
7715
|
}
|
|
@@ -5483,8 +7960,12 @@ function Whiteboard({
|
|
|
5483
7960
|
Excalidraw2,
|
|
5484
7961
|
{
|
|
5485
7962
|
excalidrawAPI: (a) => {
|
|
5486
|
-
|
|
5487
|
-
|
|
7963
|
+
if (apiRef.current === a) return;
|
|
7964
|
+
apiRef.current = a;
|
|
7965
|
+
queueMicrotask(() => {
|
|
7966
|
+
setApi(a);
|
|
7967
|
+
onApi?.(a);
|
|
7968
|
+
});
|
|
5488
7969
|
},
|
|
5489
7970
|
langCode,
|
|
5490
7971
|
viewModeEnabled: readOnly,
|
|
@@ -5526,8 +8007,10 @@ exports.Whiteboard = Whiteboard;
|
|
|
5526
8007
|
exports.findStampForCustomData = findStampForCustomData;
|
|
5527
8008
|
exports.geometry3dStamp = geometry3dStamp;
|
|
5528
8009
|
exports.geometryStamp = geometryStamp;
|
|
8010
|
+
exports.graph2dStamp = graph2dStamp;
|
|
5529
8011
|
exports.isGeometry3DCustomData = isGeometry3DCustomData;
|
|
5530
8012
|
exports.isGeometryCustomData = isGeometryCustomData;
|
|
8013
|
+
exports.isGraph2DCustomData = isGraph2DCustomData;
|
|
5531
8014
|
exports.isLatexCustomData = isLatexCustomData;
|
|
5532
8015
|
exports.isStampElement = isStampElement;
|
|
5533
8016
|
exports.latexStamp = latexStamp;
|