@xom11/whiteboard 0.6.2 → 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 +2910 -427
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2906 -432
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
- package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import './index.css';
|
|
3
|
+
import { __require } from './chunk-BJTO5JO5.mjs';
|
|
3
4
|
import dynamic from 'next/dynamic';
|
|
4
|
-
import { forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useId } from 'react';
|
|
5
|
+
import { forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useId, useLayoutEffect } from 'react';
|
|
5
6
|
import { createPortal } from 'react-dom';
|
|
6
7
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
8
|
import '@excalidraw/excalidraw/index.css';
|
|
8
9
|
|
|
9
|
-
// src/Whiteboard.tsx
|
|
10
|
-
|
|
11
10
|
// src/serialize.ts
|
|
12
11
|
function pickSyncableAppState(s) {
|
|
13
12
|
return {
|
|
@@ -252,6 +251,22 @@ var GROUP_LABELS = {
|
|
|
252
251
|
edit: "Ch\u1EC9nh s\u1EEDa",
|
|
253
252
|
transform: "Ph\xE9p bi\u1EBFn h\xECnh"
|
|
254
253
|
};
|
|
254
|
+
var GROUP_ORDER = [
|
|
255
|
+
"move",
|
|
256
|
+
"point",
|
|
257
|
+
"line",
|
|
258
|
+
"construct",
|
|
259
|
+
"polygon",
|
|
260
|
+
"circle",
|
|
261
|
+
"measure",
|
|
262
|
+
"edit",
|
|
263
|
+
"transform"
|
|
264
|
+
];
|
|
265
|
+
var A_CODE = "A".charCodeAt(0);
|
|
266
|
+
function letterForGroup(g) {
|
|
267
|
+
const idx = GROUP_ORDER.indexOf(g);
|
|
268
|
+
return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
|
|
269
|
+
}
|
|
255
270
|
function objKind(obj) {
|
|
256
271
|
if (!obj) return "other";
|
|
257
272
|
const e = (obj.elType || obj.type || "").toString().toLowerCase();
|
|
@@ -1644,7 +1659,24 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1644
1659
|
});
|
|
1645
1660
|
onReady({
|
|
1646
1661
|
getContainer: () => containerRef.current,
|
|
1647
|
-
|
|
1662
|
+
// Sync toạ độ live của free point về log trước khi trả ra. JSXGraph
|
|
1663
|
+
// cho phép drag free point (args=[x,y] không có ref), việc drag chỉ
|
|
1664
|
+
// cập nhật obj.X()/Y() trên board chứ không đụng log → re-edit + Chèn
|
|
1665
|
+
// sẽ serialize toạ độ cũ → SVG không đổi → fileId trùng → user thấy
|
|
1666
|
+
// "k thay đổi". Line/segment/circle/polygon tham chiếu point qua id
|
|
1667
|
+
// nên auto-update theo.
|
|
1668
|
+
getCreationLog: () => creationLogRef.current.map((e) => {
|
|
1669
|
+
if (e.type !== "point") return { ...e };
|
|
1670
|
+
const args = e.args;
|
|
1671
|
+
if (!Array.isArray(args) || args.length !== 2) return { ...e };
|
|
1672
|
+
if (typeof args[0] !== "number" || typeof args[1] !== "number") return { ...e };
|
|
1673
|
+
const obj = objMapRef.current.get(e.id);
|
|
1674
|
+
if (!obj || typeof obj.X !== "function" || typeof obj.Y !== "function") return { ...e };
|
|
1675
|
+
const x = obj.X();
|
|
1676
|
+
const y = obj.Y();
|
|
1677
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { ...e };
|
|
1678
|
+
return { ...e, args: [x, y] };
|
|
1679
|
+
}),
|
|
1648
1680
|
getBbox: () => boardRef.current ? boardRef.current.getBoundingBox() : [-10, 10, 10, -10],
|
|
1649
1681
|
getShowAxis: () => showAxisRef.current,
|
|
1650
1682
|
getShowGrid: () => showGridRef.current,
|
|
@@ -1842,8 +1874,140 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1842
1874
|
}
|
|
1843
1875
|
);
|
|
1844
1876
|
};
|
|
1877
|
+
function MobileToolDrawer({
|
|
1878
|
+
title,
|
|
1879
|
+
headerIcon,
|
|
1880
|
+
chips,
|
|
1881
|
+
actions,
|
|
1882
|
+
groups,
|
|
1883
|
+
activeTool,
|
|
1884
|
+
onToolSelect,
|
|
1885
|
+
drawerOpen,
|
|
1886
|
+
onDrawerClose,
|
|
1887
|
+
isDark,
|
|
1888
|
+
testId
|
|
1889
|
+
}) {
|
|
1890
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1891
|
+
drawerOpen && /* @__PURE__ */ jsx(
|
|
1892
|
+
"div",
|
|
1893
|
+
{
|
|
1894
|
+
className: "stamp-drawer-backdrop",
|
|
1895
|
+
onPointerDown: onDrawerClose,
|
|
1896
|
+
"aria-hidden": "true"
|
|
1897
|
+
}
|
|
1898
|
+
),
|
|
1899
|
+
/* @__PURE__ */ jsxs(
|
|
1900
|
+
"aside",
|
|
1901
|
+
{
|
|
1902
|
+
role: "complementary",
|
|
1903
|
+
"aria-label": title,
|
|
1904
|
+
"aria-hidden": !drawerOpen ? "true" : void 0,
|
|
1905
|
+
"data-testid": testId,
|
|
1906
|
+
"data-stamp-area": "true",
|
|
1907
|
+
"data-mobile-drawer": "true",
|
|
1908
|
+
"data-geo-mobile": "true",
|
|
1909
|
+
"data-drawer-state": drawerOpen ? "open" : "closed",
|
|
1910
|
+
className: [
|
|
1911
|
+
isDark ? "theme--dark " : "",
|
|
1912
|
+
"stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
|
|
1913
|
+
].join(""),
|
|
1914
|
+
children: [
|
|
1915
|
+
/* @__PURE__ */ 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: [
|
|
1916
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
|
|
1917
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
|
|
1918
|
+
title
|
|
1919
|
+
] }),
|
|
1920
|
+
/* @__PURE__ */ jsx(
|
|
1921
|
+
"button",
|
|
1922
|
+
{
|
|
1923
|
+
type: "button",
|
|
1924
|
+
onClick: onDrawerClose,
|
|
1925
|
+
"aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
|
|
1926
|
+
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",
|
|
1927
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1928
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1929
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1930
|
+
] })
|
|
1931
|
+
}
|
|
1932
|
+
)
|
|
1933
|
+
] }),
|
|
1934
|
+
/* @__PURE__ */ 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: [
|
|
1935
|
+
chips.map((c) => /* @__PURE__ */ jsxs(
|
|
1936
|
+
"button",
|
|
1937
|
+
{
|
|
1938
|
+
type: "button",
|
|
1939
|
+
role: "switch",
|
|
1940
|
+
"aria-pressed": c.pressed,
|
|
1941
|
+
"aria-label": c.label,
|
|
1942
|
+
"data-testid": c.testId,
|
|
1943
|
+
onClick: () => c.onToggle(!c.pressed),
|
|
1944
|
+
className: "geo-mobile-chip",
|
|
1945
|
+
children: [
|
|
1946
|
+
c.icon,
|
|
1947
|
+
c.label
|
|
1948
|
+
]
|
|
1949
|
+
},
|
|
1950
|
+
c.label
|
|
1951
|
+
)),
|
|
1952
|
+
actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsx(
|
|
1953
|
+
"button",
|
|
1954
|
+
{
|
|
1955
|
+
type: "button",
|
|
1956
|
+
onClick: a.onClick,
|
|
1957
|
+
disabled: a.disabled,
|
|
1958
|
+
"aria-label": a.label,
|
|
1959
|
+
title: a.title ?? a.label,
|
|
1960
|
+
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",
|
|
1961
|
+
children: a.icon
|
|
1962
|
+
},
|
|
1963
|
+
a.label
|
|
1964
|
+
)) })
|
|
1965
|
+
] }),
|
|
1966
|
+
/* @__PURE__ */ jsx(
|
|
1967
|
+
"div",
|
|
1968
|
+
{
|
|
1969
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
1970
|
+
style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
|
|
1971
|
+
children: groups.map((g) => /* @__PURE__ */ jsxs("section", { className: "px-3 pt-3 pb-1", children: [
|
|
1972
|
+
/* @__PURE__ */ jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
1973
|
+
/* @__PURE__ */ jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
|
|
1974
|
+
g.groupLabel
|
|
1975
|
+
] }),
|
|
1976
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
|
|
1977
|
+
const active = activeTool === t.key;
|
|
1978
|
+
return /* @__PURE__ */ jsxs(
|
|
1979
|
+
"button",
|
|
1980
|
+
{
|
|
1981
|
+
type: "button",
|
|
1982
|
+
"aria-label": t.label,
|
|
1983
|
+
"aria-pressed": active,
|
|
1984
|
+
"data-tool": t.key,
|
|
1985
|
+
onClick: () => {
|
|
1986
|
+
onToolSelect(t.key);
|
|
1987
|
+
onDrawerClose();
|
|
1988
|
+
},
|
|
1989
|
+
className: [
|
|
1990
|
+
"flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
|
|
1991
|
+
active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
|
|
1992
|
+
].join(" "),
|
|
1993
|
+
children: [
|
|
1994
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-6 w-6 items-center justify-center", children: t.icon }),
|
|
1995
|
+
/* @__PURE__ */ jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
|
|
1996
|
+
]
|
|
1997
|
+
},
|
|
1998
|
+
t.key
|
|
1999
|
+
);
|
|
2000
|
+
}) })
|
|
2001
|
+
] }, g.group))
|
|
2002
|
+
}
|
|
2003
|
+
)
|
|
2004
|
+
]
|
|
2005
|
+
}
|
|
2006
|
+
)
|
|
2007
|
+
] });
|
|
2008
|
+
}
|
|
1845
2009
|
var TOOLTIP_DELAY_MS = 400;
|
|
1846
|
-
function Shell({ title, icon, onClose, children, isDark }) {
|
|
2010
|
+
function Shell({ title, icon, onClose, children, isDark, closeLabel = "\u0110\xF3ng" }) {
|
|
1847
2011
|
return /* @__PURE__ */ jsxs(
|
|
1848
2012
|
"aside",
|
|
1849
2013
|
{
|
|
@@ -1851,7 +2015,10 @@ function Shell({ title, icon, onClose, children, isDark }) {
|
|
|
1851
2015
|
"aria-label": title,
|
|
1852
2016
|
"data-testid": "stamp-left-panel",
|
|
1853
2017
|
"data-stamp-area": "true",
|
|
1854
|
-
className:
|
|
2018
|
+
className: [
|
|
2019
|
+
isDark ? "theme--dark " : "",
|
|
2020
|
+
"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"
|
|
2021
|
+
].join(""),
|
|
1855
2022
|
children: [
|
|
1856
2023
|
/* @__PURE__ */ 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: [
|
|
1857
2024
|
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -1862,12 +2029,9 @@ function Shell({ title, icon, onClose, children, isDark }) {
|
|
|
1862
2029
|
"button",
|
|
1863
2030
|
{
|
|
1864
2031
|
onClick: onClose,
|
|
1865
|
-
"aria-label":
|
|
2032
|
+
"aria-label": closeLabel,
|
|
1866
2033
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
1867
|
-
children: /* @__PURE__ */
|
|
1868
|
-
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1869
|
-
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1870
|
-
] })
|
|
2034
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
1871
2035
|
}
|
|
1872
2036
|
)
|
|
1873
2037
|
] }),
|
|
@@ -1888,24 +2052,36 @@ var GeometryIconHeader = /* @__PURE__ */ jsxs("svg", { width: "14", height: "14"
|
|
|
1888
2052
|
/* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
|
|
1889
2053
|
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" })
|
|
1890
2054
|
] });
|
|
1891
|
-
function
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
(
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2055
|
+
function CloseIcon() {
|
|
2056
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2057
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2058
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2059
|
+
] });
|
|
2060
|
+
}
|
|
2061
|
+
function UndoIcon() {
|
|
2062
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2063
|
+
/* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
2064
|
+
/* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
2065
|
+
] });
|
|
2066
|
+
}
|
|
2067
|
+
function AxisIcon() {
|
|
2068
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2069
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
|
|
2070
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "4", y2: "4" }),
|
|
2071
|
+
/* @__PURE__ */ jsx("polyline", { points: "2 6 4 4 6 6" }),
|
|
2072
|
+
/* @__PURE__ */ jsx("polyline", { points: "18 18 20 20 18 22" })
|
|
2073
|
+
] });
|
|
2074
|
+
}
|
|
2075
|
+
function GridIcon() {
|
|
2076
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2077
|
+
/* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "1" }),
|
|
2078
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "10", x2: "20", y2: "10" }),
|
|
2079
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "16", x2: "20", y2: "16" }),
|
|
2080
|
+
/* @__PURE__ */ jsx("line", { x1: "10", y1: "4", x2: "10", y2: "20" }),
|
|
2081
|
+
/* @__PURE__ */ jsx("line", { x1: "16", y1: "4", x2: "16", y2: "20" })
|
|
2082
|
+
] });
|
|
2083
|
+
}
|
|
2084
|
+
function useToolHoverTooltip() {
|
|
1909
2085
|
const [hover, setHover] = useState(null);
|
|
1910
2086
|
const [portalReady, setPortalReady] = useState(false);
|
|
1911
2087
|
const hoverTimerRef = useRef(null);
|
|
@@ -1929,6 +2105,23 @@ function LeftPanel({
|
|
|
1929
2105
|
}
|
|
1930
2106
|
setHover(null);
|
|
1931
2107
|
}, []);
|
|
2108
|
+
return { hover, portalReady, showHover, hideHover };
|
|
2109
|
+
}
|
|
2110
|
+
function DesktopGeometryPanel(props) {
|
|
2111
|
+
const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onClose, isDark, chordGroup } = props;
|
|
2112
|
+
const grouped = useMemo(() => {
|
|
2113
|
+
return TOOLS.reduce((acc, t) => {
|
|
2114
|
+
var _a;
|
|
2115
|
+
(acc[_a = t.group] ?? (acc[_a] = [])).push(t);
|
|
2116
|
+
return acc;
|
|
2117
|
+
}, {});
|
|
2118
|
+
}, []);
|
|
2119
|
+
const groupKeys = useMemo(
|
|
2120
|
+
() => GROUP_ORDER.filter((g) => grouped[g]),
|
|
2121
|
+
[grouped]
|
|
2122
|
+
);
|
|
2123
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
2124
|
+
const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
|
|
1932
2125
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1933
2126
|
/* @__PURE__ */ jsxs(Shell, { title: "H\xECnh h\u1ECDc", icon: GeometryIconHeader, onClose, isDark, children: [
|
|
1934
2127
|
/* @__PURE__ */ jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
|
|
@@ -1965,36 +2158,95 @@ function LeftPanel({
|
|
|
1965
2158
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
1966
2159
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
1967
2160
|
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",
|
|
1968
|
-
children: /* @__PURE__ */
|
|
1969
|
-
/* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
1970
|
-
/* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
1971
|
-
] })
|
|
2161
|
+
children: /* @__PURE__ */ jsx(UndoIcon, {})
|
|
1972
2162
|
}
|
|
1973
2163
|
)
|
|
1974
2164
|
] }) }),
|
|
1975
|
-
groupKeys.map((group) =>
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
2165
|
+
groupKeys.map((group) => {
|
|
2166
|
+
const isChordActive = chordGroup === group;
|
|
2167
|
+
const dimmed = chordGroup !== null && !isChordActive;
|
|
2168
|
+
return /* @__PURE__ */ jsxs(
|
|
2169
|
+
"section",
|
|
1979
2170
|
{
|
|
1980
|
-
|
|
1981
|
-
"
|
|
1982
|
-
"aria-pressed": active,
|
|
1983
|
-
"data-tool": t.key,
|
|
1984
|
-
onClick: () => onToolChange(t.key),
|
|
1985
|
-
onMouseEnter: (e) => showHover(e.currentTarget, t),
|
|
1986
|
-
onMouseLeave: hideHover,
|
|
1987
|
-
onFocus: (e) => showHover(e.currentTarget, t),
|
|
1988
|
-
onBlur: hideHover,
|
|
2171
|
+
"data-chord-group": group,
|
|
2172
|
+
"data-chord-active": isChordActive ? "true" : "false",
|
|
1989
2173
|
className: [
|
|
1990
|
-
"
|
|
1991
|
-
|
|
2174
|
+
"rounded-md transition",
|
|
2175
|
+
isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
|
|
2176
|
+
dimmed ? "opacity-55" : "opacity-100"
|
|
1992
2177
|
].join(" "),
|
|
1993
|
-
children:
|
|
2178
|
+
children: [
|
|
2179
|
+
/* @__PURE__ */ jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
2180
|
+
/* @__PURE__ */ jsx("span", { children: GROUP_LABELS[group] }),
|
|
2181
|
+
/* @__PURE__ */ jsx(
|
|
2182
|
+
"span",
|
|
2183
|
+
{
|
|
2184
|
+
"data-testid": `chord-letter-${group}`,
|
|
2185
|
+
className: [
|
|
2186
|
+
"font-mono text-[10px] leading-none transition",
|
|
2187
|
+
isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
|
|
2188
|
+
].join(" "),
|
|
2189
|
+
children: letterForGroup(group)
|
|
2190
|
+
}
|
|
2191
|
+
)
|
|
2192
|
+
] }),
|
|
2193
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
|
|
2194
|
+
const active = activeTool === t.key;
|
|
2195
|
+
return /* @__PURE__ */ jsxs(
|
|
2196
|
+
"button",
|
|
2197
|
+
{
|
|
2198
|
+
type: "button",
|
|
2199
|
+
"aria-label": t.label,
|
|
2200
|
+
"aria-pressed": active,
|
|
2201
|
+
"data-tool": t.key,
|
|
2202
|
+
onClick: () => onToolChange(t.key),
|
|
2203
|
+
onMouseEnter: (e) => showHover(e.currentTarget, t),
|
|
2204
|
+
onMouseLeave: hideHover,
|
|
2205
|
+
onFocus: (e) => showHover(e.currentTarget, t),
|
|
2206
|
+
onBlur: hideHover,
|
|
2207
|
+
className: [
|
|
2208
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
2209
|
+
active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
2210
|
+
].join(" "),
|
|
2211
|
+
children: [
|
|
2212
|
+
t.icon,
|
|
2213
|
+
/* @__PURE__ */ jsx(
|
|
2214
|
+
"span",
|
|
2215
|
+
{
|
|
2216
|
+
"data-testid": `chord-num-${t.key}`,
|
|
2217
|
+
className: [
|
|
2218
|
+
"pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
|
|
2219
|
+
active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
|
|
2220
|
+
].join(" "),
|
|
2221
|
+
children: i + 1
|
|
2222
|
+
}
|
|
2223
|
+
)
|
|
2224
|
+
]
|
|
2225
|
+
},
|
|
2226
|
+
t.key
|
|
2227
|
+
);
|
|
2228
|
+
}) })
|
|
2229
|
+
]
|
|
1994
2230
|
},
|
|
1995
|
-
|
|
2231
|
+
group
|
|
1996
2232
|
);
|
|
1997
|
-
})
|
|
2233
|
+
}),
|
|
2234
|
+
chordGroup && activeGroupTools && /* @__PURE__ */ jsxs(
|
|
2235
|
+
"div",
|
|
2236
|
+
{
|
|
2237
|
+
"data-testid": "chord-hint",
|
|
2238
|
+
className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
|
|
2239
|
+
children: [
|
|
2240
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: letterForGroup(chordGroup) }),
|
|
2241
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
|
|
2242
|
+
activeGroupTools.map((t, i) => /* @__PURE__ */ jsxs("span", { className: "mr-2 inline-block", children: [
|
|
2243
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
|
|
2244
|
+
/* @__PURE__ */ jsx("span", { className: "ml-1", children: t.label })
|
|
2245
|
+
] }, t.key)),
|
|
2246
|
+
/* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
|
|
2247
|
+
]
|
|
2248
|
+
}
|
|
2249
|
+
)
|
|
1998
2250
|
] }),
|
|
1999
2251
|
portalReady && hover && typeof document !== "undefined" ? createPortal(
|
|
2000
2252
|
/* @__PURE__ */ jsxs(
|
|
@@ -2018,6 +2270,78 @@ function LeftPanel({
|
|
|
2018
2270
|
) : null
|
|
2019
2271
|
] });
|
|
2020
2272
|
}
|
|
2273
|
+
function MobileGeometryPanel(props) {
|
|
2274
|
+
const {
|
|
2275
|
+
activeTool,
|
|
2276
|
+
onToolChange,
|
|
2277
|
+
showAxis,
|
|
2278
|
+
showGrid,
|
|
2279
|
+
onShowAxisChange,
|
|
2280
|
+
onShowGridChange,
|
|
2281
|
+
onUndo,
|
|
2282
|
+
canUndo,
|
|
2283
|
+
isDark,
|
|
2284
|
+
drawerOpen,
|
|
2285
|
+
onDrawerClose
|
|
2286
|
+
} = props;
|
|
2287
|
+
const groups = useMemo(() => {
|
|
2288
|
+
const acc = /* @__PURE__ */ new Map();
|
|
2289
|
+
for (const t of TOOLS) {
|
|
2290
|
+
if (!acc.has(t.group)) acc.set(t.group, []);
|
|
2291
|
+
acc.get(t.group).push(t);
|
|
2292
|
+
}
|
|
2293
|
+
return Array.from(acc.entries()).map(([group, tools]) => ({
|
|
2294
|
+
group,
|
|
2295
|
+
groupLabel: GROUP_LABELS[group],
|
|
2296
|
+
tools: tools.map((t) => ({ key: t.key, label: t.label, icon: t.icon }))
|
|
2297
|
+
}));
|
|
2298
|
+
}, []);
|
|
2299
|
+
return /* @__PURE__ */ jsx(
|
|
2300
|
+
MobileToolDrawer,
|
|
2301
|
+
{
|
|
2302
|
+
title: "H\xECnh h\u1ECDc",
|
|
2303
|
+
headerIcon: GeometryIconHeader,
|
|
2304
|
+
testId: "stamp-left-panel",
|
|
2305
|
+
isDark,
|
|
2306
|
+
drawerOpen: !!drawerOpen,
|
|
2307
|
+
onDrawerClose: () => onDrawerClose?.(),
|
|
2308
|
+
chips: [
|
|
2309
|
+
{
|
|
2310
|
+
label: "Tr\u1EE5c",
|
|
2311
|
+
icon: /* @__PURE__ */ jsx(AxisIcon, {}),
|
|
2312
|
+
pressed: showAxis,
|
|
2313
|
+
onToggle: onShowAxisChange,
|
|
2314
|
+
testId: "toggle-axis"
|
|
2315
|
+
},
|
|
2316
|
+
{
|
|
2317
|
+
label: "L\u01B0\u1EDBi",
|
|
2318
|
+
icon: /* @__PURE__ */ jsx(GridIcon, {}),
|
|
2319
|
+
pressed: showGrid,
|
|
2320
|
+
onToggle: onShowGridChange,
|
|
2321
|
+
testId: "toggle-grid"
|
|
2322
|
+
}
|
|
2323
|
+
],
|
|
2324
|
+
actions: [
|
|
2325
|
+
{
|
|
2326
|
+
label: "Ho\xE0n t\xE1c",
|
|
2327
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
2328
|
+
icon: /* @__PURE__ */ jsx(UndoIcon, {}),
|
|
2329
|
+
onClick: onUndo,
|
|
2330
|
+
disabled: !canUndo
|
|
2331
|
+
}
|
|
2332
|
+
],
|
|
2333
|
+
groups,
|
|
2334
|
+
activeTool,
|
|
2335
|
+
onToolSelect: onToolChange
|
|
2336
|
+
}
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
function LeftPanel(props) {
|
|
2340
|
+
if (props.isMobile) {
|
|
2341
|
+
return /* @__PURE__ */ jsx(MobileGeometryPanel, { ...props });
|
|
2342
|
+
}
|
|
2343
|
+
return /* @__PURE__ */ jsx(DesktopGeometryPanel, { ...props });
|
|
2344
|
+
}
|
|
2021
2345
|
|
|
2022
2346
|
// src/stamps/geometry-2d/serialize.ts
|
|
2023
2347
|
function serializeBoard(board, log, options = {}) {
|
|
@@ -2162,6 +2486,38 @@ var STROKE_PALETTE = [
|
|
|
2162
2486
|
"#868e96"
|
|
2163
2487
|
// gray
|
|
2164
2488
|
];
|
|
2489
|
+
var MOBILE_QUERY = "(max-width: 768px)";
|
|
2490
|
+
var NO_HOVER_QUERY = "(hover: none)";
|
|
2491
|
+
function readMatch(query) {
|
|
2492
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
2493
|
+
try {
|
|
2494
|
+
return window.matchMedia(query).matches;
|
|
2495
|
+
} catch {
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
function useIsMobile() {
|
|
2500
|
+
const [state, setState] = useState(() => ({
|
|
2501
|
+
isMobile: readMatch(MOBILE_QUERY),
|
|
2502
|
+
isTouchOnly: readMatch(NO_HOVER_QUERY)
|
|
2503
|
+
}));
|
|
2504
|
+
useEffect(() => {
|
|
2505
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
2506
|
+
const mql = window.matchMedia(MOBILE_QUERY);
|
|
2507
|
+
const tql = window.matchMedia(NO_HOVER_QUERY);
|
|
2508
|
+
const update = () => {
|
|
2509
|
+
setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
|
|
2510
|
+
};
|
|
2511
|
+
update();
|
|
2512
|
+
mql.addEventListener("change", update);
|
|
2513
|
+
tql.addEventListener("change", update);
|
|
2514
|
+
return () => {
|
|
2515
|
+
mql.removeEventListener("change", update);
|
|
2516
|
+
tql.removeEventListener("change", update);
|
|
2517
|
+
};
|
|
2518
|
+
}, []);
|
|
2519
|
+
return state;
|
|
2520
|
+
}
|
|
2165
2521
|
var DASH_OPTIONS = [
|
|
2166
2522
|
{ value: 0, label: "N\xE9t li\u1EC1n" },
|
|
2167
2523
|
{ value: 2, label: "N\xE9t \u0111\u1EE9t" },
|
|
@@ -2220,6 +2576,26 @@ var PropertiesPopover = (props) => {
|
|
|
2220
2576
|
const { anchor, onClose, onMutate, isDark, getAllNames } = props;
|
|
2221
2577
|
const rootRef = useRef(null);
|
|
2222
2578
|
const [section, setSection] = useState(null);
|
|
2579
|
+
const { isMobile } = useIsMobile();
|
|
2580
|
+
const [clamped, setClamped] = useState(null);
|
|
2581
|
+
useLayoutEffect(() => {
|
|
2582
|
+
if (typeof window === "undefined") return;
|
|
2583
|
+
const margin = 8;
|
|
2584
|
+
if (isMobile) {
|
|
2585
|
+
const rect2 = rootRef.current?.getBoundingClientRect();
|
|
2586
|
+
const w2 = rect2?.width ?? 280;
|
|
2587
|
+
const left2 = Math.max(margin, (window.innerWidth - w2) / 2);
|
|
2588
|
+
const top2 = window.innerHeight - (rect2?.height ?? 80) - margin - 12;
|
|
2589
|
+
setClamped({ left: left2, top: Math.max(margin, top2) });
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
2593
|
+
const w = rect?.width ?? 280;
|
|
2594
|
+
const h = rect?.height ?? 80;
|
|
2595
|
+
const left = Math.max(margin, Math.min(anchor.x, window.innerWidth - w - margin));
|
|
2596
|
+
const top = Math.max(margin, Math.min(anchor.y, window.innerHeight - h - margin));
|
|
2597
|
+
setClamped({ left, top });
|
|
2598
|
+
}, [anchor.x, anchor.y, isMobile, section]);
|
|
2223
2599
|
const initialName = props.kind === "point" ? props.currentName : props.kind === "line" || props.kind === "circle" ? props.currentName : "";
|
|
2224
2600
|
const [name, setName] = useState(initialName);
|
|
2225
2601
|
useEffect(() => {
|
|
@@ -2232,14 +2608,14 @@ var PropertiesPopover = (props) => {
|
|
|
2232
2608
|
onClose();
|
|
2233
2609
|
}
|
|
2234
2610
|
};
|
|
2235
|
-
const
|
|
2611
|
+
const onPointerDown = (e) => {
|
|
2236
2612
|
if (!rootRef.current?.contains(e.target)) onClose();
|
|
2237
2613
|
};
|
|
2238
2614
|
document.addEventListener("keydown", onKey);
|
|
2239
|
-
document.addEventListener("
|
|
2615
|
+
document.addEventListener("pointerdown", onPointerDown, { capture: true });
|
|
2240
2616
|
return () => {
|
|
2241
2617
|
document.removeEventListener("keydown", onKey);
|
|
2242
|
-
document.removeEventListener("
|
|
2618
|
+
document.removeEventListener("pointerdown", onPointerDown, { capture: true });
|
|
2243
2619
|
};
|
|
2244
2620
|
}, [onClose]);
|
|
2245
2621
|
const pickColor = (c) => {
|
|
@@ -2276,6 +2652,7 @@ var PropertiesPopover = (props) => {
|
|
|
2276
2652
|
{
|
|
2277
2653
|
type: "button",
|
|
2278
2654
|
"data-section": id,
|
|
2655
|
+
"data-pill-btn": id,
|
|
2279
2656
|
"aria-label": label,
|
|
2280
2657
|
"aria-pressed": !!active,
|
|
2281
2658
|
onClick,
|
|
@@ -2294,13 +2671,14 @@ var PropertiesPopover = (props) => {
|
|
|
2294
2671
|
}
|
|
2295
2672
|
);
|
|
2296
2673
|
const colorIndicatorTint = useMemo(() => currentColor, [currentColor]);
|
|
2674
|
+
const pos = clamped ?? { left: anchor.x, top: anchor.y };
|
|
2297
2675
|
const node = /* @__PURE__ */ jsxs(
|
|
2298
2676
|
"div",
|
|
2299
2677
|
{
|
|
2300
2678
|
ref: rootRef,
|
|
2301
2679
|
"data-stamp-area": "true",
|
|
2302
2680
|
className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-1.5`,
|
|
2303
|
-
style: { left:
|
|
2681
|
+
style: { left: pos.left, top: pos.top },
|
|
2304
2682
|
role: "dialog",
|
|
2305
2683
|
"aria-label": "Thu\u1ED9c t\xEDnh \u0111\u1ED1i t\u01B0\u1EE3ng",
|
|
2306
2684
|
children: [
|
|
@@ -2490,7 +2868,7 @@ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel,
|
|
|
2490
2868
|
return createPortal(node, document.body);
|
|
2491
2869
|
};
|
|
2492
2870
|
var GeometryEditorPanel = forwardRef(
|
|
2493
|
-
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark }, ref) {
|
|
2871
|
+
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer }, ref) {
|
|
2494
2872
|
const handleRef = useRef(null);
|
|
2495
2873
|
const [ready, setReady] = useState(false);
|
|
2496
2874
|
const [propsPopover, setPropsPopover] = useState(null);
|
|
@@ -2552,7 +2930,7 @@ var GeometryEditorPanel = forwardRef(
|
|
|
2552
2930
|
insert: performInsert,
|
|
2553
2931
|
hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
|
|
2554
2932
|
}), [performInsert]);
|
|
2555
|
-
const wrapperStyle = {
|
|
2933
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
2556
2934
|
position: "absolute",
|
|
2557
2935
|
top: "50%",
|
|
2558
2936
|
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
@@ -2566,11 +2944,30 @@ var GeometryEditorPanel = forwardRef(
|
|
|
2566
2944
|
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
|
|
2567
2945
|
"data-testid": "geometry-editor-panel",
|
|
2568
2946
|
"data-stamp-area": "true",
|
|
2947
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
2569
2948
|
style: wrapperStyle,
|
|
2570
|
-
className:
|
|
2949
|
+
className: [
|
|
2950
|
+
isDark ? "theme--dark " : "",
|
|
2951
|
+
"flex flex-col overflow-hidden bg-white",
|
|
2952
|
+
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"
|
|
2953
|
+
].join(" "),
|
|
2571
2954
|
children: [
|
|
2572
|
-
/* @__PURE__ */ jsxs("header", { className: "flex items-center
|
|
2573
|
-
/* @__PURE__ */
|
|
2955
|
+
/* @__PURE__ */ 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: [
|
|
2956
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
2957
|
+
"button",
|
|
2958
|
+
{
|
|
2959
|
+
type: "button",
|
|
2960
|
+
onClick: onOpenDrawer,
|
|
2961
|
+
"aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
|
|
2962
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
2963
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2964
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
2965
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
2966
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
2967
|
+
] })
|
|
2968
|
+
}
|
|
2969
|
+
),
|
|
2970
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
2574
2971
|
/* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2575
2972
|
/* @__PURE__ */ jsx("polygon", { points: "3,18 12,3 21,18" }),
|
|
2576
2973
|
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
|
|
@@ -2579,12 +2976,23 @@ var GeometryEditorPanel = forwardRef(
|
|
|
2579
2976
|
] }),
|
|
2580
2977
|
"D\u1EF1ng h\xECnh h\u1ECDc"
|
|
2581
2978
|
] }),
|
|
2582
|
-
|
|
2979
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
2980
|
+
"button",
|
|
2981
|
+
{
|
|
2982
|
+
type: "button",
|
|
2983
|
+
onClick: handleInsert,
|
|
2984
|
+
disabled: !ready,
|
|
2985
|
+
"data-testid": "geometry-insert-btn-mobile",
|
|
2986
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
2987
|
+
children: "Ch\xE8n"
|
|
2988
|
+
}
|
|
2989
|
+
),
|
|
2990
|
+
/* @__PURE__ */ 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__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2583
2991
|
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2584
2992
|
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2585
2993
|
] }) })
|
|
2586
2994
|
] }),
|
|
2587
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", style: { height: "420px" }, children: /* @__PURE__ */ jsx(
|
|
2995
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", style: isMobile ? void 0 : { height: "420px" }, children: /* @__PURE__ */ jsx(
|
|
2588
2996
|
JSXGraphMiniBoard,
|
|
2589
2997
|
{
|
|
2590
2998
|
onReady: handleReady,
|
|
@@ -2657,7 +3065,7 @@ var GeometryEditorPanel = forwardRef(
|
|
|
2657
3065
|
}
|
|
2658
3066
|
}
|
|
2659
3067
|
),
|
|
2660
|
-
/* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
3068
|
+
!isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
2661
3069
|
/* @__PURE__ */ 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." }),
|
|
2662
3070
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
2663
3071
|
/* @__PURE__ */ jsx(
|
|
@@ -2685,6 +3093,76 @@ var GeometryEditorPanel = forwardRef(
|
|
|
2685
3093
|
);
|
|
2686
3094
|
}
|
|
2687
3095
|
);
|
|
3096
|
+
var A_CODE2 = "a".charCodeAt(0);
|
|
3097
|
+
function isFieldFocused() {
|
|
3098
|
+
const ae = typeof document !== "undefined" ? document.activeElement : null;
|
|
3099
|
+
return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
|
|
3100
|
+
}
|
|
3101
|
+
function useChordShortcut(args) {
|
|
3102
|
+
const { groupOrder, tools, onSelect, enabled } = args;
|
|
3103
|
+
const [chordGroup, setChordGroup] = useState(null);
|
|
3104
|
+
const groupOrderRef = useRef(groupOrder);
|
|
3105
|
+
const toolsRef = useRef(tools);
|
|
3106
|
+
const onSelectRef = useRef(onSelect);
|
|
3107
|
+
const chordGroupRef = useRef(null);
|
|
3108
|
+
groupOrderRef.current = groupOrder;
|
|
3109
|
+
toolsRef.current = tools;
|
|
3110
|
+
onSelectRef.current = onSelect;
|
|
3111
|
+
const cancel = useCallback(() => {
|
|
3112
|
+
chordGroupRef.current = null;
|
|
3113
|
+
setChordGroup(null);
|
|
3114
|
+
}, []);
|
|
3115
|
+
useEffect(() => {
|
|
3116
|
+
if (!enabled) return;
|
|
3117
|
+
const setChord = (next) => {
|
|
3118
|
+
chordGroupRef.current = next;
|
|
3119
|
+
setChordGroup(next);
|
|
3120
|
+
};
|
|
3121
|
+
const onKey = (e) => {
|
|
3122
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
3123
|
+
if (isFieldFocused()) return;
|
|
3124
|
+
const key = e.key;
|
|
3125
|
+
const lower = key.length === 1 ? key.toLowerCase() : key;
|
|
3126
|
+
if (key === "Escape") {
|
|
3127
|
+
if (chordGroupRef.current !== null) {
|
|
3128
|
+
e.preventDefault();
|
|
3129
|
+
e.stopPropagation();
|
|
3130
|
+
setChord(null);
|
|
3131
|
+
}
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if (lower.length === 1 && lower >= "a" && lower <= "z") {
|
|
3135
|
+
const idx = lower.charCodeAt(0) - A_CODE2;
|
|
3136
|
+
if (idx < groupOrderRef.current.length) {
|
|
3137
|
+
e.preventDefault();
|
|
3138
|
+
e.stopPropagation();
|
|
3139
|
+
setChord(groupOrderRef.current[idx]);
|
|
3140
|
+
}
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
if (key >= "1" && key <= "9") {
|
|
3144
|
+
const active = chordGroupRef.current;
|
|
3145
|
+
if (active === null) return;
|
|
3146
|
+
const n = key.charCodeAt(0) - "1".charCodeAt(0);
|
|
3147
|
+
const toolsInGroup = toolsRef.current.filter(
|
|
3148
|
+
(t) => t.group === active
|
|
3149
|
+
);
|
|
3150
|
+
e.preventDefault();
|
|
3151
|
+
e.stopPropagation();
|
|
3152
|
+
if (n < toolsInGroup.length) {
|
|
3153
|
+
onSelectRef.current(toolsInGroup[n].key);
|
|
3154
|
+
}
|
|
3155
|
+
setChord(null);
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
3160
|
+
return () => {
|
|
3161
|
+
window.removeEventListener("keydown", onKey, { capture: true });
|
|
3162
|
+
};
|
|
3163
|
+
}, [enabled]);
|
|
3164
|
+
return { chordGroup, cancel };
|
|
3165
|
+
}
|
|
2688
3166
|
|
|
2689
3167
|
// src/stamps/shared/svgToImage.ts
|
|
2690
3168
|
async function hashString(input) {
|
|
@@ -2807,6 +3285,14 @@ var GeometryStampHost = forwardRef(
|
|
|
2807
3285
|
function GeometryStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
2808
3286
|
const panelRef = useRef(null);
|
|
2809
3287
|
const [geomState, setGeomState] = useState(INITIAL_GEOM_STATE);
|
|
3288
|
+
const { isMobile } = useIsMobile();
|
|
3289
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
3290
|
+
const { chordGroup } = useChordShortcut({
|
|
3291
|
+
groupOrder: GROUP_ORDER,
|
|
3292
|
+
tools: TOOLS,
|
|
3293
|
+
onSelect: (key) => panelRef.current?.setTool(key),
|
|
3294
|
+
enabled: !isMobile
|
|
3295
|
+
});
|
|
2810
3296
|
const initialState = useMemo(() => {
|
|
2811
3297
|
if (!editingElement) return null;
|
|
2812
3298
|
if (!isGeometryCustomData(editingElement.customData)) return null;
|
|
@@ -2860,7 +3346,11 @@ var GeometryStampHost = forwardRef(
|
|
|
2860
3346
|
onUndo: () => panelRef.current?.undo(),
|
|
2861
3347
|
canUndo: geomState.canUndo,
|
|
2862
3348
|
onClose,
|
|
2863
|
-
isDark
|
|
3349
|
+
isDark,
|
|
3350
|
+
isMobile,
|
|
3351
|
+
drawerOpen,
|
|
3352
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
3353
|
+
chordGroup
|
|
2864
3354
|
}
|
|
2865
3355
|
),
|
|
2866
3356
|
/* @__PURE__ */ jsx(
|
|
@@ -2871,8 +3361,10 @@ var GeometryStampHost = forwardRef(
|
|
|
2871
3361
|
onInsert: handleInsert,
|
|
2872
3362
|
onClose,
|
|
2873
3363
|
onStateChange: setGeomState,
|
|
2874
|
-
withLeftPanel:
|
|
2875
|
-
isDark
|
|
3364
|
+
withLeftPanel: !isMobile,
|
|
3365
|
+
isDark,
|
|
3366
|
+
isMobile,
|
|
3367
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
2876
3368
|
}
|
|
2877
3369
|
)
|
|
2878
3370
|
] });
|
|
@@ -2910,38 +3402,58 @@ var geometryStamp = {
|
|
|
2910
3402
|
},
|
|
2911
3403
|
Host: GeometryStampHost
|
|
2912
3404
|
};
|
|
2913
|
-
function Shell2({ title, icon, onClose, children }) {
|
|
2914
|
-
|
|
2915
|
-
"
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3405
|
+
function Shell2({ title, icon, onClose, children, isMobile, drawerOpen, onDrawerClose }) {
|
|
3406
|
+
const mobileAttrs = isMobile ? {
|
|
3407
|
+
"data-mobile-drawer": "true",
|
|
3408
|
+
"data-drawer-state": drawerOpen ? "open" : "closed"
|
|
3409
|
+
} : {};
|
|
3410
|
+
const handleHeaderClose = () => {
|
|
3411
|
+
if (isMobile) onDrawerClose?.();
|
|
3412
|
+
else onClose();
|
|
3413
|
+
};
|
|
3414
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3415
|
+
isMobile && drawerOpen && /* @__PURE__ */ jsx(
|
|
3416
|
+
"div",
|
|
3417
|
+
{
|
|
3418
|
+
className: "stamp-drawer-backdrop",
|
|
3419
|
+
onPointerDown: onDrawerClose,
|
|
3420
|
+
"aria-hidden": "true"
|
|
3421
|
+
}
|
|
3422
|
+
),
|
|
3423
|
+
/* @__PURE__ */ jsxs(
|
|
3424
|
+
"aside",
|
|
3425
|
+
{
|
|
3426
|
+
role: "complementary",
|
|
3427
|
+
"aria-label": title,
|
|
3428
|
+
"aria-hidden": isMobile && !drawerOpen ? "true" : void 0,
|
|
3429
|
+
"data-testid": "stamp-left-panel",
|
|
3430
|
+
"data-stamp-area": "true",
|
|
3431
|
+
...mobileAttrs,
|
|
3432
|
+
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",
|
|
3433
|
+
children: [
|
|
3434
|
+
/* @__PURE__ */ 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: [
|
|
3435
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
3436
|
+
/* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: icon }),
|
|
3437
|
+
title
|
|
3438
|
+
] }),
|
|
3439
|
+
/* @__PURE__ */ jsx(
|
|
3440
|
+
"button",
|
|
3441
|
+
{
|
|
3442
|
+
onClick: handleHeaderClose,
|
|
3443
|
+
"aria-label": isMobile ? "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5" : "\u0110\xF3ng",
|
|
3444
|
+
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
3445
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3446
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
3447
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
3448
|
+
] })
|
|
3449
|
+
}
|
|
3450
|
+
)
|
|
2927
3451
|
] }),
|
|
2928
|
-
/* @__PURE__ */ jsx(
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
2934
|
-
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2935
|
-
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2936
|
-
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2937
|
-
] })
|
|
2938
|
-
}
|
|
2939
|
-
)
|
|
2940
|
-
] }),
|
|
2941
|
-
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
2942
|
-
]
|
|
2943
|
-
}
|
|
2944
|
-
);
|
|
3452
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
3453
|
+
]
|
|
3454
|
+
}
|
|
3455
|
+
)
|
|
3456
|
+
] });
|
|
2945
3457
|
}
|
|
2946
3458
|
function Section2({ label, children }) {
|
|
2947
3459
|
return /* @__PURE__ */ jsxs("section", { children: [
|
|
@@ -2988,65 +3500,80 @@ function LeftPanel2({
|
|
|
2988
3500
|
displayMode,
|
|
2989
3501
|
onDisplayModeChange,
|
|
2990
3502
|
onInsertSnippet,
|
|
2991
|
-
onClose
|
|
3503
|
+
onClose,
|
|
3504
|
+
isMobile,
|
|
3505
|
+
drawerOpen,
|
|
3506
|
+
onDrawerClose
|
|
2992
3507
|
}) {
|
|
2993
|
-
return /* @__PURE__ */ jsxs(
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3508
|
+
return /* @__PURE__ */ jsxs(
|
|
3509
|
+
Shell2,
|
|
3510
|
+
{
|
|
3511
|
+
title: "C\xF4ng th\u1EE9c LaTeX",
|
|
3512
|
+
icon: "\u2211",
|
|
3513
|
+
onClose,
|
|
3514
|
+
isMobile,
|
|
3515
|
+
drawerOpen,
|
|
3516
|
+
onDrawerClose,
|
|
3517
|
+
children: [
|
|
3518
|
+
/* @__PURE__ */ jsx(Section2, { label: "Ch\u1EBF \u0111\u1ED9 hi\u1EC3n th\u1ECB", children: /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [
|
|
3519
|
+
/* @__PURE__ */ jsxs(
|
|
3520
|
+
"button",
|
|
3521
|
+
{
|
|
3522
|
+
type: "button",
|
|
3523
|
+
onClick: () => onDisplayModeChange(false),
|
|
3524
|
+
"aria-pressed": !displayMode,
|
|
3525
|
+
className: [
|
|
3526
|
+
"rounded-md border px-2 py-1.5 text-xs transition",
|
|
3527
|
+
!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"
|
|
3528
|
+
].join(" "),
|
|
3529
|
+
children: [
|
|
3530
|
+
/* @__PURE__ */ jsx("span", { className: "block font-medium", children: "Inline" }),
|
|
3531
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] text-slate-500", children: "$ ... $" })
|
|
3532
|
+
]
|
|
3533
|
+
}
|
|
3534
|
+
),
|
|
3535
|
+
/* @__PURE__ */ jsxs(
|
|
3536
|
+
"button",
|
|
3537
|
+
{
|
|
3538
|
+
type: "button",
|
|
3539
|
+
onClick: () => onDisplayModeChange(true),
|
|
3540
|
+
"aria-pressed": displayMode,
|
|
3541
|
+
className: [
|
|
3542
|
+
"rounded-md border px-2 py-1.5 text-xs transition",
|
|
3543
|
+
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"
|
|
3544
|
+
].join(" "),
|
|
3545
|
+
children: [
|
|
3546
|
+
/* @__PURE__ */ jsx("span", { className: "block font-medium", children: "Block" }),
|
|
3547
|
+
/* @__PURE__ */ jsx("span", { className: "block text-[10px] text-slate-500", children: "$$ ... $$" })
|
|
3548
|
+
]
|
|
3549
|
+
}
|
|
3550
|
+
)
|
|
3551
|
+
] }) }),
|
|
3552
|
+
SNIPPETS.map((group) => /* @__PURE__ */ jsx(Section2, { label: group.group, children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: group.items.map((s) => /* @__PURE__ */ jsx(
|
|
3553
|
+
"button",
|
|
3554
|
+
{
|
|
3555
|
+
type: "button",
|
|
3556
|
+
"data-snippet": s.snippet,
|
|
3557
|
+
onClick: () => onInsertSnippet(s.snippet),
|
|
3558
|
+
title: s.snippet,
|
|
3559
|
+
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",
|
|
3560
|
+
children: s.preview
|
|
3561
|
+
},
|
|
3562
|
+
s.snippet
|
|
3563
|
+
)) }) }, group.group)),
|
|
3564
|
+
/* @__PURE__ */ jsx(Section2, { label: "Ph\xEDm t\u1EAFt", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 text-[11px] text-slate-600", children: [
|
|
3565
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
3566
|
+
/* @__PURE__ */ jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Enter" }),
|
|
3567
|
+
"ch\xE8n"
|
|
3568
|
+
] }),
|
|
3569
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
3570
|
+
/* @__PURE__ */ jsx("kbd", { className: "rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 font-mono", children: "Esc" }),
|
|
3571
|
+
"\u0111\xF3ng"
|
|
3572
|
+
] })
|
|
3573
|
+
] }) })
|
|
3574
|
+
]
|
|
3575
|
+
}
|
|
3576
|
+
);
|
|
3050
3577
|
}
|
|
3051
3578
|
|
|
3052
3579
|
// src/stamps/latex/render.ts
|
|
@@ -3098,7 +3625,9 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3098
3625
|
onClose,
|
|
3099
3626
|
displayMode: controlledDisplayMode,
|
|
3100
3627
|
onDisplayModeChange,
|
|
3101
|
-
withLeftPanel = false
|
|
3628
|
+
withLeftPanel = false,
|
|
3629
|
+
isMobile = false,
|
|
3630
|
+
onOpenDrawer
|
|
3102
3631
|
}, ref) {
|
|
3103
3632
|
const [value, setValue] = useState(initialValue);
|
|
3104
3633
|
const [internalDisplayMode] = useState(false);
|
|
@@ -3169,7 +3698,7 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3169
3698
|
[value, previewSvg, error, displayMode, onInsert]
|
|
3170
3699
|
);
|
|
3171
3700
|
const isLegacyPosition = x > 0 || y > 0;
|
|
3172
|
-
const wrapperStyle = isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
|
|
3701
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 50 } : isLegacyPosition ? { position: "absolute", top: y, left: x, zIndex: 50 } : {
|
|
3173
3702
|
position: "absolute",
|
|
3174
3703
|
top: "50%",
|
|
3175
3704
|
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
@@ -3181,29 +3710,55 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3181
3710
|
{
|
|
3182
3711
|
style: wrapperStyle,
|
|
3183
3712
|
"data-stamp-area": "true",
|
|
3184
|
-
|
|
3713
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
3714
|
+
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",
|
|
3185
3715
|
role: "dialog",
|
|
3186
3716
|
"aria-label": "Nh\u1EADp c\xF4ng th\u1EE9c LaTeX",
|
|
3187
3717
|
children: [
|
|
3188
|
-
/* @__PURE__ */ jsxs("header", { className:
|
|
3189
|
-
/* @__PURE__ */
|
|
3718
|
+
/* @__PURE__ */ 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: [
|
|
3719
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
3720
|
+
"button",
|
|
3721
|
+
{
|
|
3722
|
+
type: "button",
|
|
3723
|
+
onClick: onOpenDrawer,
|
|
3724
|
+
"aria-label": "M\u1EDF ng\u0103n snippet",
|
|
3725
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
3726
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3727
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
3728
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
3729
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
3730
|
+
] })
|
|
3731
|
+
}
|
|
3732
|
+
),
|
|
3733
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
3190
3734
|
/* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: "\u2211" }),
|
|
3191
3735
|
"C\xF4ng th\u1EE9c LaTeX"
|
|
3192
3736
|
] }),
|
|
3737
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
3738
|
+
"button",
|
|
3739
|
+
{
|
|
3740
|
+
type: "button",
|
|
3741
|
+
onClick: handleInsert,
|
|
3742
|
+
disabled: !previewSvg || !!error,
|
|
3743
|
+
"data-testid": "latex-insert-btn-mobile",
|
|
3744
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
3745
|
+
children: "Ch\xE8n"
|
|
3746
|
+
}
|
|
3747
|
+
),
|
|
3193
3748
|
/* @__PURE__ */ jsx(
|
|
3194
3749
|
"button",
|
|
3195
3750
|
{
|
|
3196
3751
|
onClick: onClose,
|
|
3197
3752
|
"aria-label": "\u0110\xF3ng",
|
|
3198
|
-
className: "
|
|
3199
|
-
children: /* @__PURE__ */ jsxs("svg", { width: "
|
|
3753
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
3754
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3200
3755
|
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
3201
3756
|
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
3202
3757
|
] })
|
|
3203
3758
|
}
|
|
3204
3759
|
)
|
|
3205
3760
|
] }),
|
|
3206
|
-
/* @__PURE__ */ jsxs("div", { className:
|
|
3761
|
+
/* @__PURE__ */ jsxs("div", { className: `space-y-2 p-3${isMobile ? " flex min-h-0 flex-1 flex-col" : ""}`, children: [
|
|
3207
3762
|
/* @__PURE__ */ jsx(
|
|
3208
3763
|
"input",
|
|
3209
3764
|
{
|
|
@@ -3214,7 +3769,7 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3214
3769
|
onChange: (e) => setValue(e.target.value),
|
|
3215
3770
|
onKeyDown: handleKeyDown,
|
|
3216
3771
|
placeholder: "Vd: \\frac{a^2+b^2}{c}",
|
|
3217
|
-
className:
|
|
3772
|
+
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"}`,
|
|
3218
3773
|
autoFocus: true
|
|
3219
3774
|
}
|
|
3220
3775
|
),
|
|
@@ -3222,7 +3777,8 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3222
3777
|
"div",
|
|
3223
3778
|
{
|
|
3224
3779
|
className: [
|
|
3225
|
-
"flex
|
|
3780
|
+
"flex items-center justify-center rounded border p-3 text-center",
|
|
3781
|
+
isMobile ? "min-h-0 flex-1 overflow-auto" : "min-h-[64px]",
|
|
3226
3782
|
error ? "border-rose-300 bg-rose-50 text-rose-700" : "border-slate-200 bg-slate-50"
|
|
3227
3783
|
].join(" "),
|
|
3228
3784
|
children: error ? /* @__PURE__ */ jsxs("span", { className: "text-xs", children: [
|
|
@@ -3231,7 +3787,7 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3231
3787
|
] }) : previewSvg ? /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: previewSvg } }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-400", children: "(xem tr\u01B0\u1EDBc)" })
|
|
3232
3788
|
}
|
|
3233
3789
|
),
|
|
3234
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3790
|
+
!isMobile && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3235
3791
|
/* @__PURE__ */ jsxs("span", { className: "text-[11px] text-slate-500", children: [
|
|
3236
3792
|
displayMode ? "Block" : "Inline",
|
|
3237
3793
|
" \xB7 Enter \u0111\u1EC3 ch\xE8n"
|
|
@@ -3255,6 +3811,10 @@ var EditorPopover = forwardRef(function EditorPopover2({
|
|
|
3255
3811
|
}
|
|
3256
3812
|
)
|
|
3257
3813
|
] })
|
|
3814
|
+
] }),
|
|
3815
|
+
isMobile && /* @__PURE__ */ jsxs("div", { className: "text-center text-[11px] text-slate-500", children: [
|
|
3816
|
+
displayMode ? "Block" : "Inline",
|
|
3817
|
+
" \xB7 B\u1EA5m Ch\xE8n \u1EDF thanh tr\xEAn"
|
|
3258
3818
|
] })
|
|
3259
3819
|
] })
|
|
3260
3820
|
]
|
|
@@ -3269,6 +3829,8 @@ function isLatexCustomData(data) {
|
|
|
3269
3829
|
var LatexStampHost = forwardRef(
|
|
3270
3830
|
function LatexStampHost2({ api, editingElement, onClose }, ref) {
|
|
3271
3831
|
const editorRef = useRef(null);
|
|
3832
|
+
const { isMobile } = useIsMobile();
|
|
3833
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
3272
3834
|
const initial = useMemo(() => {
|
|
3273
3835
|
if (editingElement && isLatexCustomData(editingElement.customData)) {
|
|
3274
3836
|
return {
|
|
@@ -3315,7 +3877,10 @@ var LatexStampHost = forwardRef(
|
|
|
3315
3877
|
displayMode,
|
|
3316
3878
|
onDisplayModeChange: setDisplayMode,
|
|
3317
3879
|
onInsertSnippet: (s) => editorRef.current?.insertAtCursor(s),
|
|
3318
|
-
onClose
|
|
3880
|
+
onClose,
|
|
3881
|
+
isMobile,
|
|
3882
|
+
drawerOpen,
|
|
3883
|
+
onDrawerClose: () => setDrawerOpen(false)
|
|
3319
3884
|
}
|
|
3320
3885
|
),
|
|
3321
3886
|
/* @__PURE__ */ jsx(
|
|
@@ -3329,7 +3894,9 @@ var LatexStampHost = forwardRef(
|
|
|
3329
3894
|
onDisplayModeChange: setDisplayMode,
|
|
3330
3895
|
onInsert: handleInsert,
|
|
3331
3896
|
onClose,
|
|
3332
|
-
withLeftPanel:
|
|
3897
|
+
withLeftPanel: !isMobile,
|
|
3898
|
+
isMobile,
|
|
3899
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
3333
3900
|
}
|
|
3334
3901
|
)
|
|
3335
3902
|
] });
|
|
@@ -3961,7 +4528,25 @@ var MiniBoard3D = forwardRef(function MiniBoard3D2({ isDark, initialState }, ref
|
|
|
3961
4528
|
toolRef.current = t;
|
|
3962
4529
|
notify();
|
|
3963
4530
|
},
|
|
3964
|
-
|
|
4531
|
+
// Sync toạ độ live của free point3d về log trước khi trả ra. JSXGraph
|
|
4532
|
+
// cho phép drag point3d (parents=[x,y,z] không có ref), việc drag chỉ
|
|
4533
|
+
// cập nhật obj.X()/Y()/Z() chứ không đụng log → re-edit + Chèn sẽ
|
|
4534
|
+
// serialize toạ độ cũ → SVG không đổi → fileId trùng → user thấy
|
|
4535
|
+
// "k thay đổi". Line/plane/polygon/sphere tham chiếu point qua @id nên
|
|
4536
|
+
// auto-update theo.
|
|
4537
|
+
getCreationLog: () => logRef.current.map((e) => {
|
|
4538
|
+
if (e.type !== "point3d") return { ...e };
|
|
4539
|
+
const parents = e.parents;
|
|
4540
|
+
if (!Array.isArray(parents) || parents.length !== 3) return { ...e };
|
|
4541
|
+
if (typeof parents[0] !== "number" || typeof parents[1] !== "number" || typeof parents[2] !== "number") return { ...e };
|
|
4542
|
+
const obj = objMapRef.current.get(e.id);
|
|
4543
|
+
if (!obj || typeof obj.X !== "function" || typeof obj.Y !== "function" || typeof obj.Z !== "function") return { ...e };
|
|
4544
|
+
const x = obj.X();
|
|
4545
|
+
const y = obj.Y();
|
|
4546
|
+
const z = obj.Z();
|
|
4547
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return { ...e };
|
|
4548
|
+
return { ...e, parents: [x, y, z] };
|
|
4549
|
+
}),
|
|
3965
4550
|
pushLog: (e) => {
|
|
3966
4551
|
logRef.current.push(e);
|
|
3967
4552
|
notify();
|
|
@@ -4037,15 +4622,159 @@ var MiniBoard3D = forwardRef(function MiniBoard3D2({ isDark, initialState }, ref
|
|
|
4037
4622
|
}
|
|
4038
4623
|
);
|
|
4039
4624
|
});
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4625
|
+
var EditorPanel = forwardRef(function EditorPanel2({ isDark, initial, onInsert, onClose, isMobile = false, withLeftPanel = false, onBoardReady, onOpenDrawer }, ref) {
|
|
4626
|
+
const boardRef = useRef(null);
|
|
4627
|
+
const [ready, setReady] = useState(false);
|
|
4628
|
+
const onBoardReadyRef = useRef(onBoardReady);
|
|
4629
|
+
onBoardReadyRef.current = onBoardReady;
|
|
4630
|
+
const setBoard = useCallback((h) => {
|
|
4631
|
+
boardRef.current = h;
|
|
4632
|
+
setReady(!!h);
|
|
4633
|
+
onBoardReadyRef.current?.(h);
|
|
4634
|
+
}, []);
|
|
4635
|
+
const performInsert = useCallback(() => {
|
|
4636
|
+
const board = boardRef.current;
|
|
4637
|
+
if (!board) return false;
|
|
4638
|
+
const log = board.getCreationLog();
|
|
4639
|
+
if (log.length === 0) return false;
|
|
4640
|
+
const view = board.getViewState();
|
|
4641
|
+
const state = {
|
|
4642
|
+
version: 1,
|
|
4643
|
+
bbox: board.getBbox(),
|
|
4644
|
+
view,
|
|
4645
|
+
showAxes: board.getShowAxes(),
|
|
4646
|
+
showMesh: board.getShowMesh(),
|
|
4647
|
+
elements: log
|
|
4648
|
+
};
|
|
4649
|
+
const snap = board.snapshotSVG();
|
|
4650
|
+
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
4651
|
+
return true;
|
|
4652
|
+
}, [onInsert]);
|
|
4653
|
+
useImperativeHandle(
|
|
4654
|
+
ref,
|
|
4655
|
+
() => ({
|
|
4656
|
+
tryInsert: performInsert,
|
|
4657
|
+
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
4658
|
+
}),
|
|
4659
|
+
[performInsert]
|
|
4660
|
+
);
|
|
4661
|
+
const handleInsert = useCallback(() => {
|
|
4662
|
+
performInsert();
|
|
4663
|
+
}, [performInsert]);
|
|
4664
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
4665
|
+
position: "absolute",
|
|
4666
|
+
top: "50%",
|
|
4667
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
4668
|
+
transform: "translate(-50%, -50%)",
|
|
4669
|
+
zIndex: 40
|
|
4670
|
+
};
|
|
4671
|
+
return /* @__PURE__ */ jsxs(
|
|
4672
|
+
"div",
|
|
4673
|
+
{
|
|
4674
|
+
role: "dialog",
|
|
4675
|
+
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc 3D",
|
|
4676
|
+
"data-testid": "geom3d-editor-panel",
|
|
4677
|
+
"data-stamp-area": "true",
|
|
4678
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
4679
|
+
style: wrapperStyle,
|
|
4680
|
+
className: [
|
|
4681
|
+
isDark ? "theme--dark " : "",
|
|
4682
|
+
"flex flex-col overflow-hidden bg-white",
|
|
4683
|
+
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"
|
|
4684
|
+
].join(" "),
|
|
4685
|
+
children: [
|
|
4686
|
+
/* @__PURE__ */ 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: [
|
|
4687
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
4688
|
+
"button",
|
|
4689
|
+
{
|
|
4690
|
+
type: "button",
|
|
4691
|
+
onClick: onOpenDrawer,
|
|
4692
|
+
"aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
|
|
4693
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
4694
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4695
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
4696
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
4697
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
4698
|
+
] })
|
|
4699
|
+
}
|
|
4700
|
+
),
|
|
4701
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
4702
|
+
/* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ 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" }) }),
|
|
4703
|
+
"H\xECnh h\u1ECDc kh\xF4ng gian (3D)"
|
|
4704
|
+
] }),
|
|
4705
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
4706
|
+
"button",
|
|
4707
|
+
{
|
|
4708
|
+
type: "button",
|
|
4709
|
+
onClick: handleInsert,
|
|
4710
|
+
disabled: !ready,
|
|
4711
|
+
"data-testid": "geom3d-insert-btn-mobile",
|
|
4712
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
4713
|
+
children: "Ch\xE8n"
|
|
4714
|
+
}
|
|
4715
|
+
),
|
|
4716
|
+
/* @__PURE__ */ jsx(
|
|
4717
|
+
"button",
|
|
4718
|
+
{
|
|
4719
|
+
onClick: onClose,
|
|
4720
|
+
"aria-label": "\u0110\xF3ng",
|
|
4721
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
4722
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4723
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4724
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4725
|
+
] })
|
|
4726
|
+
}
|
|
4727
|
+
)
|
|
4728
|
+
] }),
|
|
4729
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial }) }),
|
|
4730
|
+
!isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
4731
|
+
/* @__PURE__ */ 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." }),
|
|
4732
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
4733
|
+
/* @__PURE__ */ jsx(
|
|
4734
|
+
"button",
|
|
4735
|
+
{
|
|
4736
|
+
onClick: onClose,
|
|
4737
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
4738
|
+
children: "Hu\u1EF7"
|
|
4739
|
+
}
|
|
4740
|
+
),
|
|
4741
|
+
/* @__PURE__ */ jsx(
|
|
4742
|
+
"button",
|
|
4743
|
+
{
|
|
4744
|
+
onClick: handleInsert,
|
|
4745
|
+
disabled: !ready,
|
|
4746
|
+
"data-testid": "geom3d-insert-btn",
|
|
4747
|
+
className: "rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-blue-700 disabled:opacity-50",
|
|
4748
|
+
children: "Ch\xE8n"
|
|
4749
|
+
}
|
|
4750
|
+
)
|
|
4751
|
+
] })
|
|
4752
|
+
] })
|
|
4753
|
+
]
|
|
4754
|
+
}
|
|
4755
|
+
);
|
|
4756
|
+
});
|
|
4757
|
+
|
|
4758
|
+
// src/stamps/geometry-3d/editor/tools.ts
|
|
4759
|
+
var GROUP_LABELS_3D = {
|
|
4760
|
+
view: "Xem",
|
|
4761
|
+
primitive: "C\u01A1 b\u1EA3n",
|
|
4762
|
+
solid: "Kh\u1ED1i \u0111a di\u1EC7n",
|
|
4763
|
+
curved: "Kh\u1ED1i cong",
|
|
4764
|
+
meta: "Kh\xE1c"
|
|
4765
|
+
};
|
|
4766
|
+
var GROUP_ORDER_3D = [
|
|
4767
|
+
"view",
|
|
4768
|
+
"primitive",
|
|
4769
|
+
"solid",
|
|
4770
|
+
"curved",
|
|
4771
|
+
"meta"
|
|
4772
|
+
];
|
|
4773
|
+
var A_CODE_3D = "A".charCodeAt(0);
|
|
4774
|
+
function letterForGroup3D(g) {
|
|
4775
|
+
const idx = GROUP_ORDER_3D.indexOf(g);
|
|
4776
|
+
return idx >= 0 ? String.fromCharCode(A_CODE_3D + idx) : "";
|
|
4777
|
+
}
|
|
4049
4778
|
var TOOLS_3D = [
|
|
4050
4779
|
{ key: "move", label: "Di chuy\u1EC3n", group: "view", stepsRequired: 0 },
|
|
4051
4780
|
{ key: "point", label: "\u0110i\u1EC3m", group: "primitive", stepsRequired: 1, hint: "Nh\u1EADp (x, y, z)" },
|
|
@@ -4099,8 +4828,8 @@ var TOOLS_3D = [
|
|
|
4099
4828
|
},
|
|
4100
4829
|
{ key: "label", label: "Nh\xE3n", group: "meta", stepsRequired: 1, hint: "G\u1EAFn v\xE0o \u0111i\u1EC3m" }
|
|
4101
4830
|
];
|
|
4102
|
-
function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
4103
|
-
return /* @__PURE__ */
|
|
4831
|
+
function ToolButton({ toolKey, label, hint, active, onClick, icon, badge }) {
|
|
4832
|
+
return /* @__PURE__ */ jsxs(
|
|
4104
4833
|
"button",
|
|
4105
4834
|
{
|
|
4106
4835
|
type: "button",
|
|
@@ -4111,10 +4840,13 @@ function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
|
4111
4840
|
"data-active": active || void 0,
|
|
4112
4841
|
"data-tool": toolKey,
|
|
4113
4842
|
className: [
|
|
4114
|
-
"flex h-8 items-center justify-center rounded-md transition",
|
|
4843
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
4115
4844
|
active ? "bg-blue-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
4116
4845
|
].join(" "),
|
|
4117
|
-
children:
|
|
4846
|
+
children: [
|
|
4847
|
+
icon,
|
|
4848
|
+
badge
|
|
4849
|
+
]
|
|
4118
4850
|
}
|
|
4119
4851
|
);
|
|
4120
4852
|
}
|
|
@@ -4166,7 +4898,10 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4166
4898
|
"aria-label": title,
|
|
4167
4899
|
"data-testid": "geom3d-left-panel",
|
|
4168
4900
|
"data-stamp-area": "true",
|
|
4169
|
-
className:
|
|
4901
|
+
className: [
|
|
4902
|
+
isDark ? "theme--dark " : "",
|
|
4903
|
+
"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"
|
|
4904
|
+
].join(""),
|
|
4170
4905
|
children: [
|
|
4171
4906
|
/* @__PURE__ */ 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: [
|
|
4172
4907
|
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -4179,10 +4914,7 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4179
4914
|
onClick: onClose,
|
|
4180
4915
|
"aria-label": "\u0110\xF3ng",
|
|
4181
4916
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
4182
|
-
children: /* @__PURE__ */
|
|
4183
|
-
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4184
|
-
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4185
|
-
] })
|
|
4917
|
+
children: /* @__PURE__ */ jsx(CloseIcon2, {})
|
|
4186
4918
|
}
|
|
4187
4919
|
)
|
|
4188
4920
|
] }),
|
|
@@ -4198,11 +4930,39 @@ function Section3({ label, children }) {
|
|
|
4198
4930
|
] });
|
|
4199
4931
|
}
|
|
4200
4932
|
var Geom3DIconHeader = /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ 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" }) });
|
|
4201
|
-
function
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4933
|
+
function CloseIcon2() {
|
|
4934
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4935
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4936
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4937
|
+
] });
|
|
4938
|
+
}
|
|
4939
|
+
function UndoIcon2() {
|
|
4940
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4941
|
+
/* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
4942
|
+
/* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
4943
|
+
] });
|
|
4944
|
+
}
|
|
4945
|
+
function ResetViewIcon() {
|
|
4946
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4947
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
4948
|
+
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
4949
|
+
] });
|
|
4950
|
+
}
|
|
4951
|
+
function AxisIcon3D() {
|
|
4952
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4953
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "20", x2: "12", y2: "4" }),
|
|
4954
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "22", y2: "6" }),
|
|
4955
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "2", y2: "18" })
|
|
4956
|
+
] });
|
|
4957
|
+
}
|
|
4958
|
+
function MeshIcon() {
|
|
4959
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
4960
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L12 4 L20 8 L12 12 Z" }),
|
|
4961
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L4 16 L12 20 L12 12" }),
|
|
4962
|
+
/* @__PURE__ */ jsx("path", { d: "M12 20 L20 16 L20 8" })
|
|
4963
|
+
] });
|
|
4964
|
+
}
|
|
4965
|
+
function useToolHoverTooltip2() {
|
|
4206
4966
|
const [hover, setHover] = useState(null);
|
|
4207
4967
|
const [portalReady, setPortalReady] = useState(false);
|
|
4208
4968
|
const hoverTimerRef = useRef(null);
|
|
@@ -4212,17 +4972,6 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4212
4972
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4213
4973
|
};
|
|
4214
4974
|
}, []);
|
|
4215
|
-
useEffect(() => {
|
|
4216
|
-
if (!handle) return;
|
|
4217
|
-
const sync = () => {
|
|
4218
|
-
setTool(handle.getTool());
|
|
4219
|
-
setShowAxes(handle.getShowAxes());
|
|
4220
|
-
setShowMesh(handle.getShowMesh());
|
|
4221
|
-
setCanUndo(handle.canUndo());
|
|
4222
|
-
};
|
|
4223
|
-
sync();
|
|
4224
|
-
return handle.subscribe(sync);
|
|
4225
|
-
}, [handle]);
|
|
4226
4975
|
const showHover = useCallback((el, t) => {
|
|
4227
4976
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4228
4977
|
hoverTimerRef.current = setTimeout(() => {
|
|
@@ -4237,14 +4986,45 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4237
4986
|
}
|
|
4238
4987
|
setHover(null);
|
|
4239
4988
|
}, []);
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4989
|
+
return { hover, portalReady, showHover, hideHover };
|
|
4990
|
+
}
|
|
4991
|
+
function useHandleState(handle) {
|
|
4992
|
+
const [tool, setTool] = useState("move");
|
|
4993
|
+
const [showAxes, setShowAxes] = useState(true);
|
|
4994
|
+
const [showMesh, setShowMesh] = useState(false);
|
|
4995
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
4996
|
+
useEffect(() => {
|
|
4997
|
+
if (!handle) return;
|
|
4998
|
+
const sync = () => {
|
|
4999
|
+
setTool(handle.getTool());
|
|
5000
|
+
setShowAxes(handle.getShowAxes());
|
|
5001
|
+
setShowMesh(handle.getShowMesh());
|
|
5002
|
+
setCanUndo(handle.canUndo());
|
|
5003
|
+
};
|
|
5004
|
+
sync();
|
|
5005
|
+
return handle.subscribe(sync);
|
|
5006
|
+
}, [handle]);
|
|
5007
|
+
return { tool, showAxes, showMesh, canUndo };
|
|
5008
|
+
}
|
|
5009
|
+
function DesktopPanel(props) {
|
|
5010
|
+
const { handle, onResetView, onClose, isDark, chordGroup } = props;
|
|
5011
|
+
const { tool, showAxes, showMesh, canUndo } = useHandleState(handle);
|
|
5012
|
+
const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip2();
|
|
5013
|
+
const grouped = useMemo(() => {
|
|
5014
|
+
return TOOLS_3D.reduce(
|
|
5015
|
+
(acc, t) => {
|
|
5016
|
+
var _a;
|
|
5017
|
+
(acc[_a = t.group] ?? (acc[_a] = [])).push(t);
|
|
5018
|
+
return acc;
|
|
5019
|
+
},
|
|
5020
|
+
{}
|
|
5021
|
+
);
|
|
5022
|
+
}, []);
|
|
5023
|
+
const orderedGroups = useMemo(
|
|
5024
|
+
() => GROUP_ORDER_3D.filter((g) => grouped[g]),
|
|
5025
|
+
[grouped]
|
|
4247
5026
|
);
|
|
5027
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
4248
5028
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4249
5029
|
/* @__PURE__ */ jsxs(Shell3, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
|
|
4250
5030
|
/* @__PURE__ */ jsx(Section3, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
|
|
@@ -4280,10 +5060,7 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4280
5060
|
title: "Reset g\xF3c nh\xECn",
|
|
4281
5061
|
"aria-label": "Reset view",
|
|
4282
5062
|
className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
|
|
4283
|
-
children: /* @__PURE__ */
|
|
4284
|
-
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
4285
|
-
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
4286
|
-
] })
|
|
5063
|
+
children: /* @__PURE__ */ jsx(ResetViewIcon, {})
|
|
4287
5064
|
}
|
|
4288
5065
|
),
|
|
4289
5066
|
/* @__PURE__ */ jsx(
|
|
@@ -4295,34 +5072,95 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4295
5072
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
4296
5073
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
4297
5074
|
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",
|
|
4298
|
-
children: /* @__PURE__ */
|
|
4299
|
-
/* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
4300
|
-
/* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
4301
|
-
] })
|
|
5075
|
+
children: /* @__PURE__ */ jsx(UndoIcon2, {})
|
|
4302
5076
|
}
|
|
4303
5077
|
)
|
|
4304
5078
|
] }) }),
|
|
4305
|
-
|
|
4306
|
-
|
|
5079
|
+
orderedGroups.map((group) => {
|
|
5080
|
+
const tools = grouped[group];
|
|
5081
|
+
const isChordActive = chordGroup === group;
|
|
5082
|
+
const dimmed = chordGroup !== null && !isChordActive;
|
|
5083
|
+
return /* @__PURE__ */ jsxs(
|
|
5084
|
+
"section",
|
|
5085
|
+
{
|
|
5086
|
+
"data-chord-group": group,
|
|
5087
|
+
"data-chord-active": isChordActive ? "true" : "false",
|
|
5088
|
+
className: [
|
|
5089
|
+
"rounded-md transition",
|
|
5090
|
+
isChordActive ? "bg-blue-50 ring-1 ring-blue-400 p-1" : "p-0",
|
|
5091
|
+
dimmed ? "opacity-55" : "opacity-100"
|
|
5092
|
+
].join(" "),
|
|
5093
|
+
children: [
|
|
5094
|
+
/* @__PURE__ */ jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
5095
|
+
/* @__PURE__ */ jsx("span", { children: GROUP_LABELS_3D[group] }),
|
|
5096
|
+
/* @__PURE__ */ jsx(
|
|
5097
|
+
"span",
|
|
5098
|
+
{
|
|
5099
|
+
"data-testid": `chord-letter-${group}`,
|
|
5100
|
+
className: [
|
|
5101
|
+
"font-mono text-[10px] leading-none transition",
|
|
5102
|
+
isChordActive ? "text-blue-700 font-bold" : "text-slate-400"
|
|
5103
|
+
].join(" "),
|
|
5104
|
+
children: letterForGroup3D(group)
|
|
5105
|
+
}
|
|
5106
|
+
)
|
|
5107
|
+
] }),
|
|
5108
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: tools.map((t, i) => {
|
|
5109
|
+
const isActive = tool === t.key;
|
|
5110
|
+
return /* @__PURE__ */ jsx(
|
|
5111
|
+
ToolButton,
|
|
5112
|
+
{
|
|
5113
|
+
toolKey: t.key,
|
|
5114
|
+
label: t.label,
|
|
5115
|
+
hint: t.hint,
|
|
5116
|
+
active: isActive,
|
|
5117
|
+
onClick: () => handle?.setTool(t.key),
|
|
5118
|
+
icon: /* @__PURE__ */ jsx(
|
|
5119
|
+
"span",
|
|
5120
|
+
{
|
|
5121
|
+
onMouseEnter: (e) => showHover(e.currentTarget.closest("button"), t),
|
|
5122
|
+
onMouseLeave: hideHover,
|
|
5123
|
+
onFocus: (e) => showHover(e.currentTarget.closest("button"), t),
|
|
5124
|
+
onBlur: hideHover,
|
|
5125
|
+
children: ICONS_3D[t.key]
|
|
5126
|
+
}
|
|
5127
|
+
),
|
|
5128
|
+
badge: /* @__PURE__ */ jsx(
|
|
5129
|
+
"span",
|
|
5130
|
+
{
|
|
5131
|
+
"data-testid": `chord-num-${t.key}`,
|
|
5132
|
+
className: [
|
|
5133
|
+
"pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
|
|
5134
|
+
isActive ? "text-white/70" : isChordActive ? "text-blue-700 font-bold" : "text-slate-400"
|
|
5135
|
+
].join(" "),
|
|
5136
|
+
children: i + 1
|
|
5137
|
+
}
|
|
5138
|
+
)
|
|
5139
|
+
},
|
|
5140
|
+
t.key
|
|
5141
|
+
);
|
|
5142
|
+
}) })
|
|
5143
|
+
]
|
|
5144
|
+
},
|
|
5145
|
+
group
|
|
5146
|
+
);
|
|
5147
|
+
}),
|
|
5148
|
+
chordGroup && activeGroupTools && /* @__PURE__ */ jsxs(
|
|
5149
|
+
"div",
|
|
4307
5150
|
{
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
}
|
|
4322
|
-
)
|
|
4323
|
-
},
|
|
4324
|
-
t.key
|
|
4325
|
-
)) }) }, group))
|
|
5151
|
+
"data-testid": "chord-hint",
|
|
5152
|
+
className: "mt-1 rounded border border-blue-200 bg-blue-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
|
|
5153
|
+
children: [
|
|
5154
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-blue-700", children: letterForGroup3D(chordGroup) }),
|
|
5155
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
|
|
5156
|
+
activeGroupTools.map((t, i) => /* @__PURE__ */ jsxs("span", { className: "mr-2 inline-block", children: [
|
|
5157
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-blue-700", children: i + 1 }),
|
|
5158
|
+
/* @__PURE__ */ jsx("span", { className: "ml-1", children: t.label })
|
|
5159
|
+
] }, t.key)),
|
|
5160
|
+
/* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
|
|
5161
|
+
]
|
|
5162
|
+
}
|
|
5163
|
+
)
|
|
4326
5164
|
] }),
|
|
4327
5165
|
portalReady && hover && typeof document !== "undefined" ? createPortal(
|
|
4328
5166
|
/* @__PURE__ */ jsxs(
|
|
@@ -4346,107 +5184,71 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4346
5184
|
) : null
|
|
4347
5185
|
] });
|
|
4348
5186
|
}
|
|
4349
|
-
|
|
4350
|
-
const
|
|
4351
|
-
const
|
|
4352
|
-
const
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
() => ({
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
if (log.length === 0) return false;
|
|
4364
|
-
const view = board.getViewState();
|
|
4365
|
-
const state = {
|
|
4366
|
-
version: 1,
|
|
4367
|
-
bbox: board.getBbox(),
|
|
4368
|
-
view,
|
|
4369
|
-
showAxes: board.getShowAxes(),
|
|
4370
|
-
showMesh: board.getShowMesh(),
|
|
4371
|
-
elements: log
|
|
4372
|
-
};
|
|
4373
|
-
const snap = board.snapshotSVG();
|
|
4374
|
-
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
4375
|
-
return true;
|
|
4376
|
-
},
|
|
4377
|
-
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
4378
|
-
}),
|
|
4379
|
-
[onInsert]
|
|
4380
|
-
);
|
|
4381
|
-
const handleResetView = useCallback(() => {
|
|
4382
|
-
boardRef.current?.resetView();
|
|
5187
|
+
function MobilePanel(props) {
|
|
5188
|
+
const { handle, onResetView, isDark, drawerOpen, onDrawerClose } = props;
|
|
5189
|
+
const { tool, showAxes, showMesh, canUndo } = useHandleState(handle);
|
|
5190
|
+
const groups = useMemo(() => {
|
|
5191
|
+
const acc = /* @__PURE__ */ new Map();
|
|
5192
|
+
for (const t of TOOLS_3D) {
|
|
5193
|
+
if (!acc.has(t.group)) acc.set(t.group, []);
|
|
5194
|
+
acc.get(t.group).push(t);
|
|
5195
|
+
}
|
|
5196
|
+
return Array.from(acc.entries()).map(([group, tools]) => ({
|
|
5197
|
+
group,
|
|
5198
|
+
groupLabel: GROUP_LABELS_3D[group],
|
|
5199
|
+
tools: tools.map((t) => ({ key: t.key, label: t.label, icon: ICONS_3D[t.key] }))
|
|
5200
|
+
}));
|
|
4383
5201
|
}, []);
|
|
4384
|
-
return /* @__PURE__ */
|
|
4385
|
-
|
|
5202
|
+
return /* @__PURE__ */ jsx(
|
|
5203
|
+
MobileToolDrawer,
|
|
4386
5204
|
{
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
"
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
onClose,
|
|
4428
|
-
isDark
|
|
4429
|
-
}
|
|
4430
|
-
),
|
|
4431
|
-
/* @__PURE__ */ jsx(
|
|
4432
|
-
"div",
|
|
4433
|
-
{
|
|
4434
|
-
style: {
|
|
4435
|
-
position: "absolute",
|
|
4436
|
-
left: 120,
|
|
4437
|
-
top: 0,
|
|
4438
|
-
right: 0,
|
|
4439
|
-
bottom: 0,
|
|
4440
|
-
overflow: "hidden"
|
|
4441
|
-
},
|
|
4442
|
-
children: /* @__PURE__ */ jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial })
|
|
4443
|
-
}
|
|
4444
|
-
)
|
|
4445
|
-
] })
|
|
4446
|
-
]
|
|
5205
|
+
title: "H\xECnh h\u1ECDc 3D",
|
|
5206
|
+
headerIcon: Geom3DIconHeader,
|
|
5207
|
+
testId: "geom3d-left-panel",
|
|
5208
|
+
isDark,
|
|
5209
|
+
drawerOpen: !!drawerOpen,
|
|
5210
|
+
onDrawerClose: () => onDrawerClose?.(),
|
|
5211
|
+
chips: [
|
|
5212
|
+
{
|
|
5213
|
+
label: "Tr\u1EE5c",
|
|
5214
|
+
icon: /* @__PURE__ */ jsx(AxisIcon3D, {}),
|
|
5215
|
+
pressed: showAxes,
|
|
5216
|
+
onToggle: (b) => handle?.setShowAxes(b),
|
|
5217
|
+
testId: "toggle-axes"
|
|
5218
|
+
},
|
|
5219
|
+
{
|
|
5220
|
+
label: "L\u01B0\u1EDBi",
|
|
5221
|
+
icon: /* @__PURE__ */ jsx(MeshIcon, {}),
|
|
5222
|
+
pressed: showMesh,
|
|
5223
|
+
onToggle: (b) => handle?.setShowMesh(b),
|
|
5224
|
+
testId: "toggle-mesh"
|
|
5225
|
+
}
|
|
5226
|
+
],
|
|
5227
|
+
actions: [
|
|
5228
|
+
{
|
|
5229
|
+
label: "Reset view",
|
|
5230
|
+
title: "Reset g\xF3c nh\xECn",
|
|
5231
|
+
icon: /* @__PURE__ */ jsx(ResetViewIcon, {}),
|
|
5232
|
+
onClick: onResetView
|
|
5233
|
+
},
|
|
5234
|
+
{
|
|
5235
|
+
label: "Ho\xE0n t\xE1c",
|
|
5236
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
5237
|
+
icon: /* @__PURE__ */ jsx(UndoIcon2, {}),
|
|
5238
|
+
onClick: () => handle?.undo(),
|
|
5239
|
+
disabled: !canUndo
|
|
5240
|
+
}
|
|
5241
|
+
],
|
|
5242
|
+
groups,
|
|
5243
|
+
activeTool: tool,
|
|
5244
|
+
onToolSelect: (k) => handle?.setTool(k)
|
|
4447
5245
|
}
|
|
4448
5246
|
);
|
|
4449
|
-
}
|
|
5247
|
+
}
|
|
5248
|
+
function LeftPanel3(props) {
|
|
5249
|
+
if (props.isMobile) return /* @__PURE__ */ jsx(MobilePanel, { ...props });
|
|
5250
|
+
return /* @__PURE__ */ jsx(DesktopPanel, { ...props });
|
|
5251
|
+
}
|
|
4450
5252
|
|
|
4451
5253
|
// src/stamps/geometry-3d/serialize.ts
|
|
4452
5254
|
function isGeometry3DCustomData(data) {
|
|
@@ -4531,66 +5333,1528 @@ async function renderGeometry3DSvgFromState(jsonState) {
|
|
|
4531
5333
|
JXG.JSXGraph.freeBoard(board);
|
|
4532
5334
|
} catch {
|
|
4533
5335
|
}
|
|
4534
|
-
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
4535
|
-
} finally {
|
|
4536
|
-
document.body.removeChild(div);
|
|
4537
|
-
}
|
|
4538
|
-
}
|
|
4539
|
-
function parseInitial(editingElement) {
|
|
4540
|
-
if (!editingElement) return null;
|
|
4541
|
-
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
4542
|
-
try {
|
|
4543
|
-
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
4544
|
-
} catch {
|
|
4545
|
-
return null;
|
|
4546
|
-
}
|
|
5336
|
+
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
5337
|
+
} finally {
|
|
5338
|
+
document.body.removeChild(div);
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
function parseInitial(editingElement) {
|
|
5342
|
+
if (!editingElement) return null;
|
|
5343
|
+
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
5344
|
+
try {
|
|
5345
|
+
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
5346
|
+
} catch {
|
|
5347
|
+
return null;
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
var Geometry3DStampHost = forwardRef(
|
|
5351
|
+
function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
5352
|
+
const editorRef = useRef(null);
|
|
5353
|
+
const { isMobile } = useIsMobile();
|
|
5354
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
5355
|
+
const [boardHandle, setBoardHandle] = useState(null);
|
|
5356
|
+
const initial = useMemo(
|
|
5357
|
+
() => parseInitial(editingElement),
|
|
5358
|
+
[editingElement]
|
|
5359
|
+
);
|
|
5360
|
+
const handleBoardReady = useCallback((h) => {
|
|
5361
|
+
setBoardHandle((prev) => prev === h ? prev : h);
|
|
5362
|
+
}, []);
|
|
5363
|
+
const { chordGroup } = useChordShortcut({
|
|
5364
|
+
groupOrder: GROUP_ORDER_3D,
|
|
5365
|
+
tools: TOOLS_3D,
|
|
5366
|
+
onSelect: (key) => boardHandle?.setTool(key),
|
|
5367
|
+
enabled: !isMobile
|
|
5368
|
+
});
|
|
5369
|
+
const handleResetView = useCallback(() => {
|
|
5370
|
+
boardHandle?.resetView();
|
|
5371
|
+
}, [boardHandle]);
|
|
5372
|
+
const handleInsert = useCallback(
|
|
5373
|
+
async (jsonState, svgString, width, height) => {
|
|
5374
|
+
if (!api) return;
|
|
5375
|
+
await insertStampImage(api, {
|
|
5376
|
+
svgString,
|
|
5377
|
+
makeCustomData: () => ({
|
|
5378
|
+
kind: "geometry3d",
|
|
5379
|
+
version: 1,
|
|
5380
|
+
jsonState,
|
|
5381
|
+
svgWidth: width,
|
|
5382
|
+
svgHeight: height
|
|
5383
|
+
}),
|
|
5384
|
+
editingElementId: editingElement?.id ?? null
|
|
5385
|
+
});
|
|
5386
|
+
onClose();
|
|
5387
|
+
},
|
|
5388
|
+
[api, editingElement, onClose]
|
|
5389
|
+
);
|
|
5390
|
+
useImperativeHandle(
|
|
5391
|
+
ref,
|
|
5392
|
+
() => ({
|
|
5393
|
+
tryInsert: () => editorRef.current?.tryInsert() ?? false,
|
|
5394
|
+
hasContent: () => editorRef.current?.hasContent() ?? false
|
|
5395
|
+
}),
|
|
5396
|
+
[]
|
|
5397
|
+
);
|
|
5398
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5399
|
+
/* @__PURE__ */ jsx(
|
|
5400
|
+
LeftPanel3,
|
|
5401
|
+
{
|
|
5402
|
+
handle: boardHandle,
|
|
5403
|
+
onResetView: handleResetView,
|
|
5404
|
+
onClose,
|
|
5405
|
+
isDark,
|
|
5406
|
+
isMobile,
|
|
5407
|
+
drawerOpen,
|
|
5408
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
5409
|
+
chordGroup
|
|
5410
|
+
}
|
|
5411
|
+
),
|
|
5412
|
+
/* @__PURE__ */ jsx(
|
|
5413
|
+
EditorPanel,
|
|
5414
|
+
{
|
|
5415
|
+
ref: editorRef,
|
|
5416
|
+
isDark,
|
|
5417
|
+
initial,
|
|
5418
|
+
onInsert: handleInsert,
|
|
5419
|
+
onClose,
|
|
5420
|
+
isMobile,
|
|
5421
|
+
withLeftPanel: !isMobile,
|
|
5422
|
+
onBoardReady: handleBoardReady,
|
|
5423
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
5424
|
+
}
|
|
5425
|
+
)
|
|
5426
|
+
] });
|
|
5427
|
+
}
|
|
5428
|
+
);
|
|
5429
|
+
var Geometry3DIcon = /* @__PURE__ */ jsxs(
|
|
5430
|
+
"svg",
|
|
5431
|
+
{
|
|
5432
|
+
width: "20",
|
|
5433
|
+
height: "20",
|
|
5434
|
+
viewBox: "0 0 24 24",
|
|
5435
|
+
fill: "none",
|
|
5436
|
+
stroke: "currentColor",
|
|
5437
|
+
strokeWidth: "1.6",
|
|
5438
|
+
strokeLinecap: "round",
|
|
5439
|
+
strokeLinejoin: "round",
|
|
5440
|
+
"aria-hidden": "true",
|
|
5441
|
+
children: [
|
|
5442
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3 L20 8 L20 16 L12 21 L4 16 L4 8 Z" }),
|
|
5443
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3 L12 21 M4 8 L12 12 L20 8 M4 16 L12 12 L20 16" })
|
|
5444
|
+
]
|
|
5445
|
+
}
|
|
5446
|
+
);
|
|
5447
|
+
var geometry3dStamp = {
|
|
5448
|
+
kind: "geometry3d",
|
|
5449
|
+
shortcutKey: "d",
|
|
5450
|
+
toolbarLabel: "D",
|
|
5451
|
+
toolbarTitle: "H\xECnh 3D (D)",
|
|
5452
|
+
toolbarIcon: Geometry3DIcon,
|
|
5453
|
+
toolbarTestId: "stamp-toolbar-geometry3d",
|
|
5454
|
+
matchesCustomData: isGeometry3DCustomData,
|
|
5455
|
+
async renderSvgFromCustomData(data) {
|
|
5456
|
+
if (!isGeometry3DCustomData(data)) {
|
|
5457
|
+
throw new Error("geometry3dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i geometry3d");
|
|
5458
|
+
}
|
|
5459
|
+
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
5460
|
+
return svgString;
|
|
5461
|
+
},
|
|
5462
|
+
restoreFileFromCustomData: async (element) => {
|
|
5463
|
+
const data = element.customData;
|
|
5464
|
+
const fileId = element.fileId;
|
|
5465
|
+
if (!data || !fileId) return null;
|
|
5466
|
+
if (!isGeometry3DCustomData(data)) return null;
|
|
5467
|
+
try {
|
|
5468
|
+
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
5469
|
+
const dataURL = `data:image/svg+xml;base64,${typeof btoa !== "undefined" ? btoa(unescape(encodeURIComponent(svgString))) : Buffer.from(svgString).toString("base64")}`;
|
|
5470
|
+
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
5471
|
+
} catch {
|
|
5472
|
+
return null;
|
|
5473
|
+
}
|
|
5474
|
+
},
|
|
5475
|
+
Host: Geometry3DStampHost
|
|
5476
|
+
};
|
|
5477
|
+
|
|
5478
|
+
// src/stamps/graph-2d/editor/tools.ts
|
|
5479
|
+
var GRAPH_TOOLS = [
|
|
5480
|
+
{ id: "move", label: "Di chuy\u1EC3n", title: "Di chuy\u1EC3n / ch\u1ECDn" },
|
|
5481
|
+
{ 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" },
|
|
5482
|
+
{ id: "intersect", label: "Giao \u0111i\u1EC3m", title: "\u0110\xE1nh d\u1EA5u giao \u0111i\u1EC3m 2 \u0111\u1ED3 th\u1ECB" },
|
|
5483
|
+
{ id: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", title: "V\u1EBD ti\u1EBFp tuy\u1EBFn t\u1EA1i \u0111i\u1EC3m tr\xEAn \u0111\u1ED3 th\u1ECB" }
|
|
5484
|
+
];
|
|
5485
|
+
function FunctionRow(props) {
|
|
5486
|
+
const { id, name, expression, color, visible, error } = props;
|
|
5487
|
+
const [draft, setDraft] = useState(expression);
|
|
5488
|
+
useEffect(() => {
|
|
5489
|
+
setDraft(expression);
|
|
5490
|
+
}, [expression]);
|
|
5491
|
+
const commit = () => {
|
|
5492
|
+
if (draft !== expression) props.onExpressionCommit(draft);
|
|
5493
|
+
};
|
|
5494
|
+
const handleKeyDown = (e) => {
|
|
5495
|
+
if (e.key === "Enter") {
|
|
5496
|
+
e.preventDefault();
|
|
5497
|
+
commit();
|
|
5498
|
+
e.target.blur();
|
|
5499
|
+
} else if (e.key === "Escape") {
|
|
5500
|
+
setDraft(expression);
|
|
5501
|
+
e.target.blur();
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5504
|
+
const handleBlur = (_) => commit();
|
|
5505
|
+
return /* @__PURE__ */ jsxs("div", { className: `graph-function-row${error ? " is-error" : ""}`, "data-testid": `graph-function-row-${id}`, children: [
|
|
5506
|
+
/* @__PURE__ */ jsx(
|
|
5507
|
+
"span",
|
|
5508
|
+
{
|
|
5509
|
+
className: "graph-function-color",
|
|
5510
|
+
style: { backgroundColor: color },
|
|
5511
|
+
"aria-hidden": "true"
|
|
5512
|
+
}
|
|
5513
|
+
),
|
|
5514
|
+
/* @__PURE__ */ jsxs("span", { className: "graph-function-name", "data-testid": `graph-function-name-${id}`, children: [
|
|
5515
|
+
name,
|
|
5516
|
+
"(x) ="
|
|
5517
|
+
] }),
|
|
5518
|
+
/* @__PURE__ */ jsx(
|
|
5519
|
+
"input",
|
|
5520
|
+
{
|
|
5521
|
+
"aria-label": "Bi\u1EC3u th\u1EE9c",
|
|
5522
|
+
className: "graph-function-input",
|
|
5523
|
+
type: "text",
|
|
5524
|
+
value: draft,
|
|
5525
|
+
onChange: (e) => setDraft(e.target.value),
|
|
5526
|
+
onKeyDown: handleKeyDown,
|
|
5527
|
+
onBlur: handleBlur,
|
|
5528
|
+
spellCheck: false,
|
|
5529
|
+
autoCorrect: "off",
|
|
5530
|
+
autoCapitalize: "off"
|
|
5531
|
+
}
|
|
5532
|
+
),
|
|
5533
|
+
/* @__PURE__ */ jsx(
|
|
5534
|
+
"button",
|
|
5535
|
+
{
|
|
5536
|
+
type: "button",
|
|
5537
|
+
"aria-label": "\u1EA8n/hi\u1EC7n \u0111\u1ED3 th\u1ECB",
|
|
5538
|
+
className: `graph-function-eye${visible ? "" : " is-hidden"}`,
|
|
5539
|
+
onClick: props.onToggleVisible,
|
|
5540
|
+
children: visible ? "\u{1F441}" : "\u2298"
|
|
5541
|
+
}
|
|
5542
|
+
),
|
|
5543
|
+
/* @__PURE__ */ jsx(
|
|
5544
|
+
"button",
|
|
5545
|
+
{
|
|
5546
|
+
type: "button",
|
|
5547
|
+
"aria-label": "Xo\xE1 \u0111\u1ED3 th\u1ECB",
|
|
5548
|
+
className: "graph-function-remove",
|
|
5549
|
+
onClick: props.onRemove,
|
|
5550
|
+
children: "\u2715"
|
|
5551
|
+
}
|
|
5552
|
+
),
|
|
5553
|
+
error ? /* @__PURE__ */ jsx("div", { className: "graph-function-error", children: error }) : null
|
|
5554
|
+
] });
|
|
5555
|
+
}
|
|
5556
|
+
function SliderRow(props) {
|
|
5557
|
+
const { name, value, min, max, step } = props;
|
|
5558
|
+
return /* @__PURE__ */ jsxs("div", { className: "graph-slider-row", "data-testid": `graph-slider-row-${name}`, children: [
|
|
5559
|
+
/* @__PURE__ */ jsxs("div", { className: "graph-slider-header", children: [
|
|
5560
|
+
/* @__PURE__ */ jsx("span", { className: "graph-slider-name", children: name }),
|
|
5561
|
+
/* @__PURE__ */ jsxs("span", { className: "graph-slider-value", children: [
|
|
5562
|
+
"= ",
|
|
5563
|
+
value.toFixed(2)
|
|
5564
|
+
] }),
|
|
5565
|
+
/* @__PURE__ */ jsx(
|
|
5566
|
+
"button",
|
|
5567
|
+
{
|
|
5568
|
+
type: "button",
|
|
5569
|
+
"aria-label": `Xo\xE1 tham s\u1ED1 ${name}`,
|
|
5570
|
+
className: "graph-slider-remove",
|
|
5571
|
+
onClick: props.onRemove,
|
|
5572
|
+
children: "\u2715"
|
|
5573
|
+
}
|
|
5574
|
+
)
|
|
5575
|
+
] }),
|
|
5576
|
+
/* @__PURE__ */ jsx(
|
|
5577
|
+
"input",
|
|
5578
|
+
{
|
|
5579
|
+
type: "range",
|
|
5580
|
+
"aria-label": `Slider ${name}`,
|
|
5581
|
+
min,
|
|
5582
|
+
max,
|
|
5583
|
+
step,
|
|
5584
|
+
value,
|
|
5585
|
+
onChange: (e) => props.onChange(parseFloat(e.target.value)),
|
|
5586
|
+
className: "graph-slider-input"
|
|
5587
|
+
}
|
|
5588
|
+
),
|
|
5589
|
+
/* @__PURE__ */ jsxs("div", { className: "graph-slider-range", children: [
|
|
5590
|
+
/* @__PURE__ */ jsx("span", { children: min }),
|
|
5591
|
+
/* @__PURE__ */ jsx("span", { children: max })
|
|
5592
|
+
] })
|
|
5593
|
+
] });
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5596
|
+
// src/stamps/graph-2d/colors.ts
|
|
5597
|
+
var GRAPH_PALETTE = [
|
|
5598
|
+
"#2563eb",
|
|
5599
|
+
// blue
|
|
5600
|
+
"#dc2626",
|
|
5601
|
+
// red
|
|
5602
|
+
"#16a34a",
|
|
5603
|
+
// green
|
|
5604
|
+
"#9333ea",
|
|
5605
|
+
// purple
|
|
5606
|
+
"#ea580c",
|
|
5607
|
+
// orange
|
|
5608
|
+
"#0891b2",
|
|
5609
|
+
// cyan
|
|
5610
|
+
"#db2777",
|
|
5611
|
+
// pink
|
|
5612
|
+
"#65a30d"
|
|
5613
|
+
// lime
|
|
5614
|
+
];
|
|
5615
|
+
var FUNCTION_NAMES = ["f", "g", "h", "i", "j", "k", "l", "m"];
|
|
5616
|
+
var MAX_FUNCTIONS = 8;
|
|
5617
|
+
var MAX_PARAMETERS = 8;
|
|
5618
|
+
function nextColor(usedColors) {
|
|
5619
|
+
for (const c of GRAPH_PALETTE) {
|
|
5620
|
+
if (!usedColors.includes(c)) return c;
|
|
5621
|
+
}
|
|
5622
|
+
return GRAPH_PALETTE[usedColors.length % GRAPH_PALETTE.length];
|
|
5623
|
+
}
|
|
5624
|
+
function nextFunctionName(usedNames) {
|
|
5625
|
+
for (const n of FUNCTION_NAMES) {
|
|
5626
|
+
if (!usedNames.includes(n)) return n;
|
|
5627
|
+
}
|
|
5628
|
+
return FUNCTION_NAMES[usedNames.length % FUNCTION_NAMES.length];
|
|
5629
|
+
}
|
|
5630
|
+
function AlgebraView(props) {
|
|
5631
|
+
const { graph, errors } = props;
|
|
5632
|
+
const atMax = graph.functions.length >= MAX_FUNCTIONS;
|
|
5633
|
+
return /* @__PURE__ */ jsxs("div", { className: "graph-algebra-view", children: [
|
|
5634
|
+
/* @__PURE__ */ jsxs("div", { className: "graph-algebra-section", children: [
|
|
5635
|
+
graph.functions.map((f) => /* @__PURE__ */ jsx(
|
|
5636
|
+
FunctionRow,
|
|
5637
|
+
{
|
|
5638
|
+
id: f.id,
|
|
5639
|
+
name: f.name,
|
|
5640
|
+
expression: f.expression,
|
|
5641
|
+
color: f.color,
|
|
5642
|
+
visible: f.visible,
|
|
5643
|
+
error: errors[f.id] ?? null,
|
|
5644
|
+
onExpressionCommit: (expr) => props.onCommitFunctionExpr(f.id, expr),
|
|
5645
|
+
onToggleVisible: () => props.onToggleFunctionVisible(f.id),
|
|
5646
|
+
onRemove: () => props.onRemoveFunction(f.id)
|
|
5647
|
+
},
|
|
5648
|
+
f.id
|
|
5649
|
+
)),
|
|
5650
|
+
/* @__PURE__ */ jsx(
|
|
5651
|
+
"button",
|
|
5652
|
+
{
|
|
5653
|
+
type: "button",
|
|
5654
|
+
"aria-label": "Th\xEAm h\xE0m s\u1ED1",
|
|
5655
|
+
className: "graph-algebra-add",
|
|
5656
|
+
onClick: props.onAddFunctionDraft,
|
|
5657
|
+
disabled: atMax,
|
|
5658
|
+
children: "+ Th\xEAm h\xE0m"
|
|
5659
|
+
}
|
|
5660
|
+
)
|
|
5661
|
+
] }),
|
|
5662
|
+
graph.parameters.length > 0 ? /* @__PURE__ */ jsx("div", { className: "graph-algebra-section graph-algebra-parameters", children: graph.parameters.map((p) => /* @__PURE__ */ jsx(
|
|
5663
|
+
SliderRow,
|
|
5664
|
+
{
|
|
5665
|
+
name: p.name,
|
|
5666
|
+
value: p.value,
|
|
5667
|
+
min: p.min,
|
|
5668
|
+
max: p.max,
|
|
5669
|
+
step: p.step,
|
|
5670
|
+
onChange: (v) => props.onParameterChange(p.name, v),
|
|
5671
|
+
onRangeChange: (min, max, step) => props.onParameterRangeChange(p.name, min, max, step),
|
|
5672
|
+
onRemove: () => props.onRemoveParameter(p.name)
|
|
5673
|
+
},
|
|
5674
|
+
p.name
|
|
5675
|
+
)) }) : null
|
|
5676
|
+
] });
|
|
5677
|
+
}
|
|
5678
|
+
var GraphIconHeader = /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5679
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21 V3" }),
|
|
5680
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21 H21" }),
|
|
5681
|
+
/* @__PURE__ */ jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
|
|
5682
|
+
] });
|
|
5683
|
+
function CloseIcon3() {
|
|
5684
|
+
return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5685
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
5686
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
5687
|
+
] });
|
|
5688
|
+
}
|
|
5689
|
+
function UndoIcon3() {
|
|
5690
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5691
|
+
/* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
5692
|
+
/* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
5693
|
+
] });
|
|
5694
|
+
}
|
|
5695
|
+
function ResetViewIcon2() {
|
|
5696
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5697
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
|
|
5698
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "21" }),
|
|
5699
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" })
|
|
5700
|
+
] });
|
|
5701
|
+
}
|
|
5702
|
+
function MoveIcon() {
|
|
5703
|
+
return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M3 4 L9 4 L9 9 L4 9 Z" }) });
|
|
5704
|
+
}
|
|
5705
|
+
function PointOnCurveIcon() {
|
|
5706
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5707
|
+
/* @__PURE__ */ jsx("path", { d: "M3 17 C7 8, 14 8, 21 14" }),
|
|
5708
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "11", r: "2.2", fill: "currentColor", stroke: "none" })
|
|
5709
|
+
] });
|
|
5710
|
+
}
|
|
5711
|
+
function IntersectIcon() {
|
|
5712
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5713
|
+
/* @__PURE__ */ jsx("path", { d: "M3 17 C8 5, 14 5, 21 17" }),
|
|
5714
|
+
/* @__PURE__ */ jsx("path", { d: "M3 5 C8 17, 14 17, 21 5" }),
|
|
5715
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "11", r: "1.6", fill: "currentColor", stroke: "none" })
|
|
5716
|
+
] });
|
|
5717
|
+
}
|
|
5718
|
+
function TangentIcon() {
|
|
5719
|
+
return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
5720
|
+
/* @__PURE__ */ jsx("path", { d: "M3 17 C8 7, 14 7, 21 16" }),
|
|
5721
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "14", x2: "20", y2: "6" }),
|
|
5722
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "10", r: "1.8", fill: "currentColor", stroke: "none" })
|
|
5723
|
+
] });
|
|
5724
|
+
}
|
|
5725
|
+
var TOOL_ICONS = {
|
|
5726
|
+
move: /* @__PURE__ */ jsx(MoveIcon, {}),
|
|
5727
|
+
"point-on-curve": /* @__PURE__ */ jsx(PointOnCurveIcon, {}),
|
|
5728
|
+
intersect: /* @__PURE__ */ jsx(IntersectIcon, {}),
|
|
5729
|
+
tangent: /* @__PURE__ */ jsx(TangentIcon, {})
|
|
5730
|
+
};
|
|
5731
|
+
function Section4({ label, children }) {
|
|
5732
|
+
return /* @__PURE__ */ jsxs("section", { children: [
|
|
5733
|
+
/* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
|
|
5734
|
+
children
|
|
5735
|
+
] });
|
|
5736
|
+
}
|
|
5737
|
+
function PanelBody(props) {
|
|
5738
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5739
|
+
/* @__PURE__ */ jsx(Section4, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
|
|
5740
|
+
/* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
|
|
5741
|
+
/* @__PURE__ */ jsx(
|
|
5742
|
+
"input",
|
|
5743
|
+
{
|
|
5744
|
+
type: "checkbox",
|
|
5745
|
+
checked: props.showAxis,
|
|
5746
|
+
onChange: (e) => props.onShowAxisChange(e.target.checked),
|
|
5747
|
+
"data-testid": "toggle-axis"
|
|
5748
|
+
}
|
|
5749
|
+
),
|
|
5750
|
+
"Tr\u1EE5c"
|
|
5751
|
+
] }),
|
|
5752
|
+
/* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
|
|
5753
|
+
/* @__PURE__ */ jsx(
|
|
5754
|
+
"input",
|
|
5755
|
+
{
|
|
5756
|
+
type: "checkbox",
|
|
5757
|
+
checked: props.showGrid,
|
|
5758
|
+
onChange: (e) => props.onShowGridChange(e.target.checked),
|
|
5759
|
+
"data-testid": "toggle-grid"
|
|
5760
|
+
}
|
|
5761
|
+
),
|
|
5762
|
+
"L\u01B0\u1EDBi"
|
|
5763
|
+
] }),
|
|
5764
|
+
/* @__PURE__ */ jsx(
|
|
5765
|
+
"button",
|
|
5766
|
+
{
|
|
5767
|
+
type: "button",
|
|
5768
|
+
onClick: props.onResetView,
|
|
5769
|
+
title: "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
|
|
5770
|
+
"aria-label": "\u0110\u1EB7t l\u1EA1i t\u1EA7m nh\xECn",
|
|
5771
|
+
className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
|
|
5772
|
+
children: /* @__PURE__ */ jsx(ResetViewIcon2, {})
|
|
5773
|
+
}
|
|
5774
|
+
),
|
|
5775
|
+
/* @__PURE__ */ jsx(
|
|
5776
|
+
"button",
|
|
5777
|
+
{
|
|
5778
|
+
type: "button",
|
|
5779
|
+
onClick: props.onUndo,
|
|
5780
|
+
disabled: !props.canUndo,
|
|
5781
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
5782
|
+
"aria-label": "Ho\xE0n t\xE1c",
|
|
5783
|
+
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",
|
|
5784
|
+
children: /* @__PURE__ */ jsx(UndoIcon3, {})
|
|
5785
|
+
}
|
|
5786
|
+
)
|
|
5787
|
+
] }) }),
|
|
5788
|
+
/* @__PURE__ */ jsx(Section4, { label: "C\xF4ng c\u1EE5", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: GRAPH_TOOLS.map((t) => {
|
|
5789
|
+
const isActive = props.activeTool === t.id;
|
|
5790
|
+
return /* @__PURE__ */ jsx(
|
|
5791
|
+
"button",
|
|
5792
|
+
{
|
|
5793
|
+
type: "button",
|
|
5794
|
+
"aria-label": t.title,
|
|
5795
|
+
title: t.title,
|
|
5796
|
+
"aria-pressed": isActive,
|
|
5797
|
+
onClick: () => props.onToolChange(t.id),
|
|
5798
|
+
"data-testid": `graph-tool-${t.id}`,
|
|
5799
|
+
className: [
|
|
5800
|
+
"flex h-8 items-center justify-center rounded-md transition",
|
|
5801
|
+
isActive ? "bg-orange-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
5802
|
+
].join(" "),
|
|
5803
|
+
children: TOOL_ICONS[t.id]
|
|
5804
|
+
},
|
|
5805
|
+
t.id
|
|
5806
|
+
);
|
|
5807
|
+
}) }) }),
|
|
5808
|
+
/* @__PURE__ */ jsx(Section4, { label: "H\xE0m s\u1ED1", children: /* @__PURE__ */ jsx(
|
|
5809
|
+
AlgebraView,
|
|
5810
|
+
{
|
|
5811
|
+
graph: props.graph,
|
|
5812
|
+
errors: props.errors,
|
|
5813
|
+
onAddFunctionDraft: props.onAddFunctionDraft,
|
|
5814
|
+
onCommitFunctionExpr: props.onCommitFunctionExpr,
|
|
5815
|
+
onToggleFunctionVisible: props.onToggleFunctionVisible,
|
|
5816
|
+
onRemoveFunction: props.onRemoveFunction,
|
|
5817
|
+
onParameterChange: props.onParameterChange,
|
|
5818
|
+
onParameterRangeChange: props.onParameterRangeChange,
|
|
5819
|
+
onRemoveParameter: props.onRemoveParameter
|
|
5820
|
+
}
|
|
5821
|
+
) })
|
|
5822
|
+
] });
|
|
5823
|
+
}
|
|
5824
|
+
function GraphLeftPanel(props) {
|
|
5825
|
+
const { isMobile, drawerOpen, isDark, onClose, onDrawerClose } = props;
|
|
5826
|
+
if (isMobile && !drawerOpen) return null;
|
|
5827
|
+
const handleClose = isMobile ? onDrawerClose : onClose;
|
|
5828
|
+
return /* @__PURE__ */ jsxs(
|
|
5829
|
+
"aside",
|
|
5830
|
+
{
|
|
5831
|
+
role: "complementary",
|
|
5832
|
+
"aria-label": "\u0110\u1ED3 th\u1ECB 2D",
|
|
5833
|
+
"data-testid": "graph-left-panel",
|
|
5834
|
+
"data-stamp-area": "true",
|
|
5835
|
+
className: [
|
|
5836
|
+
isDark ? "theme--dark " : "",
|
|
5837
|
+
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"
|
|
5838
|
+
].join(" "),
|
|
5839
|
+
children: [
|
|
5840
|
+
/* @__PURE__ */ 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: [
|
|
5841
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
5842
|
+
/* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: GraphIconHeader }),
|
|
5843
|
+
"\u0110\u1ED3 th\u1ECB 2D"
|
|
5844
|
+
] }),
|
|
5845
|
+
/* @__PURE__ */ jsx(
|
|
5846
|
+
"button",
|
|
5847
|
+
{
|
|
5848
|
+
onClick: handleClose,
|
|
5849
|
+
"aria-label": "\u0110\xF3ng",
|
|
5850
|
+
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
5851
|
+
children: /* @__PURE__ */ jsx(CloseIcon3, {})
|
|
5852
|
+
}
|
|
5853
|
+
)
|
|
5854
|
+
] }),
|
|
5855
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children: /* @__PURE__ */ jsx(PanelBody, { ...props }) })
|
|
5856
|
+
]
|
|
5857
|
+
}
|
|
5858
|
+
);
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
// src/stamps/graph-2d/parser.ts
|
|
5862
|
+
var ALLOWED_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
5863
|
+
"sin",
|
|
5864
|
+
"cos",
|
|
5865
|
+
"tan",
|
|
5866
|
+
"asin",
|
|
5867
|
+
"acos",
|
|
5868
|
+
"atan",
|
|
5869
|
+
"log",
|
|
5870
|
+
"ln",
|
|
5871
|
+
"exp",
|
|
5872
|
+
"sqrt",
|
|
5873
|
+
"abs",
|
|
5874
|
+
"floor",
|
|
5875
|
+
"ceil",
|
|
5876
|
+
"round"
|
|
5877
|
+
]);
|
|
5878
|
+
var ALLOWED_CHARS = /^[a-zA-Z0-9_.+\-*/^()\s,]+$/;
|
|
5879
|
+
var IDENTIFIER_RE = /[a-zA-Z][a-zA-Z0-9_]*/g;
|
|
5880
|
+
var SUGGESTIONS = {
|
|
5881
|
+
tg: "tan",
|
|
5882
|
+
arcsin: "asin",
|
|
5883
|
+
arccos: "acos",
|
|
5884
|
+
arctan: "atan"
|
|
5885
|
+
};
|
|
5886
|
+
function errResult(message) {
|
|
5887
|
+
return { ok: false, error: message, freeVars: /* @__PURE__ */ new Set() };
|
|
5888
|
+
}
|
|
5889
|
+
function validate(expr) {
|
|
5890
|
+
const trimmed = expr.trim();
|
|
5891
|
+
if (!trimmed) return errResult("Bi\u1EC3u th\u1EE9c r\u1ED7ng");
|
|
5892
|
+
if (!ALLOWED_CHARS.test(trimmed)) return errResult("K\xFD t\u1EF1 kh\xF4ng h\u1EE3p l\u1EC7");
|
|
5893
|
+
const ids = trimmed.match(IDENTIFIER_RE) ?? [];
|
|
5894
|
+
const freeVars = /* @__PURE__ */ new Set();
|
|
5895
|
+
for (const id of ids) {
|
|
5896
|
+
if (id === "x" || id === "pi" || id === "e") continue;
|
|
5897
|
+
if (ALLOWED_FUNCTIONS.has(id)) continue;
|
|
5898
|
+
if (id.length === 1) {
|
|
5899
|
+
freeVars.add(id);
|
|
5900
|
+
continue;
|
|
5901
|
+
}
|
|
5902
|
+
const hint = SUGGESTIONS[id];
|
|
5903
|
+
return errResult(
|
|
5904
|
+
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}"`
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
try {
|
|
5908
|
+
const paramSubs = Object.fromEntries([...freeVars].map((v) => [v, 1]));
|
|
5909
|
+
const rewritten = rewriteToJs(trimmed, paramSubs);
|
|
5910
|
+
new Function("x", `return (${rewritten})`);
|
|
5911
|
+
} catch {
|
|
5912
|
+
return errResult("L\u1ED7i c\xFA ph\xE1p");
|
|
5913
|
+
}
|
|
5914
|
+
return { ok: true, freeVars };
|
|
5915
|
+
}
|
|
5916
|
+
var FUNCTION_REPLACEMENTS = [
|
|
5917
|
+
// longest first để tránh substring conflict (asin trước sin)
|
|
5918
|
+
["asin", "Math.asin"],
|
|
5919
|
+
["acos", "Math.acos"],
|
|
5920
|
+
["atan", "Math.atan"],
|
|
5921
|
+
["sqrt", "Math.sqrt"],
|
|
5922
|
+
["floor", "Math.floor"],
|
|
5923
|
+
["round", "Math.round"],
|
|
5924
|
+
["ceil", "Math.ceil"],
|
|
5925
|
+
["sin", "Math.sin"],
|
|
5926
|
+
["cos", "Math.cos"],
|
|
5927
|
+
["tan", "Math.tan"],
|
|
5928
|
+
["abs", "Math.abs"],
|
|
5929
|
+
["exp", "Math.exp"],
|
|
5930
|
+
["log", "Math.log10"],
|
|
5931
|
+
["ln", "Math.log"]
|
|
5932
|
+
];
|
|
5933
|
+
function rewriteToJs(expr, params) {
|
|
5934
|
+
let s = expr.replace(/\^/g, "**");
|
|
5935
|
+
s = s.replace(/\bpi\b/g, "Math.PI");
|
|
5936
|
+
s = s.replace(/\be\b/g, "Math.E");
|
|
5937
|
+
for (const [from, to] of FUNCTION_REPLACEMENTS) {
|
|
5938
|
+
s = s.replace(new RegExp(`\\b${from}\\b`, "g"), to);
|
|
5939
|
+
}
|
|
5940
|
+
for (const [name, value] of Object.entries(params)) {
|
|
5941
|
+
if (name.length !== 1) continue;
|
|
5942
|
+
s = s.replace(new RegExp(`\\b${name}\\b`, "g"), `(${value})`);
|
|
5943
|
+
}
|
|
5944
|
+
return s;
|
|
5945
|
+
}
|
|
5946
|
+
function compile(expr, paramValues) {
|
|
5947
|
+
const v = validate(expr);
|
|
5948
|
+
if (!v.ok) return { error: v.error ?? "Invalid" };
|
|
5949
|
+
try {
|
|
5950
|
+
const rewritten = rewriteToJs(expr, paramValues);
|
|
5951
|
+
const raw = new Function("x", `return (${rewritten})`);
|
|
5952
|
+
return (x) => {
|
|
5953
|
+
try {
|
|
5954
|
+
const y = raw(x);
|
|
5955
|
+
return typeof y === "number" ? y : NaN;
|
|
5956
|
+
} catch {
|
|
5957
|
+
return NaN;
|
|
5958
|
+
}
|
|
5959
|
+
};
|
|
5960
|
+
} catch (err) {
|
|
5961
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5965
|
+
// src/stamps/graph-2d/editor/handlers.ts
|
|
5966
|
+
function addPointOnCurve(graph, ctx, idFactory) {
|
|
5967
|
+
if (!ctx.functionId) return graph;
|
|
5968
|
+
const point = {
|
|
5969
|
+
id: idFactory(),
|
|
5970
|
+
functionId: ctx.functionId,
|
|
5971
|
+
x: ctx.x
|
|
5972
|
+
};
|
|
5973
|
+
return { ...graph, points: [...graph.points, point] };
|
|
5974
|
+
}
|
|
5975
|
+
function addIntersection(graph, functionIdA, functionIdB, idFactory) {
|
|
5976
|
+
if (functionIdA === functionIdB) return graph;
|
|
5977
|
+
const exists = graph.intersections.some(
|
|
5978
|
+
(i) => i.functionIdA === functionIdA && i.functionIdB === functionIdB || i.functionIdA === functionIdB && i.functionIdB === functionIdA
|
|
5979
|
+
);
|
|
5980
|
+
if (exists) return graph;
|
|
5981
|
+
const intersection = {
|
|
5982
|
+
id: idFactory(),
|
|
5983
|
+
functionIdA,
|
|
5984
|
+
functionIdB
|
|
5985
|
+
};
|
|
5986
|
+
return { ...graph, intersections: [...graph.intersections, intersection] };
|
|
5987
|
+
}
|
|
5988
|
+
function numericalDerivative(expression, paramValues, x, h = 1e-4) {
|
|
5989
|
+
const fn = compile(expression, paramValues);
|
|
5990
|
+
if (typeof fn !== "function") return NaN;
|
|
5991
|
+
const y1 = fn(x - h);
|
|
5992
|
+
const y2 = fn(x + h);
|
|
5993
|
+
return (y2 - y1) / (2 * h);
|
|
5994
|
+
}
|
|
5995
|
+
function MiniBoard({ graph, activeTool, isDark, onBoardEvent }) {
|
|
5996
|
+
const containerRef = useRef(null);
|
|
5997
|
+
const boardRef = useRef(null);
|
|
5998
|
+
const curvesRef = useRef(/* @__PURE__ */ new Map());
|
|
5999
|
+
useEffect(() => {
|
|
6000
|
+
let cancelled = false;
|
|
6001
|
+
let createdBoard = null;
|
|
6002
|
+
const containerEl = containerRef.current;
|
|
6003
|
+
if (!containerEl) return;
|
|
6004
|
+
const containerId = `jxg_graph2d_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6005
|
+
containerEl.id = containerId;
|
|
6006
|
+
(async () => {
|
|
6007
|
+
const JXG = (await import('jsxgraph')).default;
|
|
6008
|
+
if (cancelled) return;
|
|
6009
|
+
const opts = JXG.Options;
|
|
6010
|
+
if (opts) {
|
|
6011
|
+
opts.text = opts.text || {};
|
|
6012
|
+
opts.text.display = "internal";
|
|
6013
|
+
opts.label = opts.label || {};
|
|
6014
|
+
opts.label.display = "internal";
|
|
6015
|
+
}
|
|
6016
|
+
const board = JXG.JSXGraph.initBoard(containerId, {
|
|
6017
|
+
boundingbox: [graph.view.xMin, graph.view.yMax, graph.view.xMax, graph.view.yMin],
|
|
6018
|
+
axis: graph.view.showAxis,
|
|
6019
|
+
grid: graph.view.showGrid,
|
|
6020
|
+
showCopyright: false,
|
|
6021
|
+
showNavigation: true,
|
|
6022
|
+
pan: { enabled: true, needShift: false },
|
|
6023
|
+
zoom: { wheel: true, needShift: false },
|
|
6024
|
+
keepAspectRatio: false
|
|
6025
|
+
});
|
|
6026
|
+
boardRef.current = board;
|
|
6027
|
+
createdBoard = board;
|
|
6028
|
+
syncObjects(board, graph, curvesRef.current);
|
|
6029
|
+
board.on("boundingbox", () => {
|
|
6030
|
+
const bb = board.getBoundingBox();
|
|
6031
|
+
onBoardEvent({
|
|
6032
|
+
type: "view-change",
|
|
6033
|
+
view: {
|
|
6034
|
+
xMin: bb[0],
|
|
6035
|
+
xMax: bb[2],
|
|
6036
|
+
yMax: bb[1],
|
|
6037
|
+
yMin: bb[3],
|
|
6038
|
+
showAxis: graph.view.showAxis,
|
|
6039
|
+
showGrid: graph.view.showGrid
|
|
6040
|
+
}
|
|
6041
|
+
});
|
|
6042
|
+
});
|
|
6043
|
+
board.on("down", (ev) => {
|
|
6044
|
+
const usrCoords = board.getUsrCoordsOfMouse?.(ev);
|
|
6045
|
+
const x = usrCoords?.[0] ?? 0;
|
|
6046
|
+
const y = usrCoords?.[1] ?? 0;
|
|
6047
|
+
let functionId;
|
|
6048
|
+
for (const [id, ref] of curvesRef.current) {
|
|
6049
|
+
const obj = ref.obj;
|
|
6050
|
+
if (obj?.hasPoint && obj.hasPoint(ev.clientX ?? 0, ev.clientY ?? 0)) {
|
|
6051
|
+
functionId = id;
|
|
6052
|
+
break;
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
if (functionId) onBoardEvent({ type: "click-curve", functionId, x, y });
|
|
6056
|
+
else onBoardEvent({ type: "click-empty", x, y });
|
|
6057
|
+
});
|
|
6058
|
+
})().catch((err) => console.error("MiniBoard init failed:", err));
|
|
6059
|
+
return () => {
|
|
6060
|
+
cancelled = true;
|
|
6061
|
+
try {
|
|
6062
|
+
if (createdBoard) __require("jsxgraph").default.JSXGraph.freeBoard(createdBoard);
|
|
6063
|
+
} catch {
|
|
6064
|
+
}
|
|
6065
|
+
boardRef.current = null;
|
|
6066
|
+
curvesRef.current.clear();
|
|
6067
|
+
};
|
|
6068
|
+
}, []);
|
|
6069
|
+
useEffect(() => {
|
|
6070
|
+
if (!boardRef.current) return;
|
|
6071
|
+
syncObjects(boardRef.current, graph, curvesRef.current);
|
|
6072
|
+
}, [graph]);
|
|
6073
|
+
useEffect(() => {
|
|
6074
|
+
const el = containerRef.current;
|
|
6075
|
+
if (!el) return;
|
|
6076
|
+
el.style.cursor = activeTool === "move" ? "" : "crosshair";
|
|
6077
|
+
}, [activeTool]);
|
|
6078
|
+
return /* @__PURE__ */ jsx(
|
|
6079
|
+
"div",
|
|
6080
|
+
{
|
|
6081
|
+
ref: containerRef,
|
|
6082
|
+
className: "graph-miniboard",
|
|
6083
|
+
style: { width: "100%", height: "100%", minHeight: "300px" },
|
|
6084
|
+
"data-testid": "graph-miniboard"
|
|
6085
|
+
}
|
|
6086
|
+
);
|
|
6087
|
+
}
|
|
6088
|
+
function paramSig(graph) {
|
|
6089
|
+
return graph.parameters.map((p) => `${p.name}=${p.value}`).join(",");
|
|
6090
|
+
}
|
|
6091
|
+
function syncObjects(board, graph, curves) {
|
|
6092
|
+
const sig = paramSig(graph);
|
|
6093
|
+
const paramMap = {};
|
|
6094
|
+
for (const p of graph.parameters) paramMap[p.name] = p.value;
|
|
6095
|
+
const wantedIds = new Set(graph.functions.map((f) => f.id));
|
|
6096
|
+
for (const [id, ref] of curves) {
|
|
6097
|
+
if (!wantedIds.has(id)) {
|
|
6098
|
+
try {
|
|
6099
|
+
board.removeObject(ref.obj);
|
|
6100
|
+
} catch {
|
|
6101
|
+
}
|
|
6102
|
+
curves.delete(id);
|
|
6103
|
+
}
|
|
6104
|
+
}
|
|
6105
|
+
for (const f of graph.functions) {
|
|
6106
|
+
const existing = curves.get(f.id);
|
|
6107
|
+
const needsRecreate = !existing || existing.expression !== f.expression || existing.color !== f.color || existing.visible !== f.visible || existing.paramSignature !== sig;
|
|
6108
|
+
if (!needsRecreate) continue;
|
|
6109
|
+
if (existing) {
|
|
6110
|
+
try {
|
|
6111
|
+
board.removeObject(existing.obj);
|
|
6112
|
+
} catch {
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
if (!f.visible) {
|
|
6116
|
+
curves.delete(f.id);
|
|
6117
|
+
continue;
|
|
6118
|
+
}
|
|
6119
|
+
const compiled = compile(f.expression, paramMap);
|
|
6120
|
+
if (typeof compiled !== "function") continue;
|
|
6121
|
+
const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
|
|
6122
|
+
const obj = board.create("functiongraph", [compiled, domain.min, domain.max], {
|
|
6123
|
+
strokeColor: f.color,
|
|
6124
|
+
strokeWidth: 2,
|
|
6125
|
+
name: f.name,
|
|
6126
|
+
withLabel: false,
|
|
6127
|
+
highlight: false
|
|
6128
|
+
});
|
|
6129
|
+
curves.set(f.id, {
|
|
6130
|
+
obj,
|
|
6131
|
+
expression: f.expression,
|
|
6132
|
+
color: f.color,
|
|
6133
|
+
visible: f.visible,
|
|
6134
|
+
paramSignature: sig
|
|
6135
|
+
});
|
|
6136
|
+
}
|
|
6137
|
+
for (const point of graph.points) {
|
|
6138
|
+
const fn = graph.functions.find((f) => f.id === point.functionId);
|
|
6139
|
+
if (!fn || !fn.visible) continue;
|
|
6140
|
+
const compiled = compile(fn.expression, paramMap);
|
|
6141
|
+
if (typeof compiled !== "function") continue;
|
|
6142
|
+
const y = compiled(point.x);
|
|
6143
|
+
board.create("point", [point.x, y], {
|
|
6144
|
+
name: point.label ?? "",
|
|
6145
|
+
size: 3,
|
|
6146
|
+
fillColor: fn.color,
|
|
6147
|
+
strokeColor: fn.color,
|
|
6148
|
+
withLabel: !!point.label
|
|
6149
|
+
});
|
|
6150
|
+
}
|
|
6151
|
+
for (const inter of graph.intersections) {
|
|
6152
|
+
const fa = graph.functions.find((f) => f.id === inter.functionIdA);
|
|
6153
|
+
const fb = graph.functions.find((f) => f.id === inter.functionIdB);
|
|
6154
|
+
if (!fa || !fb || !fa.visible || !fb.visible) continue;
|
|
6155
|
+
const cfa = compile(fa.expression, paramMap);
|
|
6156
|
+
const cfb = compile(fb.expression, paramMap);
|
|
6157
|
+
if (typeof cfa !== "function" || typeof cfb !== "function") continue;
|
|
6158
|
+
const roots = scanRoots((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
|
|
6159
|
+
for (const x of roots) {
|
|
6160
|
+
board.create("point", [x, cfa(x)], {
|
|
6161
|
+
size: 3,
|
|
6162
|
+
fillColor: "#000",
|
|
6163
|
+
strokeColor: "#000"
|
|
6164
|
+
});
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
for (const tan of graph.tangents) {
|
|
6168
|
+
const pt = graph.points.find((p) => p.id === tan.pointId);
|
|
6169
|
+
if (!pt) continue;
|
|
6170
|
+
const fn = graph.functions.find((f) => f.id === pt.functionId);
|
|
6171
|
+
if (!fn || !fn.visible) continue;
|
|
6172
|
+
const slope = numericalDerivative(fn.expression, paramMap, pt.x);
|
|
6173
|
+
const cfn = compile(fn.expression, paramMap);
|
|
6174
|
+
if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
|
|
6175
|
+
const y0 = cfn(pt.x);
|
|
6176
|
+
const x1 = graph.view.xMin;
|
|
6177
|
+
const x2 = graph.view.xMax;
|
|
6178
|
+
board.create(
|
|
6179
|
+
"line",
|
|
6180
|
+
[
|
|
6181
|
+
[x1, slope * (x1 - pt.x) + y0],
|
|
6182
|
+
[x2, slope * (x2 - pt.x) + y0]
|
|
6183
|
+
],
|
|
6184
|
+
{
|
|
6185
|
+
strokeColor: fn.color,
|
|
6186
|
+
strokeWidth: 1,
|
|
6187
|
+
dash: 2,
|
|
6188
|
+
straightFirst: false,
|
|
6189
|
+
straightLast: false
|
|
6190
|
+
}
|
|
6191
|
+
);
|
|
6192
|
+
}
|
|
6193
|
+
board.update();
|
|
6194
|
+
}
|
|
6195
|
+
function scanRoots(fn, xMin, xMax, samples = 200) {
|
|
6196
|
+
const roots = [];
|
|
6197
|
+
const step = (xMax - xMin) / samples;
|
|
6198
|
+
let prevX = xMin;
|
|
6199
|
+
let prevY = fn(prevX);
|
|
6200
|
+
for (let i = 1; i <= samples; i++) {
|
|
6201
|
+
const x = xMin + i * step;
|
|
6202
|
+
const y = fn(x);
|
|
6203
|
+
if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
|
|
6204
|
+
let a = prevX;
|
|
6205
|
+
let b = x;
|
|
6206
|
+
let ya = prevY;
|
|
6207
|
+
for (let j = 0; j < 30; j++) {
|
|
6208
|
+
const m = (a + b) / 2;
|
|
6209
|
+
const ym = fn(m);
|
|
6210
|
+
if (Math.abs(ym) < 1e-6) {
|
|
6211
|
+
a = b = m;
|
|
6212
|
+
break;
|
|
6213
|
+
}
|
|
6214
|
+
if (ya * ym < 0) {
|
|
6215
|
+
b = m;
|
|
6216
|
+
} else {
|
|
6217
|
+
a = m;
|
|
6218
|
+
ya = ym;
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
roots.push((a + b) / 2);
|
|
6222
|
+
}
|
|
6223
|
+
prevX = x;
|
|
6224
|
+
prevY = y;
|
|
6225
|
+
}
|
|
6226
|
+
return roots;
|
|
6227
|
+
}
|
|
6228
|
+
|
|
6229
|
+
// src/stamps/graph-2d/serialize.ts
|
|
6230
|
+
var EMPTY_GRAPH = {
|
|
6231
|
+
version: 1,
|
|
6232
|
+
view: { xMin: -10, xMax: 10, yMin: -10, yMax: 10, showAxis: true, showGrid: true },
|
|
6233
|
+
functions: [],
|
|
6234
|
+
parameters: [],
|
|
6235
|
+
points: [],
|
|
6236
|
+
intersections: [],
|
|
6237
|
+
tangents: []
|
|
6238
|
+
};
|
|
6239
|
+
function stringifySerializedGraph(graph) {
|
|
6240
|
+
return JSON.stringify(graph);
|
|
6241
|
+
}
|
|
6242
|
+
function parseSerializedGraph(jsonState) {
|
|
6243
|
+
let raw;
|
|
6244
|
+
try {
|
|
6245
|
+
raw = JSON.parse(jsonState);
|
|
6246
|
+
} catch {
|
|
6247
|
+
return null;
|
|
6248
|
+
}
|
|
6249
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
6250
|
+
const r = raw;
|
|
6251
|
+
if (r.version !== 1) return null;
|
|
6252
|
+
if (!r.view || typeof r.view !== "object") return null;
|
|
6253
|
+
const v = r.view;
|
|
6254
|
+
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") {
|
|
6255
|
+
return null;
|
|
6256
|
+
}
|
|
6257
|
+
for (const key of ["functions", "parameters", "points", "intersections", "tangents"]) {
|
|
6258
|
+
if (!Array.isArray(r[key])) return null;
|
|
6259
|
+
}
|
|
6260
|
+
return raw;
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
// src/stamps/graph-2d/renderObjects.ts
|
|
6264
|
+
function renderGraphObjects(board, graph) {
|
|
6265
|
+
const paramMap = {};
|
|
6266
|
+
for (const p of graph.parameters) paramMap[p.name] = p.value;
|
|
6267
|
+
for (const f of graph.functions) {
|
|
6268
|
+
if (!f.visible) continue;
|
|
6269
|
+
const compiled = compile(f.expression, paramMap);
|
|
6270
|
+
if (typeof compiled !== "function") continue;
|
|
6271
|
+
const domain = f.domain ?? { min: graph.view.xMin, max: graph.view.xMax };
|
|
6272
|
+
board.create("functiongraph", [compiled, domain.min, domain.max], {
|
|
6273
|
+
strokeColor: f.color,
|
|
6274
|
+
strokeWidth: 2,
|
|
6275
|
+
name: f.name,
|
|
6276
|
+
withLabel: false,
|
|
6277
|
+
highlight: false
|
|
6278
|
+
});
|
|
6279
|
+
}
|
|
6280
|
+
for (const point of graph.points) {
|
|
6281
|
+
const fn = graph.functions.find((f) => f.id === point.functionId);
|
|
6282
|
+
if (!fn || !fn.visible) continue;
|
|
6283
|
+
const compiled = compile(fn.expression, paramMap);
|
|
6284
|
+
if (typeof compiled !== "function") continue;
|
|
6285
|
+
const y = compiled(point.x);
|
|
6286
|
+
board.create("point", [point.x, y], {
|
|
6287
|
+
name: point.label ?? "",
|
|
6288
|
+
size: 3,
|
|
6289
|
+
fillColor: fn.color,
|
|
6290
|
+
strokeColor: fn.color,
|
|
6291
|
+
withLabel: !!point.label
|
|
6292
|
+
});
|
|
6293
|
+
}
|
|
6294
|
+
for (const inter of graph.intersections) {
|
|
6295
|
+
const fa = graph.functions.find((f) => f.id === inter.functionIdA);
|
|
6296
|
+
const fb = graph.functions.find((f) => f.id === inter.functionIdB);
|
|
6297
|
+
if (!fa || !fb || !fa.visible || !fb.visible) continue;
|
|
6298
|
+
const cfa = compile(fa.expression, paramMap);
|
|
6299
|
+
const cfb = compile(fb.expression, paramMap);
|
|
6300
|
+
if (typeof cfa !== "function" || typeof cfb !== "function") continue;
|
|
6301
|
+
const roots = scanRoots2((x) => cfa(x) - cfb(x), graph.view.xMin, graph.view.xMax);
|
|
6302
|
+
for (const x of roots) {
|
|
6303
|
+
board.create("point", [x, cfa(x)], {
|
|
6304
|
+
size: 3,
|
|
6305
|
+
fillColor: "#000",
|
|
6306
|
+
strokeColor: "#000"
|
|
6307
|
+
});
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
for (const tan of graph.tangents) {
|
|
6311
|
+
const pt = graph.points.find((p) => p.id === tan.pointId);
|
|
6312
|
+
if (!pt) continue;
|
|
6313
|
+
const fn = graph.functions.find((f) => f.id === pt.functionId);
|
|
6314
|
+
if (!fn || !fn.visible) continue;
|
|
6315
|
+
const slope = numericalDerivative(fn.expression, paramMap, pt.x);
|
|
6316
|
+
const cfn = compile(fn.expression, paramMap);
|
|
6317
|
+
if (typeof cfn !== "function" || !Number.isFinite(slope)) continue;
|
|
6318
|
+
const y0 = cfn(pt.x);
|
|
6319
|
+
const x1 = graph.view.xMin;
|
|
6320
|
+
const x2 = graph.view.xMax;
|
|
6321
|
+
board.create(
|
|
6322
|
+
"line",
|
|
6323
|
+
[
|
|
6324
|
+
[x1, slope * (x1 - pt.x) + y0],
|
|
6325
|
+
[x2, slope * (x2 - pt.x) + y0]
|
|
6326
|
+
],
|
|
6327
|
+
{
|
|
6328
|
+
strokeColor: fn.color,
|
|
6329
|
+
strokeWidth: 1,
|
|
6330
|
+
dash: 2,
|
|
6331
|
+
straightFirst: false,
|
|
6332
|
+
straightLast: false
|
|
6333
|
+
}
|
|
6334
|
+
);
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
function scanRoots2(fn, xMin, xMax, samples = 200) {
|
|
6338
|
+
const roots = [];
|
|
6339
|
+
const step = (xMax - xMin) / samples;
|
|
6340
|
+
let prevX = xMin;
|
|
6341
|
+
let prevY = fn(prevX);
|
|
6342
|
+
for (let i = 1; i <= samples; i++) {
|
|
6343
|
+
const x = xMin + i * step;
|
|
6344
|
+
const y = fn(x);
|
|
6345
|
+
if (Number.isFinite(prevY) && Number.isFinite(y) && prevY * y < 0) {
|
|
6346
|
+
let a = prevX;
|
|
6347
|
+
let b = x;
|
|
6348
|
+
let ya = prevY;
|
|
6349
|
+
for (let j = 0; j < 30; j++) {
|
|
6350
|
+
const m = (a + b) / 2;
|
|
6351
|
+
const ym = fn(m);
|
|
6352
|
+
if (Math.abs(ym) < 1e-6) {
|
|
6353
|
+
a = b = m;
|
|
6354
|
+
break;
|
|
6355
|
+
}
|
|
6356
|
+
if (ya * ym < 0) {
|
|
6357
|
+
b = m;
|
|
6358
|
+
} else {
|
|
6359
|
+
a = m;
|
|
6360
|
+
ya = ym;
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
roots.push((a + b) / 2);
|
|
6364
|
+
}
|
|
6365
|
+
prevX = x;
|
|
6366
|
+
prevY = y;
|
|
6367
|
+
}
|
|
6368
|
+
return roots;
|
|
6369
|
+
}
|
|
6370
|
+
|
|
6371
|
+
// src/stamps/graph-2d/render.ts
|
|
6372
|
+
async function renderGraph2dSvgFromState(jsonState) {
|
|
6373
|
+
const parsed = parseSerializedGraph(jsonState);
|
|
6374
|
+
if (!parsed) throw new Error("renderGraph2dSvgFromState: jsonState corrupt");
|
|
6375
|
+
const JXG = (await import('jsxgraph')).default;
|
|
6376
|
+
const opts = JXG.Options;
|
|
6377
|
+
if (opts) {
|
|
6378
|
+
opts.text = opts.text || {};
|
|
6379
|
+
opts.text.display = "internal";
|
|
6380
|
+
opts.text.useASCIIMathML = false;
|
|
6381
|
+
opts.text.useMathJax = false;
|
|
6382
|
+
opts.text.useKatex = false;
|
|
6383
|
+
opts.label = opts.label || {};
|
|
6384
|
+
opts.label.display = "internal";
|
|
6385
|
+
}
|
|
6386
|
+
const container = document.createElement("div");
|
|
6387
|
+
container.id = `jxg_graph2d_off_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
6388
|
+
container.style.cssText = "position:absolute;top:-99999px;left:-99999px;width:600px;height:400px;visibility:hidden;pointer-events:none;";
|
|
6389
|
+
document.body.appendChild(container);
|
|
6390
|
+
let board = null;
|
|
6391
|
+
try {
|
|
6392
|
+
board = JXG.JSXGraph.initBoard(container.id, {
|
|
6393
|
+
boundingbox: [parsed.view.xMin, parsed.view.yMax, parsed.view.xMax, parsed.view.yMin],
|
|
6394
|
+
axis: parsed.view.showAxis,
|
|
6395
|
+
grid: parsed.view.showGrid,
|
|
6396
|
+
showCopyright: false,
|
|
6397
|
+
showNavigation: false,
|
|
6398
|
+
keepAspectRatio: false
|
|
6399
|
+
});
|
|
6400
|
+
renderGraphObjects(board, parsed);
|
|
6401
|
+
board.update();
|
|
6402
|
+
const svgEl = container.querySelector("svg");
|
|
6403
|
+
if (!svgEl) throw new Error("renderGraph2dSvgFromState: no svg generated");
|
|
6404
|
+
return svgEl.outerHTML;
|
|
6405
|
+
} finally {
|
|
6406
|
+
try {
|
|
6407
|
+
if (board) JXG.JSXGraph.freeBoard(board);
|
|
6408
|
+
} catch {
|
|
6409
|
+
}
|
|
6410
|
+
if (container.parentNode) container.parentNode.removeChild(container);
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
var GraphEditorPanel = forwardRef(function GraphEditorPanel2(props, ref) {
|
|
6414
|
+
const initialGraph = props.initialState ?? EMPTY_GRAPH;
|
|
6415
|
+
const graphRef = useRef(initialGraph);
|
|
6416
|
+
const [, forceUpdate] = useState(0);
|
|
6417
|
+
const [errors, setErrors] = useState({});
|
|
6418
|
+
const [tool, setToolState] = useState("move");
|
|
6419
|
+
const undoStackRef = useRef([]);
|
|
6420
|
+
const idCounterRef = useRef(1);
|
|
6421
|
+
const toolRef = useRef(tool);
|
|
6422
|
+
toolRef.current = tool;
|
|
6423
|
+
const intersectFirstRef = useRef(null);
|
|
6424
|
+
const propsRef = useRef(props);
|
|
6425
|
+
propsRef.current = props;
|
|
6426
|
+
const initialGraphNotifiedRef = useRef(false);
|
|
6427
|
+
const pushUndo = useCallback((g) => {
|
|
6428
|
+
undoStackRef.current.push(g);
|
|
6429
|
+
if (undoStackRef.current.length > 30) undoStackRef.current.shift();
|
|
6430
|
+
}, []);
|
|
6431
|
+
const setErrorsWithNotify = useCallback(
|
|
6432
|
+
(updater) => {
|
|
6433
|
+
setErrors((prev) => {
|
|
6434
|
+
const next = updater(prev);
|
|
6435
|
+
propsRef.current.onErrorsChange?.(next);
|
|
6436
|
+
return next;
|
|
6437
|
+
});
|
|
6438
|
+
},
|
|
6439
|
+
[]
|
|
6440
|
+
);
|
|
6441
|
+
const notifyStateChange = useCallback((g, t) => {
|
|
6442
|
+
propsRef.current.onStateChange({
|
|
6443
|
+
tool: t,
|
|
6444
|
+
showAxis: g.view.showAxis,
|
|
6445
|
+
showGrid: g.view.showGrid,
|
|
6446
|
+
canUndo: undoStackRef.current.length > 0
|
|
6447
|
+
});
|
|
6448
|
+
}, []);
|
|
6449
|
+
const updateGraph = useCallback(
|
|
6450
|
+
(mutator) => {
|
|
6451
|
+
const prev = graphRef.current;
|
|
6452
|
+
pushUndo(prev);
|
|
6453
|
+
const next = mutator(prev);
|
|
6454
|
+
graphRef.current = next;
|
|
6455
|
+
notifyStateChange(next, toolRef.current);
|
|
6456
|
+
forceUpdate((n) => n + 1);
|
|
6457
|
+
propsRef.current.onGraphChange?.(next);
|
|
6458
|
+
},
|
|
6459
|
+
[pushUndo, notifyStateChange]
|
|
6460
|
+
);
|
|
6461
|
+
const onBoardEvent = useCallback((ev) => {
|
|
6462
|
+
const currentTool = toolRef.current;
|
|
6463
|
+
if (currentTool === "point-on-curve" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
|
|
6464
|
+
updateGraph(
|
|
6465
|
+
(g) => addPointOnCurve(
|
|
6466
|
+
g,
|
|
6467
|
+
{ x: ev.x, y: ev.y ?? 0, functionId: ev.functionId },
|
|
6468
|
+
() => `p${idCounterRef.current++}`
|
|
6469
|
+
)
|
|
6470
|
+
);
|
|
6471
|
+
setToolState("move");
|
|
6472
|
+
} else if (currentTool === "intersect" && ev.type === "click-curve" && ev.functionId) {
|
|
6473
|
+
if (!intersectFirstRef.current) {
|
|
6474
|
+
intersectFirstRef.current = ev.functionId;
|
|
6475
|
+
} else {
|
|
6476
|
+
const a = intersectFirstRef.current;
|
|
6477
|
+
const b = ev.functionId;
|
|
6478
|
+
intersectFirstRef.current = null;
|
|
6479
|
+
updateGraph(
|
|
6480
|
+
(g) => addIntersection(g, a, b, () => `i${idCounterRef.current++}`)
|
|
6481
|
+
);
|
|
6482
|
+
setToolState("move");
|
|
6483
|
+
}
|
|
6484
|
+
} else if (currentTool === "tangent" && ev.type === "click-curve" && ev.functionId && ev.x !== void 0) {
|
|
6485
|
+
const pointId = `p${idCounterRef.current++}`;
|
|
6486
|
+
const tangentId = `t${idCounterRef.current++}`;
|
|
6487
|
+
updateGraph((g) => ({
|
|
6488
|
+
...g,
|
|
6489
|
+
points: [...g.points, { id: pointId, functionId: ev.functionId, x: ev.x }],
|
|
6490
|
+
tangents: [...g.tangents, { id: tangentId, pointId }]
|
|
6491
|
+
}));
|
|
6492
|
+
setToolState("move");
|
|
6493
|
+
}
|
|
6494
|
+
}, [updateGraph]);
|
|
6495
|
+
useImperativeHandle(
|
|
6496
|
+
ref,
|
|
6497
|
+
() => ({
|
|
6498
|
+
insert: () => {
|
|
6499
|
+
const g = graphRef.current;
|
|
6500
|
+
if (g.functions.length === 0) return false;
|
|
6501
|
+
const jsonState = stringifySerializedGraph(g);
|
|
6502
|
+
renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
|
|
6503
|
+
return true;
|
|
6504
|
+
},
|
|
6505
|
+
hasContent: () => graphRef.current.functions.length > 0,
|
|
6506
|
+
setTool: (t) => {
|
|
6507
|
+
setToolState(t);
|
|
6508
|
+
const g = graphRef.current;
|
|
6509
|
+
propsRef.current.onStateChange({
|
|
6510
|
+
tool: t,
|
|
6511
|
+
showAxis: g.view.showAxis,
|
|
6512
|
+
showGrid: g.view.showGrid,
|
|
6513
|
+
canUndo: undoStackRef.current.length > 0
|
|
6514
|
+
});
|
|
6515
|
+
},
|
|
6516
|
+
setShowAxis: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showAxis: b } })),
|
|
6517
|
+
setShowGrid: (b) => updateGraph((g) => ({ ...g, view: { ...g.view, showGrid: b } })),
|
|
6518
|
+
resetView: () => updateGraph((g) => ({
|
|
6519
|
+
...g,
|
|
6520
|
+
view: { ...g.view, xMin: -10, xMax: 10, yMin: -10, yMax: 10 }
|
|
6521
|
+
})),
|
|
6522
|
+
undo: () => {
|
|
6523
|
+
const prev = undoStackRef.current.pop();
|
|
6524
|
+
if (!prev) return;
|
|
6525
|
+
graphRef.current = prev;
|
|
6526
|
+
forceUpdate((n) => n + 1);
|
|
6527
|
+
propsRef.current.onStateChange({
|
|
6528
|
+
tool: toolRef.current,
|
|
6529
|
+
showAxis: prev.view.showAxis,
|
|
6530
|
+
showGrid: prev.view.showGrid,
|
|
6531
|
+
canUndo: undoStackRef.current.length > 0
|
|
6532
|
+
});
|
|
6533
|
+
propsRef.current.onGraphChange?.(prev);
|
|
6534
|
+
},
|
|
6535
|
+
addFunction: (expr) => {
|
|
6536
|
+
const g = graphRef.current;
|
|
6537
|
+
if (g.functions.length >= MAX_FUNCTIONS) {
|
|
6538
|
+
return { ok: false, error: `T\u1ED1i \u0111a ${MAX_FUNCTIONS} h\xE0m` };
|
|
6539
|
+
}
|
|
6540
|
+
const v = validate(expr);
|
|
6541
|
+
if (!v.ok) return { ok: false, error: v.error ?? "Invalid" };
|
|
6542
|
+
const id = `f${idCounterRef.current++}`;
|
|
6543
|
+
const usedNames = g.functions.map((f) => f.name);
|
|
6544
|
+
const usedColors = g.functions.map((f) => f.color);
|
|
6545
|
+
const newFn = {
|
|
6546
|
+
id,
|
|
6547
|
+
name: nextFunctionName(usedNames),
|
|
6548
|
+
expression: expr,
|
|
6549
|
+
color: nextColor(usedColors),
|
|
6550
|
+
visible: true
|
|
6551
|
+
};
|
|
6552
|
+
const usedParamNames = new Set(g.parameters.map((p) => p.name));
|
|
6553
|
+
const newParams = [];
|
|
6554
|
+
for (const varName of v.freeVars) {
|
|
6555
|
+
if (usedParamNames.has(varName)) continue;
|
|
6556
|
+
if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
|
|
6557
|
+
newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
|
|
6558
|
+
}
|
|
6559
|
+
updateGraph((prev) => ({
|
|
6560
|
+
...prev,
|
|
6561
|
+
functions: [...prev.functions, newFn],
|
|
6562
|
+
parameters: [...prev.parameters, ...newParams]
|
|
6563
|
+
}));
|
|
6564
|
+
setErrorsWithNotify((e) => ({ ...e, [id]: null }));
|
|
6565
|
+
return { ok: true, id };
|
|
6566
|
+
},
|
|
6567
|
+
commitFunctionExpression: (id, expr) => {
|
|
6568
|
+
const g = graphRef.current;
|
|
6569
|
+
const v = validate(expr);
|
|
6570
|
+
if (!v.ok) {
|
|
6571
|
+
setErrorsWithNotify((e) => ({ ...e, [id]: v.error ?? "Invalid" }));
|
|
6572
|
+
return;
|
|
6573
|
+
}
|
|
6574
|
+
const usedParamNames = new Set(g.parameters.map((p) => p.name));
|
|
6575
|
+
const newParams = [];
|
|
6576
|
+
for (const varName of v.freeVars) {
|
|
6577
|
+
if (usedParamNames.has(varName)) continue;
|
|
6578
|
+
if (g.parameters.length + newParams.length >= MAX_PARAMETERS) break;
|
|
6579
|
+
newParams.push({ name: varName, value: 1, min: -5, max: 5, step: 0.1 });
|
|
6580
|
+
}
|
|
6581
|
+
updateGraph((prev) => ({
|
|
6582
|
+
...prev,
|
|
6583
|
+
functions: prev.functions.map(
|
|
6584
|
+
(f) => f.id === id ? { ...f, expression: expr } : f
|
|
6585
|
+
),
|
|
6586
|
+
parameters: [...prev.parameters, ...newParams]
|
|
6587
|
+
}));
|
|
6588
|
+
setErrorsWithNotify((e) => ({ ...e, [id]: null }));
|
|
6589
|
+
},
|
|
6590
|
+
toggleFunctionVisible: (id) => updateGraph((g) => ({
|
|
6591
|
+
...g,
|
|
6592
|
+
functions: g.functions.map(
|
|
6593
|
+
(f) => f.id === id ? { ...f, visible: !f.visible } : f
|
|
6594
|
+
)
|
|
6595
|
+
})),
|
|
6596
|
+
removeFunction: (id) => updateGraph((g) => ({
|
|
6597
|
+
...g,
|
|
6598
|
+
functions: g.functions.filter((f) => f.id !== id)
|
|
6599
|
+
})),
|
|
6600
|
+
// setParameter does NOT push undo — would flood the stack (slider drag)
|
|
6601
|
+
setParameter: (name, value) => {
|
|
6602
|
+
const next = {
|
|
6603
|
+
...graphRef.current,
|
|
6604
|
+
parameters: graphRef.current.parameters.map(
|
|
6605
|
+
(p) => p.name === name ? { ...p, value } : p
|
|
6606
|
+
)
|
|
6607
|
+
};
|
|
6608
|
+
graphRef.current = next;
|
|
6609
|
+
forceUpdate((n) => n + 1);
|
|
6610
|
+
propsRef.current.onGraphChange?.(next);
|
|
6611
|
+
},
|
|
6612
|
+
setParameterRange: (name, min, max, step) => updateGraph((g) => ({
|
|
6613
|
+
...g,
|
|
6614
|
+
parameters: g.parameters.map(
|
|
6615
|
+
(p) => p.name === name ? { ...p, min, max, step, value: Math.min(max, Math.max(min, p.value)) } : p
|
|
6616
|
+
)
|
|
6617
|
+
})),
|
|
6618
|
+
removeParameter: (name) => updateGraph((g) => ({
|
|
6619
|
+
...g,
|
|
6620
|
+
parameters: g.parameters.filter((p) => p.name !== name)
|
|
6621
|
+
})),
|
|
6622
|
+
getGraph: () => graphRef.current,
|
|
6623
|
+
getErrors: () => errors
|
|
6624
|
+
}),
|
|
6625
|
+
// deps: updateGraph stable; errors changes when function errors change; setErrorsWithNotify stable
|
|
6626
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
6627
|
+
[updateGraph, errors, setErrorsWithNotify]
|
|
6628
|
+
);
|
|
6629
|
+
useEffect(() => {
|
|
6630
|
+
if (!initialGraphNotifiedRef.current) {
|
|
6631
|
+
initialGraphNotifiedRef.current = true;
|
|
6632
|
+
propsRef.current.onGraphChange?.(graphRef.current);
|
|
6633
|
+
}
|
|
6634
|
+
}, []);
|
|
6635
|
+
const graph = graphRef.current;
|
|
6636
|
+
const hasContent = graph.functions.length > 0;
|
|
6637
|
+
const handleInsert = () => {
|
|
6638
|
+
const g = graphRef.current;
|
|
6639
|
+
if (g.functions.length === 0) return;
|
|
6640
|
+
const jsonState = stringifySerializedGraph(g);
|
|
6641
|
+
renderGraph2dSvgFromState(jsonState).then((svg) => propsRef.current.onInsert(jsonState, svg)).catch((err) => console.error("Graph2D insert render failed:", err));
|
|
6642
|
+
};
|
|
6643
|
+
const { isMobile, isDark, withLeftPanel } = props;
|
|
6644
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
6645
|
+
position: "absolute",
|
|
6646
|
+
top: "50%",
|
|
6647
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
6648
|
+
transform: "translate(-50%, -50%)",
|
|
6649
|
+
zIndex: 40
|
|
6650
|
+
};
|
|
6651
|
+
return /* @__PURE__ */ jsxs(
|
|
6652
|
+
"div",
|
|
6653
|
+
{
|
|
6654
|
+
role: "dialog",
|
|
6655
|
+
"aria-label": "\u0110\u1ED3 th\u1ECB 2D",
|
|
6656
|
+
"data-testid": "graph-editor-panel",
|
|
6657
|
+
"data-stamp-area": "true",
|
|
6658
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
6659
|
+
style: wrapperStyle,
|
|
6660
|
+
className: [
|
|
6661
|
+
isDark ? "theme--dark " : "",
|
|
6662
|
+
"flex flex-col overflow-hidden bg-white",
|
|
6663
|
+
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"
|
|
6664
|
+
].join(" "),
|
|
6665
|
+
children: [
|
|
6666
|
+
/* @__PURE__ */ 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: [
|
|
6667
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
6668
|
+
"button",
|
|
6669
|
+
{
|
|
6670
|
+
type: "button",
|
|
6671
|
+
onClick: props.onOpenDrawer,
|
|
6672
|
+
"aria-label": "M\u1EDF b\u1EA3ng \u0111\u1EA1i s\u1ED1",
|
|
6673
|
+
"data-testid": "graph-drawer-toggle",
|
|
6674
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
6675
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6676
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
6677
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
6678
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
6679
|
+
] })
|
|
6680
|
+
}
|
|
6681
|
+
),
|
|
6682
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
6683
|
+
/* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6684
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21 V3" }),
|
|
6685
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21 H21" }),
|
|
6686
|
+
/* @__PURE__ */ jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
|
|
6687
|
+
] }),
|
|
6688
|
+
"\u0110\u1ED3 th\u1ECB 2D"
|
|
6689
|
+
] }),
|
|
6690
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
6691
|
+
"button",
|
|
6692
|
+
{
|
|
6693
|
+
type: "button",
|
|
6694
|
+
onClick: handleInsert,
|
|
6695
|
+
disabled: !hasContent,
|
|
6696
|
+
"data-testid": "graph-insert-btn-mobile",
|
|
6697
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
6698
|
+
children: "Ch\xE8n"
|
|
6699
|
+
}
|
|
6700
|
+
),
|
|
6701
|
+
/* @__PURE__ */ jsx(
|
|
6702
|
+
"button",
|
|
6703
|
+
{
|
|
6704
|
+
onClick: props.onClose,
|
|
6705
|
+
"aria-label": "\u0110\xF3ng",
|
|
6706
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
6707
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
6708
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
6709
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
6710
|
+
] })
|
|
6711
|
+
}
|
|
6712
|
+
)
|
|
6713
|
+
] }),
|
|
6714
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
6715
|
+
MiniBoard,
|
|
6716
|
+
{
|
|
6717
|
+
graph,
|
|
6718
|
+
activeTool: tool,
|
|
6719
|
+
isDark,
|
|
6720
|
+
onBoardEvent
|
|
6721
|
+
}
|
|
6722
|
+
) }),
|
|
6723
|
+
!isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
6724
|
+
/* @__PURE__ */ 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." }),
|
|
6725
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
6726
|
+
/* @__PURE__ */ jsx(
|
|
6727
|
+
"button",
|
|
6728
|
+
{
|
|
6729
|
+
onClick: props.onClose,
|
|
6730
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
6731
|
+
children: "Hu\u1EF7"
|
|
6732
|
+
}
|
|
6733
|
+
),
|
|
6734
|
+
/* @__PURE__ */ jsx(
|
|
6735
|
+
"button",
|
|
6736
|
+
{
|
|
6737
|
+
onClick: handleInsert,
|
|
6738
|
+
disabled: !hasContent,
|
|
6739
|
+
"data-testid": "graph-insert-btn",
|
|
6740
|
+
className: "rounded bg-orange-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-orange-700 disabled:opacity-50",
|
|
6741
|
+
children: "Ch\xE8n"
|
|
6742
|
+
}
|
|
6743
|
+
)
|
|
6744
|
+
] })
|
|
6745
|
+
] })
|
|
6746
|
+
]
|
|
6747
|
+
}
|
|
6748
|
+
);
|
|
6749
|
+
});
|
|
6750
|
+
function isGraph2DCustomData(data) {
|
|
6751
|
+
if (!data || typeof data !== "object") return false;
|
|
6752
|
+
const d = data;
|
|
6753
|
+
return d.kind === "graph2d" && d.version === 1 && typeof d.jsonState === "string";
|
|
4547
6754
|
}
|
|
4548
|
-
var
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
6755
|
+
var INITIAL_GRAPH_STATE = {
|
|
6756
|
+
tool: "move",
|
|
6757
|
+
showAxis: true,
|
|
6758
|
+
showGrid: true,
|
|
6759
|
+
canUndo: false
|
|
6760
|
+
};
|
|
6761
|
+
var Graph2DStampHost = forwardRef(
|
|
6762
|
+
function Graph2DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
6763
|
+
const panelRef = useRef(null);
|
|
6764
|
+
const [graphUIState, setGraphUIState] = useState(INITIAL_GRAPH_STATE);
|
|
6765
|
+
const { isMobile } = useIsMobile();
|
|
6766
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
6767
|
+
const initialState = useMemo(() => {
|
|
6768
|
+
if (!editingElement) return null;
|
|
6769
|
+
if (!isGraph2DCustomData(editingElement.customData)) return null;
|
|
6770
|
+
return parseSerializedGraph(editingElement.customData.jsonState);
|
|
6771
|
+
}, [editingElement]);
|
|
6772
|
+
const [graphSnapshot, setGraphSnapshot] = useState(
|
|
6773
|
+
initialState ?? EMPTY_GRAPH
|
|
4554
6774
|
);
|
|
6775
|
+
const [errorsSnapshot, setErrorsSnapshot] = useState({});
|
|
4555
6776
|
const handleInsert = useCallback(
|
|
4556
|
-
async (jsonState, svgString
|
|
6777
|
+
async (jsonState, svgString) => {
|
|
4557
6778
|
if (!api) return;
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
6779
|
+
try {
|
|
6780
|
+
await insertStampImage(api, {
|
|
6781
|
+
svgString,
|
|
6782
|
+
makeCustomData: (width, height) => ({
|
|
6783
|
+
kind: "graph2d",
|
|
6784
|
+
version: 1,
|
|
6785
|
+
jsonState,
|
|
6786
|
+
svgWidth: width,
|
|
6787
|
+
svgHeight: height
|
|
6788
|
+
}),
|
|
6789
|
+
editingElementId: editingElement?.id ?? null
|
|
6790
|
+
});
|
|
6791
|
+
} catch (err) {
|
|
6792
|
+
console.error("Graph2D insert failed:", err);
|
|
6793
|
+
}
|
|
4569
6794
|
onClose();
|
|
4570
6795
|
},
|
|
4571
|
-
[api, editingElement, onClose]
|
|
6796
|
+
[api, editingElement?.id, onClose]
|
|
4572
6797
|
);
|
|
4573
6798
|
useImperativeHandle(
|
|
4574
6799
|
ref,
|
|
4575
6800
|
() => ({
|
|
4576
|
-
tryInsert: () =>
|
|
4577
|
-
hasContent: () =>
|
|
6801
|
+
tryInsert: () => panelRef.current?.insert() ?? false,
|
|
6802
|
+
hasContent: () => panelRef.current?.hasContent() ?? false
|
|
4578
6803
|
}),
|
|
4579
6804
|
[]
|
|
4580
6805
|
);
|
|
4581
|
-
return /* @__PURE__ */
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
6806
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6807
|
+
/* @__PURE__ */ jsx(
|
|
6808
|
+
GraphLeftPanel,
|
|
6809
|
+
{
|
|
6810
|
+
activeTool: graphUIState.tool,
|
|
6811
|
+
onToolChange: (t) => panelRef.current?.setTool(t),
|
|
6812
|
+
showAxis: graphUIState.showAxis,
|
|
6813
|
+
showGrid: graphUIState.showGrid,
|
|
6814
|
+
onShowAxisChange: (b) => panelRef.current?.setShowAxis(b),
|
|
6815
|
+
onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
|
|
6816
|
+
onResetView: () => panelRef.current?.resetView(),
|
|
6817
|
+
onUndo: () => panelRef.current?.undo(),
|
|
6818
|
+
canUndo: graphUIState.canUndo,
|
|
6819
|
+
onClose,
|
|
6820
|
+
isDark,
|
|
6821
|
+
isMobile,
|
|
6822
|
+
drawerOpen,
|
|
6823
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
6824
|
+
graph: graphSnapshot,
|
|
6825
|
+
errors: errorsSnapshot,
|
|
6826
|
+
onAddFunctionDraft: () => {
|
|
6827
|
+
const result = panelRef.current?.addFunction("x");
|
|
6828
|
+
if (result && !result.ok) console.warn("addFunction failed:", result.error);
|
|
6829
|
+
},
|
|
6830
|
+
onCommitFunctionExpr: (id, expr) => panelRef.current?.commitFunctionExpression(id, expr),
|
|
6831
|
+
onToggleFunctionVisible: (id) => panelRef.current?.toggleFunctionVisible(id),
|
|
6832
|
+
onRemoveFunction: (id) => panelRef.current?.removeFunction(id),
|
|
6833
|
+
onParameterChange: (name, v) => panelRef.current?.setParameter(name, v),
|
|
6834
|
+
onParameterRangeChange: (name, min, max, step) => panelRef.current?.setParameterRange(name, min, max, step),
|
|
6835
|
+
onRemoveParameter: (name) => panelRef.current?.removeParameter(name)
|
|
6836
|
+
}
|
|
6837
|
+
),
|
|
6838
|
+
/* @__PURE__ */ jsx(
|
|
6839
|
+
GraphEditorPanel,
|
|
6840
|
+
{
|
|
6841
|
+
ref: panelRef,
|
|
6842
|
+
initialState,
|
|
6843
|
+
onInsert: handleInsert,
|
|
6844
|
+
onClose,
|
|
6845
|
+
onStateChange: setGraphUIState,
|
|
6846
|
+
onGraphChange: setGraphSnapshot,
|
|
6847
|
+
onErrorsChange: setErrorsSnapshot,
|
|
6848
|
+
withLeftPanel: !isMobile,
|
|
6849
|
+
isDark,
|
|
6850
|
+
isMobile,
|
|
6851
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
6852
|
+
}
|
|
6853
|
+
)
|
|
6854
|
+
] });
|
|
4591
6855
|
}
|
|
4592
6856
|
);
|
|
4593
|
-
var
|
|
6857
|
+
var Graph2DIcon = /* @__PURE__ */ jsxs(
|
|
4594
6858
|
"svg",
|
|
4595
6859
|
{
|
|
4596
6860
|
width: "20",
|
|
@@ -4603,47 +6867,45 @@ var Geometry3DIcon = /* @__PURE__ */ jsxs(
|
|
|
4603
6867
|
strokeLinejoin: "round",
|
|
4604
6868
|
"aria-hidden": "true",
|
|
4605
6869
|
children: [
|
|
4606
|
-
/* @__PURE__ */ jsx("path", { d: "
|
|
4607
|
-
/* @__PURE__ */ jsx("path", { d: "
|
|
6870
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21 V3" }),
|
|
6871
|
+
/* @__PURE__ */ jsx("path", { d: "M3 21 H21" }),
|
|
6872
|
+
/* @__PURE__ */ jsx("path", { d: "M5 19 C8 5, 14 5, 19 17" })
|
|
4608
6873
|
]
|
|
4609
6874
|
}
|
|
4610
6875
|
);
|
|
4611
|
-
var
|
|
4612
|
-
kind: "
|
|
4613
|
-
shortcutKey: "
|
|
4614
|
-
toolbarLabel: "
|
|
4615
|
-
toolbarTitle: "
|
|
4616
|
-
toolbarIcon:
|
|
4617
|
-
toolbarTestId: "stamp-toolbar-
|
|
4618
|
-
matchesCustomData:
|
|
6876
|
+
var graph2dStamp = {
|
|
6877
|
+
kind: "graph2d",
|
|
6878
|
+
shortcutKey: "h",
|
|
6879
|
+
toolbarLabel: "H",
|
|
6880
|
+
toolbarTitle: "Ch\xE8n \u0111\u1ED3 th\u1ECB 2D (H)",
|
|
6881
|
+
toolbarIcon: Graph2DIcon,
|
|
6882
|
+
toolbarTestId: "stamp-toolbar-graph2d",
|
|
6883
|
+
matchesCustomData: isGraph2DCustomData,
|
|
4619
6884
|
async renderSvgFromCustomData(data) {
|
|
4620
|
-
if (!
|
|
4621
|
-
throw new Error("
|
|
6885
|
+
if (!isGraph2DCustomData(data)) {
|
|
6886
|
+
throw new Error("graph2dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i graph2d");
|
|
4622
6887
|
}
|
|
4623
|
-
|
|
4624
|
-
return svgString;
|
|
6888
|
+
return renderGraph2dSvgFromState(data.jsonState);
|
|
4625
6889
|
},
|
|
4626
|
-
|
|
6890
|
+
async restoreFileFromCustomData(element) {
|
|
4627
6891
|
const data = element.customData;
|
|
4628
6892
|
const fileId = element.fileId;
|
|
4629
6893
|
if (!data || !fileId) return null;
|
|
4630
|
-
if (!
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
} catch {
|
|
4636
|
-
return null;
|
|
4637
|
-
}
|
|
6894
|
+
if (!isGraph2DCustomData(data)) return null;
|
|
6895
|
+
const svgString = await renderGraph2dSvgFromState(data.jsonState);
|
|
6896
|
+
const utf8 = unescape(encodeURIComponent(svgString));
|
|
6897
|
+
const dataURL = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8).toString("base64"));
|
|
6898
|
+
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
4638
6899
|
},
|
|
4639
|
-
Host:
|
|
6900
|
+
Host: Graph2DStampHost
|
|
4640
6901
|
};
|
|
4641
6902
|
|
|
4642
6903
|
// src/stamps/shared/registry.ts
|
|
4643
6904
|
var DEFAULT_STAMPS = Object.freeze([
|
|
4644
6905
|
geometryStamp,
|
|
4645
6906
|
latexStamp,
|
|
4646
|
-
geometry3dStamp
|
|
6907
|
+
geometry3dStamp,
|
|
6908
|
+
graph2dStamp
|
|
4647
6909
|
]);
|
|
4648
6910
|
function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
4649
6911
|
for (const s of stamps) {
|
|
@@ -4654,36 +6916,90 @@ function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
|
4654
6916
|
function isStampElement(element, stamps = DEFAULT_STAMPS) {
|
|
4655
6917
|
return findStampForCustomData(element.customData, stamps) !== null;
|
|
4656
6918
|
}
|
|
4657
|
-
var
|
|
6919
|
+
var TOOLBAR_WRAPPER_ID = "stamp-toolbar-portal-wrapper";
|
|
6920
|
+
var MENU_WRAPPER_ID = "stamp-menu-portal-wrapper";
|
|
4658
6921
|
function ToolbarInjector({
|
|
4659
6922
|
enabled,
|
|
4660
6923
|
activeStampKind,
|
|
4661
6924
|
onToggle,
|
|
4662
6925
|
stamps = DEFAULT_STAMPS
|
|
4663
6926
|
}) {
|
|
4664
|
-
const [
|
|
6927
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
6928
|
+
const [toolbarMount, setToolbarMount] = useState(null);
|
|
6929
|
+
const [menuMount, setMenuMount] = useState(null);
|
|
6930
|
+
const isMobileRef = useRef(false);
|
|
6931
|
+
const toolbarMountRef = useRef(null);
|
|
6932
|
+
const menuMountRef = useRef(null);
|
|
4665
6933
|
useEffect(() => {
|
|
4666
6934
|
if (!enabled) {
|
|
4667
|
-
|
|
6935
|
+
if (isMobileRef.current !== false) {
|
|
6936
|
+
isMobileRef.current = false;
|
|
6937
|
+
setIsMobile(false);
|
|
6938
|
+
}
|
|
6939
|
+
return;
|
|
6940
|
+
}
|
|
6941
|
+
let cancelled = false;
|
|
6942
|
+
let observer = null;
|
|
6943
|
+
let timer = null;
|
|
6944
|
+
let attempts = 0;
|
|
6945
|
+
const apply = (next) => {
|
|
6946
|
+
if (cancelled || isMobileRef.current === next) return;
|
|
6947
|
+
isMobileRef.current = next;
|
|
6948
|
+
queueMicrotask(() => {
|
|
6949
|
+
if (!cancelled) setIsMobile(next);
|
|
6950
|
+
});
|
|
6951
|
+
};
|
|
6952
|
+
const attach = () => {
|
|
6953
|
+
if (cancelled) return;
|
|
6954
|
+
const root = document.querySelector(".excalidraw");
|
|
6955
|
+
if (!root) {
|
|
6956
|
+
if (attempts++ < 20) timer = setTimeout(attach, 100);
|
|
6957
|
+
return;
|
|
6958
|
+
}
|
|
6959
|
+
apply(root.classList.contains("excalidraw--mobile"));
|
|
6960
|
+
observer = new MutationObserver(() => {
|
|
6961
|
+
apply(root.classList.contains("excalidraw--mobile"));
|
|
6962
|
+
});
|
|
6963
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
6964
|
+
};
|
|
6965
|
+
attach();
|
|
6966
|
+
return () => {
|
|
6967
|
+
cancelled = true;
|
|
6968
|
+
if (timer) clearTimeout(timer);
|
|
6969
|
+
observer?.disconnect();
|
|
6970
|
+
};
|
|
6971
|
+
}, [enabled]);
|
|
6972
|
+
useEffect(() => {
|
|
6973
|
+
if (!enabled || isMobile) {
|
|
6974
|
+
if (toolbarMountRef.current !== null) {
|
|
6975
|
+
toolbarMountRef.current = null;
|
|
6976
|
+
setToolbarMount(null);
|
|
6977
|
+
}
|
|
6978
|
+
document.getElementById(TOOLBAR_WRAPPER_ID)?.remove();
|
|
4668
6979
|
return;
|
|
4669
6980
|
}
|
|
4670
6981
|
let cancelled = false;
|
|
4671
6982
|
let attempts = 0;
|
|
4672
6983
|
let observer = null;
|
|
4673
6984
|
let timer = null;
|
|
6985
|
+
const apply = (next) => {
|
|
6986
|
+
if (cancelled || toolbarMountRef.current === next) return;
|
|
6987
|
+
toolbarMountRef.current = next;
|
|
6988
|
+
queueMicrotask(() => {
|
|
6989
|
+
if (!cancelled) setToolbarMount(next);
|
|
6990
|
+
});
|
|
6991
|
+
};
|
|
4674
6992
|
const tryMount = () => {
|
|
4675
6993
|
if (cancelled) return;
|
|
4676
6994
|
const container = document.querySelector(".excalidraw .App-toolbar .Stack_horizontal") ?? document.querySelector(".App-toolbar .Stack_horizontal");
|
|
4677
6995
|
if (!container) {
|
|
4678
|
-
if (attempts++ < 20)
|
|
4679
|
-
timer = setTimeout(tryMount, 100);
|
|
4680
|
-
}
|
|
6996
|
+
if (attempts++ < 20) timer = setTimeout(tryMount, 100);
|
|
4681
6997
|
return;
|
|
4682
6998
|
}
|
|
4683
|
-
let wrapper = container.querySelector("#" +
|
|
6999
|
+
let wrapper = container.querySelector("#" + TOOLBAR_WRAPPER_ID);
|
|
4684
7000
|
if (!wrapper) {
|
|
4685
7001
|
wrapper = document.createElement("div");
|
|
4686
|
-
wrapper.id =
|
|
7002
|
+
wrapper.id = TOOLBAR_WRAPPER_ID;
|
|
4687
7003
|
wrapper.className = "Stamp-toolbar-injector";
|
|
4688
7004
|
wrapper.setAttribute("data-stamp-area", "true");
|
|
4689
7005
|
wrapper.style.display = "inline-flex";
|
|
@@ -4699,13 +7015,13 @@ function ToolbarInjector({
|
|
|
4699
7015
|
container.appendChild(wrapper);
|
|
4700
7016
|
}
|
|
4701
7017
|
}
|
|
4702
|
-
|
|
7018
|
+
apply(wrapper);
|
|
4703
7019
|
};
|
|
4704
7020
|
tryMount();
|
|
4705
7021
|
const root = document.querySelector(".excalidraw") ?? document.body;
|
|
4706
7022
|
observer = new MutationObserver(() => {
|
|
4707
7023
|
if (cancelled) return;
|
|
4708
|
-
const stillThere = document.getElementById(
|
|
7024
|
+
const stillThere = document.getElementById(TOOLBAR_WRAPPER_ID);
|
|
4709
7025
|
if (!stillThere) {
|
|
4710
7026
|
attempts = 0;
|
|
4711
7027
|
tryMount();
|
|
@@ -4716,11 +7032,73 @@ function ToolbarInjector({
|
|
|
4716
7032
|
cancelled = true;
|
|
4717
7033
|
if (timer) clearTimeout(timer);
|
|
4718
7034
|
observer?.disconnect();
|
|
4719
|
-
document.getElementById(
|
|
7035
|
+
document.getElementById(TOOLBAR_WRAPPER_ID)?.remove();
|
|
4720
7036
|
};
|
|
4721
|
-
}, [enabled]);
|
|
4722
|
-
|
|
4723
|
-
|
|
7037
|
+
}, [enabled, isMobile]);
|
|
7038
|
+
useEffect(() => {
|
|
7039
|
+
if (!enabled || !isMobile) {
|
|
7040
|
+
if (menuMountRef.current !== null) {
|
|
7041
|
+
menuMountRef.current = null;
|
|
7042
|
+
setMenuMount(null);
|
|
7043
|
+
}
|
|
7044
|
+
document.getElementById(MENU_WRAPPER_ID)?.remove();
|
|
7045
|
+
return;
|
|
7046
|
+
}
|
|
7047
|
+
let cancelled = false;
|
|
7048
|
+
let observer = null;
|
|
7049
|
+
let rafId = null;
|
|
7050
|
+
const apply = (next) => {
|
|
7051
|
+
if (cancelled || menuMountRef.current === next) return;
|
|
7052
|
+
menuMountRef.current = next;
|
|
7053
|
+
queueMicrotask(() => {
|
|
7054
|
+
if (!cancelled) setMenuMount(next);
|
|
7055
|
+
});
|
|
7056
|
+
};
|
|
7057
|
+
const findMenu = () => {
|
|
7058
|
+
if (cancelled) return;
|
|
7059
|
+
const container = document.querySelector(
|
|
7060
|
+
".dropdown-menu--mobile .dropdown-menu-container"
|
|
7061
|
+
);
|
|
7062
|
+
if (!container) {
|
|
7063
|
+
apply(null);
|
|
7064
|
+
return;
|
|
7065
|
+
}
|
|
7066
|
+
let wrapper = container.querySelector("#" + MENU_WRAPPER_ID);
|
|
7067
|
+
if (!wrapper) {
|
|
7068
|
+
wrapper = document.createElement("div");
|
|
7069
|
+
wrapper.id = MENU_WRAPPER_ID;
|
|
7070
|
+
wrapper.setAttribute("data-stamp-menu", "true");
|
|
7071
|
+
wrapper.style.display = "contents";
|
|
7072
|
+
container.insertBefore(wrapper, container.firstChild);
|
|
7073
|
+
}
|
|
7074
|
+
apply(wrapper);
|
|
7075
|
+
};
|
|
7076
|
+
const schedule = () => {
|
|
7077
|
+
if (rafId != null) return;
|
|
7078
|
+
rafId = requestAnimationFrame(() => {
|
|
7079
|
+
rafId = null;
|
|
7080
|
+
findMenu();
|
|
7081
|
+
});
|
|
7082
|
+
};
|
|
7083
|
+
findMenu();
|
|
7084
|
+
const root = document.querySelector(".excalidraw") ?? document.body;
|
|
7085
|
+
observer = new MutationObserver(schedule);
|
|
7086
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
7087
|
+
return () => {
|
|
7088
|
+
cancelled = true;
|
|
7089
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
7090
|
+
observer?.disconnect();
|
|
7091
|
+
document.getElementById(MENU_WRAPPER_ID)?.remove();
|
|
7092
|
+
};
|
|
7093
|
+
}, [enabled, isMobile]);
|
|
7094
|
+
if (!enabled) return null;
|
|
7095
|
+
const closeMobileMenu = () => {
|
|
7096
|
+
const trigger = document.querySelector(
|
|
7097
|
+
".App-toolbar__extra-tools-trigger"
|
|
7098
|
+
);
|
|
7099
|
+
trigger?.click();
|
|
7100
|
+
};
|
|
7101
|
+
const desktopButtons = !isMobile && toolbarMount ? createPortal(
|
|
4724
7102
|
/* @__PURE__ */ jsx(Fragment, { children: stamps.map((stamp) => /* @__PURE__ */ jsx(
|
|
4725
7103
|
StampToolButton,
|
|
4726
7104
|
{
|
|
@@ -4733,8 +7111,42 @@ function ToolbarInjector({
|
|
|
4733
7111
|
},
|
|
4734
7112
|
stamp.kind
|
|
4735
7113
|
)) }),
|
|
4736
|
-
|
|
4737
|
-
);
|
|
7114
|
+
toolbarMount
|
|
7115
|
+
) : null;
|
|
7116
|
+
const mobileMenuItems = isMobile && menuMount ? createPortal(
|
|
7117
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7118
|
+
stamps.map((stamp) => /* @__PURE__ */ jsx(
|
|
7119
|
+
StampMenuItem,
|
|
7120
|
+
{
|
|
7121
|
+
icon: stamp.toolbarIcon,
|
|
7122
|
+
label: stamp.toolbarTitle,
|
|
7123
|
+
active: activeStampKind === stamp.kind,
|
|
7124
|
+
onClick: () => {
|
|
7125
|
+
onToggle(stamp.kind);
|
|
7126
|
+
closeMobileMenu();
|
|
7127
|
+
},
|
|
7128
|
+
dataTestId: stamp.toolbarTestId
|
|
7129
|
+
},
|
|
7130
|
+
stamp.kind
|
|
7131
|
+
)),
|
|
7132
|
+
/* @__PURE__ */ jsx(
|
|
7133
|
+
"div",
|
|
7134
|
+
{
|
|
7135
|
+
"aria-hidden": "true",
|
|
7136
|
+
style: {
|
|
7137
|
+
height: 1,
|
|
7138
|
+
background: "var(--default-border-color, rgba(0,0,0,0.08))",
|
|
7139
|
+
margin: "6px 4px"
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
)
|
|
7143
|
+
] }),
|
|
7144
|
+
menuMount
|
|
7145
|
+
) : null;
|
|
7146
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7147
|
+
desktopButtons,
|
|
7148
|
+
mobileMenuItems
|
|
7149
|
+
] });
|
|
4738
7150
|
}
|
|
4739
7151
|
function StampToolButton({ icon, keybind, label, active, onClick, dataTestId }) {
|
|
4740
7152
|
return /* @__PURE__ */ jsxs(
|
|
@@ -4799,6 +7211,54 @@ function StampToolButton({ icon, keybind, label, active, onClick, dataTestId })
|
|
|
4799
7211
|
}
|
|
4800
7212
|
);
|
|
4801
7213
|
}
|
|
7214
|
+
function StampMenuItem({ icon, label, active, onClick, dataTestId }) {
|
|
7215
|
+
const className = [
|
|
7216
|
+
"dropdown-menu-item",
|
|
7217
|
+
"dropdown-menu-item-base",
|
|
7218
|
+
active ? "dropdown-menu-item--selected" : ""
|
|
7219
|
+
].filter(Boolean).join(" ");
|
|
7220
|
+
return /* @__PURE__ */ jsxs(
|
|
7221
|
+
"button",
|
|
7222
|
+
{
|
|
7223
|
+
type: "button",
|
|
7224
|
+
onClick,
|
|
7225
|
+
"aria-label": label,
|
|
7226
|
+
"aria-pressed": active,
|
|
7227
|
+
"data-testid": dataTestId,
|
|
7228
|
+
className,
|
|
7229
|
+
style: {
|
|
7230
|
+
display: "flex",
|
|
7231
|
+
alignItems: "center",
|
|
7232
|
+
columnGap: "0.625rem",
|
|
7233
|
+
width: "100%",
|
|
7234
|
+
boxSizing: "border-box",
|
|
7235
|
+
background: "transparent",
|
|
7236
|
+
border: "1px solid transparent",
|
|
7237
|
+
cursor: "pointer",
|
|
7238
|
+
fontFamily: "inherit",
|
|
7239
|
+
fontSize: "0.875rem",
|
|
7240
|
+
color: "var(--color-on-surface)"
|
|
7241
|
+
},
|
|
7242
|
+
children: [
|
|
7243
|
+
/* @__PURE__ */ jsx(
|
|
7244
|
+
"span",
|
|
7245
|
+
{
|
|
7246
|
+
"aria-hidden": "true",
|
|
7247
|
+
style: {
|
|
7248
|
+
display: "inline-flex",
|
|
7249
|
+
alignItems: "center",
|
|
7250
|
+
justifyContent: "center",
|
|
7251
|
+
width: "1rem",
|
|
7252
|
+
height: "1rem"
|
|
7253
|
+
},
|
|
7254
|
+
children: icon
|
|
7255
|
+
}
|
|
7256
|
+
),
|
|
7257
|
+
/* @__PURE__ */ jsx("span", { children: label })
|
|
7258
|
+
]
|
|
7259
|
+
}
|
|
7260
|
+
);
|
|
7261
|
+
}
|
|
4802
7262
|
function isEditableTarget(t) {
|
|
4803
7263
|
if (!t || !(t instanceof HTMLElement)) return false;
|
|
4804
7264
|
if (t.isContentEditable) return true;
|
|
@@ -5098,7 +7558,7 @@ async function pruneFiles(storageKey, keepIds) {
|
|
|
5098
7558
|
}
|
|
5099
7559
|
}
|
|
5100
7560
|
var Excalidraw = dynamic(
|
|
5101
|
-
async () => (await import('./ExcalidrawWithMenus-
|
|
7561
|
+
async () => (await import('./ExcalidrawWithMenus-EAVPOPJZ.mjs')).ExcalidrawWithMenus,
|
|
5102
7562
|
{
|
|
5103
7563
|
ssr: false,
|
|
5104
7564
|
loading: () => /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-gray-500", children: "\u0110ang t\u1EA3i b\u1EA3ng\u2026" })
|
|
@@ -5116,7 +7576,9 @@ function Whiteboard({
|
|
|
5116
7576
|
stamps = DEFAULT_STAMPS
|
|
5117
7577
|
}) {
|
|
5118
7578
|
const [api, setApi] = useState(null);
|
|
7579
|
+
const apiRef = useRef(null);
|
|
5119
7580
|
const [isDarkTheme, setIsDarkTheme] = useState(false);
|
|
7581
|
+
const isDarkThemeRef = useRef(false);
|
|
5120
7582
|
const knownFileIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
5121
7583
|
const lastSceneHashRef = useRef("");
|
|
5122
7584
|
const sceneThrottleRef = useRef(null);
|
|
@@ -5177,7 +7639,10 @@ function Whiteboard({
|
|
|
5177
7639
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5178
7640
|
(elements, appState, files) => {
|
|
5179
7641
|
const nextDark = appState?.theme === "dark";
|
|
5180
|
-
|
|
7642
|
+
if (isDarkThemeRef.current !== nextDark) {
|
|
7643
|
+
isDarkThemeRef.current = nextDark;
|
|
7644
|
+
queueMicrotask(() => setIsDarkTheme(nextDark));
|
|
7645
|
+
}
|
|
5181
7646
|
if (readOnly) return;
|
|
5182
7647
|
latestSceneRef.current = { elements, appState };
|
|
5183
7648
|
const cropId = appState?.croppingElementId;
|
|
@@ -5187,12 +7652,17 @@ function Whiteboard({
|
|
|
5187
7652
|
const stamp = findStampForCustomData(el.customData, stamps);
|
|
5188
7653
|
if (stamp) {
|
|
5189
7654
|
handledCropIdRef.current = cropId;
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
7655
|
+
const elId = el.id;
|
|
7656
|
+
const elCustom = el.customData;
|
|
7657
|
+
const stampKind = stamp.kind;
|
|
7658
|
+
queueMicrotask(() => {
|
|
7659
|
+
try {
|
|
7660
|
+
api.updateScene({
|
|
7661
|
+
appState: { croppingElementId: null, selectedElementIds: {} }
|
|
7662
|
+
});
|
|
7663
|
+
} catch {
|
|
7664
|
+
}
|
|
7665
|
+
openStamp(stampKind, { id: elId, customData: elCustom });
|
|
5196
7666
|
});
|
|
5197
7667
|
return;
|
|
5198
7668
|
}
|
|
@@ -5443,8 +7913,12 @@ function Whiteboard({
|
|
|
5443
7913
|
Excalidraw,
|
|
5444
7914
|
{
|
|
5445
7915
|
excalidrawAPI: (a) => {
|
|
5446
|
-
|
|
5447
|
-
|
|
7916
|
+
if (apiRef.current === a) return;
|
|
7917
|
+
apiRef.current = a;
|
|
7918
|
+
queueMicrotask(() => {
|
|
7919
|
+
setApi(a);
|
|
7920
|
+
onApi?.(a);
|
|
7921
|
+
});
|
|
5448
7922
|
},
|
|
5449
7923
|
langCode,
|
|
5450
7924
|
viewModeEnabled: readOnly,
|
|
@@ -5481,6 +7955,6 @@ function Whiteboard({
|
|
|
5481
7955
|
] });
|
|
5482
7956
|
}
|
|
5483
7957
|
|
|
5484
|
-
export { DEFAULT_STAMPS, Whiteboard, findStampForCustomData, geometry3dStamp, geometryStamp, isGeometry3DCustomData, isGeometryCustomData, isLatexCustomData, isStampElement, latexStamp, pickSyncableAppState, restoreMissingStampFiles };
|
|
7958
|
+
export { DEFAULT_STAMPS, Whiteboard, findStampForCustomData, geometry3dStamp, geometryStamp, graph2dStamp, isGeometry3DCustomData, isGeometryCustomData, isGraph2DCustomData, isLatexCustomData, isStampElement, latexStamp, pickSyncableAppState, restoreMissingStampFiles };
|
|
5485
7959
|
//# sourceMappingURL=index.mjs.map
|
|
5486
7960
|
//# sourceMappingURL=index.mjs.map
|