@xom11/whiteboard 0.9.1 → 0.10.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/dist/{chunk-KEYZ5EZT.mjs → chunk-G7FR3AIV.mjs} +44 -5
- package/dist/chunk-G7FR3AIV.mjs.map +1 -0
- package/dist/{chunk-DU3RHKT5.mjs → chunk-PDKKDZ4H.mjs} +4 -4
- package/dist/{chunk-DU3RHKT5.mjs.map → chunk-PDKKDZ4H.mjs.map} +1 -1
- package/dist/chunk-PWIMZIB6.mjs +62 -0
- package/dist/chunk-PWIMZIB6.mjs.map +1 -0
- package/dist/chunk-WQOABS6N.mjs +197 -0
- package/dist/chunk-WQOABS6N.mjs.map +1 -0
- package/dist/geometry-2d.js +96 -12
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +2 -2
- package/dist/geometry-3d.js +152 -93
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +2 -2
- package/dist/{host-VDNAJMLC.mjs → host-DJETSFCG.mjs} +56 -12
- package/dist/host-DJETSFCG.mjs.map +1 -0
- package/dist/{host-PIIDSMVE.mjs → host-N6ACNJKI.mjs} +51 -12
- package/dist/host-N6ACNJKI.mjs.map +1 -0
- package/dist/index.d.mts +127 -1
- package/dist/index.d.ts +127 -1
- package/dist/index.js +1265 -174
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +991 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-DU2NFHRR.mjs +0 -103
- package/dist/chunk-DU2NFHRR.mjs.map +0 -1
- package/dist/chunk-IUVV52HO.mjs +0 -144
- package/dist/chunk-IUVV52HO.mjs.map +0 -1
- package/dist/chunk-KEYZ5EZT.mjs.map +0 -1
- package/dist/host-PIIDSMVE.mjs.map +0 -1
- package/dist/host-VDNAJMLC.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -145,7 +145,17 @@ function deserializeIntoBoard(board, serialized, options = {}) {
|
|
|
145
145
|
const palette = options.palette ?? paletteFor(false);
|
|
146
146
|
const idMap = /* @__PURE__ */ new Map();
|
|
147
147
|
const resolve = (a) => {
|
|
148
|
-
if (typeof a === "string"
|
|
148
|
+
if (typeof a === "string") {
|
|
149
|
+
if (idMap.has(a)) return idMap.get(a);
|
|
150
|
+
const m = /^(.+):border:(\d+)$/.exec(a);
|
|
151
|
+
if (m) {
|
|
152
|
+
const poly = idMap.get(m[1]);
|
|
153
|
+
const idx = parseInt(m[2], 10);
|
|
154
|
+
if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
|
|
155
|
+
return poly.borders[idx];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
149
159
|
if (Array.isArray(a)) return a.map(resolve);
|
|
150
160
|
return a;
|
|
151
161
|
};
|
|
@@ -193,6 +203,29 @@ var init_safeJsx = __esm({
|
|
|
193
203
|
});
|
|
194
204
|
|
|
195
205
|
// src/stamps/geometry-2d/render.ts
|
|
206
|
+
function containerDimsForBbox(bbox) {
|
|
207
|
+
const [xmin, ymax, xmax, ymin] = bbox;
|
|
208
|
+
const w = Math.abs(xmax - xmin);
|
|
209
|
+
const h = Math.abs(ymax - ymin);
|
|
210
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
211
|
+
return { width: FALLBACK_W, height: FALLBACK_H };
|
|
212
|
+
}
|
|
213
|
+
let width = w * PIXELS_PER_UNIT;
|
|
214
|
+
let height = h * PIXELS_PER_UNIT;
|
|
215
|
+
const maxAxis = Math.max(width, height);
|
|
216
|
+
if (maxAxis > MAX_DIM) {
|
|
217
|
+
const ratio = MAX_DIM / maxAxis;
|
|
218
|
+
width *= ratio;
|
|
219
|
+
height *= ratio;
|
|
220
|
+
}
|
|
221
|
+
const minAxis = Math.min(width, height);
|
|
222
|
+
if (minAxis < MIN_DIM) {
|
|
223
|
+
const ratio = MIN_DIM / minAxis;
|
|
224
|
+
width *= ratio;
|
|
225
|
+
height *= ratio;
|
|
226
|
+
}
|
|
227
|
+
return { width: Math.round(width), height: Math.round(height) };
|
|
228
|
+
}
|
|
196
229
|
async function renderGeometrySvgFromState(jsonState) {
|
|
197
230
|
const parsed = JSON.parse(jsonState);
|
|
198
231
|
const palette = paletteFor(false);
|
|
@@ -215,10 +248,11 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
215
248
|
opts.grid.strokeColor = palette.grid;
|
|
216
249
|
}
|
|
217
250
|
});
|
|
251
|
+
const { width, height } = containerDimsForBbox(parsed.bbox);
|
|
218
252
|
const container = document.createElement("div");
|
|
219
253
|
const containerId = "jxg_offscreen_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
|
|
220
254
|
container.id = containerId;
|
|
221
|
-
container.style.cssText =
|
|
255
|
+
container.style.cssText = `position:absolute;top:-99999px;left:-99999px;width:${width}px;height:${height}px;visibility:hidden;pointer-events:none;`;
|
|
222
256
|
document.body.appendChild(container);
|
|
223
257
|
let board = null;
|
|
224
258
|
try {
|
|
@@ -228,7 +262,7 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
228
262
|
grid: !!parsed.showGrid,
|
|
229
263
|
showCopyright: false,
|
|
230
264
|
showNavigation: false,
|
|
231
|
-
keepAspectRatio:
|
|
265
|
+
keepAspectRatio: true
|
|
232
266
|
});
|
|
233
267
|
deserializeIntoBoard(board, parsed, { palette });
|
|
234
268
|
board.update();
|
|
@@ -240,12 +274,18 @@ async function renderGeometrySvgFromState(jsonState) {
|
|
|
240
274
|
if (container.parentNode) container.parentNode.removeChild(container);
|
|
241
275
|
}
|
|
242
276
|
}
|
|
277
|
+
var PIXELS_PER_UNIT, MIN_DIM, MAX_DIM, FALLBACK_W, FALLBACK_H;
|
|
243
278
|
var init_render = __esm({
|
|
244
279
|
"src/stamps/geometry-2d/render.ts"() {
|
|
245
280
|
init_renderInline();
|
|
246
281
|
init_serialize();
|
|
247
282
|
init_theme();
|
|
248
283
|
init_safeJsx();
|
|
284
|
+
PIXELS_PER_UNIT = 20;
|
|
285
|
+
MIN_DIM = 100;
|
|
286
|
+
MAX_DIM = 1200;
|
|
287
|
+
FALLBACK_W = 400;
|
|
288
|
+
FALLBACK_H = 300;
|
|
249
289
|
}
|
|
250
290
|
});
|
|
251
291
|
|
|
@@ -342,8 +382,12 @@ function letterForGroup(g) {
|
|
|
342
382
|
}
|
|
343
383
|
function objKind(obj) {
|
|
344
384
|
if (!obj) return "other";
|
|
385
|
+
const ec = typeof obj.elementClass === "number" ? obj.elementClass : null;
|
|
386
|
+
if (ec === 1) return "point";
|
|
387
|
+
if (ec === 2) return "line";
|
|
388
|
+
if (ec === 3) return "circle";
|
|
345
389
|
const e = (obj.elType || obj.type || "").toString().toLowerCase();
|
|
346
|
-
if (e === "point" || e === "glider" || e === "midpoint") return "point";
|
|
390
|
+
if (e === "point" || e === "glider" || e === "midpoint" || e === "intersection" || e === "otherintersection" || e === "reflection" || e === "mirrorpoint" || e === "mirrorelement" || e === "orthogonalprojection" || e === "parallelpoint") return "point";
|
|
347
391
|
if (e === "line" || e === "segment" || e === "arrow" || e === "axis" || e === "normal" || e === "parallel" || e === "perpendicular" || e === "tangent" || e === "bisector" || e === "perpendicularsegment") return "line";
|
|
348
392
|
if (e === "circle" || e === "circumcircle") return "circle";
|
|
349
393
|
return "other";
|
|
@@ -543,7 +587,7 @@ function handleDown(ctx, e) {
|
|
|
543
587
|
if (!sc) return;
|
|
544
588
|
const [sx, sy] = sc;
|
|
545
589
|
const hits2 = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
|
|
546
|
-
const obj = hits2.find((o) => objKind(o) === "point") ??
|
|
590
|
+
const obj = hits2.find((o) => objKind(o) === "point") ?? ctx.findNearestPoint(e, 12) ?? hits2[0];
|
|
547
591
|
if (obj) {
|
|
548
592
|
const shift = !!(e.shiftKey || e.altKey);
|
|
549
593
|
ctx.toggleSelect(obj, shift);
|
|
@@ -786,7 +830,7 @@ function handleUp(ctx, e) {
|
|
|
786
830
|
const moved = Math.hypot(sx - start.sx, sy - start.sy);
|
|
787
831
|
if (moved > 4) return;
|
|
788
832
|
const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
|
|
789
|
-
const best = hits.find((o) => objKind(o) === "point") ??
|
|
833
|
+
const best = hits.find((o) => objKind(o) === "point") ?? ctx.findNearestPoint(e, 12) ?? hits[0];
|
|
790
834
|
if (!best) {
|
|
791
835
|
ctx.lastMoveClickRef.current = { obj: null, time: 0 };
|
|
792
836
|
return;
|
|
@@ -925,8 +969,16 @@ var init_MiniBoard = __esm({
|
|
|
925
969
|
const nextLocalId = React8.useCallback(() => "j" + creationLogRef.current.length, []);
|
|
926
970
|
const resolveArgs = React8.useCallback((args) => {
|
|
927
971
|
return args.map((a) => {
|
|
928
|
-
if (typeof a === "string"
|
|
929
|
-
return objMapRef.current.get(a);
|
|
972
|
+
if (typeof a === "string") {
|
|
973
|
+
if (objMapRef.current.has(a)) return objMapRef.current.get(a);
|
|
974
|
+
const m = /^(.+):border:(\d+)$/.exec(a);
|
|
975
|
+
if (m) {
|
|
976
|
+
const poly = objMapRef.current.get(m[1]);
|
|
977
|
+
const idx = parseInt(m[2], 10);
|
|
978
|
+
if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
|
|
979
|
+
return poly.borders[idx];
|
|
980
|
+
}
|
|
981
|
+
}
|
|
930
982
|
}
|
|
931
983
|
return a;
|
|
932
984
|
});
|
|
@@ -956,15 +1008,27 @@ var init_MiniBoard = __esm({
|
|
|
956
1008
|
[nextLocalId, resolveArgs, pushLog]
|
|
957
1009
|
);
|
|
958
1010
|
const localIdOf = React8.useCallback((obj) => {
|
|
1011
|
+
if (!obj) return null;
|
|
959
1012
|
for (const [id, o] of objMapRef.current.entries()) {
|
|
960
1013
|
if (o === obj) return id;
|
|
961
1014
|
}
|
|
1015
|
+
for (const [id, o] of objMapRef.current.entries()) {
|
|
1016
|
+
const borders = o?.borders;
|
|
1017
|
+
if (Array.isArray(borders)) {
|
|
1018
|
+
const idx = borders.indexOf(obj);
|
|
1019
|
+
if (idx >= 0) return `${id}:border:${idx}`;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
962
1022
|
return null;
|
|
963
1023
|
}, []);
|
|
964
1024
|
const snapshotObject = React8.useCallback((obj, anchorScreen) => {
|
|
965
1025
|
const o = obj;
|
|
966
1026
|
const k = objKind(o);
|
|
967
1027
|
if (k !== "point" && k !== "line" && k !== "circle") return null;
|
|
1028
|
+
for (const owner of objMapRef.current.values()) {
|
|
1029
|
+
const borders = owner?.borders;
|
|
1030
|
+
if (Array.isArray(borders) && borders.indexOf(o) >= 0) return null;
|
|
1031
|
+
}
|
|
968
1032
|
const v = o.visProp ?? {};
|
|
969
1033
|
const showLabel = v.withlabel !== false;
|
|
970
1034
|
const showValue = valueLabelsRef.current.has(o);
|
|
@@ -1483,7 +1547,22 @@ var init_MiniBoard = __esm({
|
|
|
1483
1547
|
const board = boardRef.current;
|
|
1484
1548
|
if (!board) return false;
|
|
1485
1549
|
const idMap = objMapRef.current;
|
|
1486
|
-
const
|
|
1550
|
+
const resolve = (a) => {
|
|
1551
|
+
if (typeof a === "string") {
|
|
1552
|
+
if (idMap.has(a)) return idMap.get(a);
|
|
1553
|
+
const m = /^(.+):border:(\d+)$/.exec(a);
|
|
1554
|
+
if (m) {
|
|
1555
|
+
const poly = idMap.get(m[1]);
|
|
1556
|
+
const idx = parseInt(m[2], 10);
|
|
1557
|
+
if (poly && Array.isArray(poly.borders) && poly.borders[idx]) {
|
|
1558
|
+
return poly.borders[idx];
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (Array.isArray(a)) return a.map(resolve);
|
|
1563
|
+
return a;
|
|
1564
|
+
};
|
|
1565
|
+
const resolved = el.args.map(resolve);
|
|
1487
1566
|
try {
|
|
1488
1567
|
if (el.type === "valueLabel") {
|
|
1489
1568
|
const target = resolved[0];
|
|
@@ -2997,6 +3076,7 @@ var init_EditorPanel = __esm({
|
|
|
2997
3076
|
function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer, onUndo, onRedo, canUndo, canRedo }, ref) {
|
|
2998
3077
|
const handleRef = React8.useRef(null);
|
|
2999
3078
|
const [ready, setReady] = React8.useState(false);
|
|
3079
|
+
const [hasContent, setHasContent] = React8.useState(false);
|
|
3000
3080
|
const [propsPopover, setPropsPopover] = React8.useState(null);
|
|
3001
3081
|
const [transformPopover, setTransformPopover] = React8.useState(null);
|
|
3002
3082
|
const onStateChangeRef = React8.useRef(onStateChange);
|
|
@@ -3005,8 +3085,10 @@ var init_EditorPanel = __esm({
|
|
|
3005
3085
|
}, [onStateChange]);
|
|
3006
3086
|
const emitState = React8.useCallback(() => {
|
|
3007
3087
|
const h = handleRef.current;
|
|
3088
|
+
if (!h) return;
|
|
3089
|
+
setHasContent(h.getCreationLog().length > 0);
|
|
3008
3090
|
const cb = onStateChangeRef.current;
|
|
3009
|
-
if (!
|
|
3091
|
+
if (!cb) return;
|
|
3010
3092
|
cb({
|
|
3011
3093
|
tool: h.getTool(),
|
|
3012
3094
|
showAxis: h.getShowAxis(),
|
|
@@ -3136,7 +3218,8 @@ var init_EditorPanel = __esm({
|
|
|
3136
3218
|
{
|
|
3137
3219
|
type: "button",
|
|
3138
3220
|
onClick: handleInsert,
|
|
3139
|
-
disabled: !ready,
|
|
3221
|
+
disabled: !ready || !hasContent,
|
|
3222
|
+
title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
|
|
3140
3223
|
"data-testid": "geometry-insert-btn-mobile",
|
|
3141
3224
|
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
3142
3225
|
children: "Ch\xE8n"
|
|
@@ -3236,7 +3319,8 @@ var init_EditorPanel = __esm({
|
|
|
3236
3319
|
"button",
|
|
3237
3320
|
{
|
|
3238
3321
|
onClick: handleInsert,
|
|
3239
|
-
disabled: !ready,
|
|
3322
|
+
disabled: !ready || !hasContent,
|
|
3323
|
+
title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
|
|
3240
3324
|
"data-testid": "geometry-insert-btn",
|
|
3241
3325
|
className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
|
|
3242
3326
|
children: "Ch\xE8n"
|
|
@@ -4213,6 +4297,108 @@ var init_theme2 = __esm({
|
|
|
4213
4297
|
}
|
|
4214
4298
|
});
|
|
4215
4299
|
|
|
4300
|
+
// src/stamps/geometry-3d/render.ts
|
|
4301
|
+
async function renderGeometry3DSvgFromState(jsonState) {
|
|
4302
|
+
const state = parseSerializedBoard3D(jsonState);
|
|
4303
|
+
const JXG = (await import('jsxgraph')).default;
|
|
4304
|
+
const div = document.createElement("div");
|
|
4305
|
+
div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
|
|
4306
|
+
document.body.appendChild(div);
|
|
4307
|
+
try {
|
|
4308
|
+
JXG.Options.text.display = "internal";
|
|
4309
|
+
const board = JXG.JSXGraph.initBoard(div, {
|
|
4310
|
+
boundingbox: state.bbox,
|
|
4311
|
+
keepaspectratio: true,
|
|
4312
|
+
axis: false,
|
|
4313
|
+
showCopyright: false,
|
|
4314
|
+
showNavigation: false,
|
|
4315
|
+
renderer: "svg"
|
|
4316
|
+
});
|
|
4317
|
+
const baseAttrs = VIEW3D_ATTRS(false);
|
|
4318
|
+
const view = board.create(
|
|
4319
|
+
"view3d",
|
|
4320
|
+
[
|
|
4321
|
+
[-5, -5],
|
|
4322
|
+
[10, 10],
|
|
4323
|
+
[
|
|
4324
|
+
[state.view.bbox3D[0], state.view.bbox3D[3]],
|
|
4325
|
+
[state.view.bbox3D[1], state.view.bbox3D[4]],
|
|
4326
|
+
[state.view.bbox3D[2], state.view.bbox3D[5]]
|
|
4327
|
+
]
|
|
4328
|
+
],
|
|
4329
|
+
{
|
|
4330
|
+
...baseAttrs,
|
|
4331
|
+
// JSXGraph view3d đọc azimuth/elevation từ az.slider.start (không phải
|
|
4332
|
+
// az.value). Nếu pass `value` → JSXGraph bỏ qua → render rơi về default
|
|
4333
|
+
// (1.0 rad / 0.3 rad), không khớp góc user xoay trong editor.
|
|
4334
|
+
az: { ...baseAttrs.az, slider: { ...baseAttrs.az.slider, start: state.view.azimuth } },
|
|
4335
|
+
el: { ...baseAttrs.el, slider: { ...baseAttrs.el.slider, start: state.view.elevation } }
|
|
4336
|
+
}
|
|
4337
|
+
);
|
|
4338
|
+
try {
|
|
4339
|
+
const v = view;
|
|
4340
|
+
v?.az_slide?.setValue?.(state.view.azimuth);
|
|
4341
|
+
v?.el_slide?.setValue?.(state.view.elevation);
|
|
4342
|
+
v?.board?.update?.();
|
|
4343
|
+
} catch {
|
|
4344
|
+
}
|
|
4345
|
+
if (!state.showAxes) {
|
|
4346
|
+
view.defaultAxes = [];
|
|
4347
|
+
}
|
|
4348
|
+
try {
|
|
4349
|
+
view.create(
|
|
4350
|
+
"plane3d",
|
|
4351
|
+
[
|
|
4352
|
+
[0, 0, 0],
|
|
4353
|
+
[1, 0, 0],
|
|
4354
|
+
[0, 1, 0],
|
|
4355
|
+
GROUND_PLANE_RANGE,
|
|
4356
|
+
GROUND_PLANE_RANGE
|
|
4357
|
+
],
|
|
4358
|
+
GROUND_PLANE_ATTRS(false)
|
|
4359
|
+
);
|
|
4360
|
+
} catch {
|
|
4361
|
+
}
|
|
4362
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
4363
|
+
for (const el of state.elements) {
|
|
4364
|
+
const parents = el.parents.map(
|
|
4365
|
+
(p) => typeof p === "string" && p.startsWith("@id:") ? idMap.get(p.slice(4)) : p
|
|
4366
|
+
);
|
|
4367
|
+
const obj = view.create(el.type, parents, {
|
|
4368
|
+
...el.attributes,
|
|
4369
|
+
id: el.id,
|
|
4370
|
+
name: el.label
|
|
4371
|
+
});
|
|
4372
|
+
idMap.set(el.id, obj);
|
|
4373
|
+
}
|
|
4374
|
+
const svg = div.querySelector("svg");
|
|
4375
|
+
if (!svg) {
|
|
4376
|
+
throw new Error("renderGeometry3DSvgFromState: SVG not produced");
|
|
4377
|
+
}
|
|
4378
|
+
const clone = svg.cloneNode(true);
|
|
4379
|
+
clone.setAttribute("width", String(OUTPUT_WIDTH));
|
|
4380
|
+
clone.setAttribute("height", String(OUTPUT_HEIGHT));
|
|
4381
|
+
const svgString = new XMLSerializer().serializeToString(clone);
|
|
4382
|
+
try {
|
|
4383
|
+
JXG.JSXGraph.freeBoard(board);
|
|
4384
|
+
} catch {
|
|
4385
|
+
}
|
|
4386
|
+
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
4387
|
+
} finally {
|
|
4388
|
+
document.body.removeChild(div);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
var OUTPUT_WIDTH, OUTPUT_HEIGHT;
|
|
4392
|
+
var init_render3 = __esm({
|
|
4393
|
+
"src/stamps/geometry-3d/render.ts"() {
|
|
4394
|
+
"use client";
|
|
4395
|
+
init_serialize2();
|
|
4396
|
+
init_theme2();
|
|
4397
|
+
OUTPUT_WIDTH = 1024;
|
|
4398
|
+
OUTPUT_HEIGHT = 768;
|
|
4399
|
+
}
|
|
4400
|
+
});
|
|
4401
|
+
|
|
4216
4402
|
// src/stamps/geometry-3d/editor/tools/handlers/_ensurePoint.ts
|
|
4217
4403
|
function hitToConstraint(hit) {
|
|
4218
4404
|
switch (hit.kind) {
|
|
@@ -5614,8 +5800,11 @@ var init_MiniBoard3D = __esm({
|
|
|
5614
5800
|
],
|
|
5615
5801
|
{
|
|
5616
5802
|
...baseAttrs,
|
|
5617
|
-
|
|
5618
|
-
|
|
5803
|
+
// JSXGraph view3d đọc giá trị khởi tạo từ az.slider.start (không
|
|
5804
|
+
// phải az.value). Pass nhầm `value` → JSXGraph dùng default
|
|
5805
|
+
// 1.0/0.3, khiến DEFAULT_VIEW3D bị bỏ qua.
|
|
5806
|
+
az: { ...baseAttrs.az, slider: { ...baseAttrs.az.slider, start: DEFAULT_VIEW3D.azimuth } },
|
|
5807
|
+
el: { ...baseAttrs.el, slider: { ...baseAttrs.el.slider, start: DEFAULT_VIEW3D.elevation } }
|
|
5619
5808
|
}
|
|
5620
5809
|
);
|
|
5621
5810
|
} catch {
|
|
@@ -6252,6 +6441,7 @@ var init_EditorPanel2 = __esm({
|
|
|
6252
6441
|
init_ensurePoint();
|
|
6253
6442
|
init_MiniBoard3D();
|
|
6254
6443
|
init_StatusHint();
|
|
6444
|
+
init_theme2();
|
|
6255
6445
|
init_persistence();
|
|
6256
6446
|
EditorPanel = React8__namespace.forwardRef(
|
|
6257
6447
|
function EditorPanel2(props, ref) {
|
|
@@ -6357,8 +6547,17 @@ var init_EditorPanel2 = __esm({
|
|
|
6357
6547
|
}, [showAxis, showGrid]);
|
|
6358
6548
|
const handleView3DReady = React8__namespace.useCallback((view) => {
|
|
6359
6549
|
rendererRef.current = new JxgRenderer(scene, view);
|
|
6550
|
+
if (initialState) {
|
|
6551
|
+
try {
|
|
6552
|
+
const v = view;
|
|
6553
|
+
v?.az_slide?.setValue?.(initialState.view.azimuth);
|
|
6554
|
+
v?.el_slide?.setValue?.(initialState.view.elevation);
|
|
6555
|
+
v?.board?.update?.();
|
|
6556
|
+
} catch {
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6360
6559
|
onReadyChange?.(true);
|
|
6361
|
-
}, [onReadyChange, scene]);
|
|
6560
|
+
}, [onReadyChange, scene, initialState]);
|
|
6362
6561
|
const handleClick = React8__namespace.useCallback((screen) => {
|
|
6363
6562
|
const board = boardRef.current;
|
|
6364
6563
|
if (!board) return;
|
|
@@ -6496,8 +6695,10 @@ var init_EditorPanel2 = __esm({
|
|
|
6496
6695
|
const elevation = typeof elSlider?.Value === "function" ? elSlider.Value() : 0;
|
|
6497
6696
|
return sceneToBoard(
|
|
6498
6697
|
scene,
|
|
6499
|
-
{ azimuth, elevation, bbox3D: [
|
|
6500
|
-
[
|
|
6698
|
+
{ azimuth, elevation, bbox3D: [...DEFAULT_VIEW3D.bbox3D] },
|
|
6699
|
+
// JSXGraph boundingbox order: [xmin, ymax, xmax, ymin]. Must match
|
|
6700
|
+
// MiniBoard3D.initBoard so render reproduces the editor's view.
|
|
6701
|
+
[-6, 6, 6, -6]
|
|
6501
6702
|
);
|
|
6502
6703
|
},
|
|
6503
6704
|
setTool: (k) => controllerRef.current.selectTool(k),
|
|
@@ -7406,6 +7607,7 @@ var init_host3 = __esm({
|
|
|
7406
7607
|
init_useChordShortcut();
|
|
7407
7608
|
init_insertImage();
|
|
7408
7609
|
init_useIsMobile();
|
|
7610
|
+
init_render3();
|
|
7409
7611
|
init_serialize2();
|
|
7410
7612
|
Geometry3DStampHost = React8.forwardRef(
|
|
7411
7613
|
function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
@@ -7420,10 +7622,25 @@ var init_host3 = __esm({
|
|
|
7420
7622
|
const [showGrid, setShowGrid] = React8.useState(true);
|
|
7421
7623
|
const [canUndo, setCanUndo] = React8.useState(false);
|
|
7422
7624
|
const [canRedo, setCanRedo] = React8.useState(false);
|
|
7625
|
+
const [hasContent, setHasContent] = React8.useState(false);
|
|
7423
7626
|
const handleHistoryChange = React8.useCallback((u, r) => {
|
|
7424
7627
|
setCanUndo(u);
|
|
7425
7628
|
setCanRedo(r);
|
|
7426
7629
|
}, []);
|
|
7630
|
+
React8.useEffect(() => {
|
|
7631
|
+
const scene = sceneRef.current;
|
|
7632
|
+
if (!scene) return;
|
|
7633
|
+
const sync = () => setHasContent(scene.list().length > 0);
|
|
7634
|
+
sync();
|
|
7635
|
+
const unsubs = [
|
|
7636
|
+
scene.on("add", sync),
|
|
7637
|
+
scene.on("delete", sync),
|
|
7638
|
+
scene.on("reset", sync)
|
|
7639
|
+
];
|
|
7640
|
+
return () => {
|
|
7641
|
+
for (const u of unsubs) u();
|
|
7642
|
+
};
|
|
7643
|
+
}, []);
|
|
7427
7644
|
const handleUndo = React8.useCallback(() => {
|
|
7428
7645
|
editorRef.current?.undo();
|
|
7429
7646
|
}, []);
|
|
@@ -7471,7 +7688,15 @@ var init_host3 = __esm({
|
|
|
7471
7688
|
if (!editorRef.current.hasContent()) return false;
|
|
7472
7689
|
const board = editorRef.current.serialize();
|
|
7473
7690
|
if (board.elements.length === 0) return false;
|
|
7474
|
-
void
|
|
7691
|
+
void (async () => {
|
|
7692
|
+
try {
|
|
7693
|
+
const jsonState = serializeBoard3D(board);
|
|
7694
|
+
const { svgString, width, height } = await renderGeometry3DSvgFromState(jsonState);
|
|
7695
|
+
await performInsert(board, width, height, svgString);
|
|
7696
|
+
} catch (err) {
|
|
7697
|
+
console.error("Geometry3D insert failed:", err);
|
|
7698
|
+
}
|
|
7699
|
+
})();
|
|
7475
7700
|
return true;
|
|
7476
7701
|
}, [performInsert]);
|
|
7477
7702
|
React8.useImperativeHandle(
|
|
@@ -7553,7 +7778,8 @@ var init_host3 = __esm({
|
|
|
7553
7778
|
{
|
|
7554
7779
|
type: "button",
|
|
7555
7780
|
onClick: tryInsert,
|
|
7556
|
-
disabled: !ready,
|
|
7781
|
+
disabled: !ready || !hasContent,
|
|
7782
|
+
title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
|
|
7557
7783
|
"data-testid": "geom3d-insert-btn-mobile",
|
|
7558
7784
|
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
7559
7785
|
children: "Ch\xE8n"
|
|
@@ -7603,7 +7829,8 @@ var init_host3 = __esm({
|
|
|
7603
7829
|
"button",
|
|
7604
7830
|
{
|
|
7605
7831
|
onClick: tryInsert,
|
|
7606
|
-
disabled: !ready,
|
|
7832
|
+
disabled: !ready || !hasContent,
|
|
7833
|
+
title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
|
|
7607
7834
|
"data-testid": "geom3d-insert-btn",
|
|
7608
7835
|
className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
|
|
7609
7836
|
children: "Ch\xE8n"
|
|
@@ -8276,7 +8503,7 @@ async function renderGraph2dSvgFromState(jsonState) {
|
|
|
8276
8503
|
if (container.parentNode) container.parentNode.removeChild(container);
|
|
8277
8504
|
}
|
|
8278
8505
|
}
|
|
8279
|
-
var
|
|
8506
|
+
var init_render4 = __esm({
|
|
8280
8507
|
"src/stamps/graph-2d/render.ts"() {
|
|
8281
8508
|
init_serialize3();
|
|
8282
8509
|
init_renderObjects();
|
|
@@ -8964,7 +9191,7 @@ var init_EditorPanel3 = __esm({
|
|
|
8964
9191
|
init_MiniBoard2();
|
|
8965
9192
|
init_serialize3();
|
|
8966
9193
|
init_parser();
|
|
8967
|
-
|
|
9194
|
+
init_render4();
|
|
8968
9195
|
init_colors();
|
|
8969
9196
|
init_handlers2();
|
|
8970
9197
|
GraphEditorPanel = React8.forwardRef(function GraphEditorPanel2(props, ref) {
|
|
@@ -9539,91 +9766,7 @@ var latexStamp = {
|
|
|
9539
9766
|
|
|
9540
9767
|
// src/stamps/geometry-3d/index.tsx
|
|
9541
9768
|
init_serialize2();
|
|
9542
|
-
|
|
9543
|
-
// src/stamps/geometry-3d/render.ts
|
|
9544
|
-
init_serialize2();
|
|
9545
|
-
init_theme2();
|
|
9546
|
-
var OUTPUT_WIDTH = 1024;
|
|
9547
|
-
var OUTPUT_HEIGHT = 768;
|
|
9548
|
-
async function renderGeometry3DSvgFromState(jsonState) {
|
|
9549
|
-
const state = parseSerializedBoard3D(jsonState);
|
|
9550
|
-
const JXG = (await import('jsxgraph')).default;
|
|
9551
|
-
const div = document.createElement("div");
|
|
9552
|
-
div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
|
|
9553
|
-
document.body.appendChild(div);
|
|
9554
|
-
try {
|
|
9555
|
-
JXG.Options.text.display = "internal";
|
|
9556
|
-
const board = JXG.JSXGraph.initBoard(div, {
|
|
9557
|
-
boundingbox: state.bbox,
|
|
9558
|
-
axis: false,
|
|
9559
|
-
showCopyright: false,
|
|
9560
|
-
showNavigation: false,
|
|
9561
|
-
renderer: "svg"
|
|
9562
|
-
});
|
|
9563
|
-
const baseAttrs = VIEW3D_ATTRS(false);
|
|
9564
|
-
const view = board.create(
|
|
9565
|
-
"view3d",
|
|
9566
|
-
[
|
|
9567
|
-
[-5, -5],
|
|
9568
|
-
[10, 10],
|
|
9569
|
-
[
|
|
9570
|
-
[state.view.bbox3D[0], state.view.bbox3D[3]],
|
|
9571
|
-
[state.view.bbox3D[1], state.view.bbox3D[4]],
|
|
9572
|
-
[state.view.bbox3D[2], state.view.bbox3D[5]]
|
|
9573
|
-
]
|
|
9574
|
-
],
|
|
9575
|
-
{
|
|
9576
|
-
...baseAttrs,
|
|
9577
|
-
az: { ...baseAttrs.az, value: state.view.azimuth },
|
|
9578
|
-
el: { ...baseAttrs.el, value: state.view.elevation }
|
|
9579
|
-
}
|
|
9580
|
-
);
|
|
9581
|
-
if (!state.showAxes) {
|
|
9582
|
-
view.defaultAxes = [];
|
|
9583
|
-
}
|
|
9584
|
-
try {
|
|
9585
|
-
view.create(
|
|
9586
|
-
"plane3d",
|
|
9587
|
-
[
|
|
9588
|
-
[0, 0, 0],
|
|
9589
|
-
[1, 0, 0],
|
|
9590
|
-
[0, 1, 0],
|
|
9591
|
-
GROUND_PLANE_RANGE,
|
|
9592
|
-
GROUND_PLANE_RANGE
|
|
9593
|
-
],
|
|
9594
|
-
GROUND_PLANE_ATTRS(false)
|
|
9595
|
-
);
|
|
9596
|
-
} catch {
|
|
9597
|
-
}
|
|
9598
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
9599
|
-
for (const el of state.elements) {
|
|
9600
|
-
const parents = el.parents.map(
|
|
9601
|
-
(p) => typeof p === "string" && p.startsWith("@id:") ? idMap.get(p.slice(4)) : p
|
|
9602
|
-
);
|
|
9603
|
-
const obj = view.create(el.type, parents, {
|
|
9604
|
-
...el.attributes,
|
|
9605
|
-
id: el.id,
|
|
9606
|
-
name: el.label
|
|
9607
|
-
});
|
|
9608
|
-
idMap.set(el.id, obj);
|
|
9609
|
-
}
|
|
9610
|
-
const svg = div.querySelector("svg");
|
|
9611
|
-
if (!svg) {
|
|
9612
|
-
throw new Error("renderGeometry3DSvgFromState: SVG not produced");
|
|
9613
|
-
}
|
|
9614
|
-
const clone = svg.cloneNode(true);
|
|
9615
|
-
clone.setAttribute("width", String(OUTPUT_WIDTH));
|
|
9616
|
-
clone.setAttribute("height", String(OUTPUT_HEIGHT));
|
|
9617
|
-
const svgString = new XMLSerializer().serializeToString(clone);
|
|
9618
|
-
try {
|
|
9619
|
-
JXG.JSXGraph.freeBoard(board);
|
|
9620
|
-
} catch {
|
|
9621
|
-
}
|
|
9622
|
-
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
9623
|
-
} finally {
|
|
9624
|
-
document.body.removeChild(div);
|
|
9625
|
-
}
|
|
9626
|
-
}
|
|
9769
|
+
init_render3();
|
|
9627
9770
|
var Geometry3DStampHost3 = React8.lazy(
|
|
9628
9771
|
() => Promise.resolve().then(() => (init_host3(), host_exports3)).then((m) => ({ default: m.Geometry3DStampHost }))
|
|
9629
9772
|
);
|
|
@@ -9679,7 +9822,7 @@ var geometry3dStamp = {
|
|
|
9679
9822
|
};
|
|
9680
9823
|
|
|
9681
9824
|
// src/stamps/graph-2d/index.tsx
|
|
9682
|
-
|
|
9825
|
+
init_render4();
|
|
9683
9826
|
init_types3();
|
|
9684
9827
|
var Graph2DStampHost3 = React8.lazy(
|
|
9685
9828
|
() => Promise.resolve().then(() => (init_host4(), host_exports4)).then((m) => ({ default: m.Graph2DStampHost }))
|
|
@@ -9846,20 +9989,28 @@ function ToolbarInjector({
|
|
|
9846
9989
|
};
|
|
9847
9990
|
return reactDom.createPortal(
|
|
9848
9991
|
/* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9849
|
-
stamps.map((stamp) =>
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9992
|
+
stamps.map((stamp) => {
|
|
9993
|
+
const { displayLabel, shortcut } = splitTitleAndShortcut(
|
|
9994
|
+
stamp.toolbarTitle,
|
|
9995
|
+
stamp.toolbarLabel
|
|
9996
|
+
);
|
|
9997
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
9998
|
+
StampMenuItem,
|
|
9999
|
+
{
|
|
10000
|
+
icon: stamp.toolbarIcon,
|
|
10001
|
+
label: displayLabel,
|
|
10002
|
+
ariaLabel: stamp.toolbarTitle,
|
|
10003
|
+
shortcut,
|
|
10004
|
+
active: activeStampKind === stamp.kind,
|
|
10005
|
+
onClick: () => {
|
|
10006
|
+
onToggle(stamp.kind);
|
|
10007
|
+
closePopover();
|
|
10008
|
+
},
|
|
10009
|
+
dataTestId: stamp.toolbarTestId
|
|
9858
10010
|
},
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
)),
|
|
10011
|
+
stamp.kind
|
|
10012
|
+
);
|
|
10013
|
+
}),
|
|
9863
10014
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9864
10015
|
"div",
|
|
9865
10016
|
{
|
|
@@ -9875,7 +10026,22 @@ function ToolbarInjector({
|
|
|
9875
10026
|
menuMount
|
|
9876
10027
|
);
|
|
9877
10028
|
}
|
|
9878
|
-
function
|
|
10029
|
+
function splitTitleAndShortcut(title, fallbackShortcut) {
|
|
10030
|
+
const match = title.match(/^(.*?)\s*\(([^()]+)\)\s*$/);
|
|
10031
|
+
if (match) {
|
|
10032
|
+
return { displayLabel: match[1].trim(), shortcut: match[2].trim() };
|
|
10033
|
+
}
|
|
10034
|
+
return { displayLabel: title, shortcut: fallbackShortcut };
|
|
10035
|
+
}
|
|
10036
|
+
function StampMenuItem({
|
|
10037
|
+
icon,
|
|
10038
|
+
label,
|
|
10039
|
+
ariaLabel,
|
|
10040
|
+
shortcut,
|
|
10041
|
+
active,
|
|
10042
|
+
onClick,
|
|
10043
|
+
dataTestId
|
|
10044
|
+
}) {
|
|
9879
10045
|
const className = [
|
|
9880
10046
|
"dropdown-menu-item",
|
|
9881
10047
|
"dropdown-menu-item-base",
|
|
@@ -9886,39 +10052,15 @@ function StampMenuItem({ icon, label, active, onClick, dataTestId }) {
|
|
|
9886
10052
|
{
|
|
9887
10053
|
type: "button",
|
|
9888
10054
|
onClick,
|
|
9889
|
-
|
|
10055
|
+
title: ariaLabel,
|
|
10056
|
+
"aria-label": ariaLabel,
|
|
9890
10057
|
"aria-pressed": active,
|
|
9891
10058
|
"data-testid": dataTestId,
|
|
9892
10059
|
className,
|
|
9893
|
-
style: {
|
|
9894
|
-
display: "flex",
|
|
9895
|
-
alignItems: "center",
|
|
9896
|
-
columnGap: "0.625rem",
|
|
9897
|
-
width: "100%",
|
|
9898
|
-
boxSizing: "border-box",
|
|
9899
|
-
background: "transparent",
|
|
9900
|
-
border: "1px solid transparent",
|
|
9901
|
-
cursor: "pointer",
|
|
9902
|
-
fontFamily: "inherit",
|
|
9903
|
-
fontSize: "0.875rem",
|
|
9904
|
-
color: "var(--color-on-surface)"
|
|
9905
|
-
},
|
|
9906
10060
|
children: [
|
|
9907
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
"aria-hidden": "true",
|
|
9911
|
-
style: {
|
|
9912
|
-
display: "inline-flex",
|
|
9913
|
-
alignItems: "center",
|
|
9914
|
-
justifyContent: "center",
|
|
9915
|
-
width: "1rem",
|
|
9916
|
-
height: "1rem"
|
|
9917
|
-
},
|
|
9918
|
-
children: icon
|
|
9919
|
-
}
|
|
9920
|
-
),
|
|
9921
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: label })
|
|
10061
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dropdown-menu-item__icon", "aria-hidden": "true", children: icon }),
|
|
10062
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dropdown-menu-item__text", children: label }),
|
|
10063
|
+
shortcut ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "dropdown-menu-item__shortcut", children: shortcut }) : null
|
|
9922
10064
|
]
|
|
9923
10065
|
}
|
|
9924
10066
|
);
|
|
@@ -9952,29 +10094,866 @@ function useShortcuts({
|
|
|
9952
10094
|
return () => window.removeEventListener("keydown", handler, { capture: true });
|
|
9953
10095
|
}, [enabled, onToggle, stamps]);
|
|
9954
10096
|
}
|
|
9955
|
-
var
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
10097
|
+
var WRAPPER_ID = "pdf-import-portal-wrapper";
|
|
10098
|
+
var POPOVER_SELECTOR2 = ".App-toolbar__extra-tools-dropdown .dropdown-menu-container";
|
|
10099
|
+
function PdfImporterButton({ enabled, onPick }) {
|
|
10100
|
+
const [mount, setMount] = React8.useState(null);
|
|
10101
|
+
const mountRef = React8.useRef(null);
|
|
10102
|
+
const inputRef = React8.useRef(null);
|
|
10103
|
+
React8.useEffect(() => {
|
|
10104
|
+
if (!enabled) {
|
|
10105
|
+
mountRef.current = null;
|
|
10106
|
+
setMount(null);
|
|
10107
|
+
document.getElementById(WRAPPER_ID)?.remove();
|
|
10108
|
+
return;
|
|
10109
|
+
}
|
|
10110
|
+
let cancelled = false;
|
|
10111
|
+
let observer = null;
|
|
10112
|
+
let rafId = null;
|
|
10113
|
+
let observedRoot = null;
|
|
10114
|
+
const apply = (next) => {
|
|
10115
|
+
if (cancelled || mountRef.current === next) return;
|
|
10116
|
+
mountRef.current = next;
|
|
10117
|
+
queueMicrotask(() => {
|
|
10118
|
+
if (!cancelled) setMount(next);
|
|
10119
|
+
});
|
|
10120
|
+
};
|
|
10121
|
+
const findMenu = () => {
|
|
10122
|
+
if (cancelled) return;
|
|
10123
|
+
const container = document.querySelector(POPOVER_SELECTOR2);
|
|
10124
|
+
if (!container) {
|
|
10125
|
+
apply(null);
|
|
10126
|
+
return;
|
|
10127
|
+
}
|
|
10128
|
+
let wrapper = container.querySelector("#" + WRAPPER_ID);
|
|
10129
|
+
if (!wrapper) {
|
|
10130
|
+
wrapper = document.createElement("div");
|
|
10131
|
+
wrapper.id = WRAPPER_ID;
|
|
10132
|
+
wrapper.setAttribute("data-pdf-import", "true");
|
|
10133
|
+
wrapper.style.display = "contents";
|
|
10134
|
+
container.appendChild(wrapper);
|
|
10135
|
+
}
|
|
10136
|
+
apply(wrapper);
|
|
10137
|
+
};
|
|
10138
|
+
const attachObserver = () => {
|
|
10139
|
+
if (cancelled) return;
|
|
10140
|
+
const excalidraw = document.querySelector(".excalidraw");
|
|
10141
|
+
const nextRoot = excalidraw ?? document.body;
|
|
10142
|
+
if (observedRoot === nextRoot) return;
|
|
10143
|
+
observer?.disconnect();
|
|
10144
|
+
observedRoot = nextRoot;
|
|
10145
|
+
observer = new MutationObserver(onMutation);
|
|
10146
|
+
observer.observe(nextRoot, { childList: true, subtree: true });
|
|
10147
|
+
};
|
|
10148
|
+
const onMutation = () => {
|
|
10149
|
+
if (rafId != null) return;
|
|
10150
|
+
rafId = requestAnimationFrame(() => {
|
|
10151
|
+
rafId = null;
|
|
10152
|
+
if (cancelled) return;
|
|
10153
|
+
if (observedRoot !== document.querySelector(".excalidraw")) {
|
|
10154
|
+
attachObserver();
|
|
10155
|
+
}
|
|
10156
|
+
findMenu();
|
|
10157
|
+
});
|
|
10158
|
+
};
|
|
10159
|
+
findMenu();
|
|
10160
|
+
attachObserver();
|
|
10161
|
+
return () => {
|
|
10162
|
+
cancelled = true;
|
|
10163
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
10164
|
+
observer?.disconnect();
|
|
10165
|
+
document.getElementById(WRAPPER_ID)?.remove();
|
|
10166
|
+
};
|
|
10167
|
+
}, [enabled]);
|
|
10168
|
+
const closePopover = () => {
|
|
10169
|
+
const trigger = document.querySelector(
|
|
10170
|
+
".App-toolbar__extra-tools-trigger"
|
|
10171
|
+
);
|
|
10172
|
+
trigger?.click();
|
|
10173
|
+
};
|
|
10174
|
+
const handleClick = () => {
|
|
10175
|
+
inputRef.current?.click();
|
|
10176
|
+
};
|
|
10177
|
+
const handleFileChange = (e) => {
|
|
10178
|
+
const file = e.target.files?.[0];
|
|
10179
|
+
if (file) onPick(file);
|
|
10180
|
+
e.target.value = "";
|
|
10181
|
+
closePopover();
|
|
10182
|
+
};
|
|
10183
|
+
if (!enabled || !mount) {
|
|
10184
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
10185
|
+
"input",
|
|
10186
|
+
{
|
|
10187
|
+
ref: inputRef,
|
|
10188
|
+
type: "file",
|
|
10189
|
+
accept: "application/pdf,.pdf",
|
|
10190
|
+
style: { display: "none" },
|
|
10191
|
+
onChange: handleFileChange
|
|
10192
|
+
}
|
|
10193
|
+
);
|
|
10194
|
+
}
|
|
10195
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10196
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10197
|
+
"input",
|
|
10198
|
+
{
|
|
10199
|
+
ref: inputRef,
|
|
10200
|
+
type: "file",
|
|
10201
|
+
accept: "application/pdf,.pdf",
|
|
10202
|
+
style: { display: "none" },
|
|
10203
|
+
onChange: handleFileChange
|
|
10204
|
+
}
|
|
10205
|
+
),
|
|
10206
|
+
reactDom.createPortal(
|
|
10207
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10208
|
+
"button",
|
|
10209
|
+
{
|
|
10210
|
+
type: "button",
|
|
10211
|
+
onClick: handleClick,
|
|
10212
|
+
title: "Ch\xE8n PDF (P)",
|
|
10213
|
+
"aria-label": "Ch\xE8n PDF",
|
|
10214
|
+
"data-testid": "pdf-import-button",
|
|
10215
|
+
className: "dropdown-menu-item dropdown-menu-item-base",
|
|
10216
|
+
children: [
|
|
10217
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dropdown-menu-item__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(PdfIcon, {}) }),
|
|
10218
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dropdown-menu-item__text", children: "Ch\xE8n PDF" }),
|
|
10219
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "dropdown-menu-item__shortcut", children: "P" })
|
|
10220
|
+
]
|
|
10221
|
+
}
|
|
10222
|
+
),
|
|
10223
|
+
mount
|
|
10224
|
+
)
|
|
10225
|
+
] });
|
|
10226
|
+
}
|
|
10227
|
+
function PdfIcon() {
|
|
10228
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10229
|
+
"svg",
|
|
10230
|
+
{
|
|
10231
|
+
width: "18",
|
|
10232
|
+
height: "18",
|
|
10233
|
+
viewBox: "0 0 24 24",
|
|
10234
|
+
fill: "none",
|
|
10235
|
+
stroke: "currentColor",
|
|
10236
|
+
strokeWidth: "1.6",
|
|
10237
|
+
strokeLinecap: "round",
|
|
10238
|
+
strokeLinejoin: "round",
|
|
10239
|
+
"aria-hidden": "true",
|
|
10240
|
+
children: [
|
|
10241
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
|
|
10242
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3v5h5" }),
|
|
10243
|
+
/* @__PURE__ */ jsxRuntime.jsx("text", { x: "7.5", y: "17", fontSize: "6", fontFamily: "sans-serif", fontWeight: "700", stroke: "none", fill: "currentColor", children: "PDF" })
|
|
10244
|
+
]
|
|
10245
|
+
}
|
|
10246
|
+
);
|
|
10247
|
+
}
|
|
10248
|
+
|
|
10249
|
+
// src/pdf/parseRange.ts
|
|
10250
|
+
function parsePageRange(input, totalPages) {
|
|
10251
|
+
if (!Number.isInteger(totalPages) || totalPages <= 0) {
|
|
10252
|
+
throw new Error("S\u1ED1 trang ph\u1EA3i l\xE0 s\u1ED1 nguy\xEAn d\u01B0\u01A1ng.");
|
|
10253
|
+
}
|
|
10254
|
+
const trimmed = input.trim();
|
|
10255
|
+
if (trimmed === "") return [];
|
|
10256
|
+
const tokens = trimmed.split(/[,\s]+/).map((t) => t.trim()).filter((t) => t.length > 0);
|
|
10257
|
+
const set = /* @__PURE__ */ new Set();
|
|
10258
|
+
for (const token of tokens) {
|
|
10259
|
+
if (token.includes("-")) {
|
|
10260
|
+
const parts = token.split("-");
|
|
10261
|
+
if (parts.length !== 2) {
|
|
10262
|
+
throw new Error(`Kho\u1EA3ng trang kh\xF4ng h\u1EE3p l\u1EC7: "${token}".`);
|
|
10263
|
+
}
|
|
10264
|
+
const start = parseStrictInt(parts[0]);
|
|
10265
|
+
const end = parseStrictInt(parts[1]);
|
|
10266
|
+
if (start === null || end === null) {
|
|
10267
|
+
throw new Error(`Kho\u1EA3ng trang kh\xF4ng h\u1EE3p l\u1EC7: "${token}".`);
|
|
10268
|
+
}
|
|
10269
|
+
if (start > end) {
|
|
10270
|
+
throw new Error(`Kho\u1EA3ng trang ng\u01B0\u1EE3c: "${token}" (\u0111\u1EA7u > cu\u1ED1i).`);
|
|
10271
|
+
}
|
|
10272
|
+
if (start < 1 || end > totalPages) {
|
|
10273
|
+
throw new Error(
|
|
10274
|
+
`Kho\u1EA3ng trang v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n: "${token}". PDF c\xF3 ${totalPages} trang.`
|
|
10275
|
+
);
|
|
10276
|
+
}
|
|
10277
|
+
for (let i = start; i <= end; i++) set.add(i);
|
|
10278
|
+
} else {
|
|
10279
|
+
const n = parseStrictInt(token);
|
|
10280
|
+
if (n === null) {
|
|
10281
|
+
throw new Error(`S\u1ED1 trang kh\xF4ng h\u1EE3p l\u1EC7: "${token}".`);
|
|
10282
|
+
}
|
|
10283
|
+
if (n < 1 || n > totalPages) {
|
|
10284
|
+
throw new Error(
|
|
10285
|
+
`S\u1ED1 trang v\u01B0\u1EE3t gi\u1EDBi h\u1EA1n: ${n}. PDF c\xF3 ${totalPages} trang.`
|
|
10286
|
+
);
|
|
10287
|
+
}
|
|
10288
|
+
set.add(n);
|
|
10289
|
+
}
|
|
10290
|
+
}
|
|
10291
|
+
return Array.from(set).sort((a, b) => a - b);
|
|
10292
|
+
}
|
|
10293
|
+
function parseStrictInt(s) {
|
|
10294
|
+
if (!/^-?\d+$/.test(s)) return null;
|
|
10295
|
+
const n = Number(s);
|
|
10296
|
+
return Number.isInteger(n) ? n : null;
|
|
10297
|
+
}
|
|
10298
|
+
|
|
10299
|
+
// src/pdf/rasterize.ts
|
|
10300
|
+
var workerSrcOverride = null;
|
|
10301
|
+
var pdfjsCache = null;
|
|
10302
|
+
function configurePdfWorker(workerSrc) {
|
|
10303
|
+
workerSrcOverride = workerSrc;
|
|
10304
|
+
if (pdfjsCache) {
|
|
10305
|
+
pdfjsCache.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
10306
|
+
}
|
|
10307
|
+
}
|
|
10308
|
+
async function loadPdfjs() {
|
|
10309
|
+
if (pdfjsCache) return pdfjsCache;
|
|
10310
|
+
const mod = await import('pdfjs-dist');
|
|
10311
|
+
const workerSrc = workerSrcOverride ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${mod.version}/build/pdf.worker.min.mjs`;
|
|
10312
|
+
mod.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
10313
|
+
pdfjsCache = mod;
|
|
10314
|
+
return mod;
|
|
10315
|
+
}
|
|
10316
|
+
async function loadPdfDocument(source) {
|
|
10317
|
+
const pdfjs = await loadPdfjs();
|
|
10318
|
+
const data = source instanceof ArrayBuffer ? source : await source.arrayBuffer();
|
|
10319
|
+
const task = pdfjs.getDocument({ data: new Uint8Array(data) });
|
|
10320
|
+
return task.promise;
|
|
10321
|
+
}
|
|
10322
|
+
async function closePdfDocument(doc) {
|
|
10323
|
+
try {
|
|
10324
|
+
await doc.cleanup();
|
|
10325
|
+
await doc.destroy();
|
|
10326
|
+
} catch {
|
|
10327
|
+
}
|
|
10328
|
+
}
|
|
10329
|
+
async function rasterizePdf(doc, options = {}) {
|
|
10330
|
+
const scale3 = options.scale ?? 2;
|
|
10331
|
+
const total = doc.numPages;
|
|
10332
|
+
const pages = options.pages ?? Array.from({ length: total }, (_, i) => i + 1);
|
|
10333
|
+
const signal = options.signal;
|
|
10334
|
+
const result = [];
|
|
10335
|
+
for (let i = 0; i < pages.length; i++) {
|
|
10336
|
+
if (signal?.aborted) {
|
|
10337
|
+
throw new DOMException("Rasterize PDF b\u1ECB hu\u1EF7.", "AbortError");
|
|
10338
|
+
}
|
|
10339
|
+
const pageNum = pages[i];
|
|
10340
|
+
const page = await doc.getPage(pageNum);
|
|
10341
|
+
try {
|
|
10342
|
+
const rendered = await renderPageToPng(page, scale3);
|
|
10343
|
+
result.push({ pageNumber: pageNum, mimeType: "image/png", ...rendered });
|
|
10344
|
+
} finally {
|
|
10345
|
+
page.cleanup();
|
|
10346
|
+
}
|
|
10347
|
+
options.onProgress?.(i + 1, pages.length);
|
|
10348
|
+
}
|
|
10349
|
+
return result;
|
|
10350
|
+
}
|
|
10351
|
+
async function renderPageToPng(page, scale3) {
|
|
10352
|
+
const viewport = page.getViewport({ scale: scale3 });
|
|
10353
|
+
const width = Math.ceil(viewport.width);
|
|
10354
|
+
const height = Math.ceil(viewport.height);
|
|
10355
|
+
const canvas = document.createElement("canvas");
|
|
10356
|
+
canvas.width = width;
|
|
10357
|
+
canvas.height = height;
|
|
10358
|
+
const ctx = canvas.getContext("2d");
|
|
10359
|
+
if (!ctx) throw new Error("Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c 2D context c\u1EE7a canvas.");
|
|
10360
|
+
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
|
10361
|
+
const dataURL = canvas.toDataURL("image/png");
|
|
10362
|
+
return { dataURL, width, height };
|
|
10363
|
+
}
|
|
10364
|
+
async function renderPageThumbnail(page, scale3 = 0.3, quality = 0.7) {
|
|
10365
|
+
const viewport = page.getViewport({ scale: scale3 });
|
|
10366
|
+
const width = Math.ceil(viewport.width);
|
|
10367
|
+
const height = Math.ceil(viewport.height);
|
|
10368
|
+
const canvas = document.createElement("canvas");
|
|
10369
|
+
canvas.width = width;
|
|
10370
|
+
canvas.height = height;
|
|
10371
|
+
const ctx = canvas.getContext("2d");
|
|
10372
|
+
if (!ctx) throw new Error("Kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c 2D context c\u1EE7a canvas.");
|
|
10373
|
+
ctx.fillStyle = "#ffffff";
|
|
10374
|
+
ctx.fillRect(0, 0, width, height);
|
|
10375
|
+
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
|
10376
|
+
const dataURL = canvas.toDataURL("image/jpeg", quality);
|
|
10377
|
+
return { dataURL, width, height };
|
|
10378
|
+
}
|
|
10379
|
+
async function renderAllThumbnails(doc, onEach, options = {}) {
|
|
10380
|
+
const total = doc.numPages;
|
|
10381
|
+
const scale3 = options.scale ?? 0.3;
|
|
10382
|
+
const quality = options.quality ?? 0.7;
|
|
10383
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
10384
|
+
const signal = options.signal;
|
|
10385
|
+
let next = 1;
|
|
10386
|
+
async function worker() {
|
|
10387
|
+
while (true) {
|
|
10388
|
+
if (signal?.aborted) return;
|
|
10389
|
+
const pageNum = next++;
|
|
10390
|
+
if (pageNum > total) return;
|
|
10391
|
+
const page = await doc.getPage(pageNum);
|
|
10392
|
+
try {
|
|
10393
|
+
if (signal?.aborted) return;
|
|
10394
|
+
const { dataURL, width, height } = await renderPageThumbnail(page, scale3, quality);
|
|
10395
|
+
if (signal?.aborted) return;
|
|
10396
|
+
onEach(pageNum, dataURL, width, height);
|
|
10397
|
+
} finally {
|
|
10398
|
+
page.cleanup();
|
|
10399
|
+
}
|
|
10400
|
+
}
|
|
10401
|
+
}
|
|
10402
|
+
await Promise.all(
|
|
10403
|
+
Array.from({ length: Math.min(concurrency, total) }, () => worker())
|
|
10404
|
+
);
|
|
10405
|
+
}
|
|
10406
|
+
function serializeSelection(pages) {
|
|
10407
|
+
if (pages.length === 0) return "";
|
|
10408
|
+
const sorted = [...pages].sort((a, b) => a - b);
|
|
10409
|
+
const groups = [];
|
|
10410
|
+
let start = sorted[0];
|
|
10411
|
+
let prev = start;
|
|
10412
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
10413
|
+
const n = sorted[i];
|
|
10414
|
+
if (n === prev + 1) {
|
|
10415
|
+
prev = n;
|
|
10416
|
+
} else {
|
|
10417
|
+
groups.push(start === prev ? `${start}` : `${start}-${prev}`);
|
|
10418
|
+
start = n;
|
|
10419
|
+
prev = n;
|
|
10420
|
+
}
|
|
10421
|
+
}
|
|
10422
|
+
groups.push(start === prev ? `${start}` : `${start}-${prev}`);
|
|
10423
|
+
return groups.join(",");
|
|
10424
|
+
}
|
|
10425
|
+
function PageRangeDialog({ doc, fileName, onConfirm, onCancel }) {
|
|
10426
|
+
const totalPages = doc.numPages;
|
|
10427
|
+
const defaultPages = React8.useMemo(
|
|
10428
|
+
() => Array.from({ length: totalPages }, (_, i) => i + 1),
|
|
10429
|
+
[totalPages]
|
|
10430
|
+
);
|
|
10431
|
+
const [selectedSet, setSelectedSet] = React8.useState(
|
|
10432
|
+
() => new Set(defaultPages)
|
|
10433
|
+
);
|
|
10434
|
+
const [inputValue, setInputValue] = React8.useState(serializeSelection(defaultPages));
|
|
10435
|
+
const [inputError, setInputError] = React8.useState(null);
|
|
10436
|
+
const [thumbs, setThumbs] = React8.useState({});
|
|
10437
|
+
const [thumbProgress, setThumbProgress] = React8.useState(0);
|
|
10438
|
+
const inputRef = React8.useRef(null);
|
|
10439
|
+
React8.useEffect(() => {
|
|
10440
|
+
const ctrl = new AbortController();
|
|
10441
|
+
void renderAllThumbnails(
|
|
10442
|
+
doc,
|
|
10443
|
+
(pageNum, dataURL, width, height) => {
|
|
10444
|
+
setThumbs((prev) => ({ ...prev, [pageNum]: { dataURL, width, height } }));
|
|
10445
|
+
setThumbProgress((prev) => prev + 1);
|
|
10446
|
+
},
|
|
10447
|
+
{ scale: 0.3, quality: 0.7, concurrency: 3, signal: ctrl.signal }
|
|
10448
|
+
).catch((err) => {
|
|
10449
|
+
if (ctrl.signal.aborted) return;
|
|
10450
|
+
console.warn("[PageRangeDialog] render thumbnails l\u1ED7i:", err);
|
|
10451
|
+
});
|
|
10452
|
+
return () => ctrl.abort();
|
|
10453
|
+
}, [doc]);
|
|
10454
|
+
React8.useEffect(() => {
|
|
10455
|
+
const onKey = (e) => {
|
|
10456
|
+
if (e.key === "Escape") {
|
|
10457
|
+
e.preventDefault();
|
|
10458
|
+
e.stopPropagation();
|
|
10459
|
+
onCancel();
|
|
10460
|
+
}
|
|
10461
|
+
};
|
|
10462
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
10463
|
+
return () => window.removeEventListener("keydown", onKey, { capture: true });
|
|
10464
|
+
}, [onCancel]);
|
|
10465
|
+
const handleInputChange = (next) => {
|
|
10466
|
+
setInputValue(next);
|
|
10467
|
+
try {
|
|
10468
|
+
const pages = parsePageRange(next, totalPages);
|
|
10469
|
+
setInputError(null);
|
|
10470
|
+
setSelectedSet(new Set(pages));
|
|
10471
|
+
} catch (e) {
|
|
10472
|
+
setInputError(e.message);
|
|
10473
|
+
}
|
|
10474
|
+
};
|
|
10475
|
+
const toggleThumb = (pageNum) => {
|
|
10476
|
+
setSelectedSet((prev) => {
|
|
10477
|
+
const next = new Set(prev);
|
|
10478
|
+
if (next.has(pageNum)) next.delete(pageNum);
|
|
10479
|
+
else next.add(pageNum);
|
|
10480
|
+
const serialized = serializeSelection([...next]);
|
|
10481
|
+
setInputValue(serialized);
|
|
10482
|
+
setInputError(null);
|
|
10483
|
+
return next;
|
|
10484
|
+
});
|
|
10485
|
+
};
|
|
10486
|
+
const selectAll = () => {
|
|
10487
|
+
setSelectedSet(new Set(defaultPages));
|
|
10488
|
+
setInputValue(serializeSelection(defaultPages));
|
|
10489
|
+
setInputError(null);
|
|
10490
|
+
};
|
|
10491
|
+
const clearAll = () => {
|
|
10492
|
+
setSelectedSet(/* @__PURE__ */ new Set());
|
|
10493
|
+
setInputValue("");
|
|
10494
|
+
setInputError(null);
|
|
10495
|
+
};
|
|
10496
|
+
const canSubmit = inputError === null && selectedSet.size > 0;
|
|
10497
|
+
const sortedSelected = React8.useMemo(
|
|
10498
|
+
() => [...selectedSet].sort((a, b) => a - b),
|
|
10499
|
+
[selectedSet]
|
|
10500
|
+
);
|
|
10501
|
+
const handleSubmit = () => {
|
|
10502
|
+
if (!canSubmit) return;
|
|
10503
|
+
onConfirm(sortedSelected);
|
|
10504
|
+
};
|
|
10505
|
+
return reactDom.createPortal(
|
|
10506
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10507
|
+
"div",
|
|
10508
|
+
{
|
|
10509
|
+
role: "dialog",
|
|
10510
|
+
"aria-modal": "true",
|
|
10511
|
+
"aria-labelledby": "pdf-range-title",
|
|
10512
|
+
style: {
|
|
10513
|
+
position: "fixed",
|
|
10514
|
+
inset: 0,
|
|
10515
|
+
background: "rgba(0,0,0,0.55)",
|
|
10516
|
+
display: "flex",
|
|
10517
|
+
alignItems: "center",
|
|
10518
|
+
justifyContent: "center",
|
|
10519
|
+
zIndex: 1e4
|
|
10520
|
+
},
|
|
10521
|
+
onClick: (e) => {
|
|
10522
|
+
if (e.target === e.currentTarget) onCancel();
|
|
10523
|
+
},
|
|
10524
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10525
|
+
"div",
|
|
10526
|
+
{
|
|
10527
|
+
style: {
|
|
10528
|
+
background: "var(--popup-bg-color, #fff)",
|
|
10529
|
+
color: "var(--text-primary-color, #1b1b1f)",
|
|
10530
|
+
borderRadius: 12,
|
|
10531
|
+
padding: "20px 22px",
|
|
10532
|
+
width: "min(880px, 92vw)",
|
|
10533
|
+
maxHeight: "88vh",
|
|
10534
|
+
boxShadow: "0 12px 40px rgba(0,0,0,0.3)",
|
|
10535
|
+
fontFamily: "inherit",
|
|
10536
|
+
display: "flex",
|
|
10537
|
+
flexDirection: "column",
|
|
10538
|
+
gap: 12
|
|
10539
|
+
},
|
|
10540
|
+
onClick: (e) => e.stopPropagation(),
|
|
10541
|
+
children: [
|
|
10542
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
10543
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10544
|
+
"h2",
|
|
10545
|
+
{
|
|
10546
|
+
id: "pdf-range-title",
|
|
10547
|
+
style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.3 },
|
|
10548
|
+
children: "Ch\xE8n PDF"
|
|
10549
|
+
}
|
|
10550
|
+
),
|
|
10551
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { style: { margin: "4px 0 0", fontSize: 12, opacity: 0.7 }, children: [
|
|
10552
|
+
fileName,
|
|
10553
|
+
" \u2014 ",
|
|
10554
|
+
totalPages,
|
|
10555
|
+
" trang",
|
|
10556
|
+
thumbProgress < totalPages && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10557
|
+
" \xB7 \u0111ang t\u1EA3i preview ",
|
|
10558
|
+
thumbProgress,
|
|
10559
|
+
"/",
|
|
10560
|
+
totalPages,
|
|
10561
|
+
"\u2026"
|
|
10562
|
+
] })
|
|
10563
|
+
] })
|
|
10564
|
+
] }),
|
|
10565
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 10 }, children: [
|
|
10566
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1 }, children: [
|
|
10567
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10568
|
+
"label",
|
|
10569
|
+
{
|
|
10570
|
+
style: { display: "block", fontSize: 12, marginBottom: 4, opacity: 0.75 },
|
|
10571
|
+
children: "Trang c\u1EA7n ch\xE8n (vd: 1,3,5-10) \u2014 ho\u1EB7c click thumbnail b\xEAn d\u01B0\u1EDBi"
|
|
10572
|
+
}
|
|
10573
|
+
),
|
|
10574
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10575
|
+
"input",
|
|
10576
|
+
{
|
|
10577
|
+
ref: inputRef,
|
|
10578
|
+
type: "text",
|
|
10579
|
+
value: inputValue,
|
|
10580
|
+
onChange: (e) => handleInputChange(e.target.value),
|
|
10581
|
+
onKeyDown: (e) => {
|
|
10582
|
+
if (e.key === "Enter") {
|
|
10583
|
+
e.preventDefault();
|
|
10584
|
+
handleSubmit();
|
|
10585
|
+
}
|
|
10586
|
+
},
|
|
10587
|
+
style: {
|
|
10588
|
+
width: "100%",
|
|
10589
|
+
boxSizing: "border-box",
|
|
10590
|
+
padding: "8px 10px",
|
|
10591
|
+
fontSize: 14,
|
|
10592
|
+
borderRadius: 6,
|
|
10593
|
+
border: `1px solid ${inputError ? "#dc2626" : "rgba(0,0,0,0.2)"}`,
|
|
10594
|
+
outline: "none",
|
|
10595
|
+
background: "var(--input-bg-color, #fff)",
|
|
10596
|
+
color: "inherit",
|
|
10597
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
)
|
|
10601
|
+
] }),
|
|
10602
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, paddingTop: 18 }, children: [
|
|
10603
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10604
|
+
"button",
|
|
10605
|
+
{
|
|
10606
|
+
type: "button",
|
|
10607
|
+
onClick: selectAll,
|
|
10608
|
+
style: quickBtnStyle,
|
|
10609
|
+
title: "Ch\u1ECDn t\u1EA5t c\u1EA3 trang",
|
|
10610
|
+
children: "T\u1EA5t c\u1EA3"
|
|
10611
|
+
}
|
|
10612
|
+
),
|
|
10613
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10614
|
+
"button",
|
|
10615
|
+
{
|
|
10616
|
+
type: "button",
|
|
10617
|
+
onClick: clearAll,
|
|
10618
|
+
style: quickBtnStyle,
|
|
10619
|
+
title: "B\u1ECF ch\u1ECDn t\u1EA5t c\u1EA3",
|
|
10620
|
+
children: "B\u1ECF h\u1EBFt"
|
|
10621
|
+
}
|
|
10622
|
+
)
|
|
10623
|
+
] })
|
|
10624
|
+
] }),
|
|
10625
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { minHeight: 18, fontSize: 12 }, "data-testid": "pdf-range-status", children: inputError ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#dc2626" }, children: inputError }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { opacity: 0.75 }, children: [
|
|
10626
|
+
"\u0110\xE3 ch\u1ECDn ",
|
|
10627
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: selectedSet.size }),
|
|
10628
|
+
" / ",
|
|
10629
|
+
totalPages,
|
|
10630
|
+
" trang"
|
|
10631
|
+
] }) }),
|
|
10632
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10633
|
+
"div",
|
|
10634
|
+
{
|
|
10635
|
+
style: {
|
|
10636
|
+
flex: 1,
|
|
10637
|
+
minHeight: 240,
|
|
10638
|
+
maxHeight: "60vh",
|
|
10639
|
+
overflow: "auto",
|
|
10640
|
+
padding: 8,
|
|
10641
|
+
background: "rgba(0,0,0,0.04)",
|
|
10642
|
+
borderRadius: 8,
|
|
10643
|
+
display: "grid",
|
|
10644
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))",
|
|
10645
|
+
gap: 10,
|
|
10646
|
+
alignContent: "start"
|
|
10647
|
+
},
|
|
10648
|
+
children: Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => {
|
|
10649
|
+
const thumb = thumbs[pageNum];
|
|
10650
|
+
const selected = selectedSet.has(pageNum);
|
|
10651
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
10652
|
+
ThumbnailItem,
|
|
10653
|
+
{
|
|
10654
|
+
pageNum,
|
|
10655
|
+
thumb,
|
|
10656
|
+
selected,
|
|
10657
|
+
onToggle: () => toggleThumb(pageNum)
|
|
10658
|
+
},
|
|
10659
|
+
pageNum
|
|
10660
|
+
);
|
|
10661
|
+
})
|
|
10662
|
+
}
|
|
10663
|
+
),
|
|
10664
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10665
|
+
"div",
|
|
10666
|
+
{
|
|
10667
|
+
style: {
|
|
10668
|
+
display: "flex",
|
|
10669
|
+
justifyContent: "flex-end",
|
|
10670
|
+
gap: 8,
|
|
10671
|
+
paddingTop: 4
|
|
10672
|
+
},
|
|
10673
|
+
children: [
|
|
10674
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10675
|
+
"button",
|
|
10676
|
+
{
|
|
10677
|
+
type: "button",
|
|
10678
|
+
onClick: onCancel,
|
|
10679
|
+
style: {
|
|
10680
|
+
padding: "8px 14px",
|
|
10681
|
+
fontSize: 13,
|
|
10682
|
+
borderRadius: 6,
|
|
10683
|
+
border: "1px solid rgba(0,0,0,0.15)",
|
|
10684
|
+
background: "transparent",
|
|
10685
|
+
color: "inherit",
|
|
10686
|
+
cursor: "pointer"
|
|
10687
|
+
},
|
|
10688
|
+
children: "Hu\u1EF7"
|
|
10689
|
+
}
|
|
10690
|
+
),
|
|
10691
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10692
|
+
"button",
|
|
10693
|
+
{
|
|
10694
|
+
type: "button",
|
|
10695
|
+
onClick: handleSubmit,
|
|
10696
|
+
disabled: !canSubmit,
|
|
10697
|
+
style: {
|
|
10698
|
+
padding: "8px 16px",
|
|
10699
|
+
fontSize: 13,
|
|
10700
|
+
borderRadius: 6,
|
|
10701
|
+
border: "none",
|
|
10702
|
+
background: canSubmit ? "#4f46e5" : "rgba(0,0,0,0.15)",
|
|
10703
|
+
color: "#fff",
|
|
10704
|
+
cursor: canSubmit ? "pointer" : "not-allowed",
|
|
10705
|
+
fontWeight: 500
|
|
10706
|
+
},
|
|
10707
|
+
children: [
|
|
10708
|
+
"Ch\xE8n ",
|
|
10709
|
+
selectedSet.size > 0 ? `${selectedSet.size} trang` : ""
|
|
10710
|
+
]
|
|
10711
|
+
}
|
|
10712
|
+
)
|
|
10713
|
+
]
|
|
10714
|
+
}
|
|
10715
|
+
)
|
|
10716
|
+
]
|
|
10717
|
+
}
|
|
10718
|
+
)
|
|
10719
|
+
}
|
|
10720
|
+
),
|
|
10721
|
+
document.body
|
|
10722
|
+
);
|
|
10723
|
+
}
|
|
10724
|
+
var quickBtnStyle = {
|
|
10725
|
+
padding: "7px 10px",
|
|
10726
|
+
fontSize: 12,
|
|
10727
|
+
borderRadius: 6,
|
|
10728
|
+
border: "1px solid rgba(0,0,0,0.15)",
|
|
10729
|
+
background: "transparent",
|
|
10730
|
+
color: "inherit",
|
|
10731
|
+
cursor: "pointer",
|
|
10732
|
+
whiteSpace: "nowrap"
|
|
10733
|
+
};
|
|
10734
|
+
function ThumbnailItem({ pageNum, thumb, selected, onToggle }) {
|
|
10735
|
+
const aspect = thumb ? thumb.width / thumb.height : 0.77;
|
|
10736
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10737
|
+
"button",
|
|
10738
|
+
{
|
|
10739
|
+
type: "button",
|
|
10740
|
+
onClick: onToggle,
|
|
10741
|
+
"aria-pressed": selected,
|
|
10742
|
+
"aria-label": `Trang ${pageNum}${selected ? " (\u0111\xE3 ch\u1ECDn)" : ""}`,
|
|
10743
|
+
title: `Trang ${pageNum}`,
|
|
10744
|
+
style: {
|
|
10745
|
+
position: "relative",
|
|
10746
|
+
padding: 0,
|
|
10747
|
+
background: "#fff",
|
|
10748
|
+
border: `2px solid ${selected ? "#4f46e5" : "rgba(0,0,0,0.12)"}`,
|
|
10749
|
+
borderRadius: 6,
|
|
10750
|
+
overflow: "hidden",
|
|
10751
|
+
cursor: "pointer",
|
|
10752
|
+
boxShadow: selected ? "0 0 0 3px rgba(79,70,229,0.18)" : "none",
|
|
10753
|
+
transition: "border-color 80ms ease, box-shadow 80ms ease"
|
|
10754
|
+
},
|
|
10755
|
+
children: [
|
|
10756
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10757
|
+
"div",
|
|
10758
|
+
{
|
|
10759
|
+
style: {
|
|
10760
|
+
width: "100%",
|
|
10761
|
+
aspectRatio: aspect.toString(),
|
|
10762
|
+
background: "#f5f5f5",
|
|
10763
|
+
display: "flex",
|
|
10764
|
+
alignItems: "center",
|
|
10765
|
+
justifyContent: "center"
|
|
10766
|
+
},
|
|
10767
|
+
children: thumb ? (
|
|
10768
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
10769
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10770
|
+
"img",
|
|
10771
|
+
{
|
|
10772
|
+
src: thumb.dataURL,
|
|
10773
|
+
alt: "",
|
|
10774
|
+
style: { width: "100%", height: "100%", display: "block", objectFit: "contain" },
|
|
10775
|
+
draggable: false
|
|
10776
|
+
}
|
|
10777
|
+
)
|
|
10778
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, opacity: 0.5 }, children: "\u2026" })
|
|
10779
|
+
}
|
|
10780
|
+
),
|
|
10781
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10782
|
+
"div",
|
|
10783
|
+
{
|
|
10784
|
+
style: {
|
|
10785
|
+
position: "absolute",
|
|
10786
|
+
bottom: 4,
|
|
10787
|
+
left: 4,
|
|
10788
|
+
fontSize: 10,
|
|
10789
|
+
fontWeight: 600,
|
|
10790
|
+
padding: "2px 6px",
|
|
10791
|
+
borderRadius: 4,
|
|
10792
|
+
background: selected ? "#4f46e5" : "rgba(0,0,0,0.6)",
|
|
10793
|
+
color: "#fff"
|
|
10794
|
+
},
|
|
10795
|
+
children: pageNum
|
|
10796
|
+
}
|
|
10797
|
+
),
|
|
10798
|
+
selected && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10799
|
+
"div",
|
|
10800
|
+
{
|
|
10801
|
+
"aria-hidden": "true",
|
|
10802
|
+
style: {
|
|
10803
|
+
position: "absolute",
|
|
10804
|
+
top: 4,
|
|
10805
|
+
right: 4,
|
|
10806
|
+
width: 18,
|
|
10807
|
+
height: 18,
|
|
10808
|
+
borderRadius: "50%",
|
|
10809
|
+
background: "#4f46e5",
|
|
10810
|
+
color: "#fff",
|
|
10811
|
+
display: "flex",
|
|
10812
|
+
alignItems: "center",
|
|
10813
|
+
justifyContent: "center",
|
|
10814
|
+
fontSize: 11,
|
|
10815
|
+
fontWeight: 700,
|
|
10816
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.3)"
|
|
10817
|
+
},
|
|
10818
|
+
children: "\u2713"
|
|
10819
|
+
}
|
|
10820
|
+
)
|
|
10821
|
+
]
|
|
10822
|
+
}
|
|
10823
|
+
);
|
|
10824
|
+
}
|
|
10825
|
+
|
|
10826
|
+
// src/pdf/insertPdfPages.ts
|
|
10827
|
+
var PAGE_GAP = 24;
|
|
10828
|
+
var DEFAULT_SCALE = 2;
|
|
10829
|
+
function insertRasterizedPagesIntoScene(api, rendered, options) {
|
|
10830
|
+
if (!api) throw new Error("Excalidraw API ch\u01B0a s\u1EB5n s\xE0ng.");
|
|
10831
|
+
if (rendered.length === 0) return { insertedElementIds: [], fileIds: [] };
|
|
10832
|
+
const { scale: scale3 } = options;
|
|
10833
|
+
const filesPayload = rendered.map((p) => ({
|
|
10834
|
+
id: generateFileId(),
|
|
10835
|
+
dataURL: p.dataURL,
|
|
10836
|
+
mimeType: p.mimeType,
|
|
10837
|
+
created: Date.now()
|
|
10838
|
+
}));
|
|
10839
|
+
api.addFiles(filesPayload);
|
|
10840
|
+
const origin = options.origin ?? getViewportCenter(api);
|
|
10841
|
+
const sceneSizes = rendered.map((p) => pixelsToSceneSize(p.width, p.height, scale3));
|
|
10842
|
+
const maxSceneWidth = Math.max(...sceneSizes.map((s) => s.width));
|
|
10843
|
+
const baseX = origin.x - maxSceneWidth / 2;
|
|
10844
|
+
let cursorY = origin.y - sceneSizes[0].height / 2;
|
|
10845
|
+
const newElements = rendered.map((_, i) => {
|
|
10846
|
+
const { width, height } = sceneSizes[i];
|
|
10847
|
+
const x = baseX + (maxSceneWidth - width) / 2;
|
|
10848
|
+
const y = cursorY;
|
|
10849
|
+
cursorY = y + height + PAGE_GAP;
|
|
10850
|
+
return buildPdfImageElement(filesPayload[i].id, x, y, width, height);
|
|
10851
|
+
});
|
|
10852
|
+
const existing = api.getSceneElements();
|
|
10853
|
+
api.updateScene({
|
|
10854
|
+
elements: [...existing, ...newElements],
|
|
10855
|
+
appState: { selectedElementIds: {}, croppingElementId: null }
|
|
10856
|
+
});
|
|
10857
|
+
return {
|
|
10858
|
+
insertedElementIds: newElements.map((e) => e.id),
|
|
10859
|
+
fileIds: filesPayload.map((f) => f.id)
|
|
10860
|
+
};
|
|
10861
|
+
}
|
|
10862
|
+
function pixelsToSceneSize(pxWidth, pxHeight, scale3) {
|
|
10863
|
+
return { width: pxWidth / scale3, height: pxHeight / scale3 };
|
|
10864
|
+
}
|
|
10865
|
+
function buildPdfImageElement(fileId, x, y, width, height) {
|
|
10866
|
+
return {
|
|
10867
|
+
type: "image",
|
|
10868
|
+
id: "pdf_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
|
|
10869
|
+
x,
|
|
10870
|
+
y,
|
|
10871
|
+
width,
|
|
10872
|
+
height,
|
|
10873
|
+
fileId,
|
|
10874
|
+
angle: 0,
|
|
10875
|
+
strokeColor: "transparent",
|
|
10876
|
+
backgroundColor: "transparent",
|
|
10877
|
+
fillStyle: "solid",
|
|
10878
|
+
strokeWidth: 1,
|
|
10879
|
+
strokeStyle: "solid",
|
|
10880
|
+
roughness: 0,
|
|
10881
|
+
opacity: 100,
|
|
10882
|
+
groupIds: [],
|
|
10883
|
+
roundness: null,
|
|
10884
|
+
seed: Math.floor(Math.random() * 1e9),
|
|
10885
|
+
versionNonce: 0,
|
|
10886
|
+
version: 1,
|
|
10887
|
+
isDeleted: false,
|
|
10888
|
+
boundElements: null,
|
|
10889
|
+
updated: Date.now(),
|
|
10890
|
+
link: null,
|
|
10891
|
+
locked: false,
|
|
10892
|
+
status: "saved",
|
|
10893
|
+
scale: [1, 1]
|
|
10894
|
+
};
|
|
10895
|
+
}
|
|
10896
|
+
function generateFileId() {
|
|
10897
|
+
return "pdf_" + Date.now() + "_" + Math.random().toString(36).slice(2, 10);
|
|
10898
|
+
}
|
|
10899
|
+
function getViewportCenter(api) {
|
|
10900
|
+
const appState = api?.getAppState?.() ?? {
|
|
10901
|
+
scrollX: 0,
|
|
10902
|
+
scrollY: 0,
|
|
10903
|
+
width: 800,
|
|
10904
|
+
height: 600,
|
|
10905
|
+
zoom: { value: 1 }
|
|
10906
|
+
};
|
|
10907
|
+
const zoom = appState.zoom?.value ?? 1;
|
|
10908
|
+
return {
|
|
10909
|
+
x: appState.scrollX + (appState.width ?? 800) / 2 / zoom,
|
|
10910
|
+
y: appState.scrollY + (appState.height ?? 600) / 2 / zoom
|
|
10911
|
+
};
|
|
10912
|
+
}
|
|
10913
|
+
async function insertPdfPages(api, source, options = {}) {
|
|
10914
|
+
if (!api) throw new Error("Excalidraw API ch\u01B0a s\u1EB5n s\xE0ng.");
|
|
10915
|
+
const scale3 = options.scale ?? DEFAULT_SCALE;
|
|
10916
|
+
const doc = await loadPdfDocument(source);
|
|
10917
|
+
let rendered;
|
|
10918
|
+
try {
|
|
10919
|
+
rendered = await rasterizePdf(doc, {
|
|
10920
|
+
pages: options.pages,
|
|
10921
|
+
scale: scale3,
|
|
10922
|
+
onProgress: options.onProgress,
|
|
10923
|
+
signal: options.signal
|
|
10924
|
+
});
|
|
10925
|
+
} finally {
|
|
10926
|
+
void closePdfDocument(doc);
|
|
10927
|
+
}
|
|
10928
|
+
const { insertedElementIds } = insertRasterizedPagesIntoScene(api, rendered, {
|
|
10929
|
+
scale: scale3,
|
|
10930
|
+
origin: options.origin
|
|
10931
|
+
});
|
|
10932
|
+
return { insertedElementIds, pages: rendered };
|
|
10933
|
+
}
|
|
10934
|
+
var DOUBLE_CLICK_MS = 400;
|
|
10935
|
+
function useStampDoubleClick({ enabled, stamps, onOpen }) {
|
|
10936
|
+
const lastClickRef = React8.useRef({
|
|
10937
|
+
time: 0,
|
|
10938
|
+
elementId: null
|
|
10939
|
+
});
|
|
10940
|
+
return React8.useCallback(
|
|
10941
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10942
|
+
(_activeTool, pointerDownState) => {
|
|
10943
|
+
if (!enabled) return;
|
|
10944
|
+
const hitElement = pointerDownState?.hit?.element;
|
|
10945
|
+
if (!hitElement || hitElement.type !== "image") return;
|
|
10946
|
+
const stamp = findStampForCustomData(hitElement.customData, stamps);
|
|
10947
|
+
if (!stamp) return;
|
|
10948
|
+
const now = Date.now();
|
|
10949
|
+
const isDouble = lastClickRef.current.elementId === hitElement.id && now - lastClickRef.current.time < DOUBLE_CLICK_MS;
|
|
10950
|
+
lastClickRef.current = { time: now, elementId: hitElement.id };
|
|
10951
|
+
if (!isDouble) return;
|
|
10952
|
+
onOpen(stamp.kind, {
|
|
10953
|
+
id: hitElement.id,
|
|
10954
|
+
customData: hitElement.customData
|
|
10955
|
+
});
|
|
10956
|
+
},
|
|
9978
10957
|
[enabled, stamps, onOpen]
|
|
9979
10958
|
);
|
|
9980
10959
|
}
|
|
@@ -10420,6 +11399,8 @@ function Whiteboard({
|
|
|
10420
11399
|
activeStampRef.current = activeStamp;
|
|
10421
11400
|
const [editingElement, setEditingElement] = React8.useState(null);
|
|
10422
11401
|
const hostRef = React8.useRef(null);
|
|
11402
|
+
const [pdfPending, setPdfPending] = React8.useState(null);
|
|
11403
|
+
const [pdfBusy, setPdfBusy] = React8.useState(false);
|
|
10423
11404
|
const handledCropIdRef = React8.useRef(null);
|
|
10424
11405
|
const prevExcalidrawToolRef = React8.useRef("selection");
|
|
10425
11406
|
const stampByKind = React8.useMemo(() => {
|
|
@@ -10708,6 +11689,80 @@ function Whiteboard({
|
|
|
10708
11689
|
return () => window.removeEventListener("keydown", onKey, { capture: true });
|
|
10709
11690
|
}, [activeStamp, closeStamp]);
|
|
10710
11691
|
useStampClickOutside({ activeStamp, hostRef, onClose: closeStamp });
|
|
11692
|
+
const handlePdfPick = React8.useCallback(
|
|
11693
|
+
async (file) => {
|
|
11694
|
+
if (readOnly || pdfBusy) return;
|
|
11695
|
+
setPdfBusy(true);
|
|
11696
|
+
try {
|
|
11697
|
+
const doc = await loadPdfDocument(file);
|
|
11698
|
+
setPdfPending({ doc, fileName: file.name, totalPages: doc.numPages });
|
|
11699
|
+
} catch (err) {
|
|
11700
|
+
console.warn("[whiteboard] \u0110\u1ECDc PDF th\u1EA5t b\u1EA1i:", err);
|
|
11701
|
+
window.alert("Kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c PDF. File c\xF3 th\u1EC3 \u0111\xE3 h\u1ECFng ho\u1EB7c b\u1ECB m\u1EADt kh\u1EA9u b\u1EA3o v\u1EC7.");
|
|
11702
|
+
} finally {
|
|
11703
|
+
setPdfBusy(false);
|
|
11704
|
+
}
|
|
11705
|
+
},
|
|
11706
|
+
[readOnly, pdfBusy]
|
|
11707
|
+
);
|
|
11708
|
+
const handlePdfConfirm = React8.useCallback(
|
|
11709
|
+
async (pages) => {
|
|
11710
|
+
if (!pdfPending || !api) return;
|
|
11711
|
+
const { doc } = pdfPending;
|
|
11712
|
+
setPdfPending(null);
|
|
11713
|
+
setPdfBusy(true);
|
|
11714
|
+
const scale3 = 2;
|
|
11715
|
+
try {
|
|
11716
|
+
const rendered = await rasterizePdf(doc, { pages, scale: scale3 });
|
|
11717
|
+
await closePdfDocument(doc);
|
|
11718
|
+
insertRasterizedPagesIntoScene(api, rendered, { scale: scale3 });
|
|
11719
|
+
} catch (err) {
|
|
11720
|
+
console.warn("[whiteboard] Ch\xE8n PDF th\u1EA5t b\u1EA1i:", err);
|
|
11721
|
+
window.alert("Ch\xE8n PDF th\u1EA5t b\u1EA1i. Xem console \u0111\u1EC3 bi\u1EBFt chi ti\u1EBFt.");
|
|
11722
|
+
} finally {
|
|
11723
|
+
setPdfBusy(false);
|
|
11724
|
+
}
|
|
11725
|
+
},
|
|
11726
|
+
[pdfPending, api]
|
|
11727
|
+
);
|
|
11728
|
+
const handlePdfCancel = React8.useCallback(() => {
|
|
11729
|
+
if (pdfPending) {
|
|
11730
|
+
void closePdfDocument(pdfPending.doc);
|
|
11731
|
+
}
|
|
11732
|
+
setPdfPending(null);
|
|
11733
|
+
}, [pdfPending]);
|
|
11734
|
+
React8.useEffect(() => {
|
|
11735
|
+
if (readOnly) return;
|
|
11736
|
+
const root = document.querySelector(".excalidraw");
|
|
11737
|
+
if (!root) return;
|
|
11738
|
+
const onDragOver = (e) => {
|
|
11739
|
+
const items = e.dataTransfer?.items;
|
|
11740
|
+
if (!items) return;
|
|
11741
|
+
for (let i = 0; i < items.length; i++) {
|
|
11742
|
+
if (items[i].kind === "file" && items[i].type === "application/pdf") {
|
|
11743
|
+
e.preventDefault();
|
|
11744
|
+
e.stopPropagation();
|
|
11745
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
|
|
11746
|
+
return;
|
|
11747
|
+
}
|
|
11748
|
+
}
|
|
11749
|
+
};
|
|
11750
|
+
const onDrop = (e) => {
|
|
11751
|
+
const files = e.dataTransfer?.files;
|
|
11752
|
+
if (!files || files.length === 0) return;
|
|
11753
|
+
const pdf = Array.from(files).find((f) => f.type === "application/pdf");
|
|
11754
|
+
if (!pdf) return;
|
|
11755
|
+
e.preventDefault();
|
|
11756
|
+
e.stopPropagation();
|
|
11757
|
+
void handlePdfPick(pdf);
|
|
11758
|
+
};
|
|
11759
|
+
root.addEventListener("dragover", onDragOver, { capture: true });
|
|
11760
|
+
root.addEventListener("drop", onDrop, { capture: true });
|
|
11761
|
+
return () => {
|
|
11762
|
+
root.removeEventListener("dragover", onDragOver, { capture: true });
|
|
11763
|
+
root.removeEventListener("drop", onDrop, { capture: true });
|
|
11764
|
+
};
|
|
11765
|
+
}, [readOnly, handlePdfPick, api]);
|
|
10711
11766
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative h-full w-full${isDarkTheme ? " theme--dark" : ""}`, children: [
|
|
10712
11767
|
/* @__PURE__ */ jsxRuntime.jsx(React8.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(ExcalidrawLoadingFallback, {}), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
10713
11768
|
Excalidraw2,
|
|
@@ -10742,6 +11797,35 @@ function Whiteboard({
|
|
|
10742
11797
|
stamps
|
|
10743
11798
|
}
|
|
10744
11799
|
),
|
|
11800
|
+
/* @__PURE__ */ jsxRuntime.jsx(PdfImporterButton, { enabled: !readOnly, onPick: handlePdfPick }),
|
|
11801
|
+
pdfPending && /* @__PURE__ */ jsxRuntime.jsx(
|
|
11802
|
+
PageRangeDialog,
|
|
11803
|
+
{
|
|
11804
|
+
doc: pdfPending.doc,
|
|
11805
|
+
fileName: pdfPending.fileName,
|
|
11806
|
+
onConfirm: handlePdfConfirm,
|
|
11807
|
+
onCancel: handlePdfCancel
|
|
11808
|
+
}
|
|
11809
|
+
),
|
|
11810
|
+
pdfBusy && !pdfPending && /* @__PURE__ */ jsxRuntime.jsx(
|
|
11811
|
+
"div",
|
|
11812
|
+
{
|
|
11813
|
+
"aria-live": "polite",
|
|
11814
|
+
role: "status",
|
|
11815
|
+
style: {
|
|
11816
|
+
position: "fixed",
|
|
11817
|
+
bottom: 16,
|
|
11818
|
+
right: 16,
|
|
11819
|
+
padding: "8px 14px",
|
|
11820
|
+
background: "rgba(0,0,0,0.75)",
|
|
11821
|
+
color: "#fff",
|
|
11822
|
+
borderRadius: 6,
|
|
11823
|
+
fontSize: 12,
|
|
11824
|
+
zIndex: 1e4
|
|
11825
|
+
},
|
|
11826
|
+
children: "\u0110ang x\u1EED l\xFD PDF\u2026"
|
|
11827
|
+
}
|
|
11828
|
+
),
|
|
10745
11829
|
HostComponent && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10746
11830
|
HostComponent,
|
|
10747
11831
|
{
|
|
@@ -10760,17 +11844,24 @@ exports.DEFAULT_STAMPS = DEFAULT_STAMPS;
|
|
|
10760
11844
|
exports.EXPERIMENTAL_STAMPS = EXPERIMENTAL_STAMPS;
|
|
10761
11845
|
exports.STABLE_STAMPS = STABLE_STAMPS;
|
|
10762
11846
|
exports.Whiteboard = Whiteboard;
|
|
11847
|
+
exports.closePdfDocument = closePdfDocument;
|
|
11848
|
+
exports.configurePdfWorker = configurePdfWorker;
|
|
10763
11849
|
exports.findStampForCustomData = findStampForCustomData;
|
|
10764
11850
|
exports.geometry3dStamp = geometry3dStamp;
|
|
10765
11851
|
exports.geometryStamp = geometryStamp;
|
|
10766
11852
|
exports.graph2dStamp = graph2dStamp;
|
|
11853
|
+
exports.insertPdfPages = insertPdfPages;
|
|
11854
|
+
exports.insertRasterizedPagesIntoScene = insertRasterizedPagesIntoScene;
|
|
10767
11855
|
exports.isGeometry3DCustomData = isGeometry3DCustomData;
|
|
10768
11856
|
exports.isGeometryCustomData = isGeometryCustomData;
|
|
10769
11857
|
exports.isGraph2DCustomData = isGraph2DCustomData;
|
|
10770
11858
|
exports.isLatexCustomData = isLatexCustomData;
|
|
10771
11859
|
exports.isStampElement = isStampElement;
|
|
10772
11860
|
exports.latexStamp = latexStamp;
|
|
11861
|
+
exports.loadPdfDocument = loadPdfDocument;
|
|
11862
|
+
exports.parsePageRange = parsePageRange;
|
|
10773
11863
|
exports.pickSyncableAppState = pickSyncableAppState;
|
|
11864
|
+
exports.rasterizePdf = rasterizePdf;
|
|
10774
11865
|
exports.restoreMissingStampFiles = restoreMissingStampFiles;
|
|
10775
11866
|
//# sourceMappingURL=index.js.map
|
|
10776
11867
|
//# sourceMappingURL=index.js.map
|