@xom11/whiteboard 0.6.1 → 0.6.4
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 +3042 -549
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3035 -550
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
require('./index.css');
|
|
2
3
|
'use strict';
|
|
3
4
|
|
|
4
5
|
var excalidraw = require('@excalidraw/excalidraw');
|
|
@@ -6,16 +7,20 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
6
7
|
var dynamic = require('next/dynamic');
|
|
7
8
|
var react = require('react');
|
|
8
9
|
var reactDom = require('react-dom');
|
|
9
|
-
var JXG = require('jsxgraph');
|
|
10
10
|
require('@excalidraw/excalidraw/index.css');
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
|
|
14
14
|
var dynamic__default = /*#__PURE__*/_interopDefault(dynamic);
|
|
15
|
-
var JXG__default = /*#__PURE__*/_interopDefault(JXG);
|
|
16
15
|
|
|
17
16
|
var __defProp = Object.defineProperty;
|
|
18
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
|
+
});
|
|
19
24
|
var __esm = (fn, res) => function __init() {
|
|
20
25
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
21
26
|
};
|
|
@@ -293,6 +298,22 @@ var GROUP_LABELS = {
|
|
|
293
298
|
edit: "Ch\u1EC9nh s\u1EEDa",
|
|
294
299
|
transform: "Ph\xE9p bi\u1EBFn h\xECnh"
|
|
295
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
|
+
}
|
|
296
317
|
function objKind(obj) {
|
|
297
318
|
if (!obj) return "other";
|
|
298
319
|
const e = (obj.elType || obj.type || "").toString().toLowerCase();
|
|
@@ -678,9 +699,9 @@ function handleMove(ctx, e) {
|
|
|
678
699
|
if (!ctx.boardRef.current || !ctx.phantomRef.current) return;
|
|
679
700
|
try {
|
|
680
701
|
const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
|
|
681
|
-
const
|
|
682
|
-
if (!
|
|
683
|
-
ctx.phantomRef.current.setPositionDirectly(
|
|
702
|
+
const JXG = ctx.jxgRef.current;
|
|
703
|
+
if (!JXG) return;
|
|
704
|
+
ctx.phantomRef.current.setPositionDirectly(JXG.COORDS_BY_USER, [coords[0], coords[1]]);
|
|
684
705
|
ctx.boardRef.current.update();
|
|
685
706
|
} catch {
|
|
686
707
|
}
|
|
@@ -1487,11 +1508,11 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1487
1508
|
if (typeof window === "undefined" || !containerRef.current) return;
|
|
1488
1509
|
let cancelled = false;
|
|
1489
1510
|
(async () => {
|
|
1490
|
-
const
|
|
1511
|
+
const JXG = (await import('jsxgraph')).default;
|
|
1491
1512
|
if (cancelled || !containerRef.current) return;
|
|
1492
|
-
jxgRef.current =
|
|
1513
|
+
jxgRef.current = JXG;
|
|
1493
1514
|
try {
|
|
1494
|
-
const opts =
|
|
1515
|
+
const opts = JXG.Options;
|
|
1495
1516
|
if (opts) {
|
|
1496
1517
|
opts.text = opts.text || {};
|
|
1497
1518
|
opts.text.display = "internal";
|
|
@@ -1505,7 +1526,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1505
1526
|
}
|
|
1506
1527
|
} catch {
|
|
1507
1528
|
}
|
|
1508
|
-
const board =
|
|
1529
|
+
const board = JXG.JSXGraph.initBoard(containerId, {
|
|
1509
1530
|
boundingbox: initialState?.bbox ?? [-10, 10, 10, -10],
|
|
1510
1531
|
axis: false,
|
|
1511
1532
|
// We manage axis manually via toggle for clean default
|
|
@@ -1685,7 +1706,24 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1685
1706
|
});
|
|
1686
1707
|
onReady({
|
|
1687
1708
|
getContainer: () => containerRef.current,
|
|
1688
|
-
|
|
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
|
+
}),
|
|
1689
1727
|
getBbox: () => boardRef.current ? boardRef.current.getBoundingBox() : [-10, 10, 10, -10],
|
|
1690
1728
|
getShowAxis: () => showAxisRef.current,
|
|
1691
1729
|
getShowGrid: () => showGridRef.current,
|
|
@@ -1883,8 +1921,140 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1883
1921
|
}
|
|
1884
1922
|
);
|
|
1885
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
|
+
}
|
|
1886
2056
|
var TOOLTIP_DELAY_MS = 400;
|
|
1887
|
-
function Shell({ title, icon, onClose, children, isDark }) {
|
|
2057
|
+
function Shell({ title, icon, onClose, children, isDark, closeLabel = "\u0110\xF3ng" }) {
|
|
1888
2058
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1889
2059
|
"aside",
|
|
1890
2060
|
{
|
|
@@ -1892,7 +2062,10 @@ function Shell({ title, icon, onClose, children, isDark }) {
|
|
|
1892
2062
|
"aria-label": title,
|
|
1893
2063
|
"data-testid": "stamp-left-panel",
|
|
1894
2064
|
"data-stamp-area": "true",
|
|
1895
|
-
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(""),
|
|
1896
2069
|
children: [
|
|
1897
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: [
|
|
1898
2071
|
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -1903,12 +2076,9 @@ function Shell({ title, icon, onClose, children, isDark }) {
|
|
|
1903
2076
|
"button",
|
|
1904
2077
|
{
|
|
1905
2078
|
onClick: onClose,
|
|
1906
|
-
"aria-label":
|
|
2079
|
+
"aria-label": closeLabel,
|
|
1907
2080
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
1908
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
1909
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1910
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1911
|
-
] })
|
|
2081
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
|
|
1912
2082
|
}
|
|
1913
2083
|
)
|
|
1914
2084
|
] }),
|
|
@@ -1929,24 +2099,36 @@ var GeometryIconHeader = /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", h
|
|
|
1929
2099
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
|
|
1930
2100
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" })
|
|
1931
2101
|
] });
|
|
1932
|
-
function
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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() {
|
|
1950
2132
|
const [hover, setHover] = react.useState(null);
|
|
1951
2133
|
const [portalReady, setPortalReady] = react.useState(false);
|
|
1952
2134
|
const hoverTimerRef = react.useRef(null);
|
|
@@ -1970,6 +2152,23 @@ function LeftPanel({
|
|
|
1970
2152
|
}
|
|
1971
2153
|
setHover(null);
|
|
1972
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();
|
|
1973
2172
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1974
2173
|
/* @__PURE__ */ jsxRuntime.jsxs(Shell, { title: "H\xECnh h\u1ECDc", icon: GeometryIconHeader, onClose, isDark, children: [
|
|
1975
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: [
|
|
@@ -2006,36 +2205,95 @@ function LeftPanel({
|
|
|
2006
2205
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
2007
2206
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
2008
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",
|
|
2009
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
2010
|
-
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
2011
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
2012
|
-
] })
|
|
2208
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
|
|
2013
2209
|
}
|
|
2014
2210
|
)
|
|
2015
2211
|
] }) }),
|
|
2016
|
-
groupKeys.map((group) =>
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2212
|
+
groupKeys.map((group) => {
|
|
2213
|
+
const isChordActive = chordGroup === group;
|
|
2214
|
+
const dimmed = chordGroup !== null && !isChordActive;
|
|
2215
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2216
|
+
"section",
|
|
2020
2217
|
{
|
|
2021
|
-
|
|
2022
|
-
"
|
|
2023
|
-
"aria-pressed": active,
|
|
2024
|
-
"data-tool": t.key,
|
|
2025
|
-
onClick: () => onToolChange(t.key),
|
|
2026
|
-
onMouseEnter: (e) => showHover(e.currentTarget, t),
|
|
2027
|
-
onMouseLeave: hideHover,
|
|
2028
|
-
onFocus: (e) => showHover(e.currentTarget, t),
|
|
2029
|
-
onBlur: hideHover,
|
|
2218
|
+
"data-chord-group": group,
|
|
2219
|
+
"data-chord-active": isChordActive ? "true" : "false",
|
|
2030
2220
|
className: [
|
|
2031
|
-
"
|
|
2032
|
-
|
|
2221
|
+
"rounded-md transition",
|
|
2222
|
+
isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
|
|
2223
|
+
dimmed ? "opacity-55" : "opacity-100"
|
|
2033
2224
|
].join(" "),
|
|
2034
|
-
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
|
+
]
|
|
2035
2277
|
},
|
|
2036
|
-
|
|
2278
|
+
group
|
|
2037
2279
|
);
|
|
2038
|
-
})
|
|
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
|
+
)
|
|
2039
2297
|
] }),
|
|
2040
2298
|
portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
|
|
2041
2299
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -2059,6 +2317,78 @@ function LeftPanel({
|
|
|
2059
2317
|
) : null
|
|
2060
2318
|
] });
|
|
2061
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
|
+
}
|
|
2062
2392
|
|
|
2063
2393
|
// src/stamps/geometry-2d/serialize.ts
|
|
2064
2394
|
function serializeBoard(board, log, options = {}) {
|
|
@@ -2137,9 +2467,9 @@ function renderGeometryToSvg(boardContainer) {
|
|
|
2137
2467
|
async function renderGeometrySvgFromState(jsonState) {
|
|
2138
2468
|
const parsed = JSON.parse(jsonState);
|
|
2139
2469
|
const palette = paletteFor(false);
|
|
2140
|
-
const
|
|
2470
|
+
const JXG = (await import('jsxgraph')).default;
|
|
2141
2471
|
try {
|
|
2142
|
-
const opts =
|
|
2472
|
+
const opts = JXG.Options;
|
|
2143
2473
|
if (opts) {
|
|
2144
2474
|
opts.text = opts.text || {};
|
|
2145
2475
|
opts.text.display = "internal";
|
|
@@ -2164,7 +2494,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
2164
2494
|
document.body.appendChild(container);
|
|
2165
2495
|
let board = null;
|
|
2166
2496
|
try {
|
|
2167
|
-
board =
|
|
2497
|
+
board = JXG.JSXGraph.initBoard(containerId, {
|
|
2168
2498
|
boundingbox: parsed.bbox,
|
|
2169
2499
|
axis: !!parsed.showAxis,
|
|
2170
2500
|
grid: !!parsed.showGrid,
|
|
@@ -2177,7 +2507,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
2177
2507
|
return renderGeometryToSvg(container);
|
|
2178
2508
|
} finally {
|
|
2179
2509
|
try {
|
|
2180
|
-
if (board)
|
|
2510
|
+
if (board) JXG.JSXGraph.freeBoard(board);
|
|
2181
2511
|
} catch {
|
|
2182
2512
|
}
|
|
2183
2513
|
if (container.parentNode) container.parentNode.removeChild(container);
|
|
@@ -2203,6 +2533,38 @@ var STROKE_PALETTE = [
|
|
|
2203
2533
|
"#868e96"
|
|
2204
2534
|
// gray
|
|
2205
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
|
+
}
|
|
2206
2568
|
var DASH_OPTIONS = [
|
|
2207
2569
|
{ value: 0, label: "N\xE9t li\u1EC1n" },
|
|
2208
2570
|
{ value: 2, label: "N\xE9t \u0111\u1EE9t" },
|
|
@@ -2261,6 +2623,26 @@ var PropertiesPopover = (props) => {
|
|
|
2261
2623
|
const { anchor, onClose, onMutate, isDark, getAllNames } = props;
|
|
2262
2624
|
const rootRef = react.useRef(null);
|
|
2263
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]);
|
|
2264
2646
|
const initialName = props.kind === "point" ? props.currentName : props.kind === "line" || props.kind === "circle" ? props.currentName : "";
|
|
2265
2647
|
const [name, setName] = react.useState(initialName);
|
|
2266
2648
|
react.useEffect(() => {
|
|
@@ -2273,14 +2655,14 @@ var PropertiesPopover = (props) => {
|
|
|
2273
2655
|
onClose();
|
|
2274
2656
|
}
|
|
2275
2657
|
};
|
|
2276
|
-
const
|
|
2658
|
+
const onPointerDown = (e) => {
|
|
2277
2659
|
if (!rootRef.current?.contains(e.target)) onClose();
|
|
2278
2660
|
};
|
|
2279
2661
|
document.addEventListener("keydown", onKey);
|
|
2280
|
-
document.addEventListener("
|
|
2662
|
+
document.addEventListener("pointerdown", onPointerDown, { capture: true });
|
|
2281
2663
|
return () => {
|
|
2282
2664
|
document.removeEventListener("keydown", onKey);
|
|
2283
|
-
document.removeEventListener("
|
|
2665
|
+
document.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
2284
2666
|
};
|
|
2285
2667
|
}, [onClose]);
|
|
2286
2668
|
const pickColor = (c) => {
|
|
@@ -2317,6 +2699,7 @@ var PropertiesPopover = (props) => {
|
|
|
2317
2699
|
{
|
|
2318
2700
|
type: "button",
|
|
2319
2701
|
"data-section": id,
|
|
2702
|
+
"data-pill-btn": id,
|
|
2320
2703
|
"aria-label": label,
|
|
2321
2704
|
"aria-pressed": !!active,
|
|
2322
2705
|
onClick,
|
|
@@ -2335,13 +2718,14 @@ var PropertiesPopover = (props) => {
|
|
|
2335
2718
|
}
|
|
2336
2719
|
);
|
|
2337
2720
|
const colorIndicatorTint = react.useMemo(() => currentColor, [currentColor]);
|
|
2721
|
+
const pos = clamped ?? { left: anchor.x, top: anchor.y };
|
|
2338
2722
|
const node = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2339
2723
|
"div",
|
|
2340
2724
|
{
|
|
2341
2725
|
ref: rootRef,
|
|
2342
2726
|
"data-stamp-area": "true",
|
|
2343
2727
|
className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-1.5`,
|
|
2344
|
-
style: { left:
|
|
2728
|
+
style: { left: pos.left, top: pos.top },
|
|
2345
2729
|
role: "dialog",
|
|
2346
2730
|
"aria-label": "Thu\u1ED9c t\xEDnh \u0111\u1ED1i t\u01B0\u1EE3ng",
|
|
2347
2731
|
children: [
|
|
@@ -2531,7 +2915,7 @@ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel,
|
|
|
2531
2915
|
return reactDom.createPortal(node, document.body);
|
|
2532
2916
|
};
|
|
2533
2917
|
var GeometryEditorPanel = react.forwardRef(
|
|
2534
|
-
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark }, ref) {
|
|
2918
|
+
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer }, ref) {
|
|
2535
2919
|
const handleRef = react.useRef(null);
|
|
2536
2920
|
const [ready, setReady] = react.useState(false);
|
|
2537
2921
|
const [propsPopover, setPropsPopover] = react.useState(null);
|
|
@@ -2593,7 +2977,7 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2593
2977
|
insert: performInsert,
|
|
2594
2978
|
hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
|
|
2595
2979
|
}), [performInsert]);
|
|
2596
|
-
const wrapperStyle = {
|
|
2980
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
2597
2981
|
position: "absolute",
|
|
2598
2982
|
top: "50%",
|
|
2599
2983
|
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
@@ -2607,11 +2991,30 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2607
2991
|
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
|
|
2608
2992
|
"data-testid": "geometry-editor-panel",
|
|
2609
2993
|
"data-stamp-area": "true",
|
|
2994
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
2610
2995
|
style: wrapperStyle,
|
|
2611
|
-
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(" "),
|
|
2612
3001
|
children: [
|
|
2613
|
-
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center
|
|
2614
|
-
/* @__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: [
|
|
2615
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: [
|
|
2616
3019
|
/* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "3,18 12,3 21,18" }),
|
|
2617
3020
|
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
|
|
@@ -2620,12 +3023,23 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2620
3023
|
] }),
|
|
2621
3024
|
"D\u1EF1ng h\xECnh h\u1ECDc"
|
|
2622
3025
|
] }),
|
|
2623
|
-
|
|
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: [
|
|
2624
3038
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2625
3039
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2626
3040
|
] }) })
|
|
2627
3041
|
] }),
|
|
2628
|
-
/* @__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(
|
|
2629
3043
|
JSXGraphMiniBoard,
|
|
2630
3044
|
{
|
|
2631
3045
|
onReady: handleReady,
|
|
@@ -2698,7 +3112,7 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2698
3112
|
}
|
|
2699
3113
|
}
|
|
2700
3114
|
),
|
|
2701
|
-
/* @__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: [
|
|
2702
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." }),
|
|
2703
3117
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
2704
3118
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2726,6 +3140,76 @@ var GeometryEditorPanel = react.forwardRef(
|
|
|
2726
3140
|
);
|
|
2727
3141
|
}
|
|
2728
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
|
+
}
|
|
2729
3213
|
|
|
2730
3214
|
// src/stamps/shared/svgToImage.ts
|
|
2731
3215
|
async function hashString(input) {
|
|
@@ -2848,6 +3332,14 @@ var GeometryStampHost = react.forwardRef(
|
|
|
2848
3332
|
function GeometryStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
2849
3333
|
const panelRef = react.useRef(null);
|
|
2850
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
|
+
});
|
|
2851
3343
|
const initialState = react.useMemo(() => {
|
|
2852
3344
|
if (!editingElement) return null;
|
|
2853
3345
|
if (!isGeometryCustomData(editingElement.customData)) return null;
|
|
@@ -2901,7 +3393,11 @@ var GeometryStampHost = react.forwardRef(
|
|
|
2901
3393
|
onUndo: () => panelRef.current?.undo(),
|
|
2902
3394
|
canUndo: geomState.canUndo,
|
|
2903
3395
|
onClose,
|
|
2904
|
-
isDark
|
|
3396
|
+
isDark,
|
|
3397
|
+
isMobile,
|
|
3398
|
+
drawerOpen,
|
|
3399
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
3400
|
+
chordGroup
|
|
2905
3401
|
}
|
|
2906
3402
|
),
|
|
2907
3403
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2912,8 +3408,10 @@ var GeometryStampHost = react.forwardRef(
|
|
|
2912
3408
|
onInsert: handleInsert,
|
|
2913
3409
|
onClose,
|
|
2914
3410
|
onStateChange: setGeomState,
|
|
2915
|
-
withLeftPanel:
|
|
2916
|
-
isDark
|
|
3411
|
+
withLeftPanel: !isMobile,
|
|
3412
|
+
isDark,
|
|
3413
|
+
isMobile,
|
|
3414
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
2917
3415
|
}
|
|
2918
3416
|
)
|
|
2919
3417
|
] });
|
|
@@ -2951,38 +3449,58 @@ var geometryStamp = {
|
|
|
2951
3449
|
},
|
|
2952
3450
|
Host: GeometryStampHost
|
|
2953
3451
|
};
|
|
2954
|
-
function Shell2({ title, icon, onClose, children }) {
|
|
2955
|
-
|
|
2956
|
-
"
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
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
|
+
)
|
|
2968
3498
|
] }),
|
|
2969
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
2975
|
-
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: [
|
|
2976
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2977
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2978
|
-
] })
|
|
2979
|
-
}
|
|
2980
|
-
)
|
|
2981
|
-
] }),
|
|
2982
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
2983
|
-
]
|
|
2984
|
-
}
|
|
2985
|
-
);
|
|
3499
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
3500
|
+
]
|
|
3501
|
+
}
|
|
3502
|
+
)
|
|
3503
|
+
] });
|
|
2986
3504
|
}
|
|
2987
3505
|
function Section2({ label, children }) {
|
|
2988
3506
|
return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
|
|
@@ -3029,65 +3547,80 @@ function LeftPanel2({
|
|
|
3029
3547
|
displayMode,
|
|
3030
3548
|
onDisplayModeChange,
|
|
3031
3549
|
onInsertSnippet,
|
|
3032
|
-
onClose
|
|
3550
|
+
onClose,
|
|
3551
|
+
isMobile,
|
|
3552
|
+
drawerOpen,
|
|
3553
|
+
onDrawerClose
|
|
3033
3554
|
}) {
|
|
3034
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
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
|
-
|
|
3090
|
-
|
|
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
|
+
);
|
|
3091
3624
|
}
|
|
3092
3625
|
|
|
3093
3626
|
// src/stamps/latex/render.ts
|
|
@@ -3139,7 +3672,9 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3139
3672
|
onClose,
|
|
3140
3673
|
displayMode: controlledDisplayMode,
|
|
3141
3674
|
onDisplayModeChange,
|
|
3142
|
-
withLeftPanel = false
|
|
3675
|
+
withLeftPanel = false,
|
|
3676
|
+
isMobile = false,
|
|
3677
|
+
onOpenDrawer
|
|
3143
3678
|
}, ref) {
|
|
3144
3679
|
const [value, setValue] = react.useState(initialValue);
|
|
3145
3680
|
const [internalDisplayMode] = react.useState(false);
|
|
@@ -3210,7 +3745,7 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3210
3745
|
[value, previewSvg, error, displayMode, onInsert]
|
|
3211
3746
|
);
|
|
3212
3747
|
const isLegacyPosition = x > 0 || y > 0;
|
|
3213
|
-
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 } : {
|
|
3214
3749
|
position: "absolute",
|
|
3215
3750
|
top: "50%",
|
|
3216
3751
|
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
@@ -3222,29 +3757,55 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3222
3757
|
{
|
|
3223
3758
|
style: wrapperStyle,
|
|
3224
3759
|
"data-stamp-area": "true",
|
|
3225
|
-
|
|
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",
|
|
3226
3762
|
role: "dialog",
|
|
3227
3763
|
"aria-label": "Nh\u1EADp c\xF4ng th\u1EE9c LaTeX",
|
|
3228
3764
|
children: [
|
|
3229
|
-
/* @__PURE__ */ jsxRuntime.jsxs("header", { className:
|
|
3230
|
-
/* @__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: [
|
|
3231
3781
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: "\u2211" }),
|
|
3232
3782
|
"C\xF4ng th\u1EE9c LaTeX"
|
|
3233
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
|
+
),
|
|
3234
3795
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3235
3796
|
"button",
|
|
3236
3797
|
{
|
|
3237
3798
|
onClick: onClose,
|
|
3238
3799
|
"aria-label": "\u0110\xF3ng",
|
|
3239
|
-
className: "
|
|
3240
|
-
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: [
|
|
3241
3802
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
3242
3803
|
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
3243
3804
|
] })
|
|
3244
3805
|
}
|
|
3245
3806
|
)
|
|
3246
3807
|
] }),
|
|
3247
|
-
/* @__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: [
|
|
3248
3809
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3249
3810
|
"input",
|
|
3250
3811
|
{
|
|
@@ -3255,7 +3816,7 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3255
3816
|
onChange: (e) => setValue(e.target.value),
|
|
3256
3817
|
onKeyDown: handleKeyDown,
|
|
3257
3818
|
placeholder: "Vd: \\frac{a^2+b^2}{c}",
|
|
3258
|
-
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"}`,
|
|
3259
3820
|
autoFocus: true
|
|
3260
3821
|
}
|
|
3261
3822
|
),
|
|
@@ -3263,7 +3824,8 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3263
3824
|
"div",
|
|
3264
3825
|
{
|
|
3265
3826
|
className: [
|
|
3266
|
-
"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]",
|
|
3267
3829
|
error ? "border-rose-300 bg-rose-50 text-rose-700" : "border-slate-200 bg-slate-50"
|
|
3268
3830
|
].join(" "),
|
|
3269
3831
|
children: error ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs", children: [
|
|
@@ -3272,7 +3834,7 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3272
3834
|
] }) : previewSvg ? /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: previewSvg } }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-400", children: "(xem tr\u01B0\u1EDBc)" })
|
|
3273
3835
|
}
|
|
3274
3836
|
),
|
|
3275
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3837
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3276
3838
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-slate-500", children: [
|
|
3277
3839
|
displayMode ? "Block" : "Inline",
|
|
3278
3840
|
" \xB7 Enter \u0111\u1EC3 ch\xE8n"
|
|
@@ -3296,6 +3858,10 @@ var EditorPopover = react.forwardRef(function EditorPopover2({
|
|
|
3296
3858
|
}
|
|
3297
3859
|
)
|
|
3298
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"
|
|
3299
3865
|
] })
|
|
3300
3866
|
] })
|
|
3301
3867
|
]
|
|
@@ -3310,6 +3876,8 @@ function isLatexCustomData(data) {
|
|
|
3310
3876
|
var LatexStampHost = react.forwardRef(
|
|
3311
3877
|
function LatexStampHost2({ api, editingElement, onClose }, ref) {
|
|
3312
3878
|
const editorRef = react.useRef(null);
|
|
3879
|
+
const { isMobile } = useIsMobile();
|
|
3880
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
3313
3881
|
const initial = react.useMemo(() => {
|
|
3314
3882
|
if (editingElement && isLatexCustomData(editingElement.customData)) {
|
|
3315
3883
|
return {
|
|
@@ -3356,7 +3924,10 @@ var LatexStampHost = react.forwardRef(
|
|
|
3356
3924
|
displayMode,
|
|
3357
3925
|
onDisplayModeChange: setDisplayMode,
|
|
3358
3926
|
onInsertSnippet: (s) => editorRef.current?.insertAtCursor(s),
|
|
3359
|
-
onClose
|
|
3927
|
+
onClose,
|
|
3928
|
+
isMobile,
|
|
3929
|
+
drawerOpen,
|
|
3930
|
+
onDrawerClose: () => setDrawerOpen(false)
|
|
3360
3931
|
}
|
|
3361
3932
|
),
|
|
3362
3933
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3370,7 +3941,9 @@ var LatexStampHost = react.forwardRef(
|
|
|
3370
3941
|
onDisplayModeChange: setDisplayMode,
|
|
3371
3942
|
onInsert: handleInsert,
|
|
3372
3943
|
onClose,
|
|
3373
|
-
withLeftPanel:
|
|
3944
|
+
withLeftPanel: !isMobile,
|
|
3945
|
+
isMobile,
|
|
3946
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
3374
3947
|
}
|
|
3375
3948
|
)
|
|
3376
3949
|
] });
|
|
@@ -3849,126 +4422,134 @@ var MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState
|
|
|
3849
4422
|
react.useEffect(() => {
|
|
3850
4423
|
const div = containerRef.current;
|
|
3851
4424
|
if (!div) return;
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
4425
|
+
let cancelled = false;
|
|
4426
|
+
let JXG = null;
|
|
4427
|
+
let board = null;
|
|
4428
|
+
void (async () => {
|
|
4429
|
+
JXG = (await import('jsxgraph')).default;
|
|
4430
|
+
if (cancelled || !containerRef.current) return;
|
|
4431
|
+
JXG.Options.text.display = "internal";
|
|
4432
|
+
board = JXG.JSXGraph.initBoard(div, {
|
|
4433
|
+
boundingbox: [-6, 6, 6, -6],
|
|
4434
|
+
axis: false,
|
|
4435
|
+
showCopyright: false,
|
|
4436
|
+
showNavigation: false,
|
|
4437
|
+
renderer: "svg"
|
|
4438
|
+
});
|
|
4439
|
+
boardRef.current = board;
|
|
4440
|
+
const initView = initialState?.view ?? DEFAULT_VIEW3D;
|
|
4441
|
+
const baseAttrs = VIEW3D_ATTRS(isDark);
|
|
4442
|
+
const view = board.create(
|
|
4443
|
+
"view3d",
|
|
3868
4444
|
[
|
|
3869
|
-
[
|
|
3870
|
-
[
|
|
3871
|
-
[
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
view
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
4445
|
+
[-5, -5],
|
|
4446
|
+
[10, 10],
|
|
4447
|
+
[
|
|
4448
|
+
[initView.bbox3D[0], initView.bbox3D[3]],
|
|
4449
|
+
[initView.bbox3D[1], initView.bbox3D[4]],
|
|
4450
|
+
[initView.bbox3D[2], initView.bbox3D[5]]
|
|
4451
|
+
]
|
|
4452
|
+
],
|
|
4453
|
+
{
|
|
4454
|
+
...baseAttrs,
|
|
4455
|
+
az: { ...baseAttrs.az, value: initView.azimuth },
|
|
4456
|
+
el: { ...baseAttrs.el, value: initView.elevation }
|
|
4457
|
+
}
|
|
4458
|
+
);
|
|
4459
|
+
viewRef.current = view;
|
|
4460
|
+
let idCounter = 1;
|
|
4461
|
+
const ctx = createHandlerContext({
|
|
4462
|
+
view,
|
|
4463
|
+
pushLog: (e) => {
|
|
4464
|
+
logRef.current.push(e);
|
|
4465
|
+
notify();
|
|
4466
|
+
},
|
|
4467
|
+
objMap: objMapRef.current,
|
|
4468
|
+
nextId: () => `obj_${Date.now().toString(36)}_${(idCounter++).toString(36)}`,
|
|
4469
|
+
isDark,
|
|
4470
|
+
promptCoords: (label) => {
|
|
4471
|
+
const raw = window.prompt(`${label}
|
|
3893
4472
|
(\u0111\u1ECBnh d\u1EA1ng "x,y,z")`, "0,0,0");
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
}
|
|
3928
|
-
return void 0;
|
|
3929
|
-
}
|
|
3930
|
-
const handlePointerDown = (e) => {
|
|
3931
|
-
const tool = toolRef.current;
|
|
3932
|
-
if (tool === "move") return;
|
|
3933
|
-
const existingPointId = findExistingPointAt(e.clientX, e.clientY);
|
|
3934
|
-
let x3 = 0;
|
|
3935
|
-
let y3 = 0;
|
|
3936
|
-
const z3 = 0;
|
|
3937
|
-
try {
|
|
3938
|
-
const board2d = boardRef.current;
|
|
3939
|
-
if (board2d?.getUsrCoordsOfMouse) {
|
|
3940
|
-
const uc = board2d.getUsrCoordsOfMouse(e);
|
|
3941
|
-
if (Array.isArray(uc) && uc.length >= 2) {
|
|
3942
|
-
x3 = uc[0];
|
|
3943
|
-
y3 = uc[1];
|
|
3944
|
-
}
|
|
4473
|
+
if (!raw) return null;
|
|
4474
|
+
const parts = raw.split(",").map((s) => Number(s.trim()));
|
|
4475
|
+
if (parts.length !== 3 || parts.some((n) => !isFinite(n))) return null;
|
|
4476
|
+
return { x: parts[0], y: parts[1], z: parts[2] };
|
|
4477
|
+
},
|
|
4478
|
+
promptNumber: (label) => {
|
|
4479
|
+
const raw = window.prompt(label, "1");
|
|
4480
|
+
if (raw == null) return null;
|
|
4481
|
+
const n = Number(raw);
|
|
4482
|
+
return isFinite(n) ? n : null;
|
|
4483
|
+
},
|
|
4484
|
+
promptText: (label) => {
|
|
4485
|
+
const raw = window.prompt(label, "");
|
|
4486
|
+
return raw == null ? null : raw;
|
|
4487
|
+
},
|
|
4488
|
+
notify
|
|
4489
|
+
});
|
|
4490
|
+
ctxRef.current = ctx;
|
|
4491
|
+
function findExistingPointAt(clientX, clientY) {
|
|
4492
|
+
const containerRect = div.getBoundingClientRect();
|
|
4493
|
+
const localX = clientX - containerRect.left;
|
|
4494
|
+
const localY = clientY - containerRect.top;
|
|
4495
|
+
const PICK = 18;
|
|
4496
|
+
const svg = div.querySelector("svg");
|
|
4497
|
+
if (!svg) return void 0;
|
|
4498
|
+
for (const [id, obj] of objMapRef.current) {
|
|
4499
|
+
const entry = obj;
|
|
4500
|
+
if (entry?.elType !== "point3d") continue;
|
|
4501
|
+
const sc = entry.element2D?.coords?.scrCoords;
|
|
4502
|
+
if (!sc || sc.length < 3) continue;
|
|
4503
|
+
const dx = sc[1] - localX;
|
|
4504
|
+
const dy = sc[2] - localY;
|
|
4505
|
+
if (dx * dx + dy * dy <= PICK * PICK) return id;
|
|
3945
4506
|
}
|
|
3946
|
-
|
|
4507
|
+
return void 0;
|
|
3947
4508
|
}
|
|
3948
|
-
const
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
}
|
|
3967
|
-
|
|
3968
|
-
|
|
4509
|
+
const handlePointerDown = (e) => {
|
|
4510
|
+
const tool = toolRef.current;
|
|
4511
|
+
if (tool === "move") return;
|
|
4512
|
+
const existingPointId = findExistingPointAt(e.clientX, e.clientY);
|
|
4513
|
+
let x3 = 0;
|
|
4514
|
+
let y3 = 0;
|
|
4515
|
+
const z3 = 0;
|
|
4516
|
+
try {
|
|
4517
|
+
const board2d = boardRef.current;
|
|
4518
|
+
if (board2d?.getUsrCoordsOfMouse) {
|
|
4519
|
+
const uc = board2d.getUsrCoordsOfMouse(e);
|
|
4520
|
+
if (Array.isArray(uc) && uc.length >= 2) {
|
|
4521
|
+
x3 = uc[0];
|
|
4522
|
+
y3 = uc[1];
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
} catch {
|
|
4526
|
+
}
|
|
4527
|
+
const hit = { x3, y3, z3, existingPointId };
|
|
4528
|
+
handleToolStep(ctx, tool, hit);
|
|
4529
|
+
};
|
|
4530
|
+
const svgEl = div.querySelector("svg");
|
|
4531
|
+
const targetEl = svgEl ?? div;
|
|
4532
|
+
const handlePointerDownEv = (e) => handlePointerDown(e);
|
|
4533
|
+
targetEl.addEventListener("pointerdown", handlePointerDownEv);
|
|
4534
|
+
pointerHandlerRef.current = { el: targetEl, fn: handlePointerDownEv };
|
|
4535
|
+
if (initialState?.elements?.length) {
|
|
4536
|
+
const map = objMapRef.current;
|
|
4537
|
+
for (const el of initialState.elements) {
|
|
4538
|
+
const parents = el.parents.map(
|
|
4539
|
+
(p2) => typeof p2 === "string" && p2.startsWith("@id:") ? map.get(p2.slice(4)) : p2
|
|
4540
|
+
);
|
|
4541
|
+
const obj = view.create(el.type, parents, {
|
|
4542
|
+
...el.attributes,
|
|
4543
|
+
id: el.id,
|
|
4544
|
+
name: el.label
|
|
4545
|
+
});
|
|
4546
|
+
map.set(el.id, obj);
|
|
4547
|
+
logRef.current.push(el);
|
|
4548
|
+
}
|
|
3969
4549
|
}
|
|
3970
|
-
}
|
|
4550
|
+
})();
|
|
3971
4551
|
return () => {
|
|
4552
|
+
cancelled = true;
|
|
3972
4553
|
if (pointerHandlerRef.current) {
|
|
3973
4554
|
pointerHandlerRef.current.el.removeEventListener(
|
|
3974
4555
|
"pointerdown",
|
|
@@ -3977,7 +4558,7 @@ var MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState
|
|
|
3977
4558
|
pointerHandlerRef.current = null;
|
|
3978
4559
|
}
|
|
3979
4560
|
try {
|
|
3980
|
-
|
|
4561
|
+
if (board && JXG) JXG.JSXGraph.freeBoard(board);
|
|
3981
4562
|
} catch {
|
|
3982
4563
|
}
|
|
3983
4564
|
boardRef.current = null;
|
|
@@ -3994,7 +4575,25 @@ var MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState
|
|
|
3994
4575
|
toolRef.current = t;
|
|
3995
4576
|
notify();
|
|
3996
4577
|
},
|
|
3997
|
-
|
|
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
|
+
}),
|
|
3998
4597
|
pushLog: (e) => {
|
|
3999
4598
|
logRef.current.push(e);
|
|
4000
4599
|
notify();
|
|
@@ -4070,6 +4669,138 @@ var MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState
|
|
|
4070
4669
|
}
|
|
4071
4670
|
);
|
|
4072
4671
|
});
|
|
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
|
+
});
|
|
4073
4804
|
|
|
4074
4805
|
// src/stamps/geometry-3d/editor/tools.ts
|
|
4075
4806
|
var GROUP_LABELS_3D = {
|
|
@@ -4079,6 +4810,18 @@ var GROUP_LABELS_3D = {
|
|
|
4079
4810
|
curved: "Kh\u1ED1i cong",
|
|
4080
4811
|
meta: "Kh\xE1c"
|
|
4081
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
|
+
}
|
|
4082
4825
|
var TOOLS_3D = [
|
|
4083
4826
|
{ key: "move", label: "Di chuy\u1EC3n", group: "view", stepsRequired: 0 },
|
|
4084
4827
|
{ key: "point", label: "\u0110i\u1EC3m", group: "primitive", stepsRequired: 1, hint: "Nh\u1EADp (x, y, z)" },
|
|
@@ -4132,8 +4875,8 @@ var TOOLS_3D = [
|
|
|
4132
4875
|
},
|
|
4133
4876
|
{ key: "label", label: "Nh\xE3n", group: "meta", stepsRequired: 1, hint: "G\u1EAFn v\xE0o \u0111i\u1EC3m" }
|
|
4134
4877
|
];
|
|
4135
|
-
function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
4136
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4878
|
+
function ToolButton({ toolKey, label, hint, active, onClick, icon, badge }) {
|
|
4879
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4137
4880
|
"button",
|
|
4138
4881
|
{
|
|
4139
4882
|
type: "button",
|
|
@@ -4144,10 +4887,13 @@ function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
|
4144
4887
|
"data-active": active || void 0,
|
|
4145
4888
|
"data-tool": toolKey,
|
|
4146
4889
|
className: [
|
|
4147
|
-
"flex h-8 items-center justify-center rounded-md transition",
|
|
4890
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
4148
4891
|
active ? "bg-blue-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
4149
4892
|
].join(" "),
|
|
4150
|
-
children:
|
|
4893
|
+
children: [
|
|
4894
|
+
icon,
|
|
4895
|
+
badge
|
|
4896
|
+
]
|
|
4151
4897
|
}
|
|
4152
4898
|
);
|
|
4153
4899
|
}
|
|
@@ -4199,7 +4945,10 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4199
4945
|
"aria-label": title,
|
|
4200
4946
|
"data-testid": "geom3d-left-panel",
|
|
4201
4947
|
"data-stamp-area": "true",
|
|
4202
|
-
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(""),
|
|
4203
4952
|
children: [
|
|
4204
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: [
|
|
4205
4954
|
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -4212,10 +4961,7 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4212
4961
|
onClick: onClose,
|
|
4213
4962
|
"aria-label": "\u0110\xF3ng",
|
|
4214
4963
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
4215
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4216
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4217
|
-
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4218
|
-
] })
|
|
4964
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon2, {})
|
|
4219
4965
|
}
|
|
4220
4966
|
)
|
|
4221
4967
|
] }),
|
|
@@ -4231,11 +4977,39 @@ function Section3({ label, children }) {
|
|
|
4231
4977
|
] });
|
|
4232
4978
|
}
|
|
4233
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" }) });
|
|
4234
|
-
function
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
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() {
|
|
4239
5013
|
const [hover, setHover] = react.useState(null);
|
|
4240
5014
|
const [portalReady, setPortalReady] = react.useState(false);
|
|
4241
5015
|
const hoverTimerRef = react.useRef(null);
|
|
@@ -4245,17 +5019,6 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4245
5019
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4246
5020
|
};
|
|
4247
5021
|
}, []);
|
|
4248
|
-
react.useEffect(() => {
|
|
4249
|
-
if (!handle) return;
|
|
4250
|
-
const sync = () => {
|
|
4251
|
-
setTool(handle.getTool());
|
|
4252
|
-
setShowAxes(handle.getShowAxes());
|
|
4253
|
-
setShowMesh(handle.getShowMesh());
|
|
4254
|
-
setCanUndo(handle.canUndo());
|
|
4255
|
-
};
|
|
4256
|
-
sync();
|
|
4257
|
-
return handle.subscribe(sync);
|
|
4258
|
-
}, [handle]);
|
|
4259
5022
|
const showHover = react.useCallback((el, t) => {
|
|
4260
5023
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4261
5024
|
hoverTimerRef.current = setTimeout(() => {
|
|
@@ -4270,14 +5033,45 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4270
5033
|
}
|
|
4271
5034
|
setHover(null);
|
|
4272
5035
|
}, []);
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
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]
|
|
4280
5073
|
);
|
|
5074
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
4281
5075
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4282
5076
|
/* @__PURE__ */ jsxRuntime.jsxs(Shell3, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
|
|
4283
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: [
|
|
@@ -4313,10 +5107,7 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4313
5107
|
title: "Reset g\xF3c nh\xECn",
|
|
4314
5108
|
"aria-label": "Reset view",
|
|
4315
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",
|
|
4316
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4317
|
-
/* @__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" }),
|
|
4318
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
|
|
4319
|
-
] })
|
|
5110
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon, {})
|
|
4320
5111
|
}
|
|
4321
5112
|
),
|
|
4322
5113
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4328,34 +5119,95 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4328
5119
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
4329
5120
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
4330
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",
|
|
4331
|
-
children: /* @__PURE__ */ jsxRuntime.
|
|
4332
|
-
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
4333
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
4334
|
-
] })
|
|
5122
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon2, {})
|
|
4335
5123
|
}
|
|
4336
5124
|
)
|
|
4337
5125
|
] }) }),
|
|
4338
|
-
|
|
4339
|
-
|
|
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",
|
|
4340
5197
|
{
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
}
|
|
4355
|
-
)
|
|
4356
|
-
},
|
|
4357
|
-
t.key
|
|
4358
|
-
)) }) }, 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
|
+
)
|
|
4359
5211
|
] }),
|
|
4360
5212
|
portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
|
|
4361
5213
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -4379,107 +5231,71 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4379
5231
|
) : null
|
|
4380
5232
|
] });
|
|
4381
5233
|
}
|
|
4382
|
-
|
|
4383
|
-
const
|
|
4384
|
-
const
|
|
4385
|
-
const
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
() => ({
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
if (log.length === 0) return false;
|
|
4397
|
-
const view = board.getViewState();
|
|
4398
|
-
const state = {
|
|
4399
|
-
version: 1,
|
|
4400
|
-
bbox: board.getBbox(),
|
|
4401
|
-
view,
|
|
4402
|
-
showAxes: board.getShowAxes(),
|
|
4403
|
-
showMesh: board.getShowMesh(),
|
|
4404
|
-
elements: log
|
|
4405
|
-
};
|
|
4406
|
-
const snap = board.snapshotSVG();
|
|
4407
|
-
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
4408
|
-
return true;
|
|
4409
|
-
},
|
|
4410
|
-
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
4411
|
-
}),
|
|
4412
|
-
[onInsert]
|
|
4413
|
-
);
|
|
4414
|
-
const handleResetView = react.useCallback(() => {
|
|
4415
|
-
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
|
+
}));
|
|
4416
5248
|
}, []);
|
|
4417
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4418
|
-
|
|
5249
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5250
|
+
MobileToolDrawer,
|
|
4419
5251
|
{
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
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
|
-
onClose,
|
|
4461
|
-
isDark
|
|
4462
|
-
}
|
|
4463
|
-
),
|
|
4464
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4465
|
-
"div",
|
|
4466
|
-
{
|
|
4467
|
-
style: {
|
|
4468
|
-
position: "absolute",
|
|
4469
|
-
left: 120,
|
|
4470
|
-
top: 0,
|
|
4471
|
-
right: 0,
|
|
4472
|
-
bottom: 0,
|
|
4473
|
-
overflow: "hidden"
|
|
4474
|
-
},
|
|
4475
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial })
|
|
4476
|
-
}
|
|
4477
|
-
)
|
|
4478
|
-
] })
|
|
4479
|
-
]
|
|
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)
|
|
4480
5292
|
}
|
|
4481
5293
|
);
|
|
4482
|
-
}
|
|
5294
|
+
}
|
|
5295
|
+
function LeftPanel3(props) {
|
|
5296
|
+
if (props.isMobile) return /* @__PURE__ */ jsxRuntime.jsx(MobilePanel, { ...props });
|
|
5297
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DesktopPanel, { ...props });
|
|
5298
|
+
}
|
|
4483
5299
|
|
|
4484
5300
|
// src/stamps/geometry-3d/serialize.ts
|
|
4485
5301
|
function isGeometry3DCustomData(data) {
|
|
@@ -4501,16 +5317,19 @@ function parseSerializedBoard3D(json) {
|
|
|
4501
5317
|
}
|
|
4502
5318
|
return parsed;
|
|
4503
5319
|
}
|
|
5320
|
+
|
|
5321
|
+
// src/stamps/geometry-3d/render.ts
|
|
4504
5322
|
var OUTPUT_WIDTH = 1024;
|
|
4505
5323
|
var OUTPUT_HEIGHT = 768;
|
|
4506
5324
|
async function renderGeometry3DSvgFromState(jsonState) {
|
|
4507
5325
|
const state = parseSerializedBoard3D(jsonState);
|
|
5326
|
+
const JXG = (await import('jsxgraph')).default;
|
|
4508
5327
|
const div = document.createElement("div");
|
|
4509
5328
|
div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
|
|
4510
5329
|
document.body.appendChild(div);
|
|
4511
5330
|
try {
|
|
4512
|
-
|
|
4513
|
-
const board =
|
|
5331
|
+
JXG.Options.text.display = "internal";
|
|
5332
|
+
const board = JXG.JSXGraph.initBoard(div, {
|
|
4514
5333
|
boundingbox: state.bbox,
|
|
4515
5334
|
axis: false,
|
|
4516
5335
|
showCopyright: false,
|
|
@@ -4558,69 +5377,1531 @@ async function renderGeometry3DSvgFromState(jsonState) {
|
|
|
4558
5377
|
clone.setAttribute("height", String(OUTPUT_HEIGHT));
|
|
4559
5378
|
const svgString = new XMLSerializer().serializeToString(clone);
|
|
4560
5379
|
try {
|
|
4561
|
-
|
|
5380
|
+
JXG.JSXGraph.freeBoard(board);
|
|
4562
5381
|
} catch {
|
|
4563
5382
|
}
|
|
4564
|
-
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
4565
|
-
} finally {
|
|
4566
|
-
document.body.removeChild(div);
|
|
4567
|
-
}
|
|
4568
|
-
}
|
|
4569
|
-
function parseInitial(editingElement) {
|
|
4570
|
-
if (!editingElement) return null;
|
|
4571
|
-
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
4572
|
-
try {
|
|
4573
|
-
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
4574
|
-
} catch {
|
|
4575
|
-
return null;
|
|
4576
|
-
}
|
|
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";
|
|
4577
6801
|
}
|
|
4578
|
-
var
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
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
|
|
4584
6821
|
);
|
|
6822
|
+
const [errorsSnapshot, setErrorsSnapshot] = react.useState({});
|
|
4585
6823
|
const handleInsert = react.useCallback(
|
|
4586
|
-
async (jsonState, svgString
|
|
6824
|
+
async (jsonState, svgString) => {
|
|
4587
6825
|
if (!api) return;
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
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
|
+
}
|
|
4599
6841
|
onClose();
|
|
4600
6842
|
},
|
|
4601
|
-
[api, editingElement, onClose]
|
|
6843
|
+
[api, editingElement?.id, onClose]
|
|
4602
6844
|
);
|
|
4603
6845
|
react.useImperativeHandle(
|
|
4604
6846
|
ref,
|
|
4605
6847
|
() => ({
|
|
4606
|
-
tryInsert: () =>
|
|
4607
|
-
hasContent: () =>
|
|
6848
|
+
tryInsert: () => panelRef.current?.insert() ?? false,
|
|
6849
|
+
hasContent: () => panelRef.current?.hasContent() ?? false
|
|
4608
6850
|
}),
|
|
4609
6851
|
[]
|
|
4610
6852
|
);
|
|
4611
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
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
|
+
] });
|
|
4621
6902
|
}
|
|
4622
6903
|
);
|
|
4623
|
-
var
|
|
6904
|
+
var Graph2DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4624
6905
|
"svg",
|
|
4625
6906
|
{
|
|
4626
6907
|
width: "20",
|
|
@@ -4633,47 +6914,45 @@ var Geometry3DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
|
4633
6914
|
strokeLinejoin: "round",
|
|
4634
6915
|
"aria-hidden": "true",
|
|
4635
6916
|
children: [
|
|
4636
|
-
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "
|
|
4637
|
-
/* @__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" })
|
|
4638
6920
|
]
|
|
4639
6921
|
}
|
|
4640
6922
|
);
|
|
4641
|
-
var
|
|
4642
|
-
kind: "
|
|
4643
|
-
shortcutKey: "
|
|
4644
|
-
toolbarLabel: "
|
|
4645
|
-
toolbarTitle: "
|
|
4646
|
-
toolbarIcon:
|
|
4647
|
-
toolbarTestId: "stamp-toolbar-
|
|
4648
|
-
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,
|
|
4649
6931
|
async renderSvgFromCustomData(data) {
|
|
4650
|
-
if (!
|
|
4651
|
-
throw new Error("
|
|
6932
|
+
if (!isGraph2DCustomData(data)) {
|
|
6933
|
+
throw new Error("graph2dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i graph2d");
|
|
4652
6934
|
}
|
|
4653
|
-
|
|
4654
|
-
return svgString;
|
|
6935
|
+
return renderGraph2dSvgFromState(data.jsonState);
|
|
4655
6936
|
},
|
|
4656
|
-
|
|
6937
|
+
async restoreFileFromCustomData(element) {
|
|
4657
6938
|
const data = element.customData;
|
|
4658
6939
|
const fileId = element.fileId;
|
|
4659
6940
|
if (!data || !fileId) return null;
|
|
4660
|
-
if (!
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
} catch {
|
|
4666
|
-
return null;
|
|
4667
|
-
}
|
|
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" };
|
|
4668
6946
|
},
|
|
4669
|
-
Host:
|
|
6947
|
+
Host: Graph2DStampHost
|
|
4670
6948
|
};
|
|
4671
6949
|
|
|
4672
6950
|
// src/stamps/shared/registry.ts
|
|
4673
6951
|
var DEFAULT_STAMPS = Object.freeze([
|
|
4674
6952
|
geometryStamp,
|
|
4675
6953
|
latexStamp,
|
|
4676
|
-
geometry3dStamp
|
|
6954
|
+
geometry3dStamp,
|
|
6955
|
+
graph2dStamp
|
|
4677
6956
|
]);
|
|
4678
6957
|
function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
4679
6958
|
for (const s of stamps) {
|
|
@@ -4684,36 +6963,90 @@ function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
|
4684
6963
|
function isStampElement(element, stamps = DEFAULT_STAMPS) {
|
|
4685
6964
|
return findStampForCustomData(element.customData, stamps) !== null;
|
|
4686
6965
|
}
|
|
4687
|
-
var
|
|
6966
|
+
var TOOLBAR_WRAPPER_ID = "stamp-toolbar-portal-wrapper";
|
|
6967
|
+
var MENU_WRAPPER_ID = "stamp-menu-portal-wrapper";
|
|
4688
6968
|
function ToolbarInjector({
|
|
4689
6969
|
enabled,
|
|
4690
6970
|
activeStampKind,
|
|
4691
6971
|
onToggle,
|
|
4692
6972
|
stamps = DEFAULT_STAMPS
|
|
4693
6973
|
}) {
|
|
4694
|
-
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);
|
|
4695
6980
|
react.useEffect(() => {
|
|
4696
6981
|
if (!enabled) {
|
|
4697
|
-
|
|
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();
|
|
4698
7026
|
return;
|
|
4699
7027
|
}
|
|
4700
7028
|
let cancelled = false;
|
|
4701
7029
|
let attempts = 0;
|
|
4702
7030
|
let observer = null;
|
|
4703
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
|
+
};
|
|
4704
7039
|
const tryMount = () => {
|
|
4705
7040
|
if (cancelled) return;
|
|
4706
7041
|
const container = document.querySelector(".excalidraw .App-toolbar .Stack_horizontal") ?? document.querySelector(".App-toolbar .Stack_horizontal");
|
|
4707
7042
|
if (!container) {
|
|
4708
|
-
if (attempts++ < 20)
|
|
4709
|
-
timer = setTimeout(tryMount, 100);
|
|
4710
|
-
}
|
|
7043
|
+
if (attempts++ < 20) timer = setTimeout(tryMount, 100);
|
|
4711
7044
|
return;
|
|
4712
7045
|
}
|
|
4713
|
-
let wrapper = container.querySelector("#" +
|
|
7046
|
+
let wrapper = container.querySelector("#" + TOOLBAR_WRAPPER_ID);
|
|
4714
7047
|
if (!wrapper) {
|
|
4715
7048
|
wrapper = document.createElement("div");
|
|
4716
|
-
wrapper.id =
|
|
7049
|
+
wrapper.id = TOOLBAR_WRAPPER_ID;
|
|
4717
7050
|
wrapper.className = "Stamp-toolbar-injector";
|
|
4718
7051
|
wrapper.setAttribute("data-stamp-area", "true");
|
|
4719
7052
|
wrapper.style.display = "inline-flex";
|
|
@@ -4729,13 +7062,13 @@ function ToolbarInjector({
|
|
|
4729
7062
|
container.appendChild(wrapper);
|
|
4730
7063
|
}
|
|
4731
7064
|
}
|
|
4732
|
-
|
|
7065
|
+
apply(wrapper);
|
|
4733
7066
|
};
|
|
4734
7067
|
tryMount();
|
|
4735
7068
|
const root = document.querySelector(".excalidraw") ?? document.body;
|
|
4736
7069
|
observer = new MutationObserver(() => {
|
|
4737
7070
|
if (cancelled) return;
|
|
4738
|
-
const stillThere = document.getElementById(
|
|
7071
|
+
const stillThere = document.getElementById(TOOLBAR_WRAPPER_ID);
|
|
4739
7072
|
if (!stillThere) {
|
|
4740
7073
|
attempts = 0;
|
|
4741
7074
|
tryMount();
|
|
@@ -4746,11 +7079,73 @@ function ToolbarInjector({
|
|
|
4746
7079
|
cancelled = true;
|
|
4747
7080
|
if (timer) clearTimeout(timer);
|
|
4748
7081
|
observer?.disconnect();
|
|
4749
|
-
document.getElementById(
|
|
7082
|
+
document.getElementById(TOOLBAR_WRAPPER_ID)?.remove();
|
|
4750
7083
|
};
|
|
4751
|
-
}, [enabled]);
|
|
4752
|
-
|
|
4753
|
-
|
|
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(
|
|
4754
7149
|
/* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: stamps.map((stamp) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4755
7150
|
StampToolButton,
|
|
4756
7151
|
{
|
|
@@ -4763,8 +7158,42 @@ function ToolbarInjector({
|
|
|
4763
7158
|
},
|
|
4764
7159
|
stamp.kind
|
|
4765
7160
|
)) }),
|
|
4766
|
-
|
|
4767
|
-
);
|
|
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
|
+
] });
|
|
4768
7197
|
}
|
|
4769
7198
|
function StampToolButton({ icon, keybind, label, active, onClick, dataTestId }) {
|
|
4770
7199
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -4829,6 +7258,54 @@ function StampToolButton({ icon, keybind, label, active, onClick, dataTestId })
|
|
|
4829
7258
|
}
|
|
4830
7259
|
);
|
|
4831
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
|
+
}
|
|
4832
7309
|
function isEditableTarget(t) {
|
|
4833
7310
|
if (!t || !(t instanceof HTMLElement)) return false;
|
|
4834
7311
|
if (t.isContentEditable) return true;
|
|
@@ -5146,7 +7623,9 @@ function Whiteboard({
|
|
|
5146
7623
|
stamps = DEFAULT_STAMPS
|
|
5147
7624
|
}) {
|
|
5148
7625
|
const [api, setApi] = react.useState(null);
|
|
7626
|
+
const apiRef = react.useRef(null);
|
|
5149
7627
|
const [isDarkTheme, setIsDarkTheme] = react.useState(false);
|
|
7628
|
+
const isDarkThemeRef = react.useRef(false);
|
|
5150
7629
|
const knownFileIdsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
5151
7630
|
const lastSceneHashRef = react.useRef("");
|
|
5152
7631
|
const sceneThrottleRef = react.useRef(null);
|
|
@@ -5207,7 +7686,10 @@ function Whiteboard({
|
|
|
5207
7686
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5208
7687
|
(elements, appState, files) => {
|
|
5209
7688
|
const nextDark = appState?.theme === "dark";
|
|
5210
|
-
|
|
7689
|
+
if (isDarkThemeRef.current !== nextDark) {
|
|
7690
|
+
isDarkThemeRef.current = nextDark;
|
|
7691
|
+
queueMicrotask(() => setIsDarkTheme(nextDark));
|
|
7692
|
+
}
|
|
5211
7693
|
if (readOnly) return;
|
|
5212
7694
|
latestSceneRef.current = { elements, appState };
|
|
5213
7695
|
const cropId = appState?.croppingElementId;
|
|
@@ -5217,12 +7699,17 @@ function Whiteboard({
|
|
|
5217
7699
|
const stamp = findStampForCustomData(el.customData, stamps);
|
|
5218
7700
|
if (stamp) {
|
|
5219
7701
|
handledCropIdRef.current = cropId;
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
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 });
|
|
5226
7713
|
});
|
|
5227
7714
|
return;
|
|
5228
7715
|
}
|
|
@@ -5473,8 +7960,12 @@ function Whiteboard({
|
|
|
5473
7960
|
Excalidraw2,
|
|
5474
7961
|
{
|
|
5475
7962
|
excalidrawAPI: (a) => {
|
|
5476
|
-
|
|
5477
|
-
|
|
7963
|
+
if (apiRef.current === a) return;
|
|
7964
|
+
apiRef.current = a;
|
|
7965
|
+
queueMicrotask(() => {
|
|
7966
|
+
setApi(a);
|
|
7967
|
+
onApi?.(a);
|
|
7968
|
+
});
|
|
5478
7969
|
},
|
|
5479
7970
|
langCode,
|
|
5480
7971
|
viewModeEnabled: readOnly,
|
|
@@ -5516,8 +8007,10 @@ exports.Whiteboard = Whiteboard;
|
|
|
5516
8007
|
exports.findStampForCustomData = findStampForCustomData;
|
|
5517
8008
|
exports.geometry3dStamp = geometry3dStamp;
|
|
5518
8009
|
exports.geometryStamp = geometryStamp;
|
|
8010
|
+
exports.graph2dStamp = graph2dStamp;
|
|
5519
8011
|
exports.isGeometry3DCustomData = isGeometry3DCustomData;
|
|
5520
8012
|
exports.isGeometryCustomData = isGeometryCustomData;
|
|
8013
|
+
exports.isGraph2DCustomData = isGraph2DCustomData;
|
|
5521
8014
|
exports.isLatexCustomData = isLatexCustomData;
|
|
5522
8015
|
exports.isStampElement = isStampElement;
|
|
5523
8016
|
exports.latexStamp = latexStamp;
|