@xom11/whiteboard 0.6.1 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ExcalidrawWithMenus-KBLDWPM2.mjs → ExcalidrawWithMenus-EAVPOPJZ.mjs} +3 -2
- package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +11 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/index.css +249 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.js +3042 -549
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3035 -550
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import './index.css';
|
|
3
|
+
import { __require } from './chunk-BJTO5JO5.mjs';
|
|
2
4
|
import dynamic from 'next/dynamic';
|
|
3
|
-
import { forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useId } from 'react';
|
|
5
|
+
import { forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useId, useLayoutEffect } from 'react';
|
|
4
6
|
import { createPortal } from 'react-dom';
|
|
5
7
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import JXG from 'jsxgraph';
|
|
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();
|
|
@@ -637,9 +652,9 @@ function handleMove(ctx, e) {
|
|
|
637
652
|
if (!ctx.boardRef.current || !ctx.phantomRef.current) return;
|
|
638
653
|
try {
|
|
639
654
|
const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
|
|
640
|
-
const
|
|
641
|
-
if (!
|
|
642
|
-
ctx.phantomRef.current.setPositionDirectly(
|
|
655
|
+
const JXG = ctx.jxgRef.current;
|
|
656
|
+
if (!JXG) return;
|
|
657
|
+
ctx.phantomRef.current.setPositionDirectly(JXG.COORDS_BY_USER, [coords[0], coords[1]]);
|
|
643
658
|
ctx.boardRef.current.update();
|
|
644
659
|
} catch {
|
|
645
660
|
}
|
|
@@ -1446,11 +1461,11 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1446
1461
|
if (typeof window === "undefined" || !containerRef.current) return;
|
|
1447
1462
|
let cancelled = false;
|
|
1448
1463
|
(async () => {
|
|
1449
|
-
const
|
|
1464
|
+
const JXG = (await import('jsxgraph')).default;
|
|
1450
1465
|
if (cancelled || !containerRef.current) return;
|
|
1451
|
-
jxgRef.current =
|
|
1466
|
+
jxgRef.current = JXG;
|
|
1452
1467
|
try {
|
|
1453
|
-
const opts =
|
|
1468
|
+
const opts = JXG.Options;
|
|
1454
1469
|
if (opts) {
|
|
1455
1470
|
opts.text = opts.text || {};
|
|
1456
1471
|
opts.text.display = "internal";
|
|
@@ -1464,7 +1479,7 @@ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
|
|
|
1464
1479
|
}
|
|
1465
1480
|
} catch {
|
|
1466
1481
|
}
|
|
1467
|
-
const board =
|
|
1482
|
+
const board = JXG.JSXGraph.initBoard(containerId, {
|
|
1468
1483
|
boundingbox: initialState?.bbox ?? [-10, 10, 10, -10],
|
|
1469
1484
|
axis: false,
|
|
1470
1485
|
// We manage axis manually via toggle for clean default
|
|
@@ -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 = {}) {
|
|
@@ -2096,9 +2420,9 @@ function renderGeometryToSvg(boardContainer) {
|
|
|
2096
2420
|
async function renderGeometrySvgFromState(jsonState) {
|
|
2097
2421
|
const parsed = JSON.parse(jsonState);
|
|
2098
2422
|
const palette = paletteFor(false);
|
|
2099
|
-
const
|
|
2423
|
+
const JXG = (await import('jsxgraph')).default;
|
|
2100
2424
|
try {
|
|
2101
|
-
const opts =
|
|
2425
|
+
const opts = JXG.Options;
|
|
2102
2426
|
if (opts) {
|
|
2103
2427
|
opts.text = opts.text || {};
|
|
2104
2428
|
opts.text.display = "internal";
|
|
@@ -2123,7 +2447,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
2123
2447
|
document.body.appendChild(container);
|
|
2124
2448
|
let board = null;
|
|
2125
2449
|
try {
|
|
2126
|
-
board =
|
|
2450
|
+
board = JXG.JSXGraph.initBoard(containerId, {
|
|
2127
2451
|
boundingbox: parsed.bbox,
|
|
2128
2452
|
axis: !!parsed.showAxis,
|
|
2129
2453
|
grid: !!parsed.showGrid,
|
|
@@ -2136,7 +2460,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
2136
2460
|
return renderGeometryToSvg(container);
|
|
2137
2461
|
} finally {
|
|
2138
2462
|
try {
|
|
2139
|
-
if (board)
|
|
2463
|
+
if (board) JXG.JSXGraph.freeBoard(board);
|
|
2140
2464
|
} catch {
|
|
2141
2465
|
}
|
|
2142
2466
|
if (container.parentNode) container.parentNode.removeChild(container);
|
|
@@ -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
|
] });
|
|
@@ -3808,126 +4375,134 @@ var MiniBoard3D = forwardRef(function MiniBoard3D2({ isDark, initialState }, ref
|
|
|
3808
4375
|
useEffect(() => {
|
|
3809
4376
|
const div = containerRef.current;
|
|
3810
4377
|
if (!div) return;
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
4378
|
+
let cancelled = false;
|
|
4379
|
+
let JXG = null;
|
|
4380
|
+
let board = null;
|
|
4381
|
+
void (async () => {
|
|
4382
|
+
JXG = (await import('jsxgraph')).default;
|
|
4383
|
+
if (cancelled || !containerRef.current) return;
|
|
4384
|
+
JXG.Options.text.display = "internal";
|
|
4385
|
+
board = JXG.JSXGraph.initBoard(div, {
|
|
4386
|
+
boundingbox: [-6, 6, 6, -6],
|
|
4387
|
+
axis: false,
|
|
4388
|
+
showCopyright: false,
|
|
4389
|
+
showNavigation: false,
|
|
4390
|
+
renderer: "svg"
|
|
4391
|
+
});
|
|
4392
|
+
boardRef.current = board;
|
|
4393
|
+
const initView = initialState?.view ?? DEFAULT_VIEW3D;
|
|
4394
|
+
const baseAttrs = VIEW3D_ATTRS(isDark);
|
|
4395
|
+
const view = board.create(
|
|
4396
|
+
"view3d",
|
|
3827
4397
|
[
|
|
3828
|
-
[
|
|
3829
|
-
[
|
|
3830
|
-
[
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
view
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
4398
|
+
[-5, -5],
|
|
4399
|
+
[10, 10],
|
|
4400
|
+
[
|
|
4401
|
+
[initView.bbox3D[0], initView.bbox3D[3]],
|
|
4402
|
+
[initView.bbox3D[1], initView.bbox3D[4]],
|
|
4403
|
+
[initView.bbox3D[2], initView.bbox3D[5]]
|
|
4404
|
+
]
|
|
4405
|
+
],
|
|
4406
|
+
{
|
|
4407
|
+
...baseAttrs,
|
|
4408
|
+
az: { ...baseAttrs.az, value: initView.azimuth },
|
|
4409
|
+
el: { ...baseAttrs.el, value: initView.elevation }
|
|
4410
|
+
}
|
|
4411
|
+
);
|
|
4412
|
+
viewRef.current = view;
|
|
4413
|
+
let idCounter = 1;
|
|
4414
|
+
const ctx = createHandlerContext({
|
|
4415
|
+
view,
|
|
4416
|
+
pushLog: (e) => {
|
|
4417
|
+
logRef.current.push(e);
|
|
4418
|
+
notify();
|
|
4419
|
+
},
|
|
4420
|
+
objMap: objMapRef.current,
|
|
4421
|
+
nextId: () => `obj_${Date.now().toString(36)}_${(idCounter++).toString(36)}`,
|
|
4422
|
+
isDark,
|
|
4423
|
+
promptCoords: (label) => {
|
|
4424
|
+
const raw = window.prompt(`${label}
|
|
3852
4425
|
(\u0111\u1ECBnh d\u1EA1ng "x,y,z")`, "0,0,0");
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
}
|
|
3887
|
-
return void 0;
|
|
3888
|
-
}
|
|
3889
|
-
const handlePointerDown = (e) => {
|
|
3890
|
-
const tool = toolRef.current;
|
|
3891
|
-
if (tool === "move") return;
|
|
3892
|
-
const existingPointId = findExistingPointAt(e.clientX, e.clientY);
|
|
3893
|
-
let x3 = 0;
|
|
3894
|
-
let y3 = 0;
|
|
3895
|
-
const z3 = 0;
|
|
3896
|
-
try {
|
|
3897
|
-
const board2d = boardRef.current;
|
|
3898
|
-
if (board2d?.getUsrCoordsOfMouse) {
|
|
3899
|
-
const uc = board2d.getUsrCoordsOfMouse(e);
|
|
3900
|
-
if (Array.isArray(uc) && uc.length >= 2) {
|
|
3901
|
-
x3 = uc[0];
|
|
3902
|
-
y3 = uc[1];
|
|
3903
|
-
}
|
|
4426
|
+
if (!raw) return null;
|
|
4427
|
+
const parts = raw.split(",").map((s) => Number(s.trim()));
|
|
4428
|
+
if (parts.length !== 3 || parts.some((n) => !isFinite(n))) return null;
|
|
4429
|
+
return { x: parts[0], y: parts[1], z: parts[2] };
|
|
4430
|
+
},
|
|
4431
|
+
promptNumber: (label) => {
|
|
4432
|
+
const raw = window.prompt(label, "1");
|
|
4433
|
+
if (raw == null) return null;
|
|
4434
|
+
const n = Number(raw);
|
|
4435
|
+
return isFinite(n) ? n : null;
|
|
4436
|
+
},
|
|
4437
|
+
promptText: (label) => {
|
|
4438
|
+
const raw = window.prompt(label, "");
|
|
4439
|
+
return raw == null ? null : raw;
|
|
4440
|
+
},
|
|
4441
|
+
notify
|
|
4442
|
+
});
|
|
4443
|
+
ctxRef.current = ctx;
|
|
4444
|
+
function findExistingPointAt(clientX, clientY) {
|
|
4445
|
+
const containerRect = div.getBoundingClientRect();
|
|
4446
|
+
const localX = clientX - containerRect.left;
|
|
4447
|
+
const localY = clientY - containerRect.top;
|
|
4448
|
+
const PICK = 18;
|
|
4449
|
+
const svg = div.querySelector("svg");
|
|
4450
|
+
if (!svg) return void 0;
|
|
4451
|
+
for (const [id, obj] of objMapRef.current) {
|
|
4452
|
+
const entry = obj;
|
|
4453
|
+
if (entry?.elType !== "point3d") continue;
|
|
4454
|
+
const sc = entry.element2D?.coords?.scrCoords;
|
|
4455
|
+
if (!sc || sc.length < 3) continue;
|
|
4456
|
+
const dx = sc[1] - localX;
|
|
4457
|
+
const dy = sc[2] - localY;
|
|
4458
|
+
if (dx * dx + dy * dy <= PICK * PICK) return id;
|
|
3904
4459
|
}
|
|
3905
|
-
|
|
4460
|
+
return void 0;
|
|
3906
4461
|
}
|
|
3907
|
-
const
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
|
-
|
|
4462
|
+
const handlePointerDown = (e) => {
|
|
4463
|
+
const tool = toolRef.current;
|
|
4464
|
+
if (tool === "move") return;
|
|
4465
|
+
const existingPointId = findExistingPointAt(e.clientX, e.clientY);
|
|
4466
|
+
let x3 = 0;
|
|
4467
|
+
let y3 = 0;
|
|
4468
|
+
const z3 = 0;
|
|
4469
|
+
try {
|
|
4470
|
+
const board2d = boardRef.current;
|
|
4471
|
+
if (board2d?.getUsrCoordsOfMouse) {
|
|
4472
|
+
const uc = board2d.getUsrCoordsOfMouse(e);
|
|
4473
|
+
if (Array.isArray(uc) && uc.length >= 2) {
|
|
4474
|
+
x3 = uc[0];
|
|
4475
|
+
y3 = uc[1];
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
} catch {
|
|
4479
|
+
}
|
|
4480
|
+
const hit = { x3, y3, z3, existingPointId };
|
|
4481
|
+
handleToolStep(ctx, tool, hit);
|
|
4482
|
+
};
|
|
4483
|
+
const svgEl = div.querySelector("svg");
|
|
4484
|
+
const targetEl = svgEl ?? div;
|
|
4485
|
+
const handlePointerDownEv = (e) => handlePointerDown(e);
|
|
4486
|
+
targetEl.addEventListener("pointerdown", handlePointerDownEv);
|
|
4487
|
+
pointerHandlerRef.current = { el: targetEl, fn: handlePointerDownEv };
|
|
4488
|
+
if (initialState?.elements?.length) {
|
|
4489
|
+
const map = objMapRef.current;
|
|
4490
|
+
for (const el of initialState.elements) {
|
|
4491
|
+
const parents = el.parents.map(
|
|
4492
|
+
(p2) => typeof p2 === "string" && p2.startsWith("@id:") ? map.get(p2.slice(4)) : p2
|
|
4493
|
+
);
|
|
4494
|
+
const obj = view.create(el.type, parents, {
|
|
4495
|
+
...el.attributes,
|
|
4496
|
+
id: el.id,
|
|
4497
|
+
name: el.label
|
|
4498
|
+
});
|
|
4499
|
+
map.set(el.id, obj);
|
|
4500
|
+
logRef.current.push(el);
|
|
4501
|
+
}
|
|
3928
4502
|
}
|
|
3929
|
-
}
|
|
4503
|
+
})();
|
|
3930
4504
|
return () => {
|
|
4505
|
+
cancelled = true;
|
|
3931
4506
|
if (pointerHandlerRef.current) {
|
|
3932
4507
|
pointerHandlerRef.current.el.removeEventListener(
|
|
3933
4508
|
"pointerdown",
|
|
@@ -3936,7 +4511,7 @@ var MiniBoard3D = forwardRef(function MiniBoard3D2({ isDark, initialState }, ref
|
|
|
3936
4511
|
pointerHandlerRef.current = null;
|
|
3937
4512
|
}
|
|
3938
4513
|
try {
|
|
3939
|
-
JXG.JSXGraph.freeBoard(board);
|
|
4514
|
+
if (board && JXG) JXG.JSXGraph.freeBoard(board);
|
|
3940
4515
|
} catch {
|
|
3941
4516
|
}
|
|
3942
4517
|
boardRef.current = null;
|
|
@@ -3953,7 +4528,25 @@ var MiniBoard3D = forwardRef(function MiniBoard3D2({ isDark, initialState }, ref
|
|
|
3953
4528
|
toolRef.current = t;
|
|
3954
4529
|
notify();
|
|
3955
4530
|
},
|
|
3956
|
-
|
|
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
|
+
}),
|
|
3957
4550
|
pushLog: (e) => {
|
|
3958
4551
|
logRef.current.push(e);
|
|
3959
4552
|
notify();
|
|
@@ -4029,6 +4622,138 @@ var MiniBoard3D = forwardRef(function MiniBoard3D2({ isDark, initialState }, ref
|
|
|
4029
4622
|
}
|
|
4030
4623
|
);
|
|
4031
4624
|
});
|
|
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
|
+
});
|
|
4032
4757
|
|
|
4033
4758
|
// src/stamps/geometry-3d/editor/tools.ts
|
|
4034
4759
|
var GROUP_LABELS_3D = {
|
|
@@ -4038,6 +4763,18 @@ var GROUP_LABELS_3D = {
|
|
|
4038
4763
|
curved: "Kh\u1ED1i cong",
|
|
4039
4764
|
meta: "Kh\xE1c"
|
|
4040
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
|
+
}
|
|
4041
4778
|
var TOOLS_3D = [
|
|
4042
4779
|
{ key: "move", label: "Di chuy\u1EC3n", group: "view", stepsRequired: 0 },
|
|
4043
4780
|
{ key: "point", label: "\u0110i\u1EC3m", group: "primitive", stepsRequired: 1, hint: "Nh\u1EADp (x, y, z)" },
|
|
@@ -4091,8 +4828,8 @@ var TOOLS_3D = [
|
|
|
4091
4828
|
},
|
|
4092
4829
|
{ key: "label", label: "Nh\xE3n", group: "meta", stepsRequired: 1, hint: "G\u1EAFn v\xE0o \u0111i\u1EC3m" }
|
|
4093
4830
|
];
|
|
4094
|
-
function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
4095
|
-
return /* @__PURE__ */
|
|
4831
|
+
function ToolButton({ toolKey, label, hint, active, onClick, icon, badge }) {
|
|
4832
|
+
return /* @__PURE__ */ jsxs(
|
|
4096
4833
|
"button",
|
|
4097
4834
|
{
|
|
4098
4835
|
type: "button",
|
|
@@ -4103,10 +4840,13 @@ function ToolButton({ toolKey, label, hint, active, onClick, icon }) {
|
|
|
4103
4840
|
"data-active": active || void 0,
|
|
4104
4841
|
"data-tool": toolKey,
|
|
4105
4842
|
className: [
|
|
4106
|
-
"flex h-8 items-center justify-center rounded-md transition",
|
|
4843
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
4107
4844
|
active ? "bg-blue-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
4108
4845
|
].join(" "),
|
|
4109
|
-
children:
|
|
4846
|
+
children: [
|
|
4847
|
+
icon,
|
|
4848
|
+
badge
|
|
4849
|
+
]
|
|
4110
4850
|
}
|
|
4111
4851
|
);
|
|
4112
4852
|
}
|
|
@@ -4158,7 +4898,10 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4158
4898
|
"aria-label": title,
|
|
4159
4899
|
"data-testid": "geom3d-left-panel",
|
|
4160
4900
|
"data-stamp-area": "true",
|
|
4161
|
-
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(""),
|
|
4162
4905
|
children: [
|
|
4163
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: [
|
|
4164
4907
|
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
@@ -4171,10 +4914,7 @@ function Shell3({ title, icon, onClose, children, isDark }) {
|
|
|
4171
4914
|
onClick: onClose,
|
|
4172
4915
|
"aria-label": "\u0110\xF3ng",
|
|
4173
4916
|
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
4174
|
-
children: /* @__PURE__ */
|
|
4175
|
-
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
4176
|
-
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
4177
|
-
] })
|
|
4917
|
+
children: /* @__PURE__ */ jsx(CloseIcon2, {})
|
|
4178
4918
|
}
|
|
4179
4919
|
)
|
|
4180
4920
|
] }),
|
|
@@ -4190,11 +4930,39 @@ function Section3({ label, children }) {
|
|
|
4190
4930
|
] });
|
|
4191
4931
|
}
|
|
4192
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" }) });
|
|
4193
|
-
function
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
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() {
|
|
4198
4966
|
const [hover, setHover] = useState(null);
|
|
4199
4967
|
const [portalReady, setPortalReady] = useState(false);
|
|
4200
4968
|
const hoverTimerRef = useRef(null);
|
|
@@ -4204,17 +4972,6 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4204
4972
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4205
4973
|
};
|
|
4206
4974
|
}, []);
|
|
4207
|
-
useEffect(() => {
|
|
4208
|
-
if (!handle) return;
|
|
4209
|
-
const sync = () => {
|
|
4210
|
-
setTool(handle.getTool());
|
|
4211
|
-
setShowAxes(handle.getShowAxes());
|
|
4212
|
-
setShowMesh(handle.getShowMesh());
|
|
4213
|
-
setCanUndo(handle.canUndo());
|
|
4214
|
-
};
|
|
4215
|
-
sync();
|
|
4216
|
-
return handle.subscribe(sync);
|
|
4217
|
-
}, [handle]);
|
|
4218
4975
|
const showHover = useCallback((el, t) => {
|
|
4219
4976
|
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
4220
4977
|
hoverTimerRef.current = setTimeout(() => {
|
|
@@ -4229,14 +4986,45 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4229
4986
|
}
|
|
4230
4987
|
setHover(null);
|
|
4231
4988
|
}, []);
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
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]
|
|
4239
5026
|
);
|
|
5027
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
4240
5028
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4241
5029
|
/* @__PURE__ */ jsxs(Shell3, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
|
|
4242
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: [
|
|
@@ -4272,10 +5060,7 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4272
5060
|
title: "Reset g\xF3c nh\xECn",
|
|
4273
5061
|
"aria-label": "Reset view",
|
|
4274
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",
|
|
4275
|
-
children: /* @__PURE__ */
|
|
4276
|
-
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
4277
|
-
/* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })
|
|
4278
|
-
] })
|
|
5063
|
+
children: /* @__PURE__ */ jsx(ResetViewIcon, {})
|
|
4279
5064
|
}
|
|
4280
5065
|
),
|
|
4281
5066
|
/* @__PURE__ */ jsx(
|
|
@@ -4287,34 +5072,95 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4287
5072
|
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
4288
5073
|
"aria-label": "Ho\xE0n t\xE1c",
|
|
4289
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",
|
|
4290
|
-
children: /* @__PURE__ */
|
|
4291
|
-
/* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
4292
|
-
/* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
4293
|
-
] })
|
|
5075
|
+
children: /* @__PURE__ */ jsx(UndoIcon2, {})
|
|
4294
5076
|
}
|
|
4295
5077
|
)
|
|
4296
5078
|
] }) }),
|
|
4297
|
-
|
|
4298
|
-
|
|
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",
|
|
4299
5150
|
{
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
}
|
|
4314
|
-
)
|
|
4315
|
-
},
|
|
4316
|
-
t.key
|
|
4317
|
-
)) }) }, 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
|
+
)
|
|
4318
5164
|
] }),
|
|
4319
5165
|
portalReady && hover && typeof document !== "undefined" ? createPortal(
|
|
4320
5166
|
/* @__PURE__ */ jsxs(
|
|
@@ -4338,107 +5184,71 @@ function LeftPanel3({ handle, onResetView, onClose, isDark }) {
|
|
|
4338
5184
|
) : null
|
|
4339
5185
|
] });
|
|
4340
5186
|
}
|
|
4341
|
-
|
|
4342
|
-
const
|
|
4343
|
-
const
|
|
4344
|
-
const
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
() => ({
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
if (log.length === 0) return false;
|
|
4356
|
-
const view = board.getViewState();
|
|
4357
|
-
const state = {
|
|
4358
|
-
version: 1,
|
|
4359
|
-
bbox: board.getBbox(),
|
|
4360
|
-
view,
|
|
4361
|
-
showAxes: board.getShowAxes(),
|
|
4362
|
-
showMesh: board.getShowMesh(),
|
|
4363
|
-
elements: log
|
|
4364
|
-
};
|
|
4365
|
-
const snap = board.snapshotSVG();
|
|
4366
|
-
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
4367
|
-
return true;
|
|
4368
|
-
},
|
|
4369
|
-
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
4370
|
-
}),
|
|
4371
|
-
[onInsert]
|
|
4372
|
-
);
|
|
4373
|
-
const handleResetView = useCallback(() => {
|
|
4374
|
-
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
|
+
}));
|
|
4375
5201
|
}, []);
|
|
4376
|
-
return /* @__PURE__ */
|
|
4377
|
-
|
|
5202
|
+
return /* @__PURE__ */ jsx(
|
|
5203
|
+
MobileToolDrawer,
|
|
4378
5204
|
{
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
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
|
-
onClose,
|
|
4420
|
-
isDark
|
|
4421
|
-
}
|
|
4422
|
-
),
|
|
4423
|
-
/* @__PURE__ */ jsx(
|
|
4424
|
-
"div",
|
|
4425
|
-
{
|
|
4426
|
-
style: {
|
|
4427
|
-
position: "absolute",
|
|
4428
|
-
left: 120,
|
|
4429
|
-
top: 0,
|
|
4430
|
-
right: 0,
|
|
4431
|
-
bottom: 0,
|
|
4432
|
-
overflow: "hidden"
|
|
4433
|
-
},
|
|
4434
|
-
children: /* @__PURE__ */ jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial })
|
|
4435
|
-
}
|
|
4436
|
-
)
|
|
4437
|
-
] })
|
|
4438
|
-
]
|
|
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)
|
|
4439
5245
|
}
|
|
4440
5246
|
);
|
|
4441
|
-
}
|
|
5247
|
+
}
|
|
5248
|
+
function LeftPanel3(props) {
|
|
5249
|
+
if (props.isMobile) return /* @__PURE__ */ jsx(MobilePanel, { ...props });
|
|
5250
|
+
return /* @__PURE__ */ jsx(DesktopPanel, { ...props });
|
|
5251
|
+
}
|
|
4442
5252
|
|
|
4443
5253
|
// src/stamps/geometry-3d/serialize.ts
|
|
4444
5254
|
function isGeometry3DCustomData(data) {
|
|
@@ -4460,10 +5270,13 @@ function parseSerializedBoard3D(json) {
|
|
|
4460
5270
|
}
|
|
4461
5271
|
return parsed;
|
|
4462
5272
|
}
|
|
5273
|
+
|
|
5274
|
+
// src/stamps/geometry-3d/render.ts
|
|
4463
5275
|
var OUTPUT_WIDTH = 1024;
|
|
4464
5276
|
var OUTPUT_HEIGHT = 768;
|
|
4465
5277
|
async function renderGeometry3DSvgFromState(jsonState) {
|
|
4466
5278
|
const state = parseSerializedBoard3D(jsonState);
|
|
5279
|
+
const JXG = (await import('jsxgraph')).default;
|
|
4467
5280
|
const div = document.createElement("div");
|
|
4468
5281
|
div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
|
|
4469
5282
|
document.body.appendChild(div);
|
|
@@ -4520,66 +5333,1528 @@ async function renderGeometry3DSvgFromState(jsonState) {
|
|
|
4520
5333
|
JXG.JSXGraph.freeBoard(board);
|
|
4521
5334
|
} catch {
|
|
4522
5335
|
}
|
|
4523
|
-
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
4524
|
-
} finally {
|
|
4525
|
-
document.body.removeChild(div);
|
|
4526
|
-
}
|
|
4527
|
-
}
|
|
4528
|
-
function parseInitial(editingElement) {
|
|
4529
|
-
if (!editingElement) return null;
|
|
4530
|
-
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
4531
|
-
try {
|
|
4532
|
-
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
4533
|
-
} catch {
|
|
4534
|
-
return null;
|
|
4535
|
-
}
|
|
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";
|
|
4536
6754
|
}
|
|
4537
|
-
var
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
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
|
|
4543
6774
|
);
|
|
6775
|
+
const [errorsSnapshot, setErrorsSnapshot] = useState({});
|
|
4544
6776
|
const handleInsert = useCallback(
|
|
4545
|
-
async (jsonState, svgString
|
|
6777
|
+
async (jsonState, svgString) => {
|
|
4546
6778
|
if (!api) return;
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
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
|
+
}
|
|
4558
6794
|
onClose();
|
|
4559
6795
|
},
|
|
4560
|
-
[api, editingElement, onClose]
|
|
6796
|
+
[api, editingElement?.id, onClose]
|
|
4561
6797
|
);
|
|
4562
6798
|
useImperativeHandle(
|
|
4563
6799
|
ref,
|
|
4564
6800
|
() => ({
|
|
4565
|
-
tryInsert: () =>
|
|
4566
|
-
hasContent: () =>
|
|
6801
|
+
tryInsert: () => panelRef.current?.insert() ?? false,
|
|
6802
|
+
hasContent: () => panelRef.current?.hasContent() ?? false
|
|
4567
6803
|
}),
|
|
4568
6804
|
[]
|
|
4569
6805
|
);
|
|
4570
|
-
return /* @__PURE__ */
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
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
|
+
] });
|
|
4580
6855
|
}
|
|
4581
6856
|
);
|
|
4582
|
-
var
|
|
6857
|
+
var Graph2DIcon = /* @__PURE__ */ jsxs(
|
|
4583
6858
|
"svg",
|
|
4584
6859
|
{
|
|
4585
6860
|
width: "20",
|
|
@@ -4592,47 +6867,45 @@ var Geometry3DIcon = /* @__PURE__ */ jsxs(
|
|
|
4592
6867
|
strokeLinejoin: "round",
|
|
4593
6868
|
"aria-hidden": "true",
|
|
4594
6869
|
children: [
|
|
4595
|
-
/* @__PURE__ */ jsx("path", { d: "
|
|
4596
|
-
/* @__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" })
|
|
4597
6873
|
]
|
|
4598
6874
|
}
|
|
4599
6875
|
);
|
|
4600
|
-
var
|
|
4601
|
-
kind: "
|
|
4602
|
-
shortcutKey: "
|
|
4603
|
-
toolbarLabel: "
|
|
4604
|
-
toolbarTitle: "
|
|
4605
|
-
toolbarIcon:
|
|
4606
|
-
toolbarTestId: "stamp-toolbar-
|
|
4607
|
-
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,
|
|
4608
6884
|
async renderSvgFromCustomData(data) {
|
|
4609
|
-
if (!
|
|
4610
|
-
throw new Error("
|
|
6885
|
+
if (!isGraph2DCustomData(data)) {
|
|
6886
|
+
throw new Error("graph2dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i graph2d");
|
|
4611
6887
|
}
|
|
4612
|
-
|
|
4613
|
-
return svgString;
|
|
6888
|
+
return renderGraph2dSvgFromState(data.jsonState);
|
|
4614
6889
|
},
|
|
4615
|
-
|
|
6890
|
+
async restoreFileFromCustomData(element) {
|
|
4616
6891
|
const data = element.customData;
|
|
4617
6892
|
const fileId = element.fileId;
|
|
4618
6893
|
if (!data || !fileId) return null;
|
|
4619
|
-
if (!
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
} catch {
|
|
4625
|
-
return null;
|
|
4626
|
-
}
|
|
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" };
|
|
4627
6899
|
},
|
|
4628
|
-
Host:
|
|
6900
|
+
Host: Graph2DStampHost
|
|
4629
6901
|
};
|
|
4630
6902
|
|
|
4631
6903
|
// src/stamps/shared/registry.ts
|
|
4632
6904
|
var DEFAULT_STAMPS = Object.freeze([
|
|
4633
6905
|
geometryStamp,
|
|
4634
6906
|
latexStamp,
|
|
4635
|
-
geometry3dStamp
|
|
6907
|
+
geometry3dStamp,
|
|
6908
|
+
graph2dStamp
|
|
4636
6909
|
]);
|
|
4637
6910
|
function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
4638
6911
|
for (const s of stamps) {
|
|
@@ -4643,36 +6916,90 @@ function findStampForCustomData(data, stamps = DEFAULT_STAMPS) {
|
|
|
4643
6916
|
function isStampElement(element, stamps = DEFAULT_STAMPS) {
|
|
4644
6917
|
return findStampForCustomData(element.customData, stamps) !== null;
|
|
4645
6918
|
}
|
|
4646
|
-
var
|
|
6919
|
+
var TOOLBAR_WRAPPER_ID = "stamp-toolbar-portal-wrapper";
|
|
6920
|
+
var MENU_WRAPPER_ID = "stamp-menu-portal-wrapper";
|
|
4647
6921
|
function ToolbarInjector({
|
|
4648
6922
|
enabled,
|
|
4649
6923
|
activeStampKind,
|
|
4650
6924
|
onToggle,
|
|
4651
6925
|
stamps = DEFAULT_STAMPS
|
|
4652
6926
|
}) {
|
|
4653
|
-
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);
|
|
4654
6933
|
useEffect(() => {
|
|
4655
6934
|
if (!enabled) {
|
|
4656
|
-
|
|
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();
|
|
4657
6979
|
return;
|
|
4658
6980
|
}
|
|
4659
6981
|
let cancelled = false;
|
|
4660
6982
|
let attempts = 0;
|
|
4661
6983
|
let observer = null;
|
|
4662
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
|
+
};
|
|
4663
6992
|
const tryMount = () => {
|
|
4664
6993
|
if (cancelled) return;
|
|
4665
6994
|
const container = document.querySelector(".excalidraw .App-toolbar .Stack_horizontal") ?? document.querySelector(".App-toolbar .Stack_horizontal");
|
|
4666
6995
|
if (!container) {
|
|
4667
|
-
if (attempts++ < 20)
|
|
4668
|
-
timer = setTimeout(tryMount, 100);
|
|
4669
|
-
}
|
|
6996
|
+
if (attempts++ < 20) timer = setTimeout(tryMount, 100);
|
|
4670
6997
|
return;
|
|
4671
6998
|
}
|
|
4672
|
-
let wrapper = container.querySelector("#" +
|
|
6999
|
+
let wrapper = container.querySelector("#" + TOOLBAR_WRAPPER_ID);
|
|
4673
7000
|
if (!wrapper) {
|
|
4674
7001
|
wrapper = document.createElement("div");
|
|
4675
|
-
wrapper.id =
|
|
7002
|
+
wrapper.id = TOOLBAR_WRAPPER_ID;
|
|
4676
7003
|
wrapper.className = "Stamp-toolbar-injector";
|
|
4677
7004
|
wrapper.setAttribute("data-stamp-area", "true");
|
|
4678
7005
|
wrapper.style.display = "inline-flex";
|
|
@@ -4688,13 +7015,13 @@ function ToolbarInjector({
|
|
|
4688
7015
|
container.appendChild(wrapper);
|
|
4689
7016
|
}
|
|
4690
7017
|
}
|
|
4691
|
-
|
|
7018
|
+
apply(wrapper);
|
|
4692
7019
|
};
|
|
4693
7020
|
tryMount();
|
|
4694
7021
|
const root = document.querySelector(".excalidraw") ?? document.body;
|
|
4695
7022
|
observer = new MutationObserver(() => {
|
|
4696
7023
|
if (cancelled) return;
|
|
4697
|
-
const stillThere = document.getElementById(
|
|
7024
|
+
const stillThere = document.getElementById(TOOLBAR_WRAPPER_ID);
|
|
4698
7025
|
if (!stillThere) {
|
|
4699
7026
|
attempts = 0;
|
|
4700
7027
|
tryMount();
|
|
@@ -4705,11 +7032,73 @@ function ToolbarInjector({
|
|
|
4705
7032
|
cancelled = true;
|
|
4706
7033
|
if (timer) clearTimeout(timer);
|
|
4707
7034
|
observer?.disconnect();
|
|
4708
|
-
document.getElementById(
|
|
7035
|
+
document.getElementById(TOOLBAR_WRAPPER_ID)?.remove();
|
|
4709
7036
|
};
|
|
4710
|
-
}, [enabled]);
|
|
4711
|
-
|
|
4712
|
-
|
|
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(
|
|
4713
7102
|
/* @__PURE__ */ jsx(Fragment, { children: stamps.map((stamp) => /* @__PURE__ */ jsx(
|
|
4714
7103
|
StampToolButton,
|
|
4715
7104
|
{
|
|
@@ -4722,8 +7111,42 @@ function ToolbarInjector({
|
|
|
4722
7111
|
},
|
|
4723
7112
|
stamp.kind
|
|
4724
7113
|
)) }),
|
|
4725
|
-
|
|
4726
|
-
);
|
|
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
|
+
] });
|
|
4727
7150
|
}
|
|
4728
7151
|
function StampToolButton({ icon, keybind, label, active, onClick, dataTestId }) {
|
|
4729
7152
|
return /* @__PURE__ */ jsxs(
|
|
@@ -4788,6 +7211,54 @@ function StampToolButton({ icon, keybind, label, active, onClick, dataTestId })
|
|
|
4788
7211
|
}
|
|
4789
7212
|
);
|
|
4790
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
|
+
}
|
|
4791
7262
|
function isEditableTarget(t) {
|
|
4792
7263
|
if (!t || !(t instanceof HTMLElement)) return false;
|
|
4793
7264
|
if (t.isContentEditable) return true;
|
|
@@ -5087,7 +7558,7 @@ async function pruneFiles(storageKey, keepIds) {
|
|
|
5087
7558
|
}
|
|
5088
7559
|
}
|
|
5089
7560
|
var Excalidraw = dynamic(
|
|
5090
|
-
async () => (await import('./ExcalidrawWithMenus-
|
|
7561
|
+
async () => (await import('./ExcalidrawWithMenus-EAVPOPJZ.mjs')).ExcalidrawWithMenus,
|
|
5091
7562
|
{
|
|
5092
7563
|
ssr: false,
|
|
5093
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" })
|
|
@@ -5105,7 +7576,9 @@ function Whiteboard({
|
|
|
5105
7576
|
stamps = DEFAULT_STAMPS
|
|
5106
7577
|
}) {
|
|
5107
7578
|
const [api, setApi] = useState(null);
|
|
7579
|
+
const apiRef = useRef(null);
|
|
5108
7580
|
const [isDarkTheme, setIsDarkTheme] = useState(false);
|
|
7581
|
+
const isDarkThemeRef = useRef(false);
|
|
5109
7582
|
const knownFileIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
5110
7583
|
const lastSceneHashRef = useRef("");
|
|
5111
7584
|
const sceneThrottleRef = useRef(null);
|
|
@@ -5166,7 +7639,10 @@ function Whiteboard({
|
|
|
5166
7639
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5167
7640
|
(elements, appState, files) => {
|
|
5168
7641
|
const nextDark = appState?.theme === "dark";
|
|
5169
|
-
|
|
7642
|
+
if (isDarkThemeRef.current !== nextDark) {
|
|
7643
|
+
isDarkThemeRef.current = nextDark;
|
|
7644
|
+
queueMicrotask(() => setIsDarkTheme(nextDark));
|
|
7645
|
+
}
|
|
5170
7646
|
if (readOnly) return;
|
|
5171
7647
|
latestSceneRef.current = { elements, appState };
|
|
5172
7648
|
const cropId = appState?.croppingElementId;
|
|
@@ -5176,12 +7652,17 @@ function Whiteboard({
|
|
|
5176
7652
|
const stamp = findStampForCustomData(el.customData, stamps);
|
|
5177
7653
|
if (stamp) {
|
|
5178
7654
|
handledCropIdRef.current = cropId;
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
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 });
|
|
5185
7666
|
});
|
|
5186
7667
|
return;
|
|
5187
7668
|
}
|
|
@@ -5432,8 +7913,12 @@ function Whiteboard({
|
|
|
5432
7913
|
Excalidraw,
|
|
5433
7914
|
{
|
|
5434
7915
|
excalidrawAPI: (a) => {
|
|
5435
|
-
|
|
5436
|
-
|
|
7916
|
+
if (apiRef.current === a) return;
|
|
7917
|
+
apiRef.current = a;
|
|
7918
|
+
queueMicrotask(() => {
|
|
7919
|
+
setApi(a);
|
|
7920
|
+
onApi?.(a);
|
|
7921
|
+
});
|
|
5437
7922
|
},
|
|
5438
7923
|
langCode,
|
|
5439
7924
|
viewModeEnabled: readOnly,
|
|
@@ -5470,6 +7955,6 @@ function Whiteboard({
|
|
|
5470
7955
|
] });
|
|
5471
7956
|
}
|
|
5472
7957
|
|
|
5473
|
-
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 };
|
|
5474
7959
|
//# sourceMappingURL=index.mjs.map
|
|
5475
7960
|
//# sourceMappingURL=index.mjs.map
|