@xom11/whiteboard 0.10.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-KBLDWPM2.mjs} +2 -3
- package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +1 -0
- package/dist/catalog.json +57 -0
- package/dist/{chunk-PWIMZIB6.mjs → chunk-2SKXRBGS.mjs} +7 -8
- package/dist/chunk-2SKXRBGS.mjs.map +1 -0
- package/dist/chunk-33PEN2WC.mjs +57 -0
- package/dist/chunk-33PEN2WC.mjs.map +1 -0
- package/dist/chunk-3KBL77M6.mjs +127 -0
- package/dist/chunk-3KBL77M6.mjs.map +1 -0
- package/dist/chunk-5UTGXHLJ.mjs +57 -0
- package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
- package/dist/chunk-6XUPIGVD.mjs +467 -0
- package/dist/chunk-6XUPIGVD.mjs.map +1 -0
- package/dist/chunk-7WG2KDRF.mjs +28 -0
- package/dist/chunk-7WG2KDRF.mjs.map +1 -0
- package/dist/chunk-FZY33J6Z.mjs +95 -0
- package/dist/chunk-FZY33J6Z.mjs.map +1 -0
- package/dist/chunk-HNQLZIEP.mjs +78 -0
- package/dist/chunk-HNQLZIEP.mjs.map +1 -0
- package/dist/chunk-NVJ7K3DK.mjs +29 -0
- package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
- package/dist/chunk-O4WIZFRQ.mjs +11 -0
- package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
- package/dist/{chunk-YVJP7NRG.mjs → chunk-O6QTYAKE.mjs} +7 -9
- package/dist/chunk-O6QTYAKE.mjs.map +1 -0
- package/dist/chunk-R5FL6S7L.mjs +22 -0
- package/dist/chunk-R5FL6S7L.mjs.map +1 -0
- package/dist/chunk-RBUILBX3.mjs +388 -0
- package/dist/chunk-RBUILBX3.mjs.map +1 -0
- package/dist/chunk-RD34F5PM.mjs +57 -0
- package/dist/chunk-RD34F5PM.mjs.map +1 -0
- package/dist/{chunk-7P7SQFOW.mjs → chunk-RXOFO64U.mjs} +3 -3
- package/dist/chunk-RXOFO64U.mjs.map +1 -0
- package/dist/chunk-TOOHCAWP.mjs +1167 -0
- package/dist/chunk-TOOHCAWP.mjs.map +1 -0
- package/dist/{chunk-C6SCVOMC.mjs → chunk-TQYQVXNW.mjs} +5 -41
- package/dist/chunk-TQYQVXNW.mjs.map +1 -0
- package/dist/chunk-VBJLUHCY.mjs +23 -0
- package/dist/chunk-VBJLUHCY.mjs.map +1 -0
- package/dist/chunk-VRWZILTG.mjs +205 -0
- package/dist/chunk-VRWZILTG.mjs.map +1 -0
- package/dist/chunk-XVSO7FBM.mjs +61 -0
- package/dist/chunk-XVSO7FBM.mjs.map +1 -0
- package/dist/geometry-2d.d.mts +3 -6
- package/dist/geometry-2d.d.ts +3 -6
- package/dist/geometry-2d.js +5069 -2651
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +8 -4
- package/dist/geometry-3d.d.mts +4 -7
- package/dist/geometry-3d.d.ts +4 -7
- package/dist/geometry-3d.js +3053 -2150
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +7 -4
- package/dist/graph-2d.d.mts +4 -7
- package/dist/graph-2d.d.ts +4 -7
- package/dist/graph-2d.js +3363 -1670
- package/dist/graph-2d.js.map +1 -1
- package/dist/graph-2d.mjs +10 -3
- package/dist/host-3N4E4KJH.mjs +1142 -0
- package/dist/host-3N4E4KJH.mjs.map +1 -0
- package/dist/{host-Z3TEJKZA.mjs → host-6SNSZ332.mjs} +4 -4
- package/dist/{host-Z3TEJKZA.mjs.map → host-6SNSZ332.mjs.map} +1 -1
- package/dist/host-EVJT3LIF.mjs +3198 -0
- package/dist/host-EVJT3LIF.mjs.map +1 -0
- package/dist/host-HN4X3TBC.mjs +2374 -0
- package/dist/host-HN4X3TBC.mjs.map +1 -0
- package/dist/index.css +4 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +675 -19
- package/dist/index.d.ts +675 -19
- package/dist/index.js +11764 -9417
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1492 -335
- package/dist/index.mjs.map +1 -1
- package/dist/latex.d.mts +3 -4
- package/dist/latex.d.ts +3 -4
- package/dist/latex.js +33 -18
- package/dist/latex.js.map +1 -1
- package/dist/latex.mjs +2 -3
- package/dist/render-OCVGDKK6.mjs +8 -0
- package/dist/render-OCVGDKK6.mjs.map +1 -0
- package/dist/serialize-GKN6OVPM.mjs +6 -0
- package/dist/serialize-GKN6OVPM.mjs.map +1 -0
- package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
- package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
- package/package.json +24 -5
- package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
- package/dist/chunk-74VEEZBV.mjs +0 -619
- package/dist/chunk-74VEEZBV.mjs.map +0 -1
- package/dist/chunk-7P7SQFOW.mjs.map +0 -1
- package/dist/chunk-BJTO5JO5.mjs +0 -11
- package/dist/chunk-BJTO5JO5.mjs.map +0 -1
- package/dist/chunk-C6SCVOMC.mjs.map +0 -1
- package/dist/chunk-D257NCQW.mjs +0 -58
- package/dist/chunk-D257NCQW.mjs.map +0 -1
- package/dist/chunk-G7FR3AIV.mjs +0 -193
- package/dist/chunk-G7FR3AIV.mjs.map +0 -1
- package/dist/chunk-HTBLO5JO.mjs +0 -41
- package/dist/chunk-HTBLO5JO.mjs.map +0 -1
- package/dist/chunk-PWIMZIB6.mjs.map +0 -1
- package/dist/chunk-SBDMF4NQ.mjs +0 -212
- package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
- package/dist/chunk-WQOABS6N.mjs +0 -197
- package/dist/chunk-WQOABS6N.mjs.map +0 -1
- package/dist/chunk-YVJP7NRG.mjs.map +0 -1
- package/dist/host-N6ACNJKI.mjs +0 -3226
- package/dist/host-N6ACNJKI.mjs.map +0 -1
- package/dist/host-NKGV6RF2.mjs +0 -1134
- package/dist/host-NKGV6RF2.mjs.map +0 -1
- package/dist/host-XVK7UCRE.mjs +0 -2908
- package/dist/host-XVK7UCRE.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -61,6 +61,55 @@ export function ClassroomBoard() {
|
|
|
61
61
|
}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### AI dựng hình học 2D (opt-in)
|
|
65
|
+
|
|
66
|
+
Textarea AI chỉ xuất hiện khi truyền `generateGeometryFigure`. Callback này chạy từ client nên phải gọi một server boundary của ứng dụng; không đưa `ANTHROPIC_API_KEY` vào component hoặc biến môi trường public.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
'use client';
|
|
70
|
+
|
|
71
|
+
import { Whiteboard, type GenerateGeometryFigure } from '@xom11/whiteboard';
|
|
72
|
+
|
|
73
|
+
const generateGeometryFigure: GenerateGeometryFigure = async (problem, { signal }) => {
|
|
74
|
+
const response = await fetch('/api/geometry/ai', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'content-type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ problem }),
|
|
78
|
+
signal,
|
|
79
|
+
});
|
|
80
|
+
return response.json();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function ClassroomBoard() {
|
|
84
|
+
return <Whiteboard generateGeometryFigure={generateGeometryFigure} />;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Ví dụ route phía server trong Next.js:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { generateFigure } from '@xom11/whiteboard';
|
|
92
|
+
|
|
93
|
+
export async function POST(request: Request) {
|
|
94
|
+
const { problem } = await request.json();
|
|
95
|
+
const result = await generateFigure(problem, {
|
|
96
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? '',
|
|
97
|
+
});
|
|
98
|
+
return Response.json(
|
|
99
|
+
result.ok
|
|
100
|
+
? { ok: true, state: result.state }
|
|
101
|
+
: { ok: false, message: result.message },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Trong repo package, chạy smoke/eval với API key chỉ ở local shell:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
ANTHROPIC_API_KEY=... npm run ai:smoke
|
|
110
|
+
ANTHROPIC_API_KEY=... npm run ai:eval -- --limit 5
|
|
111
|
+
```
|
|
112
|
+
|
|
64
113
|
## Migration to v0.8.0 (geometry-3d redesign)
|
|
65
114
|
|
|
66
115
|
`geometry3dStamp` được viết lại theo UX của GeoGebra 3D Calculator:
|
|
@@ -110,6 +159,24 @@ import { latexStamp } from '@xom11/whiteboard/latex';
|
|
|
110
159
|
|
|
111
160
|
`next` không còn là peer dependency. Whiteboard dùng `React.lazy + Suspense` thuần. Consumer cần Next.js App Router vẫn hoạt động (dist có sẵn `'use client'` directive).
|
|
112
161
|
|
|
162
|
+
## Extending — thêm stamp mới
|
|
163
|
+
|
|
164
|
+
Fork repo + viết stamp mới trong ~30 phút. Tham khảo:
|
|
165
|
+
|
|
166
|
+
- **Howto:** [`docs/superpowers/specs/add-new-stamp-howto.md`](./docs/superpowers/specs/add-new-stamp-howto.md) — 6 bước có sẵn lệnh.
|
|
167
|
+
- **Template:** [`examples/stamp-template/`](./examples/stamp-template/) — skeleton "color-swatch" stamp, copy + đổi `kind`.
|
|
168
|
+
- **Contract test:** mỗi stamp PHẢI pass `runStampContract` (xem [`src/stamps/shared/__tests__/stamp-contract.ts`](./src/stamps/shared/__tests__/stamp-contract.ts)) để đảm bảo `matchesCustomData` / `renderSvgFromCustomData` / roundtrip restore không break.
|
|
169
|
+
- **Catalog:** thêm entry vào [`src/stamps/shared/catalog.ts`](./src/stamps/shared/catalog.ts). Bundle size tự tính qua `scripts/build-catalog.mjs` khi `npm run build`.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { STAMP_CATALOG, findCatalogEntry } from '@xom11/whiteboard';
|
|
173
|
+
|
|
174
|
+
// Render admin UI từ catalog
|
|
175
|
+
STAMP_CATALOG.forEach((entry) => {
|
|
176
|
+
console.log(entry.id, entry.title, entry.bundleSize.js + 'KB gzip');
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
113
180
|
## Development
|
|
114
181
|
|
|
115
182
|
```bash
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import './chunk-BJTO5JO5.mjs';
|
|
3
2
|
import { Excalidraw, MainMenu, Footer, WelcomeScreen } from '@excalidraw/excalidraw';
|
|
4
3
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
4
|
|
|
@@ -19,5 +18,5 @@ function ExcalidrawWithMenus(props) {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export { ExcalidrawWithMenus };
|
|
22
|
-
//# sourceMappingURL=ExcalidrawWithMenus-
|
|
23
|
-
//# sourceMappingURL=ExcalidrawWithMenus-
|
|
21
|
+
//# sourceMappingURL=ExcalidrawWithMenus-KBLDWPM2.mjs.map
|
|
22
|
+
//# sourceMappingURL=ExcalidrawWithMenus-KBLDWPM2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ExcalidrawWithMenus.tsx"],"names":[],"mappings":";;;AAiBO,SAAS,oBAAoB,KAAA,EAAwB;AAC1D,EAAA,MAAM,EAAE,QAAA,EAAU,GAAG,IAAA,EAAK,GAAI,KAAA;AAC9B,EAAA,uBACE,IAAA,CAAC,UAAA,EAAA,EAAY,GAAG,IAAA,EAEd,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,QAAA,EAAA,EACC,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,QAAA,CAAS,YAAA,CAAa,SAAA,EAAtB,EAAgC,CAAA;AAAA,sBACjC,GAAA,CAAC,QAAA,CAAS,YAAA,CAAa,WAAA,EAAtB,EAAkC,CAAA;AAAA,sBACnC,GAAA,CAAC,QAAA,CAAS,YAAA,CAAa,WAAA,EAAtB,EAAkC,CAAA;AAAA,sBACnC,GAAA,CAAC,QAAA,CAAS,YAAA,CAAa,WAAA,EAAtB,EAAkC;AAAA,KAAA,EACrC,CAAA;AAAA,oBAEA,GAAA,CAAC,MAAA,EAAA,EACC,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EACR,CAAA;AAAA,oBAGA,GAAA,CAAC,aAAA,EAAA,EACC,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EACR,CAAA;AAAA,IACC;AAAA,GAAA,EACH,CAAA;AAEJ","file":"ExcalidrawWithMenus-KBLDWPM2.mjs","sourcesContent":["'use client';\n\n// Client-only wrapper around Excalidraw that lets us reach for static helpers\n// like `MainMenu.DefaultItems.*`. Whiteboard dynamic-imports this\n// file so SSR never evaluates @excalidraw/excalidraw, while inside this file we\n// can use plain static imports (the entire module loads on the client).\n\nimport React from 'react';\nimport {\n Excalidraw,\n MainMenu,\n Footer,\n WelcomeScreen,\n} from '@excalidraw/excalidraw';\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExcalidrawProps = any;\n\nexport function ExcalidrawWithMenus(props: ExcalidrawProps) {\n const { children, ...rest } = props;\n return (\n <Excalidraw {...rest}>\n {/* Replace default menu with curated items — no socials/help/branding */}\n <MainMenu>\n <MainMenu.DefaultItems.LoadScene />\n <MainMenu.DefaultItems.SaveAsImage />\n <MainMenu.DefaultItems.ClearCanvas />\n <MainMenu.DefaultItems.ToggleTheme />\n </MainMenu>\n {/* Footer slot with no content suppresses default \"Made with Excalidraw\" link */}\n <Footer>\n <span />\n </Footer>\n {/* WelcomeScreen slot (empty) prevents the default Excalidraw welcome panel\n * (which includes the Excalidraw logo and social links) from appearing. */}\n <WelcomeScreen>\n <span />\n </WelcomeScreen>\n {children}\n </Excalidraw>\n );\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedAt": "2026-05-26T06:17:07.060Z",
|
|
3
|
+
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "geometry",
|
|
6
|
+
"title": "Hình học 2D (JSXGraph)",
|
|
7
|
+
"version": 1,
|
|
8
|
+
"experimental": false,
|
|
9
|
+
"runtimeDeps": [
|
|
10
|
+
"jsxgraph"
|
|
11
|
+
],
|
|
12
|
+
"bundleSize": {
|
|
13
|
+
"js": 50.75,
|
|
14
|
+
"css": 0
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "latex",
|
|
19
|
+
"title": "Công thức LaTeX (KaTeX)",
|
|
20
|
+
"version": 1,
|
|
21
|
+
"experimental": false,
|
|
22
|
+
"runtimeDeps": [
|
|
23
|
+
"katex"
|
|
24
|
+
],
|
|
25
|
+
"bundleSize": {
|
|
26
|
+
"js": 8.93,
|
|
27
|
+
"css": 0
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "geometry3d",
|
|
32
|
+
"title": "Hình học 3D (JSXGraph view3d)",
|
|
33
|
+
"version": 2,
|
|
34
|
+
"experimental": true,
|
|
35
|
+
"runtimeDeps": [
|
|
36
|
+
"jsxgraph"
|
|
37
|
+
],
|
|
38
|
+
"bundleSize": {
|
|
39
|
+
"js": 40.03,
|
|
40
|
+
"css": 0
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "graph2d",
|
|
45
|
+
"title": "Đồ thị hàm số 2D (JSXGraph)",
|
|
46
|
+
"version": 2,
|
|
47
|
+
"experimental": true,
|
|
48
|
+
"runtimeDeps": [
|
|
49
|
+
"jsxgraph"
|
|
50
|
+
],
|
|
51
|
+
"bundleSize": {
|
|
52
|
+
"js": 33.27,
|
|
53
|
+
"css": 0
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { isGeometry3DCustomData, renderGeometry3DSvgFromState } from './chunk-
|
|
2
|
+
import { isGeometry3DCustomData, renderGeometry3DSvgFromState } from './chunk-RBUILBX3.mjs';
|
|
3
|
+
import { svgToStampFile } from './chunk-5UTGXHLJ.mjs';
|
|
3
4
|
import { lazy } from 'react';
|
|
4
5
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
6
|
|
|
6
7
|
var Geometry3DStampHost = lazy(
|
|
7
|
-
() => import('./host-
|
|
8
|
+
() => import('./host-HN4X3TBC.mjs').then((m) => ({ default: m.Geometry3DStampHost }))
|
|
8
9
|
);
|
|
9
10
|
var Geometry3DIcon = /* @__PURE__ */ jsxs(
|
|
10
11
|
"svg",
|
|
@@ -44,12 +45,10 @@ var geometry3dStamp = {
|
|
|
44
45
|
restoreFileFromCustomData: async (element) => {
|
|
45
46
|
const data = element.customData;
|
|
46
47
|
const fileId = element.fileId;
|
|
47
|
-
if (!data || !fileId) return null;
|
|
48
|
-
if (!isGeometry3DCustomData(data)) return null;
|
|
48
|
+
if (!data || !fileId || !isGeometry3DCustomData(data)) return null;
|
|
49
49
|
try {
|
|
50
50
|
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
51
|
-
|
|
52
|
-
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
51
|
+
return svgToStampFile(svgString, fileId);
|
|
53
52
|
} catch {
|
|
54
53
|
return null;
|
|
55
54
|
}
|
|
@@ -58,5 +57,5 @@ var geometry3dStamp = {
|
|
|
58
57
|
};
|
|
59
58
|
|
|
60
59
|
export { geometry3dStamp };
|
|
61
|
-
//# sourceMappingURL=chunk-
|
|
62
|
-
//# sourceMappingURL=chunk-
|
|
60
|
+
//# sourceMappingURL=chunk-2SKXRBGS.mjs.map
|
|
61
|
+
//# sourceMappingURL=chunk-2SKXRBGS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/geometry-3d/index.tsx"],"names":[],"mappings":";;;;;AAgBA,IAAM,mBAAA,GAAsB,IAAA;AAAA,EAAK,MAC/B,OAAO,qBAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,MAAO,EAAE,OAAA,EAAS,CAAA,CAAE,mBAAA,EAAoB,CAAE;AACnE,CAAA;AAEA,IAAM,cAAA,mBACJ,IAAA;AAAA,EAAC,KAAA;AAAA,EAAA;AAAA,IACC,KAAA,EAAM,IAAA;AAAA,IACN,MAAA,EAAO,IAAA;AAAA,IACP,OAAA,EAAQ,WAAA;AAAA,IACR,IAAA,EAAK,MAAA;AAAA,IACL,MAAA,EAAO,cAAA;AAAA,IACP,WAAA,EAAY,KAAA;AAAA,IACZ,aAAA,EAAc,OAAA;AAAA,IACd,cAAA,EAAe,OAAA;AAAA,IACf,aAAA,EAAY,MAAA;AAAA,IAGZ,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,GAAE,2BAAA,EAA4B,CAAA;AAAA,sBAEpC,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,0BAAA,EAA2B,CAAA;AAAA,sBAEnC,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,6BAAA,EAA8B;AAAA;AAAA;AACxC,CAAA;AAGK,IAAM,eAAA,GAAmD;AAAA,EAC9D,IAAA,EAAM,YAAA;AAAA,EACN,YAAA,EAAc,IAAA;AAAA,EACd,WAAA,EAAa,GAAA;AAAA,EACb,YAAA,EAAc,GAAA;AAAA,EACd,YAAA,EAAc,gBAAA;AAAA,EACd,WAAA,EAAa,cAAA;AAAA,EACb,aAAA,EAAe,0BAAA;AAAA,EACf,iBAAA,EAAmB,sBAAA;AAAA,EACnB,MAAM,wBAAwB,IAAA,EAAgC;AAC5D,IAAA,IAAI,CAAC,sBAAA,CAAuB,IAAI,CAAA,EAAG;AACjC,MAAA,MAAM,IAAI,MAAM,mFAA2E,CAAA;AAAA,IAC7F;AACA,IAAA,MAAM,EAAE,SAAA,EAAU,GAAI,MAAM,4BAAA,CAA6B,KAAK,SAAS,CAAA;AACvE,IAAA,OAAO,SAAA;AAAA,EACT,CAAA;AAAA,EACA,yBAAA,EAA2B,OAAO,OAAA,KAA+C;AAC/E,IAAA,MAAM,OAAO,OAAA,CAAQ,UAAA;AACrB,IAAA,MAAM,SAAU,OAAA,CAAuC,MAAA;AACvD,IAAA,IAAI,CAAC,QAAQ,CAAC,MAAA,IAAU,CAAC,sBAAA,CAAuB,IAAI,GAAG,OAAO,IAAA;AAC9D,IAAA,IAAI;AACF,MAAA,MAAM,EAAE,SAAA,EAAU,GAAI,MAAM,4BAAA,CAA6B,KAAK,SAAS,CAAA;AACvE,MAAA,OAAO,cAAA,CAAe,WAAW,MAAM,CAAA;AAAA,IACzC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF,CAAA;AAAA,EACA,IAAA,EAAM;AACR","file":"chunk-2SKXRBGS.mjs","sourcesContent":["'use client';\n\nimport { lazy, type ReactNode } from 'react';\nimport {\n isGeometry3DCustomData,\n type Geometry3DCustomData,\n} from './serialize';\nimport { renderGeometry3DSvgFromState } from './render';\nimport type {\n RestoredStampFile,\n StampType,\n} from '../shared/types';\nimport { svgToStampFile } from '../shared/svgToStampFile';\n\nexport type { Geometry3DCustomData };\n\nconst Geometry3DStampHost = lazy(() =>\n import('./host').then((m) => ({ default: m.Geometry3DStampHost })),\n);\n\nconst Geometry3DIcon: ReactNode = (\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n {/* Mặt trước */}\n <path d=\"M4 9 L4 20 L14 20 L14 9 Z\" />\n {/* Mặt trên */}\n <path d=\"M4 9 L10 4 L20 4 L14 9 Z\" />\n {/* Mặt phải */}\n <path d=\"M14 9 L20 4 L20 15 L14 20 Z\" />\n </svg>\n);\n\nexport const geometry3dStamp: StampType<Geometry3DCustomData> = {\n kind: 'geometry3d',\n experimental: true,\n shortcutKey: 'd',\n toolbarLabel: 'D',\n toolbarTitle: 'Hình 3D (D)',\n toolbarIcon: Geometry3DIcon,\n toolbarTestId: 'stamp-toolbar-geometry3d',\n matchesCustomData: isGeometry3DCustomData,\n async renderSvgFromCustomData(data: unknown): Promise<string> {\n if (!isGeometry3DCustomData(data)) {\n throw new Error('geometry3dStamp.renderSvgFromCustomData: customData không phải geometry3d');\n }\n const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);\n return svgString;\n },\n restoreFileFromCustomData: async (element): Promise<RestoredStampFile | null> => {\n const data = element.customData as Geometry3DCustomData | undefined;\n const fileId = (element as { fileId?: string | null }).fileId;\n if (!data || !fileId || !isGeometry3DCustomData(data)) return null;\n try {\n const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);\n return svgToStampFile(svgString, fileId);\n } catch {\n return null;\n }\n },\n Host: Geometry3DStampHost,\n};\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { paletteFor } from './chunk-VBJLUHCY.mjs';
|
|
3
|
+
import { JxgRenderer } from './chunk-6XUPIGVD.mjs';
|
|
4
|
+
import { renderJsxgOffscreen } from './chunk-RD34F5PM.mjs';
|
|
5
|
+
import { createStore } from './chunk-VRWZILTG.mjs';
|
|
6
|
+
|
|
7
|
+
// src/stamps/graph-2d/render.ts
|
|
8
|
+
var DEFAULT_WIDTH = 600;
|
|
9
|
+
var DEFAULT_HEIGHT = 400;
|
|
10
|
+
async function renderGraphSvgFromState(state, _isDark, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT) {
|
|
11
|
+
const palette = paletteFor(false);
|
|
12
|
+
const meta = state.meta;
|
|
13
|
+
const view = meta.domain === "graph2d" ? meta.view : null;
|
|
14
|
+
const bbox = [
|
|
15
|
+
view?.xMin ?? -10,
|
|
16
|
+
view?.yMax ?? 10,
|
|
17
|
+
view?.xMax ?? 10,
|
|
18
|
+
view?.yMin ?? -10
|
|
19
|
+
];
|
|
20
|
+
try {
|
|
21
|
+
const { svgString } = await renderJsxgOffscreen({
|
|
22
|
+
bbox,
|
|
23
|
+
dims: { width, height },
|
|
24
|
+
axis: view?.showAxis ?? true,
|
|
25
|
+
grid: view?.showGrid ?? true,
|
|
26
|
+
keepAspectRatio: false,
|
|
27
|
+
applyOptions: (JXG) => {
|
|
28
|
+
const opts = JXG.Options;
|
|
29
|
+
if (!opts) return;
|
|
30
|
+
opts.text = opts.text || {};
|
|
31
|
+
opts.text.display = "internal";
|
|
32
|
+
opts.text.useASCIIMathML = false;
|
|
33
|
+
opts.text.useMathJax = false;
|
|
34
|
+
opts.text.useKatex = false;
|
|
35
|
+
opts.text.strokeColor = palette.label;
|
|
36
|
+
opts.label = opts.label || {};
|
|
37
|
+
opts.label.display = "internal";
|
|
38
|
+
opts.label.strokeColor = palette.label;
|
|
39
|
+
opts.axis = opts.axis || {};
|
|
40
|
+
opts.axis.strokeColor = palette.axis;
|
|
41
|
+
opts.grid = opts.grid || {};
|
|
42
|
+
opts.grid.strokeColor = palette.grid;
|
|
43
|
+
},
|
|
44
|
+
setup: (board) => {
|
|
45
|
+
const store = createStore(state);
|
|
46
|
+
return new JxgRenderer(store, board);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return svgString;
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { renderGraphSvgFromState };
|
|
56
|
+
//# sourceMappingURL=chunk-33PEN2WC.mjs.map
|
|
57
|
+
//# sourceMappingURL=chunk-33PEN2WC.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/graph-2d/render.ts"],"names":[],"mappings":";;;;;;AAUA,IAAM,aAAA,GAAgB,GAAA;AACtB,IAAM,cAAA,GAAiB,GAAA;AAEvB,eAAsB,wBACpB,KAAA,EACA,OAAA,EACA,KAAA,GAAQ,aAAA,EACR,SAAS,cAAA,EACQ;AACjB,EAAA,MAAM,OAAA,GAAU,WAAW,KAAK,CAAA;AAChC,EAAA,MAAM,OAAO,KAAA,CAAM,IAAA;AACnB,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,KAAW,SAAA,GAAY,KAAK,IAAA,GAAO,IAAA;AACrD,EAAA,MAAM,IAAA,GAAyC;AAAA,IAC7C,MAAM,IAAA,IAAQ,GAAA;AAAA,IACd,MAAM,IAAA,IAAQ,EAAA;AAAA,IACd,MAAM,IAAA,IAAQ,EAAA;AAAA,IACd,MAAM,IAAA,IAAQ;AAAA,GAChB;AACA,EAAA,IAAI;AACF,IAAA,MAAM,EAAE,SAAA,EAAU,GAAI,MAAM,mBAAA,CAAoB;AAAA,MAC9C,IAAA;AAAA,MACA,IAAA,EAAM,EAAE,KAAA,EAAO,MAAA,EAAO;AAAA,MACtB,IAAA,EAAM,MAAM,QAAA,IAAY,IAAA;AAAA,MACxB,IAAA,EAAM,MAAM,QAAA,IAAY,IAAA;AAAA,MACxB,eAAA,EAAiB,KAAA;AAAA,MACjB,YAAA,EAAc,CAAC,GAAA,KAAQ;AAErB,QAAA,MAAM,OAAQ,GAAA,CAAY,OAAA;AAC1B,QAAA,IAAI,CAAC,IAAA,EAAM;AACX,QAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,IAAA,IAAQ,EAAC;AAC1B,QAAA,IAAA,CAAK,KAAK,OAAA,GAAU,UAAA;AACpB,QAAA,IAAA,CAAK,KAAK,cAAA,GAAiB,KAAA;AAC3B,QAAA,IAAA,CAAK,KAAK,UAAA,GAAa,KAAA;AACvB,QAAA,IAAA,CAAK,KAAK,QAAA,GAAW,KAAA;AACrB,QAAA,IAAA,CAAK,IAAA,CAAK,cAAc,OAAA,CAAQ,KAAA;AAChC,QAAA,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,EAAC;AAC5B,QAAA,IAAA,CAAK,MAAM,OAAA,GAAU,UAAA;AACrB,QAAA,IAAA,CAAK,KAAA,CAAM,cAAc,OAAA,CAAQ,KAAA;AACjC,QAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,IAAA,IAAQ,EAAC;AAC1B,QAAA,IAAA,CAAK,IAAA,CAAK,cAAc,OAAA,CAAQ,IAAA;AAChC,QAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,IAAA,IAAQ,EAAC;AAC1B,QAAA,IAAA,CAAK,IAAA,CAAK,cAAc,OAAA,CAAQ,IAAA;AAAA,MAClC,CAAA;AAAA,MACA,KAAA,EAAO,CAAC,KAAA,KAAU;AAChB,QAAA,MAAM,KAAA,GAAQ,YAAY,KAAK,CAAA;AAC/B,QAAA,OAAO,IAAI,WAAA,CAAY,KAAA,EAAO,KAAK,CAAA;AAAA,MACrC;AAAA,KACD,CAAA;AACD,IAAA,OAAO,SAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AAEN,IAAA,OAAO,EAAA;AAAA,EACT;AACF","file":"chunk-33PEN2WC.mjs","sourcesContent":["// src/stamps/graph-2d/render.ts\n// Offscreen SVG export từ graph2d State. Dùng cho insert/restore stamp.\n//\n// LƯU Ý: Luôn dùng light palette — Excalidraw tự invert trong dark mode.\nimport type { State } from '../../core/scene/types';\nimport { createStore } from '../../core/scene/store';\nimport { JxgRenderer } from '../../core/scene/render/JxgRenderer';\nimport { paletteFor } from './editor/theme';\nimport { renderJsxgOffscreen } from '../shared/jxgOffscreenRender';\n\nconst DEFAULT_WIDTH = 600;\nconst DEFAULT_HEIGHT = 400;\n\nexport async function renderGraphSvgFromState(\n state: State,\n _isDark: boolean,\n width = DEFAULT_WIDTH,\n height = DEFAULT_HEIGHT,\n): Promise<string> {\n const palette = paletteFor(false);\n const meta = state.meta;\n const view = meta.domain === 'graph2d' ? meta.view : null;\n const bbox: [number, number, number, number] = [\n view?.xMin ?? -10,\n view?.yMax ?? 10,\n view?.xMax ?? 10,\n view?.yMin ?? -10,\n ];\n try {\n const { svgString } = await renderJsxgOffscreen({\n bbox,\n dims: { width, height },\n axis: view?.showAxis ?? true,\n grid: view?.showGrid ?? true,\n keepAspectRatio: false,\n applyOptions: (JXG) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const opts = (JXG as any).Options;\n if (!opts) return;\n opts.text = opts.text || {};\n opts.text.display = 'internal';\n opts.text.useASCIIMathML = false;\n opts.text.useMathJax = false;\n opts.text.useKatex = false;\n opts.text.strokeColor = palette.label;\n opts.label = opts.label || {};\n opts.label.display = 'internal';\n opts.label.strokeColor = palette.label;\n opts.axis = opts.axis || {};\n opts.axis.strokeColor = palette.axis;\n opts.grid = opts.grid || {};\n opts.grid.strokeColor = palette.grid;\n },\n setup: (board) => {\n const store = createStore(state);\n return new JxgRenderer(store, board);\n },\n });\n return svgString;\n } catch {\n // Match old contract: callers expect '' when no SVG produced.\n return '';\n }\n}\n"]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
// src/core/scene/types.ts
|
|
5
|
+
var DEFAULT_VIEW_2D = {
|
|
6
|
+
bbox: [-10, 10, 10, -10],
|
|
7
|
+
showAxis: false,
|
|
8
|
+
showGrid: false
|
|
9
|
+
};
|
|
10
|
+
var DEFAULT_VIEW_3D = {
|
|
11
|
+
bbox3D: [-5, 5, -5, 5, -5, 5],
|
|
12
|
+
azimuth: 60,
|
|
13
|
+
elevation: 30
|
|
14
|
+
};
|
|
15
|
+
var DEFAULT_VIEW_GRAPH2D = {
|
|
16
|
+
xMin: -10,
|
|
17
|
+
xMax: 10,
|
|
18
|
+
yMin: -10,
|
|
19
|
+
yMax: 10,
|
|
20
|
+
showAxis: true,
|
|
21
|
+
showGrid: true
|
|
22
|
+
};
|
|
23
|
+
function createEmptyState(domain) {
|
|
24
|
+
const base = { objects: {}, order: [], counter: 0 };
|
|
25
|
+
switch (domain) {
|
|
26
|
+
case "2d":
|
|
27
|
+
return { ...base, meta: { domain: "2d", version: 1, view: DEFAULT_VIEW_2D } };
|
|
28
|
+
case "3d":
|
|
29
|
+
return { ...base, meta: { domain: "3d", version: 1, view: DEFAULT_VIEW_3D } };
|
|
30
|
+
case "graph2d":
|
|
31
|
+
return { ...base, meta: { domain: "graph2d", version: 1, view: DEFAULT_VIEW_GRAPH2D } };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/core/scene/selectors.ts
|
|
36
|
+
function listObjects(state) {
|
|
37
|
+
return state.order.map((id) => state.objects[id]).filter((o) => o !== void 0);
|
|
38
|
+
}
|
|
39
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
40
|
+
var LABEL_GROUPS = {
|
|
41
|
+
point: ["point", "intersection"],
|
|
42
|
+
intersection: ["point", "intersection"]
|
|
43
|
+
};
|
|
44
|
+
function nextLabel(state, kind) {
|
|
45
|
+
const kinds = LABEL_GROUPS[kind] ?? [kind];
|
|
46
|
+
const used = new Set(
|
|
47
|
+
listObjects(state).filter((o) => kinds.includes(o.kind)).map((o) => o.label)
|
|
48
|
+
);
|
|
49
|
+
for (const c of ALPHABET) if (!used.has(c)) return c;
|
|
50
|
+
let idx = 1;
|
|
51
|
+
while (true) {
|
|
52
|
+
for (const c of ALPHABET) {
|
|
53
|
+
const candidate = `${c}${idx}`;
|
|
54
|
+
if (!used.has(candidate)) return candidate;
|
|
55
|
+
}
|
|
56
|
+
idx += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function useEditorState(opts) {
|
|
60
|
+
const { store, initialState, onHistoryChange, bindKeyboardShortcuts = true } = opts;
|
|
61
|
+
const onHistoryChangeRef = React.useRef(onHistoryChange);
|
|
62
|
+
onHistoryChangeRef.current = onHistoryChange;
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (initialState?.state) {
|
|
65
|
+
const loaded = initialState.state;
|
|
66
|
+
store.withoutHistory(() => {
|
|
67
|
+
store.dispatch({ type: "LOAD", payload: { state: loaded } });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());
|
|
73
|
+
const unsub = store.subscribe(() => {
|
|
74
|
+
onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());
|
|
75
|
+
});
|
|
76
|
+
return unsub;
|
|
77
|
+
}, [store]);
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (!bindKeyboardShortcuts) return;
|
|
80
|
+
const onKey = (e) => {
|
|
81
|
+
const ae = document.activeElement;
|
|
82
|
+
const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
|
|
83
|
+
if (inField) return;
|
|
84
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
85
|
+
const key = e.key.toLowerCase();
|
|
86
|
+
if (key === "z" && !e.shiftKey) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
store.undo();
|
|
90
|
+
} else if (key === "z" && e.shiftKey || key === "y" && !e.shiftKey) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
store.redo();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
97
|
+
return () => window.removeEventListener("keydown", onKey, { capture: true });
|
|
98
|
+
}, [store, bindKeyboardShortcuts]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/stamps/shared/serializeScene.ts
|
|
102
|
+
function serializeScene(state) {
|
|
103
|
+
return JSON.stringify(state);
|
|
104
|
+
}
|
|
105
|
+
function isValidState(value, domain) {
|
|
106
|
+
if (!value || typeof value !== "object") return false;
|
|
107
|
+
const v = value;
|
|
108
|
+
if (!v.meta || typeof v.meta !== "object") return false;
|
|
109
|
+
if (v.meta.domain !== domain) return false;
|
|
110
|
+
if (!v.meta.view || typeof v.meta.view !== "object") return false;
|
|
111
|
+
if (!v.objects || typeof v.objects !== "object") return false;
|
|
112
|
+
if (!Array.isArray(v.order)) return false;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function deserializeScene(domain, raw) {
|
|
116
|
+
if (!raw) return createEmptyState(domain);
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
if (isValidState(parsed, domain)) return parsed;
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
return createEmptyState(domain);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { DEFAULT_VIEW_2D, DEFAULT_VIEW_3D, createEmptyState, deserializeScene, listObjects, nextLabel, serializeScene, useEditorState };
|
|
126
|
+
//# sourceMappingURL=chunk-3KBL77M6.mjs.map
|
|
127
|
+
//# sourceMappingURL=chunk-3KBL77M6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/scene/types.ts","../src/core/scene/selectors.ts","../src/core/scene/hooks/useEditorState.ts","../src/stamps/shared/serializeScene.ts"],"names":[],"mappings":"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","file":"chunk-3KBL77M6.mjs","sourcesContent":["// src/core/scene/types.ts\n\nexport type SceneObject<A = Record<string, unknown>> = {\n id: string;\n kind: string;\n label: string;\n visible: boolean;\n locked: boolean;\n layer: string;\n schemaVersion: number;\n attrs: A;\n};\n\n// View per domain — narrow theo `state.meta.domain`.\nexport type View2D = {\n readonly bbox: readonly [number, number, number, number]; // [xmin, ymax, xmax, ymin]\n readonly showAxis: boolean;\n readonly showGrid: boolean;\n};\n\nexport type View3D = {\n readonly bbox3D: readonly [number, number, number, number, number, number]; // [xmin, xmax, ymin, ymax, zmin, zmax]\n readonly azimuth: number;\n readonly elevation: number;\n};\n\nexport type ViewGraph2D = {\n readonly xMin: number;\n readonly xMax: number;\n readonly yMin: number;\n readonly yMax: number;\n readonly showAxis: boolean;\n readonly showGrid: boolean;\n};\n\n// Union of all view shapes — narrow qua state.meta.domain.\nexport type SceneView = View2D | View3D | ViewGraph2D;\n\n// Discriminated union: domain narrow → view shape narrow.\nexport type StateMeta =\n | { readonly domain: '2d'; readonly version: number; readonly view: View2D }\n | { readonly domain: '3d'; readonly version: number; readonly view: View3D }\n | { readonly domain: 'graph2d'; readonly version: number; readonly view: ViewGraph2D };\n\nexport type Domain = StateMeta['domain'];\n\n// Backward-compat alias: UPDATE_VIEW patch dùng graph-2d shape (chỉ graph-2d\n// dispatch action này hiện tại).\nexport type ViewSettings = ViewGraph2D;\n\nexport type State = {\n readonly objects: Readonly<Record<string, SceneObject>>;\n readonly order: readonly string[];\n readonly counter: number;\n readonly meta: StateMeta;\n};\n\nexport type Action =\n | { type: 'ADD'; payload: { obj: SceneObject } }\n | { type: 'UPDATE'; payload: { id: string; patch: Partial<Omit<SceneObject, 'id' | 'kind' | 'attrs'>> } }\n | { type: 'UPDATE_ATTRS'; payload: { id: string; patch: Record<string, unknown> } }\n | { type: 'DELETE'; payload: { id: string } }\n | { type: 'RESET' }\n | { type: 'LOAD'; payload: { state: State } }\n | { type: 'TRANSACTION'; payload: { actions: Action[] } }\n | { type: 'UPDATE_VIEW'; payload: { patch: Partial<ViewSettings> } };\n\nexport type RenderCtx = {\n jxg: unknown;\n resolveRef: (id: string) => unknown;\n defaults: Readonly<Record<string, unknown>>;\n /** Map tham số (parameter.label → parameter.value). Chỉ graph2d dùng. */\n paramMap?: Readonly<Record<string, number>>;\n};\n\nexport type KindDef<A = Record<string, unknown>> = {\n type: string;\n schemaVersion: number;\n migrate: Record<number, (prev: any) => any>;\n validate?: (attrs: A) => void;\n dependsOn: (attrs: A) => string[];\n describe: (obj: SceneObject<A>, state?: State) => string;\n measure?: (obj: SceneObject<A>, state: State) =>\n | { label: string; value: number }[]\n | null;\n render: (obj: SceneObject<A>, ctx: RenderCtx) => unknown;\n update?: (\n obj: SceneObject<A>,\n prev: SceneObject<A>,\n ctx: RenderCtx,\n existing: unknown,\n ) => void;\n};\n\nexport const DEFAULT_VIEW_2D: View2D = {\n bbox: [-10, 10, 10, -10],\n showAxis: false,\n showGrid: false,\n};\n\nexport const DEFAULT_VIEW_3D: View3D = {\n bbox3D: [-5, 5, -5, 5, -5, 5],\n azimuth: 60,\n elevation: 30,\n};\n\nexport const DEFAULT_VIEW_GRAPH2D: ViewGraph2D = {\n xMin: -10, xMax: 10, yMin: -10, yMax: 10,\n showAxis: true, showGrid: true,\n};\n\n// EMPTY_STATE giữ shape '3d' (legacy default). Dùng `createEmptyState(domain)`\n// khi cần state cụ thể cho domain — đảm bảo meta.view khớp domain.\nexport const EMPTY_STATE: State = {\n objects: {},\n order: [],\n counter: 0,\n meta: { domain: '3d', version: 1, view: DEFAULT_VIEW_3D },\n};\n\nexport function createEmptyState(domain: Domain): State {\n const base = { objects: {}, order: [], counter: 0 } as const;\n switch (domain) {\n case '2d':\n return { ...base, meta: { domain: '2d', version: 1, view: DEFAULT_VIEW_2D } };\n case '3d':\n return { ...base, meta: { domain: '3d', version: 1, view: DEFAULT_VIEW_3D } };\n case 'graph2d':\n return { ...base, meta: { domain: 'graph2d', version: 1, view: DEFAULT_VIEW_GRAPH2D } };\n }\n}\n","// src/core/scene/selectors.ts\nimport type { State, SceneObject } from './types';\nimport { getKind } from './registry';\n\nexport function listObjects(state: State): SceneObject[] {\n return state.order\n .map(id => state.objects[id])\n .filter((o): o is SceneObject => o !== undefined);\n}\n\nexport function byKind(state: State, kind: string): SceneObject[] {\n return listObjects(state).filter(o => o.kind === kind);\n}\n\nexport function dependentsOf(state: State, rootId: string): Set<string> {\n const result = new Set<string>([rootId]);\n let grew = true;\n while (grew) {\n grew = false;\n for (const obj of Object.values(state.objects)) {\n if (result.has(obj.id)) continue;\n let def;\n try { def = getKind(obj.kind); } catch { continue; }\n const refs = def.dependsOn(obj.attrs as never);\n if (refs.some(r => result.has(r))) {\n result.add(obj.id);\n grew = true;\n }\n }\n }\n return result;\n}\n\nconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n\n/**\n * Nhóm các kind chia chung namespace label. User nhìn point free, intersection,\n * midpoint đều là \"điểm\" có tên 1 chữ cái — không được trùng tên dù khác kind\n * trong scene store. Ví dụ tam giác ABC + giao điểm 2 đường: giao điểm phải là\n * D (không phải A trùng đỉnh tam giác).\n */\nconst LABEL_GROUPS: Record<string, readonly string[]> = {\n point: ['point', 'intersection'],\n intersection: ['point', 'intersection'],\n};\n\nexport function nextLabel(state: State, kind: string): string {\n const kinds = LABEL_GROUPS[kind] ?? [kind];\n const used = new Set(\n listObjects(state).filter(o => kinds.includes(o.kind)).map(o => o.label),\n );\n for (const c of ALPHABET) if (!used.has(c)) return c;\n let idx = 1;\n while (true) {\n for (const c of ALPHABET) {\n const candidate = `${c}${idx}`;\n if (!used.has(candidate)) return candidate;\n }\n idx += 1;\n }\n}\n","// src/core/scene/hooks/useEditorState.ts\nimport * as React from 'react';\nimport type { Store } from '../store';\nimport type { State } from '../types';\n\nexport interface UseEditorStateOptions {\n /** Store sẵn có (host owns). */\n store: Store;\n /** Serialized scene để LOAD lúc mount. Null/undefined = bỏ qua. */\n initialState?: { state: State } | null;\n /** Notify host mỗi khi store fire (canUndo/canRedo có thể đổi). */\n onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void;\n /** Bind Ctrl/Cmd+Z, Cmd+Shift+Z, Ctrl+Y. Default true. */\n bindKeyboardShortcuts?: boolean;\n}\n\n// Side effects chung cho stamp editor panel mà host owns store:\n// - LOAD initial state một lần (trong withoutHistory để không bẩn undo stack).\n// - Propagate canUndo/canRedo lên host qua callback.\n// - Bind global keyboard shortcuts cho undo/redo.\nexport function useEditorState(opts: UseEditorStateOptions): void {\n const { store, initialState, onHistoryChange, bindKeyboardShortcuts = true } = opts;\n\n const onHistoryChangeRef = React.useRef(onHistoryChange);\n onHistoryChangeRef.current = onHistoryChange;\n\n React.useEffect(() => {\n if (initialState?.state) {\n const loaded = initialState.state;\n store.withoutHistory(() => {\n store.dispatch({ type: 'LOAD', payload: { state: loaded } });\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n React.useEffect(() => {\n onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());\n const unsub = store.subscribe(() => {\n onHistoryChangeRef.current?.(store.canUndo(), store.canRedo());\n });\n return unsub;\n }, [store]);\n\n React.useEffect(() => {\n if (!bindKeyboardShortcuts) return;\n const onKey = (e: KeyboardEvent) => {\n const ae = document.activeElement as HTMLElement | null;\n const inField = !!(\n ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)\n );\n if (inField) return;\n if (!(e.metaKey || e.ctrlKey)) return;\n const key = e.key.toLowerCase();\n if (key === 'z' && !e.shiftKey) {\n e.preventDefault();\n e.stopPropagation();\n store.undo();\n } else if ((key === 'z' && e.shiftKey) || (key === 'y' && !e.shiftKey)) {\n e.preventDefault();\n e.stopPropagation();\n store.redo();\n }\n };\n window.addEventListener('keydown', onKey, { capture: true });\n return () => window.removeEventListener('keydown', onKey, { capture: true });\n }, [store, bindKeyboardShortcuts]);\n}\n","// Shared serialize/deserialize cho scene State của stamp interactive\n// (geometry-2d, geometry-3d, graph-2d). Sau Tier D PR 3, customData chỉ\n// store JSON.stringify(state) — không còn envelope `{version, bbox, state}`.\n//\n// View info (bbox/axis/grid/azimuth/elevation) nằm trong `state.meta.view`\n// (discriminated union theo domain). createEmptyState set default view.\n//\n// Note: v0.20 KHÔNG support backward-compat envelope cũ. Data sessionStorage\n// cũ sẽ fallback về empty state khi parse fail (per user spec).\n\nimport { createEmptyState, type Domain, type State } from '../../core/scene';\n\nexport function serializeScene(state: State): string {\n return JSON.stringify(state);\n}\n\nfunction isValidState(value: unknown, domain: Domain): value is State {\n if (!value || typeof value !== 'object') return false;\n const v = value as Partial<State>;\n if (!v.meta || typeof v.meta !== 'object') return false;\n if (v.meta.domain !== domain) return false;\n if (!v.meta.view || typeof v.meta.view !== 'object') return false;\n if (!v.objects || typeof v.objects !== 'object') return false;\n if (!Array.isArray(v.order)) return false;\n return true;\n}\n\nexport function deserializeScene(domain: Domain, raw: string): State {\n if (!raw) return createEmptyState(domain);\n try {\n const parsed = JSON.parse(raw);\n if (isValidState(parsed, domain)) return parsed;\n } catch {\n /* swallow — fallback empty state */\n }\n return createEmptyState(domain);\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// src/stamps/shared/svgToStampFile.ts
|
|
3
|
+
var DEFAULT_WIDTH = 200;
|
|
4
|
+
var DEFAULT_HEIGHT = 100;
|
|
5
|
+
async function hashSvgToFileId(input) {
|
|
6
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
7
|
+
const buf = new TextEncoder().encode(input);
|
|
8
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
9
|
+
return Array.from(new Uint8Array(digest)).slice(0, 16).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
}
|
|
11
|
+
let h1 = 2166136261;
|
|
12
|
+
let h2 = 3421674724;
|
|
13
|
+
for (let i = 0; i < input.length; i++) {
|
|
14
|
+
const c = input.charCodeAt(i);
|
|
15
|
+
h1 ^= c;
|
|
16
|
+
h1 = Math.imul(h1, 16777619);
|
|
17
|
+
h2 ^= c + i;
|
|
18
|
+
h2 = Math.imul(h2, 1099511628211 & 4294967295);
|
|
19
|
+
}
|
|
20
|
+
return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
21
|
+
}
|
|
22
|
+
function parseDim(svg, attr) {
|
|
23
|
+
const re = new RegExp(`<svg[^>]*\\s${attr}="(\\d+(?:\\.\\d+)?)`, "i");
|
|
24
|
+
const m = svg.match(re);
|
|
25
|
+
if (m) return Math.max(1, Math.round(parseFloat(m[1])));
|
|
26
|
+
const vb = svg.match(/viewBox="([\d.\s-]+)"/i);
|
|
27
|
+
if (vb) {
|
|
28
|
+
const parts = vb[1].trim().split(/\s+/).map(parseFloat);
|
|
29
|
+
if (parts.length === 4) {
|
|
30
|
+
return Math.max(1, Math.round(attr === "width" ? parts[2] : parts[3]));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return attr === "width" ? DEFAULT_WIDTH : DEFAULT_HEIGHT;
|
|
34
|
+
}
|
|
35
|
+
function parseSvgDims(svg) {
|
|
36
|
+
return { width: parseDim(svg, "width"), height: parseDim(svg, "height") };
|
|
37
|
+
}
|
|
38
|
+
function svgToStampFile(svgString, fileId) {
|
|
39
|
+
const { width, height } = parseSvgDims(svgString);
|
|
40
|
+
const utf8 = unescape(encodeURIComponent(svgString));
|
|
41
|
+
const base64 = typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8, "binary").toString("base64");
|
|
42
|
+
return {
|
|
43
|
+
fileId,
|
|
44
|
+
dataURL: `data:image/svg+xml;base64,${base64}`,
|
|
45
|
+
mimeType: "image/svg+xml",
|
|
46
|
+
width,
|
|
47
|
+
height
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function createStampFile(svgString) {
|
|
51
|
+
const fileId = await hashSvgToFileId(svgString);
|
|
52
|
+
return svgToStampFile(svgString, fileId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { createStampFile, svgToStampFile };
|
|
56
|
+
//# sourceMappingURL=chunk-5UTGXHLJ.mjs.map
|
|
57
|
+
//# sourceMappingURL=chunk-5UTGXHLJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stamps/shared/svgToStampFile.ts"],"names":[],"mappings":"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","file":"chunk-5UTGXHLJ.mjs","sourcesContent":["// SVG inline base64 helper dùng chung cho 3 stamp (geometry-2d, geometry-3d,\n// graph-2d):\n//\n// - `svgToStampFile(svg, fileId)` — sync, dùng cho `restoreFileFromCustomData`\n// khi caller đã có fileId từ element.\n// - `createStampFile(svg)` — async, dùng cho insert path khi cần generate\n// fileId mới từ hash SVG content.\n//\n// Output luôn là SVG inline base64 — Excalidraw render native + tự đảo màu\n// trong dark mode qua CSS filter. KHÔNG raster sang PNG.\n\nconst DEFAULT_WIDTH = 200;\nconst DEFAULT_HEIGHT = 100;\n\nexport interface StampFileResult {\n fileId: string;\n dataURL: string;\n mimeType: 'image/svg+xml';\n width: number;\n height: number;\n}\n\nasync function hashSvgToFileId(input: string): Promise<string> {\n if (typeof crypto !== 'undefined' && crypto.subtle) {\n const buf = new TextEncoder().encode(input);\n const digest = await crypto.subtle.digest('SHA-256', buf);\n return Array.from(new Uint8Array(digest))\n .slice(0, 16)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n }\n // Double-hash FNV-1a (32-bit chained) → 16 hex chars. Fallback Node/jsdom.\n let h1 = 0x811c9dc5;\n let h2 = 0xcbf29ce4;\n for (let i = 0; i < input.length; i++) {\n const c = input.charCodeAt(i);\n h1 ^= c;\n h1 = Math.imul(h1, 0x01000193);\n h2 ^= c + i;\n h2 = Math.imul(h2, 0x100000001b3 & 0xffffffff);\n }\n return (\n (h1 >>> 0).toString(16).padStart(8, '0') +\n (h2 >>> 0).toString(16).padStart(8, '0')\n );\n}\n\nfunction parseDim(svg: string, attr: 'width' | 'height'): number {\n const re = new RegExp(`<svg[^>]*\\\\s${attr}=\"(\\\\d+(?:\\\\.\\\\d+)?)`, 'i');\n const m = svg.match(re);\n if (m) return Math.max(1, Math.round(parseFloat(m[1])));\n const vb = svg.match(/viewBox=\"([\\d.\\s-]+)\"/i);\n if (vb) {\n const parts = vb[1].trim().split(/\\s+/).map(parseFloat);\n if (parts.length === 4) {\n return Math.max(1, Math.round(attr === 'width' ? parts[2] : parts[3]));\n }\n }\n return attr === 'width' ? DEFAULT_WIDTH : DEFAULT_HEIGHT;\n}\n\nexport function parseSvgDims(svg: string): { width: number; height: number } {\n return { width: parseDim(svg, 'width'), height: parseDim(svg, 'height') };\n}\n\nexport function svgToStampFile(svgString: string, fileId: string): StampFileResult {\n const { width, height } = parseSvgDims(svgString);\n const utf8 = unescape(encodeURIComponent(svgString));\n const base64 = typeof btoa !== 'undefined'\n ? btoa(utf8)\n : Buffer.from(utf8, 'binary').toString('base64');\n return {\n fileId,\n dataURL: `data:image/svg+xml;base64,${base64}`,\n mimeType: 'image/svg+xml',\n width,\n height,\n };\n}\n\n// Async variant cho insert path: hash SVG → fileId rồi gọi svgToStampFile.\n// Identical SVG content → identical fileId (Excalidraw addFiles dedupe).\nexport async function createStampFile(svgString: string): Promise<StampFileResult> {\n const fileId = await hashSvgToFileId(svgString);\n return svgToStampFile(svgString, fileId);\n}\n"]}
|