@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.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-KBLDWPM2.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/{chunk-PWIMZIB6.mjs → chunk-2SKXRBGS.mjs} +7 -8
  6. package/dist/chunk-2SKXRBGS.mjs.map +1 -0
  7. package/dist/chunk-33PEN2WC.mjs +57 -0
  8. package/dist/chunk-33PEN2WC.mjs.map +1 -0
  9. package/dist/chunk-3KBL77M6.mjs +127 -0
  10. package/dist/chunk-3KBL77M6.mjs.map +1 -0
  11. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  12. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  13. package/dist/chunk-6XUPIGVD.mjs +467 -0
  14. package/dist/chunk-6XUPIGVD.mjs.map +1 -0
  15. package/dist/chunk-7WG2KDRF.mjs +28 -0
  16. package/dist/chunk-7WG2KDRF.mjs.map +1 -0
  17. package/dist/chunk-FZY33J6Z.mjs +95 -0
  18. package/dist/chunk-FZY33J6Z.mjs.map +1 -0
  19. package/dist/chunk-HNQLZIEP.mjs +78 -0
  20. package/dist/chunk-HNQLZIEP.mjs.map +1 -0
  21. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  22. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  23. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  24. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  25. package/dist/{chunk-YVJP7NRG.mjs → chunk-O6QTYAKE.mjs} +7 -9
  26. package/dist/chunk-O6QTYAKE.mjs.map +1 -0
  27. package/dist/chunk-R5FL6S7L.mjs +22 -0
  28. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  29. package/dist/chunk-RBUILBX3.mjs +388 -0
  30. package/dist/chunk-RBUILBX3.mjs.map +1 -0
  31. package/dist/chunk-RD34F5PM.mjs +57 -0
  32. package/dist/chunk-RD34F5PM.mjs.map +1 -0
  33. package/dist/{chunk-7P7SQFOW.mjs → chunk-RXOFO64U.mjs} +3 -3
  34. package/dist/chunk-RXOFO64U.mjs.map +1 -0
  35. package/dist/chunk-TOOHCAWP.mjs +1167 -0
  36. package/dist/chunk-TOOHCAWP.mjs.map +1 -0
  37. package/dist/{chunk-C6SCVOMC.mjs → chunk-TQYQVXNW.mjs} +5 -41
  38. package/dist/chunk-TQYQVXNW.mjs.map +1 -0
  39. package/dist/chunk-VBJLUHCY.mjs +23 -0
  40. package/dist/chunk-VBJLUHCY.mjs.map +1 -0
  41. package/dist/chunk-VRWZILTG.mjs +205 -0
  42. package/dist/chunk-VRWZILTG.mjs.map +1 -0
  43. package/dist/chunk-XVSO7FBM.mjs +61 -0
  44. package/dist/chunk-XVSO7FBM.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +3 -6
  46. package/dist/geometry-2d.d.ts +3 -6
  47. package/dist/geometry-2d.js +5069 -2651
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +8 -4
  50. package/dist/geometry-3d.d.mts +4 -7
  51. package/dist/geometry-3d.d.ts +4 -7
  52. package/dist/geometry-3d.js +3053 -2150
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +7 -4
  55. package/dist/graph-2d.d.mts +4 -7
  56. package/dist/graph-2d.d.ts +4 -7
  57. package/dist/graph-2d.js +3363 -1670
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-3N4E4KJH.mjs +1142 -0
  61. package/dist/host-3N4E4KJH.mjs.map +1 -0
  62. package/dist/{host-Z3TEJKZA.mjs → host-6SNSZ332.mjs} +4 -4
  63. package/dist/{host-Z3TEJKZA.mjs.map → host-6SNSZ332.mjs.map} +1 -1
  64. package/dist/host-EVJT3LIF.mjs +3198 -0
  65. package/dist/host-EVJT3LIF.mjs.map +1 -0
  66. package/dist/host-HN4X3TBC.mjs +2374 -0
  67. package/dist/host-HN4X3TBC.mjs.map +1 -0
  68. package/dist/index.css +4 -1
  69. package/dist/index.css.map +1 -1
  70. package/dist/index.d.mts +675 -19
  71. package/dist/index.d.ts +675 -19
  72. package/dist/index.js +11764 -9417
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1492 -335
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +3 -4
  77. package/dist/latex.d.ts +3 -4
  78. package/dist/latex.js +33 -18
  79. package/dist/latex.js.map +1 -1
  80. package/dist/latex.mjs +2 -3
  81. package/dist/render-OCVGDKK6.mjs +8 -0
  82. package/dist/render-OCVGDKK6.mjs.map +1 -0
  83. package/dist/serialize-GKN6OVPM.mjs +6 -0
  84. package/dist/serialize-GKN6OVPM.mjs.map +1 -0
  85. package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
  86. package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
  87. package/package.json +24 -5
  88. package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
  89. package/dist/chunk-74VEEZBV.mjs +0 -619
  90. package/dist/chunk-74VEEZBV.mjs.map +0 -1
  91. package/dist/chunk-7P7SQFOW.mjs.map +0 -1
  92. package/dist/chunk-BJTO5JO5.mjs +0 -11
  93. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  94. package/dist/chunk-C6SCVOMC.mjs.map +0 -1
  95. package/dist/chunk-D257NCQW.mjs +0 -58
  96. package/dist/chunk-D257NCQW.mjs.map +0 -1
  97. package/dist/chunk-G7FR3AIV.mjs +0 -193
  98. package/dist/chunk-G7FR3AIV.mjs.map +0 -1
  99. package/dist/chunk-HTBLO5JO.mjs +0 -41
  100. package/dist/chunk-HTBLO5JO.mjs.map +0 -1
  101. package/dist/chunk-PWIMZIB6.mjs.map +0 -1
  102. package/dist/chunk-SBDMF4NQ.mjs +0 -212
  103. package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
  104. package/dist/chunk-WQOABS6N.mjs +0 -197
  105. package/dist/chunk-WQOABS6N.mjs.map +0 -1
  106. package/dist/chunk-YVJP7NRG.mjs.map +0 -1
  107. package/dist/host-N6ACNJKI.mjs +0 -3226
  108. package/dist/host-N6ACNJKI.mjs.map +0 -1
  109. package/dist/host-NKGV6RF2.mjs +0 -1134
  110. package/dist/host-NKGV6RF2.mjs.map +0 -1
  111. package/dist/host-XVK7UCRE.mjs +0 -2908
  112. package/dist/host-XVK7UCRE.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/geometry-2d/serialize.ts","../src/stamps/geometry-2d/render.ts","../src/stamps/geometry-2d/types.ts"],"names":[],"mappings":"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","file":"chunk-FZY33J6Z.mjs","sourcesContent":["// src/stamps/geometry-2d/serialize.ts\n//\n// Sau Tier D PR 3: customData.jsonState chỉ chứa `JSON.stringify(state)`\n// (không còn envelope `{version, bbox, state, showAxis, showGrid}`).\n// View info (bbox/axis/grid) nằm trong `state.meta.view` (View2D shape).\n\nimport { serializeScene, deserializeScene } from '../shared/serializeScene';\nimport type { State, View2D } from '../../core/scene';\n\nexport function serializeBoard(state: State, view: View2D): string {\n const withView: State = {\n ...state,\n meta: { domain: '2d', version: state.meta.version, view },\n };\n return serializeScene(withView);\n}\n\nexport function deserializeBoard(raw: string): State {\n return deserializeScene('2d', raw);\n}\n","import { deserializeBoard } from './serialize';\nimport { paletteFor } from './editor/theme';\nimport { createStore } from '../../core/scene';\nimport { DEFAULT_VIEW_2D } from '../../core/scene/types';\nimport { JxgRenderer } from '../../core/scene/render/JxgRenderer';\nimport { renderJsxgOffscreen } from '../shared/jxgOffscreenRender';\n\n/**\n * Re-render geometry SVG từ jsonState đã serialize. Dùng cho:\n * 1. Restore math-stamp file sau khi reload page (Excalidraw mất binary files).\n * 2. Generate SVG lúc INSERT (thay vì clone DOM với màu theo theme editor).\n *\n * LƯU Ý quan trọng — luôn dùng LIGHT palette (nét đậm). Excalidraw apply CSS\n * `filter: invert(93%) hue-rotate(180deg)` lên canvas trong dark mode → nét\n * đậm tự đảo thành sáng. Nếu ta bake nét sáng vào SVG cho dark mode, filter\n * sẽ đảo thành đậm → chìm vào nền tối. Giải pháp: luôn dùng nét đậm + để\n * Excalidraw tự lo invert.\n *\n * Implementation: tạo 1 div ẩn (off-screen, real dimensions để JSXGraph render\n * chuẩn), initBoard, replay creation log từ jsonState, dump SVG, dọn dẹp.\n *\n * Container dimensions phải MATCH aspect ratio của bbox (đã được editor lưu\n * sau khi JSXGraph adjust với keepAspectRatio:true). Trước đây hardcode\n * 400×300 + keepAspectRatio:false làm shape bị kéo dãn (circle thành ellipse,\n * góc vuông lệch) khi bbox không 4:3 → ảnh hiển thị khác với editor lúc\n * double-click. Fix: tính container W/H từ bbox + keepAspectRatio:true để\n * SVG output khớp với view trong editor.\n *\n * Lý do JXG.Options.text.display = 'internal': JSXGraph mặc định render\n * label bằng HTML <div> overlay → clone SVG export sẽ thiếu label.\n */\n\nconst PIXELS_PER_UNIT = 20;\nconst MIN_DIM = 100;\nconst MAX_DIM = 1200;\nconst FALLBACK_W = 400;\nconst FALLBACK_H = 300;\n\nexport function containerDimsForBbox(bbox: [number, number, number, number]): { width: number; height: number } {\n const [xmin, ymax, xmax, ymin] = bbox;\n const w = Math.abs(xmax - xmin);\n const h = Math.abs(ymax - ymin);\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n return { width: FALLBACK_W, height: FALLBACK_H };\n }\n let width = w * PIXELS_PER_UNIT;\n let height = h * PIXELS_PER_UNIT;\n const maxAxis = Math.max(width, height);\n if (maxAxis > MAX_DIM) {\n const ratio = MAX_DIM / maxAxis;\n width *= ratio;\n height *= ratio;\n }\n const minAxis = Math.min(width, height);\n if (minAxis < MIN_DIM) {\n const ratio = MIN_DIM / minAxis;\n width *= ratio;\n height *= ratio;\n }\n return { width: Math.round(width), height: Math.round(height) };\n}\n\nexport async function renderGeometrySvgFromState(jsonState: string): Promise<string> {\n const state = deserializeBoard(jsonState);\n const view = state.meta.domain === '2d' ? state.meta.view : DEFAULT_VIEW_2D;\n const bbox = view.bbox as [number, number, number, number];\n // Stamps inserted vào Excalidraw canvas → luôn dùng light palette.\n // Excalidraw's THEME_FILTER tự đảo nét trong dark mode.\n const palette = paletteFor(false);\n const dims = containerDimsForBbox(bbox);\n const { svgString } = await renderJsxgOffscreen({\n bbox,\n dims,\n axis: view.showAxis,\n grid: view.showGrid,\n keepAspectRatio: true,\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}\n","import type { BaseStampCustomData } from '../shared/types';\n\nexport interface GeometryCustomData extends BaseStampCustomData {\n kind: 'geometry';\n version: 1;\n jsonState: string;\n}\n\nexport function isGeometryCustomData(data: unknown): data is GeometryCustomData {\n if (!data || typeof data !== 'object') return false;\n const d = data as Partial<GeometryCustomData>;\n return d.kind === 'geometry' && d.version === 1 && typeof d.jsonState === 'string';\n}\n"]}
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { useState, useRef, useCallback, useEffect } from 'react';
3
+
4
+ // src/stamps/shared/useChordShortcut.ts
5
+ var A_CODE = "a".charCodeAt(0);
6
+ function isFieldFocused() {
7
+ const ae = typeof document !== "undefined" ? document.activeElement : null;
8
+ return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
9
+ }
10
+ function useChordShortcut(args) {
11
+ const { groupOrder, tools, onSelect, enabled } = args;
12
+ const [chordGroup, setChordGroup] = useState(null);
13
+ const groupOrderRef = useRef(groupOrder);
14
+ const toolsRef = useRef(tools);
15
+ const onSelectRef = useRef(onSelect);
16
+ const chordGroupRef = useRef(null);
17
+ groupOrderRef.current = groupOrder;
18
+ toolsRef.current = tools;
19
+ onSelectRef.current = onSelect;
20
+ const cancel = useCallback(() => {
21
+ chordGroupRef.current = null;
22
+ setChordGroup(null);
23
+ }, []);
24
+ useEffect(() => {
25
+ if (!enabled) return;
26
+ const setChord = (next) => {
27
+ chordGroupRef.current = next;
28
+ setChordGroup(next);
29
+ };
30
+ const onKey = (e) => {
31
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
32
+ if (isFieldFocused()) return;
33
+ const key = e.key;
34
+ const lower = key.length === 1 ? key.toLowerCase() : key;
35
+ if (key === "Escape") {
36
+ if (chordGroupRef.current !== null) {
37
+ e.preventDefault();
38
+ e.stopPropagation();
39
+ setChord(null);
40
+ }
41
+ return;
42
+ }
43
+ if (lower.length === 1 && lower >= "a" && lower <= "z") {
44
+ const idx = lower.charCodeAt(0) - A_CODE;
45
+ if (idx < groupOrderRef.current.length) {
46
+ e.preventDefault();
47
+ e.stopPropagation();
48
+ setChord(groupOrderRef.current[idx]);
49
+ }
50
+ return;
51
+ }
52
+ if (key >= "1" && key <= "9") {
53
+ const active = chordGroupRef.current;
54
+ if (active === null) return;
55
+ const n = key.charCodeAt(0) - "1".charCodeAt(0);
56
+ const toolsInGroup = toolsRef.current.filter(
57
+ (t) => t.group === active
58
+ );
59
+ e.preventDefault();
60
+ e.stopPropagation();
61
+ if (n < toolsInGroup.length) {
62
+ onSelectRef.current(toolsInGroup[n].key);
63
+ }
64
+ setChord(null);
65
+ return;
66
+ }
67
+ };
68
+ window.addEventListener("keydown", onKey, { capture: true });
69
+ return () => {
70
+ window.removeEventListener("keydown", onKey, { capture: true });
71
+ };
72
+ }, [enabled]);
73
+ return { chordGroup, cancel };
74
+ }
75
+
76
+ export { useChordShortcut };
77
+ //# sourceMappingURL=chunk-HNQLZIEP.mjs.map
78
+ //# sourceMappingURL=chunk-HNQLZIEP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/shared/useChordShortcut.ts"],"names":[],"mappings":"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","file":"chunk-HNQLZIEP.mjs","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface UseChordShortcutArgs<G extends string> {\n groupOrder: readonly G[];\n tools: ReadonlyArray<{ key: string; group: G }>;\n onSelect: (toolKey: string) => void;\n enabled: boolean;\n}\n\ninterface UseChordShortcutResult<G extends string> {\n chordGroup: G | null;\n cancel: () => void;\n}\n\nconst A_CODE = 'a'.charCodeAt(0);\n\nfunction isFieldFocused(): boolean {\n const ae = (typeof document !== 'undefined'\n ? (document.activeElement as HTMLElement | null)\n : null);\n return !!(\n ae &&\n (ae.tagName === 'INPUT' ||\n ae.tagName === 'TEXTAREA' ||\n ae.isContentEditable)\n );\n}\n\nexport function useChordShortcut<G extends string>(\n args: UseChordShortcutArgs<G>,\n): UseChordShortcutResult<G> {\n const { groupOrder, tools, onSelect, enabled } = args;\n\n const [chordGroup, setChordGroup] = useState<G | null>(null);\n\n const groupOrderRef = useRef(groupOrder);\n const toolsRef = useRef(tools);\n const onSelectRef = useRef(onSelect);\n const chordGroupRef = useRef<G | null>(null);\n\n groupOrderRef.current = groupOrder;\n toolsRef.current = tools;\n onSelectRef.current = onSelect;\n // chordGroupRef được sync ngay trong handler (xem `setChord` dưới đây)\n // thay vì ghi từ render body — nếu ghi ở body sẽ bị React batch hoá khi\n // hai event xảy ra trong cùng một act() (event sau đọc giá trị cũ).\n\n const cancel = useCallback(() => {\n chordGroupRef.current = null;\n setChordGroup(null);\n }, []);\n\n useEffect(() => {\n if (!enabled) return;\n\n const setChord = (next: G | null) => {\n chordGroupRef.current = next;\n setChordGroup(next);\n };\n\n const onKey = (e: KeyboardEvent) => {\n if (e.metaKey || e.ctrlKey || e.altKey) return;\n if (isFieldFocused()) return;\n\n const key = e.key;\n const lower = key.length === 1 ? key.toLowerCase() : key;\n\n if (key === 'Escape') {\n if (chordGroupRef.current !== null) {\n e.preventDefault();\n e.stopPropagation();\n setChord(null);\n }\n return;\n }\n\n if (lower.length === 1 && lower >= 'a' && lower <= 'z') {\n const idx = lower.charCodeAt(0) - A_CODE;\n if (idx < groupOrderRef.current.length) {\n e.preventDefault();\n e.stopPropagation();\n setChord(groupOrderRef.current[idx]);\n }\n return;\n }\n\n if (key >= '1' && key <= '9') {\n const active = chordGroupRef.current;\n if (active === null) return;\n const n = key.charCodeAt(0) - '1'.charCodeAt(0); // 0-indexed\n const toolsInGroup = toolsRef.current.filter(\n (t) => t.group === active,\n );\n e.preventDefault();\n e.stopPropagation();\n if (n < toolsInGroup.length) {\n onSelectRef.current(toolsInGroup[n].key);\n }\n setChord(null);\n return;\n }\n };\n\n window.addEventListener('keydown', onKey, { capture: true });\n return () => {\n window.removeEventListener('keydown', onKey, { capture: true });\n };\n }, [enabled]);\n\n return { chordGroup, cancel };\n}\n"]}
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { useState, useRef, useCallback } from 'react';
3
+
4
+ // src/stamps/shared/useToolStateMachine.ts
5
+ function useToolStateMachine(initial) {
6
+ const [tool, setToolState] = useState(initial);
7
+ const [pendingIds, setPendingIds] = useState([]);
8
+ const toolRef = useRef(initial);
9
+ const pendingIdsRef = useRef([]);
10
+ const setTool = useCallback((t) => {
11
+ toolRef.current = t;
12
+ pendingIdsRef.current = [];
13
+ setToolState(t);
14
+ setPendingIds([]);
15
+ }, []);
16
+ const pushPending = useCallback((id) => {
17
+ pendingIdsRef.current = [...pendingIdsRef.current, id];
18
+ setPendingIds(pendingIdsRef.current);
19
+ }, []);
20
+ const clearPending = useCallback(() => {
21
+ pendingIdsRef.current = [];
22
+ setPendingIds([]);
23
+ }, []);
24
+ return { tool, pendingIds, toolRef, pendingIdsRef, setTool, pushPending, clearPending };
25
+ }
26
+
27
+ export { useToolStateMachine };
28
+ //# sourceMappingURL=chunk-NVJ7K3DK.mjs.map
29
+ //# sourceMappingURL=chunk-NVJ7K3DK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/shared/useToolStateMachine.ts"],"names":[],"mappings":";;;AAsBO,SAAS,oBAAsC,OAAA,EAAiC;AACrF,EAAA,MAAM,CAAC,IAAA,EAAM,YAAY,CAAA,GAAI,SAAY,OAAO,CAAA;AAChD,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,QAAA,CAAmB,EAAE,CAAA;AACzD,EAAA,MAAM,OAAA,GAAU,OAAU,OAAO,CAAA;AACjC,EAAA,MAAM,aAAA,GAAgB,MAAA,CAAiB,EAAE,CAAA;AAEzC,EAAA,MAAM,OAAA,GAAU,WAAA,CAAY,CAAC,CAAA,KAAS;AACpC,IAAA,OAAA,CAAQ,OAAA,GAAU,CAAA;AAClB,IAAA,aAAA,CAAc,UAAU,EAAC;AACzB,IAAA,YAAA,CAAa,CAAC,CAAA;AACd,IAAA,aAAA,CAAc,EAAE,CAAA;AAAA,EAClB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,WAAA,GAAc,WAAA,CAAY,CAAC,EAAA,KAAe;AAC9C,IAAA,aAAA,CAAc,OAAA,GAAU,CAAC,GAAG,aAAA,CAAc,SAAS,EAAE,CAAA;AACrD,IAAA,aAAA,CAAc,cAAc,OAAO,CAAA;AAAA,EACrC,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,YAAA,GAAe,YAAY,MAAM;AACrC,IAAA,aAAA,CAAc,UAAU,EAAC;AACzB,IAAA,aAAA,CAAc,EAAE,CAAA;AAAA,EAClB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,SAAS,aAAA,EAAe,OAAA,EAAS,aAAa,YAAA,EAAa;AACxF","file":"chunk-NVJ7K3DK.mjs","sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nexport type ToolStateMachine<T extends string> = {\n tool: T;\n pendingIds: string[];\n toolRef: { readonly current: T };\n pendingIdsRef: { readonly current: string[] };\n setTool: (t: T) => void;\n pushPending: (id: string) => void;\n clearPending: () => void;\n};\n\n/**\n * Tool + pending ids state machine, generic theo tool union type.\n *\n * - `setTool` clears pending — chuyển tool đang xây thì huỷ build dở.\n * - Ref + state song song để handler stable-closure read được giá trị mới\n * mà UI vẫn re-render khi giá trị đổi.\n *\n * Shared cho geometry-2d (`GeomTool`) và graph-2d (`GraphTool`); thêm consumer\n * mới chỉ cần pass union literal type.\n */\nexport function useToolStateMachine<T extends string>(initial: T): ToolStateMachine<T> {\n const [tool, setToolState] = useState<T>(initial);\n const [pendingIds, setPendingIds] = useState<string[]>([]);\n const toolRef = useRef<T>(initial);\n const pendingIdsRef = useRef<string[]>([]);\n\n const setTool = useCallback((t: T) => {\n toolRef.current = t;\n pendingIdsRef.current = [];\n setToolState(t);\n setPendingIds([]);\n }, []);\n\n const pushPending = useCallback((id: string) => {\n pendingIdsRef.current = [...pendingIdsRef.current, id];\n setPendingIds(pendingIdsRef.current);\n }, []);\n\n const clearPending = useCallback(() => {\n pendingIdsRef.current = [];\n setPendingIds([]);\n }, []);\n\n return { tool, pendingIds, toolRef, pendingIdsRef, setTool, pushPending, clearPending };\n}\n"]}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ // src/stamps/graph-2d/types.ts
3
+ function isGraph2DCustomData(data) {
4
+ if (!data || typeof data !== "object") return false;
5
+ const d = data;
6
+ return d.kind === "graph2d" && d.version === 2 && typeof d.jsonState === "string";
7
+ }
8
+
9
+ export { isGraph2DCustomData };
10
+ //# sourceMappingURL=chunk-O4WIZFRQ.mjs.map
11
+ //# sourceMappingURL=chunk-O4WIZFRQ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/graph-2d/types.ts"],"names":[],"mappings":";AASO,SAAS,oBAAoB,IAAA,EAA0C;AAC5E,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,UAAU,OAAO,KAAA;AAC9C,EAAA,MAAM,CAAA,GAAI,IAAA;AACV,EAAA,OAAO,CAAA,CAAE,SAAS,SAAA,IAAa,CAAA,CAAE,YAAY,CAAA,IAAK,OAAO,EAAE,SAAA,KAAc,QAAA;AAC3E","file":"chunk-O4WIZFRQ.mjs","sourcesContent":["// src/stamps/graph-2d/types.ts\nimport type { BaseStampCustomData } from '../shared/types';\n\nexport interface Graph2DCustomData extends BaseStampCustomData {\n kind: 'graph2d';\n version: 2;\n jsonState: string;\n}\n\nexport function isGraph2DCustomData(data: unknown): data is Graph2DCustomData {\n if (!data || typeof data !== 'object') return false;\n const d = data as Partial<Graph2DCustomData>;\n return d.kind === 'graph2d' && d.version === 2 && typeof d.jsonState === 'string';\n}\n"]}
@@ -1,10 +1,11 @@
1
1
  "use client";
2
- import { isGeometryCustomData, renderGeometrySvgFromState } from './chunk-G7FR3AIV.mjs';
2
+ import { isGeometryCustomData, renderGeometrySvgFromState } from './chunk-FZY33J6Z.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 GeometryStampHost = lazy(
7
- () => import('./host-XVK7UCRE.mjs').then((m) => ({ default: m.GeometryStampHost }))
8
+ () => import('./host-EVJT3LIF.mjs').then((m) => ({ default: m.GeometryStampHost }))
8
9
  );
9
10
  var GeometryIcon = /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
10
11
  /* @__PURE__ */ jsx("polygon", { points: "4,20 20,20 12,5" }),
@@ -29,16 +30,13 @@ var geometryStamp = {
29
30
  async restoreFileFromCustomData(element) {
30
31
  const data = element.customData;
31
32
  const fileId = element.fileId;
32
- if (!data || !fileId) return null;
33
- if (!isGeometryCustomData(data)) return null;
33
+ if (!data || !fileId || !isGeometryCustomData(data)) return null;
34
34
  const svgString = await renderGeometrySvgFromState(data.jsonState);
35
- const utf8 = unescape(encodeURIComponent(svgString));
36
- const dataURL = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(utf8) : Buffer.from(utf8).toString("base64"));
37
- return { fileId, dataURL, mimeType: "image/svg+xml" };
35
+ return svgToStampFile(svgString, fileId);
38
36
  },
39
37
  Host: GeometryStampHost
40
38
  };
41
39
 
42
40
  export { geometryStamp };
43
- //# sourceMappingURL=chunk-YVJP7NRG.mjs.map
44
- //# sourceMappingURL=chunk-YVJP7NRG.mjs.map
41
+ //# sourceMappingURL=chunk-O6QTYAKE.mjs.map
42
+ //# sourceMappingURL=chunk-O6QTYAKE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/geometry-2d/index.tsx"],"names":[],"mappings":";;;;;AAgBA,IAAM,iBAAA,GAAoB,IAAA;AAAA,EAAK,MAC7B,OAAO,qBAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,MAAO,EAAE,OAAA,EAAS,CAAA,CAAE,iBAAA,EAAkB,CAAE;AACjE,CAAA;AAEA,IAAM,YAAA,wBACH,KAAA,EAAA,EAAI,KAAA,EAAM,MAAK,MAAA,EAAO,IAAA,EAAK,SAAQ,WAAA,EAAY,IAAA,EAAK,QAAO,MAAA,EAAO,cAAA,EAAe,aAAY,KAAA,EAAM,aAAA,EAAc,SAAQ,cAAA,EAAe,OAAA,EAAQ,eAAY,MAAA,EAC3J,QAAA,EAAA;AAAA,kBAAA,GAAA,CAAC,SAAA,EAAA,EAAQ,QAAO,iBAAA,EAAkB,CAAA;AAAA,kBAClC,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,GAAA,EAAI,EAAA,EAAG,IAAA,EAAK,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,MAAA,EAAO,MAAA,EAAO,CAAA;AAAA,kBACjE,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,MAAA,EAAO,MAAA,EAAO,CAAA;AAAA,kBAClE,GAAA,CAAC,QAAA,EAAA,EAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,GAAA,EAAI,CAAA,EAAE,KAAA,EAAM,IAAA,EAAK,cAAA,EAAe,MAAA,EAAO,MAAA,EAAO;AAAA,CAAA,EACnE,CAAA;AAGK,IAAM,aAAA,GAA+C;AAAA,EAC1D,IAAA,EAAM,UAAA;AAAA,EACN,WAAA,EAAa,GAAA;AAAA,EACb,YAAA,EAAc,GAAA;AAAA,EACd,YAAA,EAAc,8BAAA;AAAA,EACd,WAAA,EAAa,YAAA;AAAA,EACb,aAAA,EAAe,wBAAA;AAAA,EACf,iBAAA,EAAmB,oBAAA;AAAA,EACnB,MAAM,wBAAwB,IAAA,EAAM;AAClC,IAAA,IAAI,CAAC,oBAAA,CAAqB,IAAI,CAAA,EAAG;AAC/B,MAAA,MAAM,IAAI,MAAM,+EAAuE,CAAA;AAAA,IACzF;AACA,IAAA,OAAO,0BAAA,CAA2B,KAAK,SAAS,CAAA;AAAA,EAClD,CAAA;AAAA,EACA,MAAM,0BAA0B,OAAA,EAA4C;AAC1E,IAAA,MAAM,OAAO,OAAA,CAAQ,UAAA;AACrB,IAAA,MAAM,SAAU,OAAA,CAAuC,MAAA;AACvD,IAAA,IAAI,CAAC,QAAQ,CAAC,MAAA,IAAU,CAAC,oBAAA,CAAqB,IAAI,GAAG,OAAO,IAAA;AAC5D,IAAA,MAAM,SAAA,GAAY,MAAM,0BAAA,CAA2B,IAAA,CAAK,SAAS,CAAA;AACjE,IAAA,OAAO,cAAA,CAAe,WAAW,MAAM,CAAA;AAAA,EACzC,CAAA;AAAA,EACA,IAAA,EAAM;AACR","file":"chunk-O6QTYAKE.mjs","sourcesContent":["'use client';\n\nimport { lazy, type ReactNode } from 'react';\nimport { renderGeometrySvgFromState } from './render';\nimport type {\n RestoredStampFile,\n StampType,\n} from '../shared/types';\nimport { svgToStampFile } from '../shared/svgToStampFile';\nimport {\n isGeometryCustomData,\n type GeometryCustomData,\n} from './types';\n\nexport type { GeometryCustomData };\n\nconst GeometryStampHost = lazy(() =>\n import('./host').then((m) => ({ default: m.GeometryStampHost })),\n);\n\nconst GeometryIcon: ReactNode = (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <polygon points=\"4,20 20,20 12,5\" />\n <circle cx=\"4\" cy=\"20\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\" />\n <circle cx=\"20\" cy=\"20\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\" />\n <circle cx=\"12\" cy=\"5\" r=\"1.4\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n);\n\nexport const geometryStamp: StampType<GeometryCustomData> = {\n kind: 'geometry',\n shortcutKey: 'g',\n toolbarLabel: 'G',\n toolbarTitle: 'Chèn hình học (G)',\n toolbarIcon: GeometryIcon,\n toolbarTestId: 'stamp-toolbar-geometry',\n matchesCustomData: isGeometryCustomData,\n async renderSvgFromCustomData(data) {\n if (!isGeometryCustomData(data)) {\n throw new Error('geometryStamp.renderSvgFromCustomData: customData không phải geometry');\n }\n return renderGeometrySvgFromState(data.jsonState);\n },\n async restoreFileFromCustomData(element): Promise<RestoredStampFile | null> {\n const data = element.customData as GeometryCustomData | undefined;\n const fileId = (element as { fileId?: string | null }).fileId;\n if (!data || !fileId || !isGeometryCustomData(data)) return null;\n const svgString = await renderGeometrySvgFromState(data.jsonState);\n return svgToStampFile(svgString, fileId);\n },\n Host: GeometryStampHost,\n};\n"]}
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ // src/stamps/geometry-2d/editor/theme.ts
3
+ var themeStroke = (dark) => dark ? "#e2e8f0" : "#0f172a";
4
+ var themeAxis = (dark) => dark ? "#cbd5e1" : "#94a3b8";
5
+ var themeGrid = (dark) => dark ? "#475569" : "#e2e8f0";
6
+ var themeLabel = (dark) => dark ? "#e2e8f0" : "#000000";
7
+ function paletteFor(isDark) {
8
+ const stroke = themeStroke(isDark);
9
+ return {
10
+ stroke,
11
+ fill: "#60a5fa",
12
+ axis: themeAxis(isDark),
13
+ grid: themeGrid(isDark),
14
+ label: themeLabel(isDark),
15
+ // Geometry-2d điểm fill = stroke color (đồng bộ chấm điểm với nét vẽ).
16
+ pointFill: stroke
17
+ };
18
+ }
19
+
20
+ export { paletteFor, themeAxis, themeGrid, themeLabel };
21
+ //# sourceMappingURL=chunk-R5FL6S7L.mjs.map
22
+ //# sourceMappingURL=chunk-R5FL6S7L.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stamps/geometry-2d/editor/theme.ts"],"names":[],"mappings":";AAQO,IAAM,WAAA,GAAc,CAAC,IAAA,KAA2B,IAAA,GAAO,SAAA,GAAY,SAAA;AACnE,IAAM,SAAA,GAAY,CAAC,IAAA,KAA2B,IAAA,GAAO,SAAA,GAAY;AACjE,IAAM,SAAA,GAAY,CAAC,IAAA,KAA2B,IAAA,GAAO,SAAA,GAAY;AACjE,IAAM,UAAA,GAAa,CAAC,IAAA,KAA2B,IAAA,GAAO,SAAA,GAAY;AAElE,SAAS,WAAW,MAAA,EAA0B;AACnD,EAAA,MAAM,MAAA,GAAS,YAAY,MAAM,CAAA;AACjC,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM,UAAU,MAAM,CAAA;AAAA,IACtB,IAAA,EAAM,UAAU,MAAM,CAAA;AAAA,IACtB,KAAA,EAAO,WAAW,MAAM,CAAA;AAAA;AAAA,IAExB,SAAA,EAAW;AAAA,GACb;AACF","file":"chunk-R5FL6S7L.mjs","sourcesContent":["// Theme-aware palette cho geometry stamps.\n//\n// Dùng `Theme2D` từ `core/scene/render/types2d` (cùng abstraction graph-2d).\n// JxgRenderer nhận theme qua constructor options và phân phối qua RenderCtx\n// đến mỗi kind handler — không cần sentinel string trong attrs nữa.\n\nimport { type Theme2D } from '../../../core/scene/render/types2d';\n\nexport const themeStroke = (dark: boolean): string => (dark ? '#e2e8f0' : '#0f172a');\nexport const themeAxis = (dark: boolean): string => (dark ? '#cbd5e1' : '#94a3b8');\nexport const themeGrid = (dark: boolean): string => (dark ? '#475569' : '#e2e8f0');\nexport const themeLabel = (dark: boolean): string => (dark ? '#e2e8f0' : '#000000');\n\nexport function paletteFor(isDark: boolean): Theme2D {\n const stroke = themeStroke(isDark);\n return {\n stroke,\n fill: '#60a5fa',\n axis: themeAxis(isDark),\n grid: themeGrid(isDark),\n label: themeLabel(isDark),\n // Geometry-2d điểm fill = stroke color (đồng bộ chấm điểm với nét vẽ).\n pointFill: stroke,\n };\n}\n"]}
@@ -0,0 +1,388 @@
1
+ "use client";
2
+ import { paletteFor } from './chunk-R5FL6S7L.mjs';
3
+ import { renderJsxgOffscreen } from './chunk-RD34F5PM.mjs';
4
+ import { serializeScene, deserializeScene, DEFAULT_VIEW_3D } from './chunk-3KBL77M6.mjs';
5
+ import { getKind, createStore } from './chunk-VRWZILTG.mjs';
6
+
7
+ // src/stamps/geometry-3d/serialize.ts
8
+ function isGeometry3DCustomData(data) {
9
+ if (!data || typeof data !== "object") return false;
10
+ const d = data;
11
+ return d.kind === "geometry3d" && d.version === 2 && typeof d.jsonState === "string";
12
+ }
13
+ function serializeBoard3D(state, view) {
14
+ const withView = {
15
+ ...state,
16
+ meta: { domain: "3d", version: state.meta.version, view }
17
+ };
18
+ return serializeScene(withView);
19
+ }
20
+ function deserializeBoard3D(raw) {
21
+ return deserializeScene("3d", raw);
22
+ }
23
+
24
+ // src/core/scene/render/types.ts
25
+ var DEFAULT_THEME_3D = {
26
+ point: { size: 4, color: "#1e40af" },
27
+ line: { strokeWidth: 2, color: "#0f172a" },
28
+ plane: { fillOpacity: 0.15, color: "#60a5fa" }
29
+ };
30
+
31
+ // src/core/scene/render/JxgRenderer3D.ts
32
+ var JxgRenderer3D = class {
33
+ constructor(store, view, options = {}) {
34
+ this.elements = /* @__PURE__ */ new Map();
35
+ this.disposed = false;
36
+ // Selection halo overlay (3D): multi-select, halo phía sau element gốc cho
37
+ // các kind đơn giản (point3d, segment/line/ray/vector). Các composite shape
38
+ // (polyhedron/cone/cylinder/plane) chưa hỗ trợ halo overlay — bỏ qua.
39
+ this.selectedIds = /* @__PURE__ */ new Set();
40
+ this.haloMap = /* @__PURE__ */ new Map();
41
+ this.store = store;
42
+ this.view = view;
43
+ this.theme = options.theme ?? DEFAULT_THEME_3D;
44
+ this.unsubscribe = store.subscribe((next, prev) => this.applyDiff(prev, next));
45
+ this.applyDiff(void 0, store.getState());
46
+ }
47
+ ctx() {
48
+ return {
49
+ jxg: this.view,
50
+ resolveRef: (id) => {
51
+ const el = this.elements.get(id);
52
+ if (el === void 0) {
53
+ throw new Error(`[scene] resolveRef: ch\u01B0a render id="${id}"`);
54
+ }
55
+ return el;
56
+ },
57
+ defaults: {}
58
+ };
59
+ }
60
+ create(obj) {
61
+ try {
62
+ const def = getKind(obj.kind);
63
+ const el = def.render(obj, this.ctx());
64
+ this.elements.set(obj.id, el);
65
+ } catch (err) {
66
+ console.warn(`[scene/render] kh\xF4ng render \u0111\u01B0\u1EE3c ${obj.kind} id="${obj.id}":`, err);
67
+ }
68
+ }
69
+ remove(id) {
70
+ this.removeHalo(id);
71
+ this.selectedIds.delete(id);
72
+ const el = this.elements.get(id);
73
+ if (el === void 0) return;
74
+ try {
75
+ this.removeFromView(el);
76
+ } catch (err) {
77
+ console.warn(`[scene/render] kh\xF4ng remove \u0111\u01B0\u1EE3c id="${id}":`, err);
78
+ }
79
+ this.elements.delete(id);
80
+ }
81
+ removeFromView(el) {
82
+ const view = this.view;
83
+ if (el && typeof el === "object") {
84
+ const asObj = el;
85
+ if (Array.isArray(asObj["faces"])) {
86
+ for (const face of asObj["faces"]) {
87
+ view.removeObject?.(face);
88
+ }
89
+ if (Array.isArray(asObj["_verts"])) {
90
+ for (const v of asObj["_verts"]) {
91
+ view.removeObject?.(v);
92
+ }
93
+ }
94
+ return;
95
+ }
96
+ }
97
+ view.removeObject?.(el);
98
+ }
99
+ applyDiff(prev, next) {
100
+ if (this.disposed) return;
101
+ const prevObjs = prev?.objects ?? {};
102
+ const nextObjs = next.objects;
103
+ for (const id of Object.keys(prevObjs)) {
104
+ if (!(id in nextObjs)) {
105
+ this.remove(id);
106
+ }
107
+ }
108
+ for (const id of next.order) {
109
+ const cur = nextObjs[id];
110
+ if (!cur) continue;
111
+ const old = prevObjs[id];
112
+ if (!old) {
113
+ this.create(cur);
114
+ continue;
115
+ }
116
+ if (Object.is(old, cur)) {
117
+ continue;
118
+ }
119
+ let def;
120
+ try {
121
+ def = getKind(cur.kind);
122
+ } catch {
123
+ continue;
124
+ }
125
+ const existing = this.elements.get(id);
126
+ if (def.update && existing !== void 0) {
127
+ try {
128
+ def.update(cur, old, this.ctx(), existing);
129
+ continue;
130
+ } catch (err) {
131
+ console.warn(`[scene/render] update fail, recreate id="${id}":`, err);
132
+ }
133
+ }
134
+ this.remove(id);
135
+ this.create(cur);
136
+ }
137
+ }
138
+ dispose() {
139
+ if (this.disposed) return;
140
+ this.unsubscribe();
141
+ this.disposed = true;
142
+ for (const id of Array.from(this.elements.keys())) {
143
+ this.remove(id);
144
+ }
145
+ }
146
+ listElements() {
147
+ return this.elements;
148
+ }
149
+ highlight(ids) {
150
+ if (this.disposed) return;
151
+ const newIds = new Set(
152
+ ids == null ? [] : Array.isArray(ids) ? ids : [ids]
153
+ );
154
+ for (const id of this.selectedIds) {
155
+ if (!newIds.has(id)) this.removeHalo(id);
156
+ }
157
+ for (const id of newIds) {
158
+ if (!this.selectedIds.has(id) && this.elements.has(id)) this.addHalo(id);
159
+ }
160
+ this.selectedIds = newIds;
161
+ try {
162
+ this.view.update?.();
163
+ } catch {
164
+ }
165
+ }
166
+ removeHalo(id) {
167
+ const halos = this.haloMap.get(id);
168
+ if (!halos) return;
169
+ const view = this.view;
170
+ for (const h of halos) {
171
+ try {
172
+ view.removeObject?.(h);
173
+ } catch {
174
+ }
175
+ }
176
+ this.haloMap.delete(id);
177
+ }
178
+ addHalo(id) {
179
+ const el = this.elements.get(id);
180
+ if (!el) return;
181
+ const view = this.view;
182
+ if (!view.create) return;
183
+ const SEL_STROKE = "#475569";
184
+ const SEL_FILL = "#cbd5e1";
185
+ const haloBase = {
186
+ strokeColor: SEL_STROKE,
187
+ strokeOpacity: 0.55,
188
+ fillColor: SEL_FILL,
189
+ fillOpacity: 0.3,
190
+ fixed: true,
191
+ withLabel: false,
192
+ name: "",
193
+ highlight: false,
194
+ needsRegularUpdate: true
195
+ };
196
+ const halos = [];
197
+ try {
198
+ switch (el.elType) {
199
+ case "point3d": {
200
+ const elAny = el;
201
+ const baseSize = elAny.getAttribute?.("size") ?? 4;
202
+ if (typeof elAny.X === "function" && typeof elAny.Y === "function" && typeof elAny.Z === "function") {
203
+ const halo = view.create("point3d", [
204
+ () => elAny.X?.() ?? 0,
205
+ () => elAny.Y?.() ?? 0,
206
+ () => elAny.Z?.() ?? 0
207
+ ], {
208
+ ...haloBase,
209
+ size: baseSize + 6,
210
+ face: "o",
211
+ strokeWidth: 2,
212
+ strokeOpacity: 0.75,
213
+ fillOpacity: 0.25
214
+ });
215
+ halos.push(halo);
216
+ }
217
+ break;
218
+ }
219
+ case "line3d": {
220
+ if (el.point1 && el.point2) {
221
+ const halo = view.create("line3d", [el.point1, el.point2], {
222
+ ...haloBase,
223
+ strokeWidth: 9,
224
+ straightFirst: el.getAttribute?.("straightFirst") ?? false,
225
+ straightLast: el.getAttribute?.("straightLast") ?? false
226
+ });
227
+ halos.push(halo);
228
+ }
229
+ break;
230
+ }
231
+ default:
232
+ break;
233
+ }
234
+ } catch (err) {
235
+ console.warn("[scene/render/3d] halo create fail:", err);
236
+ }
237
+ if (halos.length) this.haloMap.set(id, halos);
238
+ }
239
+ };
240
+
241
+ // src/stamps/geometry-3d/editor/theme.ts
242
+ function paletteFor2(isDark) {
243
+ const base = paletteFor(isDark);
244
+ return {
245
+ ...base,
246
+ view3dBg: isDark ? "#1a1a1a" : "#ffffff",
247
+ axisX: "#d63b3b",
248
+ axisY: "#2d8a2d",
249
+ axisZ: "#2d6dd6"
250
+ };
251
+ }
252
+ var DEFAULT_VIEW3D = {
253
+ azimuth: 0.7,
254
+ elevation: 0.4,
255
+ bbox3D: [-3, -3, -3, 3, 3, 3]
256
+ };
257
+ var VIEW3D_ATTRS = (isDark) => {
258
+ const p = paletteFor2(isDark);
259
+ const axisLabel = (color) => ({
260
+ strokeColor: color,
261
+ fontSize: 14,
262
+ offset: [10, 0]
263
+ });
264
+ return {
265
+ az: { slider: { visible: false }, point2: { visible: false } },
266
+ el: { slider: { visible: false } },
267
+ projection: "central",
268
+ // GeoGebra-style: axes pass through origin (0,0,0) instead of bbox border.
269
+ axesPosition: "center",
270
+ xAxis: {
271
+ strokeColor: p.axisX,
272
+ strokeWidth: 2,
273
+ lastArrow: { type: 2, size: 8 },
274
+ name: "x",
275
+ withLabel: true,
276
+ label: axisLabel(p.axisX)
277
+ },
278
+ yAxis: {
279
+ strokeColor: p.axisY,
280
+ strokeWidth: 2,
281
+ lastArrow: { type: 2, size: 8 },
282
+ name: "y",
283
+ withLabel: true,
284
+ label: axisLabel(p.axisY)
285
+ },
286
+ zAxis: {
287
+ strokeColor: p.axisZ,
288
+ strokeWidth: 2,
289
+ lastArrow: { type: 2, size: 8 },
290
+ name: "z",
291
+ withLabel: true,
292
+ label: axisLabel(p.axisZ)
293
+ },
294
+ // GeoGebra-style: hide ALL bbox wall planes; the XY ground plane is drawn
295
+ // explicitly at z=0 via the helper below (so it coincides with Ox/Oy).
296
+ xPlaneRear: { visible: false, mesh3d: { visible: false } },
297
+ yPlaneRear: { visible: false, mesh3d: { visible: false } },
298
+ zPlaneRear: { visible: false, mesh3d: { visible: false } }
299
+ };
300
+ };
301
+ var GROUND_PLANE_ATTRS = (isDark) => ({
302
+ fillColor: isDark ? "#2a2a2a" : "#e6e6e6",
303
+ fillOpacity: isDark ? 0.5 : 0.55,
304
+ strokeColor: isDark ? "#3a3a3a" : "#cfcfcf",
305
+ strokeOpacity: 0.7,
306
+ strokeWidth: 1,
307
+ fixed: true,
308
+ highlight: false,
309
+ withLabel: false,
310
+ layer: 0
311
+ });
312
+ var GROUND_PLANE_RANGE = [-3, 3];
313
+
314
+ // src/stamps/geometry-3d/render.ts
315
+ var OUTPUT_WIDTH = 1024;
316
+ var OUTPUT_HEIGHT = 768;
317
+ var BBOX_2D = [-6, 6, 6, -6];
318
+ async function renderGeometry3DSvgFromState(jsonState) {
319
+ const state = deserializeBoard3D(jsonState);
320
+ const view3DInfo = state.meta.domain === "3d" ? state.meta.view : DEFAULT_VIEW_3D;
321
+ const { svgString } = await renderJsxgOffscreen({
322
+ bbox: BBOX_2D,
323
+ dims: { width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT },
324
+ axis: false,
325
+ grid: false,
326
+ keepAspectRatio: true,
327
+ applyOptions: (JXG) => {
328
+ JXG.Options.text.display = "internal";
329
+ },
330
+ setup: (board) => {
331
+ const baseAttrs = VIEW3D_ATTRS(false);
332
+ const view = board.create(
333
+ "view3d",
334
+ [
335
+ [-5, -5],
336
+ [10, 10],
337
+ [
338
+ [view3DInfo.bbox3D[0], view3DInfo.bbox3D[3]],
339
+ [view3DInfo.bbox3D[1], view3DInfo.bbox3D[4]],
340
+ [view3DInfo.bbox3D[2], view3DInfo.bbox3D[5]]
341
+ ]
342
+ ],
343
+ {
344
+ ...baseAttrs,
345
+ az: { ...baseAttrs.az, slider: { ...baseAttrs.az.slider, start: view3DInfo.azimuth } },
346
+ el: { ...baseAttrs.el, slider: { ...baseAttrs.el.slider, start: view3DInfo.elevation } }
347
+ }
348
+ );
349
+ try {
350
+ const v = view;
351
+ v?.az_slide?.setValue?.(view3DInfo.azimuth);
352
+ v?.el_slide?.setValue?.(view3DInfo.elevation);
353
+ v?.board?.update?.();
354
+ } catch {
355
+ }
356
+ try {
357
+ view.create(
358
+ "plane3d",
359
+ [
360
+ [0, 0, 0],
361
+ [1, 0, 0],
362
+ [0, 1, 0],
363
+ GROUND_PLANE_RANGE,
364
+ GROUND_PLANE_RANGE
365
+ ],
366
+ GROUND_PLANE_ATTRS(false)
367
+ );
368
+ } catch {
369
+ }
370
+ const store = createStore(state);
371
+ const renderer = new JxgRenderer3D(store, view);
372
+ try {
373
+ view?.board?.update?.();
374
+ } catch {
375
+ }
376
+ return renderer;
377
+ },
378
+ postProcessSvg: (clone) => {
379
+ clone.setAttribute("width", String(OUTPUT_WIDTH));
380
+ clone.setAttribute("height", String(OUTPUT_HEIGHT));
381
+ }
382
+ });
383
+ return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
384
+ }
385
+
386
+ export { DEFAULT_VIEW3D, GROUND_PLANE_ATTRS, GROUND_PLANE_RANGE, JxgRenderer3D, VIEW3D_ATTRS, deserializeBoard3D, isGeometry3DCustomData, paletteFor2 as paletteFor, renderGeometry3DSvgFromState, serializeBoard3D };
387
+ //# sourceMappingURL=chunk-RBUILBX3.mjs.map
388
+ //# sourceMappingURL=chunk-RBUILBX3.mjs.map