@kokoa/clotho-editor 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -26
- package/dist/{chunk-UE32Q6U5.js → chunk-GMGB7NIL.js} +198 -103
- package/dist/chunk-GMGB7NIL.js.map +1 -0
- package/dist/clotho-editor.css +150 -18
- package/dist/index.d.ts +40 -2
- package/dist/index.js +129 -17
- package/dist/index.js.map +1 -1
- package/dist/{main-SWHHYTLM.js → main-NCPWDB2U.js} +2417 -1603
- package/dist/main-NCPWDB2U.js.map +1 -0
- package/docs/PORTING.md +16 -33
- package/package.json +10 -4
- package/dist/chunk-UE32Q6U5.js.map +0 -1
- package/dist/main-SWHHYTLM.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { updateCanvas, getDef, subscribe, updateMeta, undo, redo, isDirty, canUndo, canRedo, isDraft, uniqueChapterId, getCurrentTime, addChapter, setSelection, uniqueElementId, registerExternalAsset, addElement, registerDataUriAsset, listAnimations, saveAnimation,
|
|
1
|
+
import { updateCanvas, getDef, subscribe, updateMeta, downloadAnimationJson, importAnimation, setDef, markClean, undo, redo, updateSettings, isDirty, canUndo, canRedo, isDraft, uniqueChapterId, getCurrentTime, addChapter, setSelection, uniqueElementId, registerExternalAsset, addElement, registerDataUriAsset, listAnimations, saveAnimation, deleteAnimation, duplicateAnimation, createAnimation, getSelection, getSelectedElementIds, deleteElement, deleteChapter, deleteEffect, updateChapter, isGroup, ungroupElement, groupElements, moveElementToEnd, moveElementToFront, reorderElement, loadAnimation, setDraft, getCurrentSnapshot, setElementValueAtTime, findContainingGroup, isElementSelected, updateElementBase, uniqueEffectId, addEffect, removeAppearance, removeTrack, removeTrackKeyframe, setTrackKeyframe, setCurrentTime, addAppearance, jumpBack, jumpForward, getHistory, promoteDraftToSaved, toggleSelectionFor, moveGroupBy, placeholderImageUrl, groupBbox, updateLocales, updateDuration, updateAppearance, updateEffect, childIdsOf } from './chunk-GMGB7NIL.js';
|
|
2
2
|
import { activeAppearance, animationDocumentSchema, resolveAsset } from '@kokoa/clotho';
|
|
3
3
|
|
|
4
4
|
// src/legacy/grid.ts
|
|
@@ -86,7 +86,11 @@ function anchorPointsOf(baseEl, state) {
|
|
|
86
86
|
{ x: x + w / 2, y, anchor: "top" },
|
|
87
87
|
{ x: x + w, y: y + h / 2, anchor: "right" },
|
|
88
88
|
{ x: x + w / 2, y: y + h, anchor: "bottom" },
|
|
89
|
-
{ x, y: y + h / 2, anchor: "left" }
|
|
89
|
+
{ x, y: y + h / 2, anchor: "left" },
|
|
90
|
+
{ x, y, anchor: "top-left" },
|
|
91
|
+
{ x: x + w, y, anchor: "top-right" },
|
|
92
|
+
{ x, y: y + h, anchor: "bottom-left" },
|
|
93
|
+
{ x: x + w, y: y + h, anchor: "bottom-right" }
|
|
90
94
|
];
|
|
91
95
|
}
|
|
92
96
|
if (baseEl.type === "circle") {
|
|
@@ -119,6 +123,14 @@ function anchorPointOf(el, state, anchor) {
|
|
|
119
123
|
return { x: cx, y: y + h };
|
|
120
124
|
case "left":
|
|
121
125
|
return { x, y: cy };
|
|
126
|
+
case "top-left":
|
|
127
|
+
return { x, y };
|
|
128
|
+
case "top-right":
|
|
129
|
+
return { x: x + w, y };
|
|
130
|
+
case "bottom-left":
|
|
131
|
+
return { x, y: y + h };
|
|
132
|
+
case "bottom-right":
|
|
133
|
+
return { x: x + w, y: y + h };
|
|
122
134
|
case "center":
|
|
123
135
|
case "auto":
|
|
124
136
|
default:
|
|
@@ -419,1694 +431,2077 @@ function parsePolygonPoints(points) {
|
|
|
419
431
|
return result;
|
|
420
432
|
}
|
|
421
433
|
|
|
422
|
-
// src/legacy/
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
let box;
|
|
428
|
-
let handles = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
429
|
-
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
430
|
-
box = {
|
|
431
|
-
x: state.x,
|
|
432
|
-
y: state.y,
|
|
433
|
-
w: state.width,
|
|
434
|
-
h: state.height
|
|
435
|
-
};
|
|
436
|
-
} else if (baseEl.type === "circle") {
|
|
437
|
-
const r = state.r;
|
|
438
|
-
box = {
|
|
439
|
-
x: state.cx - r,
|
|
440
|
-
y: state.cy - r,
|
|
441
|
-
w: r * 2,
|
|
442
|
-
h: r * 2
|
|
443
|
-
};
|
|
444
|
-
handles = ["n", "e", "s", "w"];
|
|
445
|
-
} else if (baseEl.type === "text") {
|
|
446
|
-
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
447
|
-
if (!bbox) return null;
|
|
448
|
-
box = bbox;
|
|
449
|
-
handles = ["se"];
|
|
450
|
-
} else {
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
const positions = {
|
|
454
|
-
nw: { x: box.x, y: box.y, cursor: "nwse-resize" },
|
|
455
|
-
n: { x: box.x + box.w / 2, y: box.y, cursor: "ns-resize" },
|
|
456
|
-
ne: { x: box.x + box.w, y: box.y, cursor: "nesw-resize" },
|
|
457
|
-
e: { x: box.x + box.w, y: box.y + box.h / 2, cursor: "ew-resize" },
|
|
458
|
-
se: { x: box.x + box.w, y: box.y + box.h, cursor: "nwse-resize" },
|
|
459
|
-
s: { x: box.x + box.w / 2, y: box.y + box.h, cursor: "ns-resize" },
|
|
460
|
-
sw: { x: box.x, y: box.y + box.h, cursor: "nesw-resize" },
|
|
461
|
-
w: { x: box.x, y: box.y + box.h / 2, cursor: "ew-resize" }
|
|
462
|
-
};
|
|
463
|
-
const g = document.createElementNS(SVG_NS, "g");
|
|
464
|
-
const rotation = state.rotation || 0;
|
|
465
|
-
if (rotation && (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "circle")) {
|
|
466
|
-
const cx = box.x + box.w / 2;
|
|
467
|
-
const cy = box.y + box.h / 2;
|
|
468
|
-
g.setAttribute("transform", `rotate(${rotation} ${cx} ${cy})`);
|
|
469
|
-
}
|
|
470
|
-
let html = "";
|
|
471
|
-
for (const h of handles) {
|
|
472
|
-
const p = positions[h];
|
|
473
|
-
html += `<rect x="${p.x - 5}" y="${p.y - 5}" width="10" height="10"
|
|
474
|
-
fill="white" stroke="var(--color-accent)" stroke-width="2" rx="2"
|
|
475
|
-
data-resize-handle="${h}" data-elem-id="${escapeXml(elId)}" style="cursor: ${p.cursor}" />`;
|
|
476
|
-
}
|
|
477
|
-
g.innerHTML = html;
|
|
478
|
-
return g;
|
|
434
|
+
// src/legacy/tool-state.ts
|
|
435
|
+
var activeTool = "select";
|
|
436
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
437
|
+
function getActiveTool() {
|
|
438
|
+
return activeTool;
|
|
479
439
|
}
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
if (!baseEl || !state) return null;
|
|
484
|
-
const center = centerOfElement(baseEl, state);
|
|
485
|
-
if (!center) return null;
|
|
486
|
-
let topY = null;
|
|
487
|
-
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
488
|
-
topY = state.y - 24;
|
|
489
|
-
} else if (baseEl.type === "circle") {
|
|
490
|
-
topY = state.cy - state.r - 24;
|
|
491
|
-
} else if (baseEl.type === "text") {
|
|
492
|
-
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
493
|
-
if (bbox) topY = bbox.y - 24;
|
|
494
|
-
} else if (baseEl.type === "polygon") {
|
|
495
|
-
const bbox = polygonBoundingBox(String(state.points ?? ""));
|
|
496
|
-
if (bbox) topY = bbox.y - 24;
|
|
497
|
-
} else if (baseEl.type === "path") {
|
|
498
|
-
const bbox = pathBBoxOnCanvas(canvasEl2, elId);
|
|
499
|
-
if (bbox) topY = bbox.y - 24;
|
|
500
|
-
}
|
|
501
|
-
if (topY === null) return null;
|
|
502
|
-
const g = document.createElementNS(SVG_NS, "g");
|
|
503
|
-
const rotation = state.rotation || 0;
|
|
504
|
-
if (rotation)
|
|
505
|
-
g.setAttribute("transform", `rotate(${rotation} ${center.x} ${center.y})`);
|
|
506
|
-
g.innerHTML = `
|
|
507
|
-
<line x1="${center.x}" y1="${center.y}" x2="${center.x}" y2="${topY + 8}" stroke="var(--color-accent)" stroke-width="1.5" stroke-dasharray="3 2" pointer-events="none" />
|
|
508
|
-
<circle cx="${center.x}" cy="${topY}" r="8" fill="var(--color-accent)" stroke="white" stroke-width="2" data-rotate-handle="${escapeXml(elId)}" style="cursor: grab" />
|
|
509
|
-
<text x="${center.x + 14}" y="${topY + 4}" font-size="10" fill="var(--color-accent)" font-family="ui-monospace, monospace" pointer-events="none">${Math.round(rotation)}\xB0</text>
|
|
510
|
-
`;
|
|
511
|
-
return g;
|
|
440
|
+
function setActiveTool(tool) {
|
|
441
|
+
activeTool = tool;
|
|
442
|
+
for (const listener of listeners2) listener(tool);
|
|
512
443
|
}
|
|
513
|
-
function
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (baseEl.type !== "rect" && baseEl.type !== "circle" && baseEl.type !== "image")
|
|
518
|
-
return null;
|
|
519
|
-
const points = anchorPointsOf(baseEl, state);
|
|
520
|
-
const g = document.createElementNS(SVG_NS, "g");
|
|
521
|
-
for (const p of points) {
|
|
522
|
-
g.innerHTML += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="white" stroke="#6366f1" stroke-width="2"
|
|
523
|
-
data-anchor-handle data-elem-id="${escapeXml(elId)}" data-anchor="${p.anchor}" style="cursor: crosshair" />`;
|
|
524
|
-
}
|
|
525
|
-
return g;
|
|
444
|
+
function subscribeActiveTool(listener) {
|
|
445
|
+
listeners2.add(listener);
|
|
446
|
+
listener(activeTool);
|
|
447
|
+
return () => listeners2.delete(listener);
|
|
526
448
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (baseEl.type !== "line" && baseEl.type !== "arrow") return null;
|
|
532
|
-
const coords = baseEl.type === "line" ? resolveLineCoords(state, snap2, byId) : resolveArrowCoords(state, snap2, byId);
|
|
533
|
-
if (!coords) return null;
|
|
534
|
-
const midX = (coords.x1 + coords.x2) / 2;
|
|
535
|
-
const midY = (coords.y1 + coords.y2) / 2;
|
|
536
|
-
const g = document.createElementNS(SVG_NS, "g");
|
|
537
|
-
g.innerHTML = `
|
|
538
|
-
<rect x="${coords.x1 - 6}" y="${coords.y1 - 6}" width="12" height="12" rx="2"
|
|
539
|
-
fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
540
|
-
data-endpoint-handle="start" data-elem-id="${escapeXml(elId)}" style="cursor: grab" />
|
|
541
|
-
<rect x="${coords.x2 - 6}" y="${coords.y2 - 6}" width="12" height="12" rx="2"
|
|
542
|
-
fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
543
|
-
data-endpoint-handle="end" data-elem-id="${escapeXml(elId)}" style="cursor: grab" />
|
|
544
|
-
<circle cx="${midX}" cy="${midY}" r="6"
|
|
545
|
-
fill="var(--color-accent)" stroke="white" stroke-width="2"
|
|
546
|
-
data-line-mid-handle data-elem-id="${escapeXml(elId)}" style="cursor: move" />
|
|
547
|
-
`;
|
|
548
|
-
return g;
|
|
449
|
+
|
|
450
|
+
// src/legacy/ui-interactions.ts
|
|
451
|
+
function hasSelectionModifier(modifiers) {
|
|
452
|
+
return modifiers.shiftKey || modifiers.ctrlKey || modifiers.metaKey;
|
|
549
453
|
}
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
454
|
+
function shouldDeferElementPointerDown(modifiers, options) {
|
|
455
|
+
return hasSelectionModifier(modifiers) && options.isElement && !options.isHandle;
|
|
456
|
+
}
|
|
457
|
+
function nextPointerSelection(current, elementId, modifiers) {
|
|
458
|
+
return hasSelectionModifier(modifiers) ? toggleSelectionFor(current, elementId) : { kind: "element", elementId };
|
|
459
|
+
}
|
|
460
|
+
var TOOL_SHORTCUTS = {
|
|
461
|
+
v: "select",
|
|
462
|
+
r: "rect",
|
|
463
|
+
o: "circle",
|
|
464
|
+
l: "line",
|
|
465
|
+
a: "arrow",
|
|
466
|
+
t: "text",
|
|
467
|
+
i: "image",
|
|
468
|
+
b: "path",
|
|
469
|
+
y: "polygon"
|
|
470
|
+
};
|
|
471
|
+
function toolForShortcut(key, options) {
|
|
472
|
+
if (options.isEditing || options.modifierKey || options.altKey) return null;
|
|
473
|
+
return TOOL_SHORTCUTS[key.toLowerCase()] ?? null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/legacy/element-list.ts
|
|
477
|
+
var listEl = null;
|
|
478
|
+
var toolsRootEl = null;
|
|
479
|
+
var searchEl = null;
|
|
480
|
+
var dragSourceId = null;
|
|
481
|
+
function initElementList(root, toolsRoot) {
|
|
482
|
+
listEl = root;
|
|
483
|
+
toolsRootEl = toolsRoot;
|
|
484
|
+
searchEl = document.getElementById(
|
|
485
|
+
"studio-element-search"
|
|
486
|
+
);
|
|
487
|
+
subscribe(render);
|
|
488
|
+
root.addEventListener("click", onClick);
|
|
489
|
+
root.addEventListener("dragstart", onDragStart);
|
|
490
|
+
root.addEventListener("dragover", onDragOver);
|
|
491
|
+
root.addEventListener("dragleave", onDragLeave);
|
|
492
|
+
root.addEventListener("drop", onDrop);
|
|
493
|
+
root.addEventListener("dragend", onDragEnd);
|
|
494
|
+
toolsRootEl?.addEventListener("click", onToolsClick);
|
|
495
|
+
subscribeActiveTool((tool) => {
|
|
496
|
+
toolsRootEl?.setAttribute("data-active-tool", tool);
|
|
497
|
+
document.body.dataset.studioTool = tool;
|
|
498
|
+
toolsRootEl?.querySelectorAll("[data-studio-tool], [data-add-element]").forEach((button) => {
|
|
499
|
+
const value = button.dataset.studioTool ?? button.dataset.addElement;
|
|
500
|
+
const active = value === tool;
|
|
501
|
+
button.classList.toggle("is-active", active);
|
|
502
|
+
button.setAttribute("aria-pressed", String(active));
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
searchEl?.addEventListener("input", render);
|
|
506
|
+
searchEl?.addEventListener("keydown", (e) => {
|
|
507
|
+
if (e.key === "Escape" && searchEl) {
|
|
508
|
+
searchEl.value = "";
|
|
509
|
+
render();
|
|
574
510
|
}
|
|
511
|
+
});
|
|
512
|
+
render();
|
|
513
|
+
}
|
|
514
|
+
function escapeHtml(s) {
|
|
515
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
516
|
+
}
|
|
517
|
+
function friendlyElementLabel(el) {
|
|
518
|
+
const e = el;
|
|
519
|
+
const visible = e.name?.trim() || e.label?.trim() || e.content?.trim();
|
|
520
|
+
return visible ? visible : el.id;
|
|
521
|
+
}
|
|
522
|
+
function matchesSearch(el, query) {
|
|
523
|
+
if (!query) return true;
|
|
524
|
+
const e = el;
|
|
525
|
+
const haystack = [el.id, el.type, e.name, e.label, e.content].filter(Boolean).join(" ").toLowerCase();
|
|
526
|
+
return haystack.includes(query);
|
|
527
|
+
}
|
|
528
|
+
function render() {
|
|
529
|
+
if (!listEl) return;
|
|
530
|
+
const def = getDef();
|
|
531
|
+
if (!def || def.elements.length === 0) {
|
|
532
|
+
listEl.innerHTML = '<li style="color: var(--color-fg-muted); padding: 0.4rem;">\uC694\uC18C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</li>';
|
|
533
|
+
return;
|
|
575
534
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
535
|
+
const sel = getSelection();
|
|
536
|
+
const query = searchEl?.value.trim().toLowerCase() ?? "";
|
|
537
|
+
const allReversed = [...def.elements].reverse();
|
|
538
|
+
const items = query ? allReversed.filter((el) => matchesSearch(el, query)) : allReversed;
|
|
539
|
+
const multiHint = sel.kind === "elements" ? `<div class="studio-element-list-hint">\uB2E4\uC911 \uC120\uD0DD: ${sel.elementIds.length}\uAC1C (Shift+\uD074\uB9AD\uC73C\uB85C \uCD94\uAC00/\uD574\uC81C)</div>` : "";
|
|
540
|
+
const filterHint = query ? `<div class="studio-element-list-hint">\uAC80\uC0C9 "${escapeHtml(query)}" \xB7 ${items.length}/${def.elements.length} \uC77C\uCE58 \xB7 Esc = \uC9C0\uC6B0\uAE30</div>` : `<div class="studio-element-list-hint">\uC704 = \uC55E\uCABD (z-index \u2191) \xB7 Shift+\uD074\uB9AD = \uB2E4\uC911 \uC120\uD0DD</div>`;
|
|
541
|
+
const emptyMsg = query && items.length === 0 ? `<li style="color: var(--color-fg-muted); padding: 0.4rem; font-size:0.78rem;">"${escapeHtml(query)}" \uC77C\uCE58 \uC5C6\uC74C</li>` : "";
|
|
542
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const element of items) {
|
|
544
|
+
if (!element.parentId) continue;
|
|
545
|
+
const children = byParent.get(element.parentId) ?? [];
|
|
546
|
+
children.push(element);
|
|
547
|
+
byParent.set(element.parentId, children);
|
|
583
548
|
}
|
|
584
|
-
|
|
585
|
-
|
|
549
|
+
const renderItem = (el, depth = 0) => {
|
|
550
|
+
const isSel = isElementSelected(sel, el.id);
|
|
551
|
+
const label = friendlyElementLabel(el);
|
|
552
|
+
const subId = label !== el.id ? `<span class="studio-element-list-item-sub">${escapeHtml(el.id)}</span>` : "";
|
|
553
|
+
const group = el.type === "group";
|
|
554
|
+
const children = query ? [] : byParent.get(el.id) ?? [];
|
|
555
|
+
return `<li class="studio-element-list-item ${isSel ? "is-selected" : ""} ${group ? "is-group" : ""}" data-elem-id="${escapeHtml(el.id)}" draggable="true" style="--studio-tree-depth:${depth}">
|
|
556
|
+
<span class="studio-element-grip" aria-hidden="true">${group ? "\u25BE" : "\u22EE\u22EE"}</span>
|
|
557
|
+
<span class="studio-element-list-item-label">${group ? "\uADF8\uB8F9 \xB7 " : ""}${escapeHtml(label)} <span class="studio-element-list-item-type">${escapeHtml(el.type)}</span>${subId}</span>
|
|
558
|
+
<button type="button" class="studio-element-list-delete" data-delete title="\uC0AD\uC81C">\u2715</button>
|
|
559
|
+
</li>${children.map((child) => renderItem(child, depth + 1)).join("")}`;
|
|
560
|
+
};
|
|
561
|
+
const visibleItems = query ? items : items.filter((element) => !element.parentId);
|
|
562
|
+
listEl.innerHTML = filterHint + multiHint + emptyMsg + visibleItems.map((element) => renderItem(element)).join("");
|
|
586
563
|
}
|
|
587
|
-
function
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const points = getAnchorPoints(baseEl, state);
|
|
596
|
-
for (const p of points) {
|
|
597
|
-
const isActive = activeSnapTarget?.elementId === baseEl.id && activeSnapTarget?.anchor === p.anchor;
|
|
598
|
-
if (isActive) {
|
|
599
|
-
html += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="var(--color-accent)" stroke="white" stroke-width="2" opacity="0.85" pointer-events="none" />`;
|
|
600
|
-
html += `<circle cx="${p.x}" cy="${p.y}" r="12" fill="none" stroke="var(--color-accent)" stroke-width="1.5" opacity="0.4" pointer-events="none" />`;
|
|
601
|
-
} else {
|
|
602
|
-
html += `<circle cx="${p.x}" cy="${p.y}" r="3" fill="white" stroke="var(--color-accent)" stroke-width="1.5" opacity="0.5" pointer-events="none" />`;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
564
|
+
function onClick(e) {
|
|
565
|
+
const target = e.target;
|
|
566
|
+
const li = target.closest(".studio-element-list-item");
|
|
567
|
+
if (!li) return;
|
|
568
|
+
const id = li.dataset.elemId ?? "";
|
|
569
|
+
if (target.closest("[data-delete]")) {
|
|
570
|
+
deleteElement(id);
|
|
571
|
+
return;
|
|
605
572
|
}
|
|
606
|
-
|
|
607
|
-
|
|
573
|
+
setActiveTool("select");
|
|
574
|
+
const me = e;
|
|
575
|
+
setSelection(nextPointerSelection(getSelection(), id, me));
|
|
608
576
|
}
|
|
609
|
-
function
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
w: state.width + 8,
|
|
619
|
-
h: state.height + 8
|
|
620
|
-
};
|
|
621
|
-
} else if (baseEl.type === "circle") {
|
|
622
|
-
const r = state.r;
|
|
623
|
-
box = {
|
|
624
|
-
x: state.cx - r - 4,
|
|
625
|
-
y: state.cy - r - 4,
|
|
626
|
-
w: r * 2 + 8,
|
|
627
|
-
h: r * 2 + 8
|
|
628
|
-
};
|
|
629
|
-
} else if (baseEl.type === "text") {
|
|
630
|
-
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
631
|
-
if (bbox)
|
|
632
|
-
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
633
|
-
} else if (baseEl.type === "line" || baseEl.type === "arrow") {
|
|
634
|
-
const coords = baseEl.type === "line" ? resolveLineCoords(state, snap2, byId) : resolveArrowCoords(state, snap2, byId);
|
|
635
|
-
if (!coords) return null;
|
|
636
|
-
box = {
|
|
637
|
-
x: Math.min(coords.x1, coords.x2) - 4,
|
|
638
|
-
y: Math.min(coords.y1, coords.y2) - 4,
|
|
639
|
-
w: Math.abs(coords.x2 - coords.x1) + 8,
|
|
640
|
-
h: Math.abs(coords.y2 - coords.y1) + 8
|
|
641
|
-
};
|
|
642
|
-
} else if (baseEl.type === "polygon") {
|
|
643
|
-
const bbox = polygonBoundingBox(String(state.points ?? ""));
|
|
644
|
-
if (bbox)
|
|
645
|
-
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
646
|
-
} else if (baseEl.type === "path") {
|
|
647
|
-
const bbox = pathBBoxOnCanvas(canvasEl2, elId);
|
|
648
|
-
if (bbox)
|
|
649
|
-
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
650
|
-
}
|
|
651
|
-
if (!box) return null;
|
|
652
|
-
const rect = document.createElementNS(SVG_NS, "rect");
|
|
653
|
-
rect.setAttribute("x", String(box.x));
|
|
654
|
-
rect.setAttribute("y", String(box.y));
|
|
655
|
-
rect.setAttribute("width", String(box.w));
|
|
656
|
-
rect.setAttribute("height", String(box.h));
|
|
657
|
-
rect.classList.add("element-selected-outline");
|
|
658
|
-
return rect;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// src/legacy/canvas-preview.ts
|
|
662
|
-
var previewRoot = null;
|
|
663
|
-
var previewHandle = null;
|
|
664
|
-
async function mountPreview(host, doc) {
|
|
665
|
-
const { mountStage } = await import('@kokoa/clotho/dom');
|
|
666
|
-
previewHandle = mountStage(host, doc, { player: { autoplay: true } });
|
|
667
|
-
}
|
|
668
|
-
function showPreview(canvasEl2, def) {
|
|
669
|
-
if (!canvasEl2) return;
|
|
670
|
-
const parent = canvasEl2.parentElement;
|
|
671
|
-
if (!parent) return;
|
|
672
|
-
hidePreview(canvasEl2);
|
|
673
|
-
canvasEl2.style.visibility = "hidden";
|
|
674
|
-
previewRoot = document.createElement("div");
|
|
675
|
-
const bg = def.canvas.background && def.canvas.background !== "transparent" ? def.canvas.background : "transparent";
|
|
676
|
-
previewRoot.style.cssText = `position:absolute;left:0;top:0;width:${def.canvas.width}px;height:${def.canvas.height}px;background:${bg};z-index:2;overflow:hidden;`;
|
|
677
|
-
parent.style.position = "relative";
|
|
678
|
-
parent.appendChild(previewRoot);
|
|
679
|
-
void mountPreview(previewRoot, def);
|
|
680
|
-
}
|
|
681
|
-
function hidePreview(canvasEl2) {
|
|
682
|
-
if (canvasEl2) canvasEl2.style.visibility = "";
|
|
683
|
-
previewHandle?.destroy();
|
|
684
|
-
previewHandle = null;
|
|
685
|
-
if (previewRoot) {
|
|
686
|
-
previewRoot.remove();
|
|
687
|
-
previewRoot = null;
|
|
577
|
+
function onDragStart(e) {
|
|
578
|
+
const li = e.target.closest(
|
|
579
|
+
".studio-element-list-item"
|
|
580
|
+
);
|
|
581
|
+
if (!li) return;
|
|
582
|
+
dragSourceId = li.dataset.elemId ?? null;
|
|
583
|
+
if (e.dataTransfer) {
|
|
584
|
+
e.dataTransfer.effectAllowed = "move";
|
|
585
|
+
e.dataTransfer.setData("text/plain", dragSourceId ?? "");
|
|
688
586
|
}
|
|
587
|
+
li.classList.add("is-dragging");
|
|
689
588
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
var SNAP_RADIUS = 12;
|
|
693
|
-
var HEAD_MARKER_DEFS = `
|
|
694
|
-
<marker id="studio-h-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
695
|
-
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
696
|
-
</marker>
|
|
697
|
-
<marker id="studio-h-arrow-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
698
|
-
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
699
|
-
</marker>
|
|
700
|
-
<marker id="studio-h-triangle" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
701
|
-
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
702
|
-
</marker>
|
|
703
|
-
<marker id="studio-h-triangle-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
704
|
-
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
705
|
-
</marker>
|
|
706
|
-
<marker id="studio-h-triangle-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto">
|
|
707
|
-
<path d="M 0 0 L 10 5 L 0 10" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
708
|
-
</marker>
|
|
709
|
-
<marker id="studio-h-triangle-open-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
|
|
710
|
-
<path d="M 0 0 L 10 5 L 0 10" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
711
|
-
</marker>
|
|
712
|
-
<marker id="studio-h-circle" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6">
|
|
713
|
-
<circle cx="5" cy="5" r="4" fill="currentColor" />
|
|
714
|
-
</marker>
|
|
715
|
-
<marker id="studio-h-circle-open" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="7" markerHeight="7">
|
|
716
|
-
<circle cx="5" cy="5" r="4" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
717
|
-
</marker>
|
|
718
|
-
<marker id="studio-h-diamond" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
719
|
-
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="currentColor" />
|
|
720
|
-
</marker>
|
|
721
|
-
<marker id="studio-h-diamond-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
722
|
-
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="currentColor" />
|
|
723
|
-
</marker>
|
|
724
|
-
<marker id="studio-h-diamond-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
725
|
-
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
726
|
-
</marker>
|
|
727
|
-
<marker id="studio-h-diamond-open-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
728
|
-
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
729
|
-
</marker>
|
|
730
|
-
<marker id="studio-h-bar" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="10" orient="auto">
|
|
731
|
-
<rect x="4" y="0" width="2" height="10" fill="currentColor" />
|
|
732
|
-
</marker>
|
|
733
|
-
`;
|
|
734
|
-
function markerUrlFor(head, end) {
|
|
735
|
-
if (!head || head === "none") return "";
|
|
736
|
-
const isOpenCircle = head === "circle-open";
|
|
737
|
-
const isCircle = head === "circle" || isOpenCircle;
|
|
738
|
-
if (isCircle) return `url(#studio-h-${head})`;
|
|
739
|
-
const idBase = `studio-h-${head}`;
|
|
740
|
-
return end === "start" ? `url(#${idBase}-start)` : `url(#${idBase})`;
|
|
741
|
-
}
|
|
742
|
-
var dragState = null;
|
|
743
|
-
var rotateState = null;
|
|
744
|
-
var connectState = null;
|
|
745
|
-
var endpointDragState = null;
|
|
746
|
-
var vertexDragState = null;
|
|
747
|
-
var resizeState = null;
|
|
748
|
-
var marqueeState = null;
|
|
749
|
-
var marqueeJustFinished = false;
|
|
750
|
-
var canvasEl = null;
|
|
751
|
-
var hoveredElementId = null;
|
|
752
|
-
var canvasZoom = 1;
|
|
753
|
-
function initCanvas(root) {
|
|
754
|
-
canvasEl = root;
|
|
755
|
-
root.addEventListener("click", onCanvasClick);
|
|
756
|
-
root.addEventListener("mousedown", onMouseDown);
|
|
757
|
-
root.addEventListener("mouseover", onMouseOver);
|
|
758
|
-
root.addEventListener("mouseleave", onMouseLeave);
|
|
759
|
-
root.addEventListener("contextmenu", onContextMenu);
|
|
760
|
-
root.addEventListener("wheel", onCanvasWheel, { passive: false });
|
|
761
|
-
document.addEventListener("mousemove", onMouseMove);
|
|
762
|
-
document.addEventListener("mouseup", onMouseUp);
|
|
763
|
-
subscribe(render);
|
|
764
|
-
render();
|
|
765
|
-
}
|
|
766
|
-
function onCanvasWheel(e) {
|
|
767
|
-
if (!e.ctrlKey && !e.metaKey) return;
|
|
589
|
+
function onDragOver(e) {
|
|
590
|
+
if (!dragSourceId) return;
|
|
768
591
|
e.preventDefault();
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
0.1,
|
|
773
|
-
Math.min(5, canvasZoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))
|
|
774
|
-
);
|
|
775
|
-
render();
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const wrapRect = wrap.getBoundingClientRect();
|
|
779
|
-
const mxInWrap = e.clientX - wrapRect.left + wrap.scrollLeft;
|
|
780
|
-
const myInWrap = e.clientY - wrapRect.top + wrap.scrollTop;
|
|
781
|
-
const old = canvasZoom;
|
|
782
|
-
canvasZoom = Math.max(
|
|
783
|
-
0.1,
|
|
784
|
-
Math.min(5, canvasZoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))
|
|
592
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
593
|
+
const li = e.target.closest(
|
|
594
|
+
".studio-element-list-item"
|
|
785
595
|
);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
596
|
+
listEl?.querySelectorAll(".is-drop-above, .is-drop-below").forEach((n) => n.classList.remove("is-drop-above", "is-drop-below"));
|
|
597
|
+
if (!li) return;
|
|
598
|
+
const rect = li.getBoundingClientRect();
|
|
599
|
+
const upper = e.clientY < rect.top + rect.height / 2;
|
|
600
|
+
li.classList.add(upper ? "is-drop-above" : "is-drop-below");
|
|
790
601
|
}
|
|
791
|
-
function
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const idx = Number(vertexEl.dataset.vertexIndex ?? "-1");
|
|
797
|
-
const def = getDef();
|
|
798
|
-
if (!def || idx < 0) return;
|
|
799
|
-
const el = def.elements.find((x) => x.id === id);
|
|
800
|
-
if (!el || el.type !== "polygon") return;
|
|
801
|
-
const snap2 = getCurrentSnapshot();
|
|
802
|
-
const state = snap2.get(id);
|
|
803
|
-
if (!state) return;
|
|
804
|
-
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
805
|
-
if (points.length <= 3) return;
|
|
806
|
-
e.preventDefault();
|
|
807
|
-
points.splice(idx, 1);
|
|
808
|
-
setElementValueAtTime(id, { points: points.join(" ") });
|
|
602
|
+
function onDragLeave(e) {
|
|
603
|
+
const li = e.target.closest(
|
|
604
|
+
".studio-element-list-item"
|
|
605
|
+
);
|
|
606
|
+
li?.classList.remove("is-drop-above", "is-drop-below");
|
|
809
607
|
}
|
|
810
|
-
function
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
608
|
+
function onDrop(e) {
|
|
609
|
+
if (!dragSourceId) return;
|
|
610
|
+
e.preventDefault();
|
|
611
|
+
const li = e.target.closest(
|
|
612
|
+
".studio-element-list-item"
|
|
613
|
+
);
|
|
614
|
+
if (!li) return;
|
|
615
|
+
const targetId = li.dataset.elemId ?? "";
|
|
616
|
+
if (!targetId || targetId === dragSourceId) return;
|
|
617
|
+
const rect = li.getBoundingClientRect();
|
|
618
|
+
const upper = e.clientY < rect.top + rect.height / 2;
|
|
619
|
+
const positionInVisualList = upper ? "before" : "after";
|
|
620
|
+
const positionInDefArray = positionInVisualList === "before" ? "after" : "before";
|
|
621
|
+
reorderElement(dragSourceId, targetId, positionInDefArray);
|
|
622
|
+
dragSourceId = null;
|
|
816
623
|
}
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
624
|
+
function onDragEnd() {
|
|
625
|
+
dragSourceId = null;
|
|
626
|
+
listEl?.querySelectorAll(".is-drop-above, .is-drop-below, .is-dragging").forEach(
|
|
627
|
+
(n) => n.classList.remove("is-drop-above", "is-drop-below", "is-dragging")
|
|
628
|
+
);
|
|
822
629
|
}
|
|
823
|
-
function
|
|
824
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
y: (clientY - rect.top) * sy
|
|
833
|
-
});
|
|
630
|
+
function onToolsClick(e) {
|
|
631
|
+
const target = e.target;
|
|
632
|
+
const btn = target.closest(
|
|
633
|
+
"[data-studio-tool], [data-add-element]"
|
|
634
|
+
);
|
|
635
|
+
if (!btn) return;
|
|
636
|
+
const type = btn.dataset.studioTool ?? btn.dataset.addElement;
|
|
637
|
+
if (!type) return;
|
|
638
|
+
setActiveTool(type);
|
|
834
639
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
640
|
+
function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
|
|
641
|
+
switch (type) {
|
|
642
|
+
case "rect":
|
|
643
|
+
return {
|
|
644
|
+
type: "rect",
|
|
645
|
+
id,
|
|
646
|
+
rotation: 0,
|
|
647
|
+
appearances: [],
|
|
648
|
+
tracks: [],
|
|
649
|
+
x: cx - 60,
|
|
650
|
+
y: cy - 30,
|
|
651
|
+
width: 120,
|
|
652
|
+
height: 60,
|
|
653
|
+
fill: "#a5b4fc",
|
|
654
|
+
stroke: "#6366f1",
|
|
655
|
+
strokeWidth: 1.5,
|
|
656
|
+
cornerRadius: 8,
|
|
657
|
+
label: id,
|
|
658
|
+
labelColor: "#0b0b0f",
|
|
659
|
+
labelSize: 14
|
|
660
|
+
};
|
|
661
|
+
case "circle":
|
|
662
|
+
return {
|
|
663
|
+
type: "circle",
|
|
664
|
+
id,
|
|
665
|
+
rotation: 0,
|
|
666
|
+
appearances: [],
|
|
667
|
+
tracks: [],
|
|
668
|
+
cx,
|
|
669
|
+
cy,
|
|
670
|
+
r: 36,
|
|
671
|
+
fill: "#a5b4fc",
|
|
672
|
+
stroke: "#6366f1",
|
|
673
|
+
strokeWidth: 1.5,
|
|
674
|
+
label: id,
|
|
675
|
+
labelColor: "#0b0b0f",
|
|
676
|
+
labelSize: 14
|
|
677
|
+
};
|
|
678
|
+
case "line":
|
|
679
|
+
return {
|
|
680
|
+
type: "line",
|
|
681
|
+
id,
|
|
682
|
+
rotation: 0,
|
|
683
|
+
appearances: [],
|
|
684
|
+
tracks: [],
|
|
685
|
+
x1: cx - 80,
|
|
686
|
+
y1: cy,
|
|
687
|
+
x2: cx + 80,
|
|
688
|
+
y2: cy,
|
|
689
|
+
stroke: "#6366f1",
|
|
690
|
+
strokeWidth: 2,
|
|
691
|
+
headStart: "none",
|
|
692
|
+
headEnd: "none"
|
|
693
|
+
};
|
|
694
|
+
case "arrow":
|
|
695
|
+
return {
|
|
696
|
+
type: "arrow",
|
|
697
|
+
id,
|
|
698
|
+
rotation: 0,
|
|
699
|
+
appearances: [],
|
|
700
|
+
tracks: [],
|
|
701
|
+
x1: cx - 100,
|
|
702
|
+
y1: cy,
|
|
703
|
+
x2: cx + 100,
|
|
704
|
+
y2: cy,
|
|
705
|
+
stroke: "#6366f1",
|
|
706
|
+
strokeWidth: 2,
|
|
707
|
+
curvature: 0,
|
|
708
|
+
labelColor: "#0b0b0f",
|
|
709
|
+
labelOffsetX: 0,
|
|
710
|
+
labelOffsetY: 4,
|
|
711
|
+
headStart: "none",
|
|
712
|
+
headEnd: "arrow"
|
|
713
|
+
};
|
|
714
|
+
case "text":
|
|
715
|
+
return {
|
|
716
|
+
type: "text",
|
|
717
|
+
id,
|
|
718
|
+
rotation: 0,
|
|
719
|
+
appearances: [],
|
|
720
|
+
tracks: [],
|
|
721
|
+
x: cx,
|
|
722
|
+
y: cy,
|
|
723
|
+
content: id,
|
|
724
|
+
translations: {},
|
|
725
|
+
fontSize: 18,
|
|
726
|
+
fontWeight: 400,
|
|
727
|
+
color: "#18181b",
|
|
728
|
+
textAnchor: "middle"
|
|
729
|
+
};
|
|
730
|
+
case "image":
|
|
731
|
+
return {
|
|
732
|
+
type: "image",
|
|
733
|
+
id,
|
|
734
|
+
rotation: 0,
|
|
735
|
+
appearances: [],
|
|
736
|
+
tracks: [],
|
|
737
|
+
x: cx - 50,
|
|
738
|
+
y: cy - 50,
|
|
739
|
+
width: 100,
|
|
740
|
+
height: 100,
|
|
741
|
+
assetId: registerExternalAsset(placeholderImageUrl()),
|
|
742
|
+
preserveAspectRatio: "xMidYMid meet",
|
|
743
|
+
opacity: 1
|
|
744
|
+
};
|
|
745
|
+
case "path":
|
|
746
|
+
return {
|
|
747
|
+
type: "path",
|
|
748
|
+
id,
|
|
749
|
+
rotation: 0,
|
|
750
|
+
appearances: [],
|
|
751
|
+
tracks: [],
|
|
752
|
+
x: cx - 40,
|
|
753
|
+
y: cy - 40,
|
|
754
|
+
d: "M 0 0 L 80 0 L 40 80 Z",
|
|
755
|
+
fill: "#a5b4fc",
|
|
756
|
+
stroke: "#6366f1",
|
|
757
|
+
strokeWidth: 2,
|
|
758
|
+
opacity: 1
|
|
759
|
+
};
|
|
760
|
+
case "polygon": {
|
|
761
|
+
const r = 40;
|
|
762
|
+
const sides = Math.max(3, Math.min(24, Math.round(polygonSides) || 6));
|
|
763
|
+
const pts = Array.from({ length: sides }, (_, i) => {
|
|
764
|
+
const a = i * Math.PI * 2 / sides - Math.PI / 2;
|
|
765
|
+
return `${(cx + r * Math.cos(a)).toFixed(1)},${(cy + r * Math.sin(a)).toFixed(1)}`;
|
|
766
|
+
}).join(" ");
|
|
767
|
+
return {
|
|
768
|
+
type: "polygon",
|
|
769
|
+
id,
|
|
770
|
+
rotation: 0,
|
|
771
|
+
appearances: [],
|
|
772
|
+
tracks: [],
|
|
773
|
+
points: pts,
|
|
774
|
+
fill: "#a5b4fc",
|
|
775
|
+
stroke: "#6366f1",
|
|
776
|
+
strokeWidth: 1.5,
|
|
777
|
+
opacity: 1
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
default:
|
|
781
|
+
return null;
|
|
841
782
|
}
|
|
842
|
-
return null;
|
|
843
783
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
if (!def || afterIdx < 0) return;
|
|
859
|
-
const el = def.elements.find((x) => x.id === id2);
|
|
860
|
-
if (!el || el.type !== "polygon") return;
|
|
861
|
-
const snap2 = getCurrentSnapshot();
|
|
862
|
-
const state = snap2.get(id2);
|
|
863
|
-
if (!state) return;
|
|
864
|
-
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
865
|
-
const a = points[afterIdx]?.split(",").map(Number) ?? [];
|
|
866
|
-
const b = points[(afterIdx + 1) % points.length]?.split(",").map(Number) ?? [];
|
|
867
|
-
if (a.length !== 2 || b.length !== 2) return;
|
|
868
|
-
const mx = (a[0] + b[0]) / 2;
|
|
869
|
-
const my = (a[1] + b[1]) / 2;
|
|
870
|
-
points.splice(afterIdx + 1, 0, `${mx.toFixed(1)},${my.toFixed(1)}`);
|
|
871
|
-
setElementValueAtTime(id2, { points: points.join(" ") });
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
if (target?.closest(
|
|
875
|
-
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle]"
|
|
876
|
-
))
|
|
877
|
-
return;
|
|
878
|
-
const id = findElementId(e.target);
|
|
879
|
-
if (id) {
|
|
880
|
-
const resolvedId = e.altKey ? id : findContainingGroup(id)?.id ?? id;
|
|
881
|
-
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
|
882
|
-
setSelection(toggleSelectionFor(getSelection(), resolvedId));
|
|
883
|
-
} else {
|
|
884
|
-
setSelection({ kind: "element", elementId: resolvedId });
|
|
885
|
-
}
|
|
886
|
-
} else if (e.target === canvasEl) setSelection({ kind: "none" });
|
|
784
|
+
|
|
785
|
+
// src/legacy/drawing-interactions.ts
|
|
786
|
+
function dragBounds(start, end) {
|
|
787
|
+
const width = Math.abs(end.x - start.x);
|
|
788
|
+
const height = Math.abs(end.y - start.y);
|
|
789
|
+
return {
|
|
790
|
+
x: Math.min(start.x, end.x),
|
|
791
|
+
y: Math.min(start.y, end.y),
|
|
792
|
+
width,
|
|
793
|
+
height,
|
|
794
|
+
centerX: (start.x + end.x) / 2,
|
|
795
|
+
centerY: (start.y + end.y) / 2,
|
|
796
|
+
distance: Math.hypot(end.x - start.x, end.y - start.y)
|
|
797
|
+
};
|
|
887
798
|
}
|
|
888
|
-
function
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
startX: 0,
|
|
913
|
-
startY: 0,
|
|
914
|
-
startW: 0,
|
|
915
|
-
startH: 0,
|
|
916
|
-
aspect: 1
|
|
799
|
+
function isShapeDrag(start, end) {
|
|
800
|
+
return dragBounds(start, end).distance >= 4;
|
|
801
|
+
}
|
|
802
|
+
function regularPolygonPoints(center, radius, requestedSides) {
|
|
803
|
+
const sides = Math.max(3, Math.min(24, Math.round(requestedSides) || 6));
|
|
804
|
+
return Array.from({ length: sides }, (_, index) => {
|
|
805
|
+
const angle = index * Math.PI * 2 / sides - Math.PI / 2;
|
|
806
|
+
return `${(center.x + radius * Math.cos(angle)).toFixed(1)},${(center.y + radius * Math.sin(angle)).toFixed(1)}`;
|
|
807
|
+
}).join(" ");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/legacy/canvas-handles.ts
|
|
811
|
+
function renderResizeHandles(canvasEl2, elId, snap2, byId) {
|
|
812
|
+
const baseEl = byId.get(elId);
|
|
813
|
+
const state = snap2.get(elId);
|
|
814
|
+
if (!baseEl || !state) return null;
|
|
815
|
+
let box;
|
|
816
|
+
let handles = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
817
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
818
|
+
box = {
|
|
819
|
+
x: state.x,
|
|
820
|
+
y: state.y,
|
|
821
|
+
w: state.width,
|
|
822
|
+
h: state.height
|
|
917
823
|
};
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
rs.startCx = elState2.cx;
|
|
926
|
-
rs.startCy = elState2.cy;
|
|
927
|
-
rs.startR = elState2.r;
|
|
928
|
-
} else if (baseEl2.type === "text") {
|
|
929
|
-
const bbox = textBBoxOnCanvas(canvasEl, id2);
|
|
930
|
-
rs.startX = bbox?.x ?? elState2.x;
|
|
931
|
-
rs.startY = bbox?.y ?? elState2.y;
|
|
932
|
-
rs.startW = bbox?.w ?? 100;
|
|
933
|
-
rs.startH = bbox?.h ?? elState2.fontSize;
|
|
934
|
-
rs.startFontSize = elState2.fontSize;
|
|
935
|
-
rs.aspect = rs.startW / Math.max(1, rs.startH);
|
|
936
|
-
} else {
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
resizeState = rs;
|
|
940
|
-
setSelection({ kind: "element", elementId: id2 });
|
|
941
|
-
render();
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const vertexHandle = target?.closest("[data-vertex-handle]");
|
|
945
|
-
if (vertexHandle) {
|
|
946
|
-
e.preventDefault();
|
|
947
|
-
const id2 = vertexHandle.dataset.elemId ?? "";
|
|
948
|
-
const vi = Number(vertexHandle.dataset.vertexIndex ?? "-1");
|
|
949
|
-
const snap3 = getCurrentSnapshot();
|
|
950
|
-
const elState2 = snap3.get(id2);
|
|
951
|
-
if (!elState2 || vi < 0) return;
|
|
952
|
-
vertexDragState = {
|
|
953
|
-
elementId: id2,
|
|
954
|
-
vertexIndex: vi,
|
|
955
|
-
originalPoints: String(elState2.points ?? "")
|
|
824
|
+
} else if (baseEl.type === "circle") {
|
|
825
|
+
const r = state.r;
|
|
826
|
+
box = {
|
|
827
|
+
x: state.cx - r,
|
|
828
|
+
y: state.cy - r,
|
|
829
|
+
w: r * 2,
|
|
830
|
+
h: r * 2
|
|
956
831
|
};
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
832
|
+
handles = ["nw", "ne", "se", "sw"];
|
|
833
|
+
} else if (baseEl.type === "text") {
|
|
834
|
+
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
835
|
+
if (!bbox) return null;
|
|
836
|
+
box = bbox;
|
|
837
|
+
handles = ["se"];
|
|
838
|
+
} else {
|
|
839
|
+
return null;
|
|
960
840
|
}
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
return;
|
|
841
|
+
const positions = {
|
|
842
|
+
nw: { x: box.x, y: box.y, cursor: "nwse-resize" },
|
|
843
|
+
n: { x: box.x + box.w / 2, y: box.y, cursor: "ns-resize" },
|
|
844
|
+
ne: { x: box.x + box.w, y: box.y, cursor: "nesw-resize" },
|
|
845
|
+
e: { x: box.x + box.w, y: box.y + box.h / 2, cursor: "ew-resize" },
|
|
846
|
+
se: { x: box.x + box.w, y: box.y + box.h, cursor: "nwse-resize" },
|
|
847
|
+
s: { x: box.x + box.w / 2, y: box.y + box.h, cursor: "ns-resize" },
|
|
848
|
+
sw: { x: box.x, y: box.y + box.h, cursor: "nesw-resize" },
|
|
849
|
+
w: { x: box.x, y: box.y + box.h / 2, cursor: "ew-resize" }
|
|
850
|
+
};
|
|
851
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
852
|
+
const rotation = state.rotation || 0;
|
|
853
|
+
if (rotation && (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "circle")) {
|
|
854
|
+
const cx = box.x + box.w / 2;
|
|
855
|
+
const cy = box.y + box.h / 2;
|
|
856
|
+
g.setAttribute("transform", `rotate(${rotation} ${cx} ${cy})`);
|
|
978
857
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
858
|
+
let html = "";
|
|
859
|
+
for (const h of handles) {
|
|
860
|
+
const p = positions[h];
|
|
861
|
+
html += `<rect x="${p.x - 5}" y="${p.y - 5}" width="10" height="10"
|
|
862
|
+
fill="white" stroke="var(--color-accent)" stroke-width="2" rx="2"
|
|
863
|
+
data-resize-handle="${h}" data-elem-id="${escapeXml(elId)}" style="cursor: ${p.cursor}" />`;
|
|
864
|
+
}
|
|
865
|
+
g.innerHTML = html;
|
|
866
|
+
return g;
|
|
867
|
+
}
|
|
868
|
+
function renderRotationHandle(canvasEl2, elId, snap2, byId) {
|
|
869
|
+
const baseEl = byId.get(elId);
|
|
870
|
+
const state = snap2.get(elId);
|
|
871
|
+
if (!baseEl || !state) return null;
|
|
872
|
+
const center = centerOfElement(baseEl, state);
|
|
873
|
+
if (!center) return null;
|
|
874
|
+
let topY = null;
|
|
875
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
876
|
+
topY = state.y - 24;
|
|
877
|
+
} else if (baseEl.type === "circle") {
|
|
878
|
+
topY = state.cy - state.r - 24;
|
|
879
|
+
} else if (baseEl.type === "text") {
|
|
880
|
+
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
881
|
+
if (bbox) topY = bbox.y - 24;
|
|
882
|
+
} else if (baseEl.type === "polygon") {
|
|
883
|
+
const bbox = polygonBoundingBox(String(state.points ?? ""));
|
|
884
|
+
if (bbox) topY = bbox.y - 24;
|
|
885
|
+
} else if (baseEl.type === "path") {
|
|
886
|
+
const bbox = pathBBoxOnCanvas(canvasEl2, elId);
|
|
887
|
+
if (bbox) topY = bbox.y - 24;
|
|
1001
888
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
889
|
+
if (topY === null) return null;
|
|
890
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
891
|
+
const rotation = state.rotation || 0;
|
|
892
|
+
if (rotation)
|
|
893
|
+
g.setAttribute("transform", `rotate(${rotation} ${center.x} ${center.y})`);
|
|
894
|
+
g.innerHTML = `
|
|
895
|
+
<line x1="${center.x}" y1="${center.y}" x2="${center.x}" y2="${topY + 8}" stroke="var(--color-accent)" stroke-width="1.5" stroke-dasharray="3 2" pointer-events="none" />
|
|
896
|
+
<circle cx="${center.x}" cy="${topY}" r="8" fill="var(--color-accent)" stroke="white" stroke-width="2" data-rotate-handle="${escapeXml(elId)}" style="cursor: grab" />
|
|
897
|
+
<text x="${center.x + 14}" y="${topY + 4}" font-size="10" fill="var(--color-accent)" font-family="ui-monospace, monospace" pointer-events="none">${Math.round(rotation)}\xB0</text>
|
|
898
|
+
`;
|
|
899
|
+
return g;
|
|
900
|
+
}
|
|
901
|
+
function renderAnchorDots(elId, snap2, byId) {
|
|
902
|
+
const baseEl = byId.get(elId);
|
|
903
|
+
const state = snap2.get(elId);
|
|
904
|
+
if (!baseEl || !state) return null;
|
|
905
|
+
if (baseEl.type !== "rect" && baseEl.type !== "circle" && baseEl.type !== "image")
|
|
906
|
+
return null;
|
|
907
|
+
const points = anchorPointsOf(baseEl, state);
|
|
908
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
909
|
+
for (const p of points) {
|
|
910
|
+
g.innerHTML += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="white" stroke="#6366f1" stroke-width="2"
|
|
911
|
+
data-anchor-handle data-elem-id="${escapeXml(elId)}" data-anchor="${p.anchor}" style="cursor: crosshair" />`;
|
|
1023
912
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
913
|
+
return g;
|
|
914
|
+
}
|
|
915
|
+
function renderLineEndpointHandles(elId, snap2, byId) {
|
|
916
|
+
const baseEl = byId.get(elId);
|
|
917
|
+
const state = snap2.get(elId);
|
|
918
|
+
if (!baseEl || !state) return null;
|
|
919
|
+
if (baseEl.type !== "line" && baseEl.type !== "arrow") return null;
|
|
920
|
+
const coords = baseEl.type === "line" ? resolveLineCoords(state, snap2, byId) : resolveArrowCoords(state, snap2, byId);
|
|
921
|
+
if (!coords) return null;
|
|
922
|
+
const midX = (coords.x1 + coords.x2) / 2;
|
|
923
|
+
const midY = (coords.y1 + coords.y2) / 2;
|
|
924
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
925
|
+
g.innerHTML = `
|
|
926
|
+
<rect x="${coords.x1 - 6}" y="${coords.y1 - 6}" width="12" height="12" rx="2"
|
|
927
|
+
fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
928
|
+
data-endpoint-handle="start" data-elem-id="${escapeXml(elId)}" style="cursor: grab" />
|
|
929
|
+
<rect x="${coords.x2 - 6}" y="${coords.y2 - 6}" width="12" height="12" rx="2"
|
|
930
|
+
fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
931
|
+
data-endpoint-handle="end" data-elem-id="${escapeXml(elId)}" style="cursor: grab" />
|
|
932
|
+
<circle cx="${midX}" cy="${midY}" r="6"
|
|
933
|
+
fill="var(--color-accent)" stroke="white" stroke-width="2"
|
|
934
|
+
data-line-mid-handle data-elem-id="${escapeXml(elId)}" style="cursor: move" />
|
|
935
|
+
`;
|
|
936
|
+
return g;
|
|
937
|
+
}
|
|
938
|
+
function renderPolygonVertexHandles(elId, snap2, byId) {
|
|
939
|
+
const baseEl = byId.get(elId);
|
|
940
|
+
const state = snap2.get(elId);
|
|
941
|
+
if (!baseEl || !state) return null;
|
|
942
|
+
if (baseEl.type !== "polygon") return null;
|
|
943
|
+
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
944
|
+
const parsed = [];
|
|
945
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
946
|
+
const [x, y] = points[i].split(",").map(Number);
|
|
947
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
948
|
+
parsed.push({ x, y, i });
|
|
1041
949
|
}
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1053
|
-
|
|
950
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
951
|
+
let html = "";
|
|
952
|
+
if (parsed.length >= 2) {
|
|
953
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
954
|
+
const a = parsed[i];
|
|
955
|
+
const b = parsed[(i + 1) % parsed.length];
|
|
956
|
+
const mx = (a.x + b.x) / 2;
|
|
957
|
+
const my = (a.y + b.y) / 2;
|
|
958
|
+
html += `<g data-vertex-add data-elem-id="${escapeXml(elId)}" data-after-index="${a.i}" style="cursor: copy">
|
|
959
|
+
<circle cx="${mx}" cy="${my}" r="6" fill="rgba(99, 102, 241, 0.15)" stroke="var(--color-accent)" stroke-width="1.5" stroke-dasharray="2 2" />
|
|
960
|
+
<text x="${mx}" y="${my + 3}" text-anchor="middle" font-size="10" fill="var(--color-accent)" font-weight="700" pointer-events="none">+</text>
|
|
961
|
+
</g>`;
|
|
1054
962
|
}
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
const id = e.altKey ? rawId : findContainingGroup(rawId)?.id ?? rawId;
|
|
1058
|
-
const snap2 = getCurrentSnapshot();
|
|
1059
|
-
const elState = snap2.get(id);
|
|
1060
|
-
const baseEl = def.elements.find((x) => x.id === id);
|
|
1061
|
-
if (!elState || !baseEl) return;
|
|
1062
|
-
if ((baseEl.type === "arrow" || baseEl.type === "line") && baseEl.fromId && baseEl.toId) {
|
|
1063
|
-
setSelection({ kind: "element", elementId: id });
|
|
1064
|
-
return;
|
|
1065
963
|
}
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
startElemY: anchor.y,
|
|
1074
|
-
originalPolygonPoints: baseEl.type === "polygon" ? String(elState.points ?? "") : void 0,
|
|
1075
|
-
extras: collectDragExtras(id, def, snap2)
|
|
1076
|
-
};
|
|
1077
|
-
if (!isElementSelected(getSelection(), id)) {
|
|
1078
|
-
setSelection({ kind: "element", elementId: id });
|
|
964
|
+
for (const p of parsed) {
|
|
965
|
+
const removable = parsed.length > 3;
|
|
966
|
+
html += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
967
|
+
data-vertex-handle data-elem-id="${escapeXml(elId)}" data-vertex-index="${p.i}"
|
|
968
|
+
style="cursor: grab" >
|
|
969
|
+
<title>${removable ? "\uB4DC\uB798\uADF8=\uC774\uB3D9, \uC6B0\uD074\uB9AD=\uC0AD\uC81C" : "\uB4DC\uB798\uADF8=\uC774\uB3D9"}</title>
|
|
970
|
+
</circle>`;
|
|
1079
971
|
}
|
|
972
|
+
g.innerHTML = html;
|
|
973
|
+
return g;
|
|
1080
974
|
}
|
|
1081
|
-
function
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
975
|
+
function renderSnapTargets(draggingElementId, snap2, byId, activeSnapTarget) {
|
|
976
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
977
|
+
let html = "";
|
|
978
|
+
for (const baseEl of byId.values()) {
|
|
979
|
+
if (baseEl.id === draggingElementId) continue;
|
|
980
|
+
if (baseEl.type === "line" || baseEl.type === "arrow") continue;
|
|
981
|
+
const state = snap2.get(baseEl.id);
|
|
982
|
+
if (!state || !state.visible) continue;
|
|
983
|
+
const points = getAnchorPoints(baseEl, state);
|
|
984
|
+
for (const p of points) {
|
|
985
|
+
const isActive = activeSnapTarget?.elementId === baseEl.id && activeSnapTarget?.anchor === p.anchor;
|
|
986
|
+
if (isActive) {
|
|
987
|
+
html += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="var(--color-accent)" stroke="white" stroke-width="2" opacity="0.85" pointer-events="none" />`;
|
|
988
|
+
html += `<circle cx="${p.x}" cy="${p.y}" r="12" fill="none" stroke="var(--color-accent)" stroke-width="1.5" opacity="0.4" pointer-events="none" />`;
|
|
989
|
+
} else {
|
|
990
|
+
html += `<circle cx="${p.x}" cy="${p.y}" r="3" fill="white" stroke="var(--color-accent)" stroke-width="1.5" opacity="0.5" pointer-events="none" />`;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
1098
993
|
}
|
|
1099
|
-
|
|
994
|
+
g.innerHTML = html;
|
|
995
|
+
return g;
|
|
1100
996
|
}
|
|
1101
|
-
function
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
997
|
+
function renderSelectionOutline(canvasEl2, elId, snap2, byId) {
|
|
998
|
+
const baseEl = byId.get(elId);
|
|
999
|
+
const state = snap2.get(elId);
|
|
1000
|
+
if (!baseEl || !state) return null;
|
|
1001
|
+
let box = null;
|
|
1002
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
1003
|
+
box = {
|
|
1004
|
+
x: state.x - 4,
|
|
1005
|
+
y: state.y - 4,
|
|
1006
|
+
w: state.width + 8,
|
|
1007
|
+
h: state.height + 8
|
|
1008
|
+
};
|
|
1009
|
+
} else if (baseEl.type === "circle") {
|
|
1010
|
+
const r = state.r;
|
|
1011
|
+
box = {
|
|
1012
|
+
x: state.cx - r - 4,
|
|
1013
|
+
y: state.cy - r - 4,
|
|
1014
|
+
w: r * 2 + 8,
|
|
1015
|
+
h: r * 2 + 8
|
|
1016
|
+
};
|
|
1017
|
+
} else if (baseEl.type === "text") {
|
|
1018
|
+
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
1019
|
+
if (bbox)
|
|
1020
|
+
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
1021
|
+
} else if (baseEl.type === "line" || baseEl.type === "arrow") {
|
|
1022
|
+
const coords = baseEl.type === "line" ? resolveLineCoords(state, snap2, byId) : resolveArrowCoords(state, snap2, byId);
|
|
1023
|
+
if (!coords) return null;
|
|
1024
|
+
box = {
|
|
1025
|
+
x: Math.min(coords.x1, coords.x2) - 4,
|
|
1026
|
+
y: Math.min(coords.y1, coords.y2) - 4,
|
|
1027
|
+
w: Math.abs(coords.x2 - coords.x1) + 8,
|
|
1028
|
+
h: Math.abs(coords.y2 - coords.y1) + 8
|
|
1029
|
+
};
|
|
1030
|
+
} else if (baseEl.type === "polygon") {
|
|
1031
|
+
const bbox = polygonBoundingBox(String(state.points ?? ""));
|
|
1032
|
+
if (bbox)
|
|
1033
|
+
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
1034
|
+
} else if (baseEl.type === "path") {
|
|
1035
|
+
const bbox = pathBBoxOnCanvas(canvasEl2, elId);
|
|
1036
|
+
if (bbox)
|
|
1037
|
+
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
1105
1038
|
}
|
|
1106
|
-
if (
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1039
|
+
if (!box) return null;
|
|
1040
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
1041
|
+
rect.setAttribute("x", String(box.x));
|
|
1042
|
+
rect.setAttribute("y", String(box.y));
|
|
1043
|
+
rect.setAttribute("width", String(box.w));
|
|
1044
|
+
rect.setAttribute("height", String(box.h));
|
|
1045
|
+
rect.classList.add("element-selected-outline");
|
|
1046
|
+
return rect;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/legacy/canvas-preview.ts
|
|
1050
|
+
var previewRoot = null;
|
|
1051
|
+
var previewHandle = null;
|
|
1052
|
+
var unsubscribePreview = null;
|
|
1053
|
+
async function mountPreview(host, doc, options) {
|
|
1054
|
+
const { mountStage } = await import('@kokoa/clotho/dom');
|
|
1055
|
+
previewHandle = mountStage(host, doc, {
|
|
1056
|
+
player: {
|
|
1057
|
+
autoplay: true,
|
|
1058
|
+
loop: options.loop,
|
|
1059
|
+
speed: options.speed,
|
|
1060
|
+
onEnd: options.onEnd
|
|
1115
1061
|
}
|
|
1116
|
-
|
|
1062
|
+
});
|
|
1063
|
+
unsubscribePreview = previewHandle.player.subscribe((state) => {
|
|
1064
|
+
options.onTime(state.time);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
function showPreview(canvasEl2, def, options) {
|
|
1068
|
+
if (!canvasEl2) return;
|
|
1069
|
+
const parent = canvasEl2.parentElement;
|
|
1070
|
+
if (!parent) return;
|
|
1071
|
+
hidePreview(canvasEl2);
|
|
1072
|
+
canvasEl2.style.visibility = "hidden";
|
|
1073
|
+
previewRoot = document.createElement("div");
|
|
1074
|
+
const bg = def.canvas.background && def.canvas.background !== "transparent" ? def.canvas.background : "transparent";
|
|
1075
|
+
previewRoot.style.cssText = `position:absolute;left:0;top:0;width:${def.canvas.width}px;height:${def.canvas.height}px;background:${bg};z-index:2;overflow:hidden;`;
|
|
1076
|
+
parent.style.position = "relative";
|
|
1077
|
+
parent.appendChild(previewRoot);
|
|
1078
|
+
void mountPreview(previewRoot, def, options);
|
|
1079
|
+
}
|
|
1080
|
+
function hidePreview(canvasEl2) {
|
|
1081
|
+
if (canvasEl2) canvasEl2.style.visibility = "";
|
|
1082
|
+
unsubscribePreview?.();
|
|
1083
|
+
unsubscribePreview = null;
|
|
1084
|
+
previewHandle?.destroy();
|
|
1085
|
+
previewHandle = null;
|
|
1086
|
+
if (previewRoot) {
|
|
1087
|
+
previewRoot.remove();
|
|
1088
|
+
previewRoot = null;
|
|
1117
1089
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/legacy/canvas.ts
|
|
1093
|
+
var SNAP_RADIUS = 12;
|
|
1094
|
+
var HEAD_MARKER_DEFS = `
|
|
1095
|
+
<marker id="studio-h-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
1096
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1097
|
+
</marker>
|
|
1098
|
+
<marker id="studio-h-arrow-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
1099
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1100
|
+
</marker>
|
|
1101
|
+
<marker id="studio-h-triangle" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
1102
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1103
|
+
</marker>
|
|
1104
|
+
<marker id="studio-h-triangle-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
1105
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1106
|
+
</marker>
|
|
1107
|
+
<marker id="studio-h-triangle-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto">
|
|
1108
|
+
<path d="M 0 0 L 10 5 L 0 10" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
1109
|
+
</marker>
|
|
1110
|
+
<marker id="studio-h-triangle-open-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
|
|
1111
|
+
<path d="M 0 0 L 10 5 L 0 10" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
1112
|
+
</marker>
|
|
1113
|
+
<marker id="studio-h-circle" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6">
|
|
1114
|
+
<circle cx="5" cy="5" r="4" fill="currentColor" />
|
|
1115
|
+
</marker>
|
|
1116
|
+
<marker id="studio-h-circle-open" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="7" markerHeight="7">
|
|
1117
|
+
<circle cx="5" cy="5" r="4" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
1118
|
+
</marker>
|
|
1119
|
+
<marker id="studio-h-diamond" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
1120
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="currentColor" />
|
|
1121
|
+
</marker>
|
|
1122
|
+
<marker id="studio-h-diamond-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
1123
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="currentColor" />
|
|
1124
|
+
</marker>
|
|
1125
|
+
<marker id="studio-h-diamond-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
1126
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
1127
|
+
</marker>
|
|
1128
|
+
<marker id="studio-h-diamond-open-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
1129
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
1130
|
+
</marker>
|
|
1131
|
+
<marker id="studio-h-bar" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="10" orient="auto">
|
|
1132
|
+
<rect x="4" y="0" width="2" height="10" fill="currentColor" />
|
|
1133
|
+
</marker>
|
|
1134
|
+
`;
|
|
1135
|
+
function markerUrlFor(head, end) {
|
|
1136
|
+
if (!head || head === "none") return "";
|
|
1137
|
+
const isOpenCircle = head === "circle-open";
|
|
1138
|
+
const isCircle = head === "circle" || isOpenCircle;
|
|
1139
|
+
if (isCircle) return `url(#studio-h-${head})`;
|
|
1140
|
+
const idBase = `studio-h-${head}`;
|
|
1141
|
+
return end === "start" ? `url(#${idBase}-start)` : `url(#${idBase})`;
|
|
1142
|
+
}
|
|
1143
|
+
var dragState = null;
|
|
1144
|
+
var rotateState = null;
|
|
1145
|
+
var connectState = null;
|
|
1146
|
+
var endpointDragState = null;
|
|
1147
|
+
var vertexDragState = null;
|
|
1148
|
+
var resizeState = null;
|
|
1149
|
+
var marqueeState = null;
|
|
1150
|
+
var marqueeJustFinished = false;
|
|
1151
|
+
var toolJustFinished = false;
|
|
1152
|
+
var canvasEl = null;
|
|
1153
|
+
var hoveredElementId = null;
|
|
1154
|
+
var canvasZoom = 1;
|
|
1155
|
+
var toolDrawState = null;
|
|
1156
|
+
var elementDrawState = null;
|
|
1157
|
+
var canvasRenderFrame = null;
|
|
1158
|
+
var pathDraftState = null;
|
|
1159
|
+
function initCanvas(root) {
|
|
1160
|
+
canvasEl = root;
|
|
1161
|
+
root.addEventListener("click", onCanvasClick);
|
|
1162
|
+
root.addEventListener("dblclick", onPathDoubleClick);
|
|
1163
|
+
root.addEventListener("mousedown", onMouseDown);
|
|
1164
|
+
root.addEventListener("mouseover", onMouseOver);
|
|
1165
|
+
root.addEventListener("mouseleave", onMouseLeave);
|
|
1166
|
+
root.addEventListener("contextmenu", onContextMenu);
|
|
1167
|
+
root.addEventListener("wheel", onCanvasWheel, { passive: false });
|
|
1168
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
1169
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
1170
|
+
document.addEventListener("keydown", onPathKeyDown);
|
|
1171
|
+
subscribe(render2);
|
|
1172
|
+
render2();
|
|
1173
|
+
}
|
|
1174
|
+
function onCanvasWheel(e) {
|
|
1175
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
1176
|
+
e.preventDefault();
|
|
1177
|
+
const wrap = canvasEl?.closest(".studio-canvas-wrap");
|
|
1178
|
+
if (!wrap) {
|
|
1179
|
+
canvasZoom = Math.max(
|
|
1180
|
+
0.1,
|
|
1181
|
+
Math.min(5, canvasZoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))
|
|
1132
1182
|
);
|
|
1133
|
-
|
|
1134
|
-
const finalX = endpointDragState.snapTarget ? endpointDragState.snapTarget.x : pt.x;
|
|
1135
|
-
const finalY = endpointDragState.snapTarget ? endpointDragState.snapTarget.y : pt.y;
|
|
1136
|
-
if (baseEl2.type === "line" || baseEl2.type === "arrow") {
|
|
1137
|
-
if (endpointDragState.snapTarget) {
|
|
1138
|
-
if (endpointDragState.end === "start") {
|
|
1139
|
-
setElementValueAtTime(baseEl2.id, {
|
|
1140
|
-
fromId: endpointDragState.snapTarget.elementId,
|
|
1141
|
-
fromAnchor: endpointDragState.snapTarget.anchor,
|
|
1142
|
-
x1: void 0,
|
|
1143
|
-
y1: void 0
|
|
1144
|
-
});
|
|
1145
|
-
} else {
|
|
1146
|
-
setElementValueAtTime(baseEl2.id, {
|
|
1147
|
-
toId: endpointDragState.snapTarget.elementId,
|
|
1148
|
-
toAnchor: endpointDragState.snapTarget.anchor,
|
|
1149
|
-
x2: void 0,
|
|
1150
|
-
y2: void 0
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
} else {
|
|
1154
|
-
if (endpointDragState.end === "start") {
|
|
1155
|
-
updateElementBase(baseEl2.id, {
|
|
1156
|
-
fromId: void 0,
|
|
1157
|
-
fromAnchor: void 0,
|
|
1158
|
-
x1: finalX,
|
|
1159
|
-
y1: finalY
|
|
1160
|
-
});
|
|
1161
|
-
} else {
|
|
1162
|
-
updateElementBase(baseEl2.id, {
|
|
1163
|
-
toId: void 0,
|
|
1164
|
-
toAnchor: void 0,
|
|
1165
|
-
x2: finalX,
|
|
1166
|
-
y2: finalY
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
render();
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
if (rotateState) {
|
|
1175
|
-
const pt = svgPoint(e.clientX, e.clientY);
|
|
1176
|
-
if (!pt) return;
|
|
1177
|
-
const angle = Math.atan2(pt.y - rotateState.centerY, pt.x - rotateState.centerX) * 180 / Math.PI;
|
|
1178
|
-
const delta = angle - rotateState.startAngle;
|
|
1179
|
-
setElementValueAtTime(rotateState.elementId, {
|
|
1180
|
-
rotation: Math.round((rotateState.startRotation + delta + 360) % 360)
|
|
1181
|
-
});
|
|
1183
|
+
render2();
|
|
1182
1184
|
return;
|
|
1183
1185
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1186
|
+
const wrapRect = wrap.getBoundingClientRect();
|
|
1187
|
+
const mxInWrap = e.clientX - wrapRect.left + wrap.scrollLeft;
|
|
1188
|
+
const myInWrap = e.clientY - wrapRect.top + wrap.scrollTop;
|
|
1189
|
+
const old = canvasZoom;
|
|
1190
|
+
canvasZoom = Math.max(
|
|
1191
|
+
0.1,
|
|
1192
|
+
Math.min(5, canvasZoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))
|
|
1193
|
+
);
|
|
1194
|
+
const ratio = canvasZoom / old;
|
|
1195
|
+
render2();
|
|
1196
|
+
wrap.scrollLeft = mxInWrap * ratio - (e.clientX - wrapRect.left);
|
|
1197
|
+
wrap.scrollTop = myInWrap * ratio - (e.clientY - wrapRect.top);
|
|
1198
|
+
}
|
|
1199
|
+
function onContextMenu(e) {
|
|
1200
|
+
const target = e.target;
|
|
1201
|
+
const vertexEl = target?.closest("[data-vertex-handle]");
|
|
1202
|
+
if (!vertexEl) return;
|
|
1203
|
+
const id = vertexEl.dataset.elemId ?? "";
|
|
1204
|
+
const idx = Number(vertexEl.dataset.vertexIndex ?? "-1");
|
|
1205
|
+
const def = getDef();
|
|
1206
|
+
if (!def || idx < 0) return;
|
|
1207
|
+
const el = def.elements.find((x) => x.id === id);
|
|
1208
|
+
if (!el || el.type !== "polygon") return;
|
|
1209
|
+
const snap2 = getCurrentSnapshot();
|
|
1210
|
+
const state = snap2.get(id);
|
|
1211
|
+
if (!state) return;
|
|
1212
|
+
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
1213
|
+
if (points.length <= 3) return;
|
|
1214
|
+
e.preventDefault();
|
|
1215
|
+
points.splice(idx, 1);
|
|
1216
|
+
setElementValueAtTime(id, { points: points.join(" ") });
|
|
1217
|
+
}
|
|
1218
|
+
function onMouseOver(e) {
|
|
1219
|
+
const id = findElementId(e.target);
|
|
1220
|
+
if (id !== hoveredElementId) {
|
|
1221
|
+
hoveredElementId = id;
|
|
1222
|
+
render2();
|
|
1191
1223
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
render();
|
|
1198
|
-
return;
|
|
1224
|
+
}
|
|
1225
|
+
function onMouseLeave() {
|
|
1226
|
+
if (hoveredElementId !== null) {
|
|
1227
|
+
hoveredElementId = null;
|
|
1228
|
+
render2();
|
|
1199
1229
|
}
|
|
1200
|
-
|
|
1230
|
+
}
|
|
1231
|
+
function svgPoint(clientX, clientY) {
|
|
1232
|
+
if (!canvasEl) return null;
|
|
1201
1233
|
const def = getDef();
|
|
1202
|
-
if (!def) return;
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const eEl = def.elements.find((x) => x.id === extra.id);
|
|
1218
|
-
const eState = snap2.get(extra.id);
|
|
1219
|
-
if (!eEl || !eState) continue;
|
|
1220
|
-
applyMove(
|
|
1221
|
-
eEl,
|
|
1222
|
-
eState,
|
|
1223
|
-
Math.round(extra.startX + dx),
|
|
1224
|
-
Math.round(extra.startY + dy),
|
|
1225
|
-
extra.originalPolygonPoints
|
|
1226
|
-
);
|
|
1234
|
+
if (!def) return null;
|
|
1235
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
1236
|
+
const sx = def.canvas.width / rect.width;
|
|
1237
|
+
const sy = def.canvas.height / rect.height;
|
|
1238
|
+
return snapPoint({
|
|
1239
|
+
x: (clientX - rect.left) * sx,
|
|
1240
|
+
y: (clientY - rect.top) * sy
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
function findElementId(target) {
|
|
1244
|
+
let node = target;
|
|
1245
|
+
while (node && node !== canvasEl) {
|
|
1246
|
+
const id = node.getAttribute?.("data-elem-id");
|
|
1247
|
+
if (id) return id;
|
|
1248
|
+
node = node.parentElement;
|
|
1227
1249
|
}
|
|
1250
|
+
return null;
|
|
1228
1251
|
}
|
|
1229
|
-
function
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
|
|
1252
|
+
function onCanvasClick(e) {
|
|
1253
|
+
if (dragState || rotateState || connectState) return;
|
|
1254
|
+
if (marqueeJustFinished) {
|
|
1255
|
+
marqueeJustFinished = false;
|
|
1256
|
+
return;
|
|
1233
1257
|
}
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
1258
|
+
if (toolJustFinished) {
|
|
1259
|
+
toolJustFinished = false;
|
|
1260
|
+
return;
|
|
1237
1261
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1262
|
+
const target = e.target;
|
|
1263
|
+
const addVertexHandle = target?.closest("[data-vertex-add]");
|
|
1264
|
+
if (addVertexHandle) {
|
|
1265
|
+
e.preventDefault();
|
|
1266
|
+
e.stopPropagation();
|
|
1267
|
+
const id2 = addVertexHandle.dataset.elemId ?? "";
|
|
1268
|
+
const afterIdx = Number(addVertexHandle.dataset.afterIndex ?? "-1");
|
|
1269
|
+
const def = getDef();
|
|
1270
|
+
if (!def || afterIdx < 0) return;
|
|
1271
|
+
const el = def.elements.find((x) => x.id === id2);
|
|
1272
|
+
if (!el || el.type !== "polygon") return;
|
|
1273
|
+
const snap2 = getCurrentSnapshot();
|
|
1274
|
+
const state = snap2.get(id2);
|
|
1275
|
+
if (!state) return;
|
|
1276
|
+
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
1277
|
+
const a = points[afterIdx]?.split(",").map(Number) ?? [];
|
|
1278
|
+
const b = points[(afterIdx + 1) % points.length]?.split(",").map(Number) ?? [];
|
|
1279
|
+
if (a.length !== 2 || b.length !== 2) return;
|
|
1280
|
+
const mx = (a[0] + b[0]) / 2;
|
|
1281
|
+
const my = (a[1] + b[1]) / 2;
|
|
1282
|
+
points.splice(afterIdx + 1, 0, `${mx.toFixed(1)},${my.toFixed(1)}`);
|
|
1283
|
+
setElementValueAtTime(id2, { points: points.join(" ") });
|
|
1284
|
+
return;
|
|
1241
1285
|
}
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
});
|
|
1286
|
+
if (target?.closest(
|
|
1287
|
+
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle]"
|
|
1288
|
+
))
|
|
1289
|
+
return;
|
|
1290
|
+
const id = findElementId(e.target);
|
|
1291
|
+
if (id) {
|
|
1292
|
+
const resolvedId = e.altKey ? id : findContainingGroup(id)?.id ?? id;
|
|
1293
|
+
setActiveTool("select");
|
|
1294
|
+
setSelection(nextPointerSelection(getSelection(), resolvedId, e));
|
|
1295
|
+
} else if (e.target === canvasEl) setSelection({ kind: "none" });
|
|
1296
|
+
}
|
|
1297
|
+
function onMouseDown(e) {
|
|
1298
|
+
if (e.button !== 0) return;
|
|
1299
|
+
const target = e.target;
|
|
1300
|
+
const def = getDef();
|
|
1301
|
+
if (!def) return;
|
|
1302
|
+
const activeTool2 = getActiveTool();
|
|
1303
|
+
if (activeTool2 !== "select" && e.target === canvasEl) {
|
|
1304
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1305
|
+
if (!point) return;
|
|
1306
|
+
e.preventDefault();
|
|
1307
|
+
if (activeTool2 === "path") {
|
|
1308
|
+
if (!pathDraftState) {
|
|
1309
|
+
pathDraftState = { points: [point], current: point };
|
|
1310
|
+
} else {
|
|
1311
|
+
pathDraftState.points.push(point);
|
|
1312
|
+
pathDraftState.current = point;
|
|
1270
1313
|
}
|
|
1314
|
+
render2();
|
|
1315
|
+
return;
|
|
1271
1316
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
if (matchedIds.length === 1) {
|
|
1297
|
-
setSelection({ kind: "element", elementId: matchedIds[0] });
|
|
1298
|
-
} else if (matchedIds.length > 1) {
|
|
1299
|
-
setSelection({ kind: "elements", elementIds: matchedIds });
|
|
1300
|
-
} else {
|
|
1301
|
-
setSelection({ kind: "none" });
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1317
|
+
if (activeTool2 === "line" || activeTool2 === "arrow") {
|
|
1318
|
+
toolDrawState = {
|
|
1319
|
+
tool: activeTool2,
|
|
1320
|
+
startX: point.x,
|
|
1321
|
+
startY: point.y,
|
|
1322
|
+
currentX: point.x,
|
|
1323
|
+
currentY: point.y
|
|
1324
|
+
};
|
|
1325
|
+
render2();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
const sidesInput = document.getElementById(
|
|
1329
|
+
"studio-polygon-sides"
|
|
1330
|
+
);
|
|
1331
|
+
if (activeTool2 === "rect" || activeTool2 === "circle" || activeTool2 === "polygon" || activeTool2 === "text" || activeTool2 === "image") {
|
|
1332
|
+
elementDrawState = {
|
|
1333
|
+
tool: activeTool2,
|
|
1334
|
+
start: point,
|
|
1335
|
+
current: point,
|
|
1336
|
+
polygonSides: Number(sidesInput?.value ?? 6)
|
|
1337
|
+
};
|
|
1338
|
+
requestCanvasRender();
|
|
1339
|
+
return;
|
|
1304
1340
|
}
|
|
1305
|
-
render();
|
|
1306
1341
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
function applyMove(baseEl, state, x, y, polygonOriginal) {
|
|
1311
|
-
if (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "text") {
|
|
1312
|
-
setElementValueAtTime(baseEl.id, { x, y });
|
|
1313
|
-
} else if (baseEl.type === "circle") {
|
|
1314
|
-
setElementValueAtTime(baseEl.id, { cx: x, cy: y });
|
|
1315
|
-
} else if ((baseEl.type === "line" || baseEl.type === "arrow") && typeof state.x1 === "number" && typeof state.y1 === "number" && typeof state.x2 === "number" && typeof state.y2 === "number") {
|
|
1316
|
-
const dx = x - state.x1;
|
|
1317
|
-
const dy = y - state.y1;
|
|
1318
|
-
setElementValueAtTime(baseEl.id, {
|
|
1319
|
-
x1: x,
|
|
1320
|
-
y1: y,
|
|
1321
|
-
x2: state.x2 + dx,
|
|
1322
|
-
y2: state.y2 + dy
|
|
1323
|
-
});
|
|
1324
|
-
} else if (baseEl.type === "path") {
|
|
1325
|
-
setElementValueAtTime(baseEl.id, { x, y });
|
|
1326
|
-
} else if (baseEl.type === "polygon") {
|
|
1327
|
-
const orig = polygonOriginal ?? dragState?.originalPolygonPoints;
|
|
1328
|
-
if (!orig) return;
|
|
1329
|
-
const startFirst = firstPolygonPoint(orig);
|
|
1330
|
-
if (!startFirst) return;
|
|
1331
|
-
const dx = x - startFirst.x;
|
|
1332
|
-
const dy = y - startFirst.y;
|
|
1333
|
-
const newPoints = shiftPolygonPoints(orig, dx, dy);
|
|
1334
|
-
setElementValueAtTime(baseEl.id, { points: newPoints });
|
|
1335
|
-
} else if (baseEl.type === "group") {
|
|
1336
|
-
const dx = x - baseEl.x;
|
|
1337
|
-
const dy = y - baseEl.y;
|
|
1338
|
-
moveGroupBy(baseEl.id, dx, dy);
|
|
1342
|
+
if (activeTool2 !== "select" && !((activeTool2 === "line" || activeTool2 === "arrow") && target?.closest("[data-anchor-handle]"))) {
|
|
1343
|
+
return;
|
|
1339
1344
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1345
|
+
if (shouldDeferElementPointerDown(e, {
|
|
1346
|
+
isElement: Boolean(findElementId(e.target)),
|
|
1347
|
+
isHandle: Boolean(
|
|
1348
|
+
target?.closest(
|
|
1349
|
+
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle], [data-endpoint-handle], [data-line-mid-handle], [data-vertex-handle], [data-vertex-add]"
|
|
1350
|
+
)
|
|
1351
|
+
)
|
|
1352
|
+
})) {
|
|
1353
|
+
return;
|
|
1344
1354
|
}
|
|
1345
|
-
if (
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
return
|
|
1355
|
+
if (target?.closest("[data-vertex-add]")) {
|
|
1356
|
+
e.preventDefault();
|
|
1357
|
+
e.stopPropagation();
|
|
1358
|
+
return;
|
|
1349
1359
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1360
|
+
const resizeHandle = target?.closest("[data-resize-handle]");
|
|
1361
|
+
if (resizeHandle && activeTool2 === "select") {
|
|
1362
|
+
e.preventDefault();
|
|
1363
|
+
const id2 = resizeHandle.dataset.elemId ?? "";
|
|
1364
|
+
const handle = resizeHandle.dataset.resizeHandle;
|
|
1365
|
+
const snap3 = getCurrentSnapshot();
|
|
1366
|
+
const elState2 = snap3.get(id2);
|
|
1367
|
+
const baseEl2 = def.elements.find((x) => x.id === id2);
|
|
1368
|
+
if (!elState2 || !baseEl2) return;
|
|
1369
|
+
const rs = {
|
|
1370
|
+
elementId: id2,
|
|
1371
|
+
handle,
|
|
1372
|
+
startMouseX: e.clientX,
|
|
1373
|
+
startMouseY: e.clientY,
|
|
1374
|
+
startX: 0,
|
|
1375
|
+
startY: 0,
|
|
1376
|
+
startW: 0,
|
|
1377
|
+
startH: 0,
|
|
1378
|
+
aspect: 1
|
|
1379
|
+
};
|
|
1380
|
+
if (baseEl2.type === "rect" || baseEl2.type === "image") {
|
|
1381
|
+
rs.startX = elState2.x;
|
|
1382
|
+
rs.startY = elState2.y;
|
|
1383
|
+
rs.startW = elState2.width;
|
|
1384
|
+
rs.startH = elState2.height;
|
|
1385
|
+
rs.aspect = rs.startW / Math.max(1, rs.startH);
|
|
1386
|
+
} else if (baseEl2.type === "circle") {
|
|
1387
|
+
rs.startCx = elState2.cx;
|
|
1388
|
+
rs.startCy = elState2.cy;
|
|
1389
|
+
rs.startR = elState2.r;
|
|
1390
|
+
} else if (baseEl2.type === "text") {
|
|
1391
|
+
const bbox = textBBoxOnCanvas(canvasEl, id2);
|
|
1392
|
+
rs.startX = bbox?.x ?? elState2.x;
|
|
1393
|
+
rs.startY = bbox?.y ?? elState2.y;
|
|
1394
|
+
rs.startW = bbox?.w ?? 100;
|
|
1395
|
+
rs.startH = bbox?.h ?? elState2.fontSize;
|
|
1396
|
+
rs.startFontSize = elState2.fontSize;
|
|
1397
|
+
rs.aspect = rs.startW / Math.max(1, rs.startH);
|
|
1398
|
+
} else {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
resizeState = rs;
|
|
1402
|
+
setSelection({ kind: "element", elementId: id2 });
|
|
1403
|
+
render2();
|
|
1404
|
+
return;
|
|
1355
1405
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1406
|
+
const vertexHandle = target?.closest("[data-vertex-handle]");
|
|
1407
|
+
if (vertexHandle) {
|
|
1408
|
+
e.preventDefault();
|
|
1409
|
+
const id2 = vertexHandle.dataset.elemId ?? "";
|
|
1410
|
+
const vi = Number(vertexHandle.dataset.vertexIndex ?? "-1");
|
|
1411
|
+
const snap3 = getCurrentSnapshot();
|
|
1412
|
+
const elState2 = snap3.get(id2);
|
|
1413
|
+
if (!elState2 || vi < 0) return;
|
|
1414
|
+
vertexDragState = {
|
|
1415
|
+
elementId: id2,
|
|
1416
|
+
vertexIndex: vi,
|
|
1417
|
+
originalPoints: String(elState2.points ?? "")
|
|
1418
|
+
};
|
|
1419
|
+
setSelection({ kind: "element", elementId: id2 });
|
|
1420
|
+
render2();
|
|
1421
|
+
return;
|
|
1358
1422
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1423
|
+
const endpointHandle = target?.closest("[data-endpoint-handle]");
|
|
1424
|
+
if (endpointHandle) {
|
|
1425
|
+
e.preventDefault();
|
|
1426
|
+
const id2 = endpointHandle.dataset.elemId ?? "";
|
|
1427
|
+
const end = endpointHandle.dataset.endpointHandle === "end" ? "end" : "start";
|
|
1428
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1429
|
+
if (!pt) return;
|
|
1430
|
+
endpointDragState = {
|
|
1431
|
+
elementId: id2,
|
|
1432
|
+
end,
|
|
1433
|
+
currentX: pt.x,
|
|
1434
|
+
currentY: pt.y,
|
|
1435
|
+
snapTarget: null
|
|
1368
1436
|
};
|
|
1437
|
+
setSelection({ kind: "element", elementId: id2 });
|
|
1438
|
+
render2();
|
|
1439
|
+
return;
|
|
1369
1440
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1441
|
+
const midHandle = target?.closest("[data-line-mid-handle]");
|
|
1442
|
+
if (midHandle) {
|
|
1443
|
+
e.preventDefault();
|
|
1444
|
+
const id2 = midHandle.dataset.elemId ?? "";
|
|
1445
|
+
const snap3 = getCurrentSnapshot();
|
|
1446
|
+
const elState2 = snap3.get(id2);
|
|
1447
|
+
const baseEl2 = def.elements.find((x) => x.id === id2);
|
|
1448
|
+
if (!elState2 || !baseEl2) return;
|
|
1449
|
+
const anchor2 = readPositionAnchor(baseEl2, elState2);
|
|
1450
|
+
if (!anchor2) return;
|
|
1451
|
+
dragState = {
|
|
1452
|
+
elementId: id2,
|
|
1453
|
+
startMouseX: e.clientX,
|
|
1454
|
+
startMouseY: e.clientY,
|
|
1455
|
+
startElemX: anchor2.x,
|
|
1456
|
+
startElemY: anchor2.y,
|
|
1457
|
+
extras: collectDragExtras(id2, def, snap3)
|
|
1377
1458
|
};
|
|
1459
|
+
if (!isElementSelected(getSelection(), id2)) {
|
|
1460
|
+
setSelection({ kind: "element", elementId: id2 });
|
|
1461
|
+
}
|
|
1462
|
+
return;
|
|
1378
1463
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1464
|
+
const rotateHandle = target?.closest("[data-rotate-handle]");
|
|
1465
|
+
if (rotateHandle) {
|
|
1466
|
+
e.preventDefault();
|
|
1467
|
+
const id2 = rotateHandle.dataset.rotateHandle ?? "";
|
|
1468
|
+
const snap3 = getCurrentSnapshot();
|
|
1469
|
+
const state = snap3.get(id2);
|
|
1470
|
+
const baseEl2 = def.elements.find((x) => x.id === id2);
|
|
1471
|
+
if (!state || !baseEl2) return;
|
|
1472
|
+
const center = centerOfElement(baseEl2, state);
|
|
1473
|
+
if (!center) return;
|
|
1474
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1475
|
+
if (!pt) return;
|
|
1476
|
+
const startAngle = Math.atan2(pt.y - center.y, pt.x - center.x) * 180 / Math.PI;
|
|
1477
|
+
rotateState = {
|
|
1478
|
+
elementId: id2,
|
|
1479
|
+
centerX: center.x,
|
|
1480
|
+
centerY: center.y,
|
|
1481
|
+
startAngle,
|
|
1482
|
+
startRotation: state.rotation || 0
|
|
1387
1483
|
};
|
|
1484
|
+
return;
|
|
1388
1485
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
w: Math.abs(coords.x2 - coords.x1),
|
|
1404
|
-
h: Math.abs(coords.y2 - coords.y1)
|
|
1486
|
+
const anchorHandle = target?.closest("[data-anchor-handle]");
|
|
1487
|
+
if (anchorHandle && (activeTool2 === "line" || activeTool2 === "arrow")) {
|
|
1488
|
+
e.preventDefault();
|
|
1489
|
+
const fromId = anchorHandle.dataset.elemId ?? "";
|
|
1490
|
+
const fromAnchor = anchorHandle.dataset.anchor ?? "auto";
|
|
1491
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1492
|
+
if (!pt) return;
|
|
1493
|
+
connectState = {
|
|
1494
|
+
fromId,
|
|
1495
|
+
fromAnchor,
|
|
1496
|
+
startX: Number(anchorHandle.getAttribute("cx")) || pt.x,
|
|
1497
|
+
startY: Number(anchorHandle.getAttribute("cy")) || pt.y,
|
|
1498
|
+
currentX: pt.x,
|
|
1499
|
+
currentY: pt.y
|
|
1405
1500
|
};
|
|
1501
|
+
render2();
|
|
1502
|
+
return;
|
|
1406
1503
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1504
|
+
const rawId = findElementId(e.target);
|
|
1505
|
+
if (!rawId) {
|
|
1506
|
+
if (!e.shiftKey && e.target === canvasEl) {
|
|
1507
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1508
|
+
if (pt) {
|
|
1509
|
+
marqueeState = {
|
|
1510
|
+
startX: pt.x,
|
|
1511
|
+
startY: pt.y,
|
|
1512
|
+
currentX: pt.x,
|
|
1513
|
+
currentY: pt.y
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return;
|
|
1409
1518
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1519
|
+
const id = e.altKey ? rawId : findContainingGroup(rawId)?.id ?? rawId;
|
|
1520
|
+
const snap2 = getCurrentSnapshot();
|
|
1521
|
+
const elState = snap2.get(id);
|
|
1522
|
+
const baseEl = def.elements.find((x) => x.id === id);
|
|
1523
|
+
if (!elState || !baseEl) return;
|
|
1524
|
+
if ((baseEl.type === "arrow" || baseEl.type === "line") && baseEl.fromId && baseEl.toId) {
|
|
1525
|
+
setSelection({ kind: "element", elementId: id });
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const anchor = readPositionAnchor(baseEl, elState);
|
|
1529
|
+
if (!anchor) return;
|
|
1530
|
+
dragState = {
|
|
1531
|
+
elementId: id,
|
|
1532
|
+
startMouseX: e.clientX,
|
|
1533
|
+
startMouseY: e.clientY,
|
|
1534
|
+
startElemX: anchor.x,
|
|
1535
|
+
startElemY: anchor.y,
|
|
1536
|
+
originalPolygonPoints: baseEl.type === "polygon" ? String(elState.points ?? "") : void 0,
|
|
1537
|
+
extras: collectDragExtras(id, def, snap2)
|
|
1538
|
+
};
|
|
1539
|
+
if (!isElementSelected(getSelection(), id)) {
|
|
1540
|
+
setSelection({ kind: "element", elementId: id });
|
|
1412
1541
|
}
|
|
1413
|
-
return null;
|
|
1414
1542
|
}
|
|
1415
|
-
function
|
|
1416
|
-
|
|
1543
|
+
function collectDragExtras(anchorId, def, snap2) {
|
|
1544
|
+
if (!def) return [];
|
|
1545
|
+
const sel = getSelection();
|
|
1546
|
+
const ids = getSelectedElementIds(sel).filter((id) => id !== anchorId);
|
|
1547
|
+
const extras = [];
|
|
1548
|
+
for (const id of ids) {
|
|
1549
|
+
const el = def.elements.find((x) => x.id === id);
|
|
1550
|
+
const st = snap2.get(id);
|
|
1551
|
+
if (!el || !st) continue;
|
|
1552
|
+
const a = readPositionAnchor(el, st);
|
|
1553
|
+
if (!a) continue;
|
|
1554
|
+
extras.push({
|
|
1555
|
+
id,
|
|
1556
|
+
startX: a.x,
|
|
1557
|
+
startY: a.y,
|
|
1558
|
+
originalPolygonPoints: el.type === "polygon" ? String(st.points ?? "") : void 0
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
return extras;
|
|
1417
1562
|
}
|
|
1418
|
-
function
|
|
1419
|
-
if (
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const rect = canvasEl.getBoundingClientRect();
|
|
1425
|
-
const sx = def.canvas.width / rect.width;
|
|
1426
|
-
const sy = def.canvas.height / rect.height;
|
|
1427
|
-
const dx = (e.clientX - resizeState.startMouseX) * sx;
|
|
1428
|
-
const dy = (e.clientY - resizeState.startMouseY) * sy;
|
|
1429
|
-
const h = resizeState.handle;
|
|
1430
|
-
const uniform = e.shiftKey;
|
|
1431
|
-
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
1432
|
-
let newX = resizeState.startX;
|
|
1433
|
-
let newY = resizeState.startY;
|
|
1434
|
-
let newW = resizeState.startW;
|
|
1435
|
-
let newH = resizeState.startH;
|
|
1436
|
-
if (h.includes("w")) {
|
|
1437
|
-
newX = resizeState.startX + dx;
|
|
1438
|
-
newW = resizeState.startW - dx;
|
|
1439
|
-
}
|
|
1440
|
-
if (h.includes("e")) {
|
|
1441
|
-
newW = resizeState.startW + dx;
|
|
1442
|
-
}
|
|
1443
|
-
if (h.includes("n")) {
|
|
1444
|
-
newY = resizeState.startY + dy;
|
|
1445
|
-
newH = resizeState.startH - dy;
|
|
1563
|
+
function onMouseMove(e) {
|
|
1564
|
+
if (pathDraftState && getActiveTool() === "path") {
|
|
1565
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1566
|
+
if (point) {
|
|
1567
|
+
pathDraftState.current = point;
|
|
1568
|
+
render2();
|
|
1446
1569
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (elementDrawState) {
|
|
1573
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1574
|
+
if (!point) return;
|
|
1575
|
+
elementDrawState.current = point;
|
|
1576
|
+
requestCanvasRender();
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (toolDrawState) {
|
|
1580
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1581
|
+
if (!point) return;
|
|
1582
|
+
toolDrawState.currentX = point.x;
|
|
1583
|
+
toolDrawState.currentY = point.y;
|
|
1584
|
+
render2();
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (resizeState) {
|
|
1588
|
+
handleResizeMove(e);
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (vertexDragState) {
|
|
1592
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1593
|
+
if (!pt) return;
|
|
1594
|
+
const pts = vertexDragState.originalPoints.trim().split(/\s+/);
|
|
1595
|
+
if (vertexDragState.vertexIndex < pts.length) {
|
|
1596
|
+
pts[vertexDragState.vertexIndex] = `${pt.x.toFixed(1)},${pt.y.toFixed(1)}`;
|
|
1597
|
+
setElementValueAtTime(vertexDragState.elementId, {
|
|
1598
|
+
points: pts.join(" ")
|
|
1599
|
+
});
|
|
1449
1600
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (endpointDragState) {
|
|
1604
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1605
|
+
if (!pt) return;
|
|
1606
|
+
endpointDragState.currentX = pt.x;
|
|
1607
|
+
endpointDragState.currentY = pt.y;
|
|
1608
|
+
endpointDragState.snapTarget = findSnapTarget(
|
|
1609
|
+
pt.x,
|
|
1610
|
+
pt.y,
|
|
1611
|
+
endpointDragState.elementId
|
|
1612
|
+
);
|
|
1613
|
+
const def2 = getDef();
|
|
1614
|
+
if (!def2) return;
|
|
1615
|
+
const baseEl2 = def2.elements.find(
|
|
1616
|
+
(x) => x.id === endpointDragState.elementId
|
|
1617
|
+
);
|
|
1618
|
+
if (!baseEl2) return;
|
|
1619
|
+
const finalX = endpointDragState.snapTarget ? endpointDragState.snapTarget.x : pt.x;
|
|
1620
|
+
const finalY = endpointDragState.snapTarget ? endpointDragState.snapTarget.y : pt.y;
|
|
1621
|
+
if (baseEl2.type === "line" || baseEl2.type === "arrow") {
|
|
1622
|
+
if (endpointDragState.snapTarget) {
|
|
1623
|
+
if (endpointDragState.end === "start") {
|
|
1624
|
+
setElementValueAtTime(baseEl2.id, {
|
|
1625
|
+
fromId: endpointDragState.snapTarget.elementId,
|
|
1626
|
+
fromAnchor: endpointDragState.snapTarget.anchor,
|
|
1627
|
+
x1: void 0,
|
|
1628
|
+
y1: void 0
|
|
1629
|
+
});
|
|
1630
|
+
} else {
|
|
1631
|
+
setElementValueAtTime(baseEl2.id, {
|
|
1632
|
+
toId: endpointDragState.snapTarget.elementId,
|
|
1633
|
+
toAnchor: endpointDragState.snapTarget.anchor,
|
|
1634
|
+
x2: void 0,
|
|
1635
|
+
y2: void 0
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
if (endpointDragState.end === "start") {
|
|
1640
|
+
updateElementBase(baseEl2.id, {
|
|
1641
|
+
fromId: void 0,
|
|
1642
|
+
fromAnchor: void 0,
|
|
1643
|
+
x1: finalX,
|
|
1644
|
+
y1: finalY
|
|
1645
|
+
});
|
|
1646
|
+
} else {
|
|
1647
|
+
updateElementBase(baseEl2.id, {
|
|
1648
|
+
toId: void 0,
|
|
1649
|
+
toAnchor: void 0,
|
|
1650
|
+
x2: finalX,
|
|
1651
|
+
y2: finalY
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1464
1654
|
}
|
|
1465
1655
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
setElementValueAtTime(baseEl.id, {
|
|
1477
|
-
x: Math.round(newX),
|
|
1478
|
-
y: Math.round(newY),
|
|
1479
|
-
width: newW,
|
|
1480
|
-
height: newH
|
|
1656
|
+
render2();
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (rotateState) {
|
|
1660
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1661
|
+
if (!pt) return;
|
|
1662
|
+
const angle = Math.atan2(pt.y - rotateState.centerY, pt.x - rotateState.centerX) * 180 / Math.PI;
|
|
1663
|
+
const delta = angle - rotateState.startAngle;
|
|
1664
|
+
setElementValueAtTime(rotateState.elementId, {
|
|
1665
|
+
rotation: Math.round((rotateState.startRotation + delta + 360) % 360)
|
|
1481
1666
|
});
|
|
1482
1667
|
return;
|
|
1483
1668
|
}
|
|
1484
|
-
if (
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
else if (h === "w") r1 = Math.max(2, r0 - dx);
|
|
1491
|
-
else if (h === "s") r1 = Math.max(2, r0 + dy);
|
|
1492
|
-
else if (h === "n") r1 = Math.max(2, r0 - dy);
|
|
1493
|
-
else {
|
|
1494
|
-
const ddx = h.includes("e") ? dx : -dx;
|
|
1495
|
-
const ddy = h.includes("s") ? dy : -dy;
|
|
1496
|
-
const dd = uniform ? Math.max(ddx, ddy) : (ddx + ddy) / 2;
|
|
1497
|
-
r1 = Math.max(2, r0 + dd);
|
|
1498
|
-
}
|
|
1499
|
-
setElementValueAtTime(baseEl.id, { r: Math.round(r1), cx, cy });
|
|
1669
|
+
if (connectState) {
|
|
1670
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1671
|
+
if (!pt) return;
|
|
1672
|
+
connectState.currentX = pt.x;
|
|
1673
|
+
connectState.currentY = pt.y;
|
|
1674
|
+
render2();
|
|
1500
1675
|
return;
|
|
1501
1676
|
}
|
|
1502
|
-
if (
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
setElementValueAtTime(baseEl.id, { fontSize: newFontSize });
|
|
1677
|
+
if (marqueeState) {
|
|
1678
|
+
const pt = svgPoint(e.clientX, e.clientY);
|
|
1679
|
+
if (!pt) return;
|
|
1680
|
+
marqueeState.currentX = pt.x;
|
|
1681
|
+
marqueeState.currentY = pt.y;
|
|
1682
|
+
render2();
|
|
1509
1683
|
return;
|
|
1510
1684
|
}
|
|
1511
|
-
|
|
1512
|
-
function render() {
|
|
1513
|
-
if (!canvasEl) return;
|
|
1685
|
+
if (!dragState || !canvasEl) return;
|
|
1514
1686
|
const def = getDef();
|
|
1515
|
-
if (!def)
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1687
|
+
if (!def) return;
|
|
1688
|
+
const baseEl = def.elements.find((x) => x.id === dragState.elementId);
|
|
1689
|
+
if (!baseEl) return;
|
|
1690
|
+
const svgRect = canvasEl.getBoundingClientRect();
|
|
1691
|
+
const scaleX = def.canvas.width / svgRect.width;
|
|
1692
|
+
const scaleY = def.canvas.height / svgRect.height;
|
|
1693
|
+
const dx = (e.clientX - dragState.startMouseX) * scaleX;
|
|
1694
|
+
const dy = (e.clientY - dragState.startMouseY) * scaleY;
|
|
1695
|
+
const targetX = Math.round(dragState.startElemX + dx);
|
|
1696
|
+
const targetY = Math.round(dragState.startElemY + dy);
|
|
1697
|
+
const snap2 = getCurrentSnapshot();
|
|
1698
|
+
const elState = snap2.get(dragState.elementId);
|
|
1699
|
+
if (!elState) return;
|
|
1700
|
+
applyMove(baseEl, elState, targetX, targetY);
|
|
1701
|
+
for (const extra of dragState.extras) {
|
|
1702
|
+
const eEl = def.elements.find((x) => x.id === extra.id);
|
|
1703
|
+
const eState = snap2.get(extra.id);
|
|
1704
|
+
if (!eEl || !eState) continue;
|
|
1705
|
+
applyMove(
|
|
1706
|
+
eEl,
|
|
1707
|
+
eState,
|
|
1708
|
+
Math.round(extra.startX + dx),
|
|
1709
|
+
Math.round(extra.startY + dy),
|
|
1710
|
+
extra.originalPolygonPoints
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
function onPathDoubleClick(e) {
|
|
1715
|
+
if (!pathDraftState || getActiveTool() !== "path") return;
|
|
1716
|
+
e.preventDefault();
|
|
1717
|
+
const points = pathDraftState.points;
|
|
1718
|
+
if (points.length >= 2 && Math.hypot(
|
|
1719
|
+
points.at(-1).x - points.at(-2).x,
|
|
1720
|
+
points.at(-1).y - points.at(-2).y
|
|
1721
|
+
) < 2) {
|
|
1722
|
+
points.pop();
|
|
1723
|
+
}
|
|
1724
|
+
finishPathDraft();
|
|
1725
|
+
}
|
|
1726
|
+
function onPathKeyDown(e) {
|
|
1727
|
+
if (!pathDraftState || getActiveTool() !== "path") return;
|
|
1728
|
+
const target = e.target;
|
|
1729
|
+
const isEditing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
|
|
1730
|
+
if (isEditing) return;
|
|
1731
|
+
if (e.key === "Enter") {
|
|
1732
|
+
e.preventDefault();
|
|
1733
|
+
finishPathDraft();
|
|
1734
|
+
} else if (e.key === "Escape") {
|
|
1735
|
+
e.preventDefault();
|
|
1736
|
+
pathDraftState = null;
|
|
1737
|
+
render2();
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function finishPathDraft() {
|
|
1741
|
+
const draft = pathDraftState;
|
|
1742
|
+
pathDraftState = null;
|
|
1743
|
+
if (!draft || draft.points.length < 2) {
|
|
1744
|
+
render2();
|
|
1520
1745
|
return;
|
|
1521
1746
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1747
|
+
const origin = draft.points[0];
|
|
1748
|
+
const element = makeDefaultElement(
|
|
1749
|
+
"path",
|
|
1750
|
+
uniqueElementId("path"),
|
|
1751
|
+
origin.x,
|
|
1752
|
+
origin.y
|
|
1525
1753
|
);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
for (const el of def.elements) elementsById.set(el.id, el);
|
|
1533
|
-
for (const el of def.elements) {
|
|
1534
|
-
const state = snap2.get(el.id);
|
|
1535
|
-
if (!state) continue;
|
|
1536
|
-
const g = renderElement(el, state, snap2, elementsById);
|
|
1537
|
-
if (g) {
|
|
1538
|
-
if (!state.visible) g.setAttribute("opacity", "0.25");
|
|
1539
|
-
canvasEl.appendChild(g);
|
|
1540
|
-
}
|
|
1754
|
+
if (element?.type === "path") {
|
|
1755
|
+
const d = draft.points.map((point, index) => {
|
|
1756
|
+
const command = index === 0 ? "M" : "L";
|
|
1757
|
+
return `${command} ${(point.x - origin.x).toFixed(1)} ${(point.y - origin.y).toFixed(1)}`;
|
|
1758
|
+
}).join(" ");
|
|
1759
|
+
addElement({ ...element, x: origin.x, y: origin.y, d });
|
|
1541
1760
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
elementsById
|
|
1554
|
-
);
|
|
1555
|
-
if (outline) canvasEl.appendChild(outline);
|
|
1556
|
-
const handle = renderRotationHandle(
|
|
1557
|
-
canvasEl,
|
|
1558
|
-
selection.elementId,
|
|
1559
|
-
snap2,
|
|
1560
|
-
elementsById
|
|
1561
|
-
);
|
|
1562
|
-
if (handle) canvasEl.appendChild(handle);
|
|
1563
|
-
const endpoints = renderLineEndpointHandles(
|
|
1564
|
-
selection.elementId,
|
|
1565
|
-
snap2,
|
|
1566
|
-
elementsById
|
|
1567
|
-
);
|
|
1568
|
-
if (endpoints) canvasEl.appendChild(endpoints);
|
|
1569
|
-
const vertices = renderPolygonVertexHandles(
|
|
1570
|
-
selection.elementId,
|
|
1571
|
-
snap2,
|
|
1572
|
-
elementsById
|
|
1573
|
-
);
|
|
1574
|
-
if (vertices) canvasEl.appendChild(vertices);
|
|
1575
|
-
const resize = renderResizeHandles(
|
|
1576
|
-
canvasEl,
|
|
1577
|
-
selection.elementId,
|
|
1578
|
-
snap2,
|
|
1579
|
-
elementsById
|
|
1580
|
-
);
|
|
1581
|
-
if (resize) canvasEl.appendChild(resize);
|
|
1761
|
+
render2();
|
|
1762
|
+
}
|
|
1763
|
+
function onMouseUp(e) {
|
|
1764
|
+
if (elementDrawState) {
|
|
1765
|
+
const draw = elementDrawState;
|
|
1766
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1767
|
+
if (point) draw.current = point;
|
|
1768
|
+
elementDrawState = null;
|
|
1769
|
+
if (canvasRenderFrame !== null) {
|
|
1770
|
+
cancelAnimationFrame(canvasRenderFrame);
|
|
1771
|
+
canvasRenderFrame = null;
|
|
1582
1772
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1773
|
+
toolJustFinished = true;
|
|
1774
|
+
finishElementDraw(draw);
|
|
1775
|
+
render2();
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (toolDrawState) {
|
|
1779
|
+
const draw = toolDrawState;
|
|
1780
|
+
toolDrawState = null;
|
|
1781
|
+
if (Math.hypot(draw.currentX - draw.startX, draw.currentY - draw.startY) >= 4) {
|
|
1782
|
+
const element = makeDefaultElement(
|
|
1783
|
+
draw.tool,
|
|
1784
|
+
uniqueElementId(draw.tool),
|
|
1785
|
+
0,
|
|
1786
|
+
0
|
|
1590
1787
|
);
|
|
1591
|
-
if (
|
|
1788
|
+
if (element && (element.type === "line" || element.type === "arrow")) {
|
|
1789
|
+
addElement({
|
|
1790
|
+
...element,
|
|
1791
|
+
x1: draw.startX,
|
|
1792
|
+
y1: draw.startY,
|
|
1793
|
+
x2: draw.currentX,
|
|
1794
|
+
y2: draw.currentY
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1592
1797
|
}
|
|
1798
|
+
render2();
|
|
1799
|
+
return;
|
|
1593
1800
|
}
|
|
1594
|
-
if (
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
snap2,
|
|
1598
|
-
elementsById,
|
|
1599
|
-
endpointDragState.snapTarget
|
|
1600
|
-
);
|
|
1601
|
-
if (guides) canvasEl.appendChild(guides);
|
|
1801
|
+
if (resizeState) {
|
|
1802
|
+
resizeState = null;
|
|
1803
|
+
render2();
|
|
1602
1804
|
}
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
1605
|
-
|
|
1805
|
+
if (vertexDragState) {
|
|
1806
|
+
vertexDragState = null;
|
|
1807
|
+
render2();
|
|
1606
1808
|
}
|
|
1607
|
-
if (
|
|
1608
|
-
|
|
1609
|
-
|
|
1809
|
+
if (endpointDragState) {
|
|
1810
|
+
endpointDragState = null;
|
|
1811
|
+
render2();
|
|
1610
1812
|
}
|
|
1611
1813
|
if (connectState) {
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1814
|
+
const target = e.target;
|
|
1815
|
+
const anchorHandle = target?.closest("[data-anchor-handle]");
|
|
1816
|
+
const elemId = anchorHandle?.dataset.elemId ?? findElementId(e.target);
|
|
1817
|
+
const toAnchor = anchorHandle?.dataset.anchor ?? "auto";
|
|
1818
|
+
if (elemId && elemId !== connectState.fromId) {
|
|
1819
|
+
const def = getDef();
|
|
1820
|
+
if (def) {
|
|
1821
|
+
const connectionType = getActiveTool() === "line" ? "line" : "arrow";
|
|
1822
|
+
const common = {
|
|
1823
|
+
id: uniqueElementId(connectionType),
|
|
1824
|
+
rotation: 0,
|
|
1825
|
+
appearances: [],
|
|
1826
|
+
tracks: [],
|
|
1827
|
+
fromId: connectState.fromId,
|
|
1828
|
+
toId: elemId,
|
|
1829
|
+
fromAnchor: connectState.fromAnchor,
|
|
1830
|
+
toAnchor,
|
|
1831
|
+
stroke: "#6366f1",
|
|
1832
|
+
strokeWidth: 2,
|
|
1833
|
+
headStart: "none"
|
|
1834
|
+
};
|
|
1835
|
+
if (connectionType === "line") {
|
|
1836
|
+
addElement({ ...common, type: "line", headEnd: "none" });
|
|
1837
|
+
} else {
|
|
1838
|
+
addElement({
|
|
1839
|
+
...common,
|
|
1840
|
+
type: "arrow",
|
|
1841
|
+
curvature: 0,
|
|
1842
|
+
labelColor: "#0b0b0f",
|
|
1843
|
+
labelOffsetX: 0,
|
|
1844
|
+
labelOffsetY: 4,
|
|
1845
|
+
headEnd: "arrow"
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
connectState = null;
|
|
1851
|
+
render2();
|
|
1623
1852
|
}
|
|
1624
1853
|
if (marqueeState) {
|
|
1625
1854
|
const mx = Math.min(marqueeState.startX, marqueeState.currentX);
|
|
1626
1855
|
const my = Math.min(marqueeState.startY, marqueeState.currentY);
|
|
1627
1856
|
const mw = Math.abs(marqueeState.currentX - marqueeState.startX);
|
|
1628
1857
|
const mh = Math.abs(marqueeState.currentY - marqueeState.startY);
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1858
|
+
marqueeState = null;
|
|
1859
|
+
marqueeJustFinished = true;
|
|
1860
|
+
if (mw > 0 && mh > 0) {
|
|
1861
|
+
const def = getDef();
|
|
1862
|
+
if (def) {
|
|
1863
|
+
const snap2 = getCurrentSnapshot();
|
|
1864
|
+
const marqueeBbox = { x: mx, y: my, w: mw, h: mh };
|
|
1865
|
+
const matchedIds = [];
|
|
1866
|
+
for (const el of def.elements) {
|
|
1867
|
+
const state = snap2.get(el.id);
|
|
1868
|
+
if (!state) continue;
|
|
1869
|
+
const bbox = elementBBox(el, state);
|
|
1870
|
+
if (bbox && intersectsMarquee(bbox, marqueeBbox)) {
|
|
1871
|
+
matchedIds.push(el.id);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (matchedIds.length === 1) {
|
|
1875
|
+
setSelection({ kind: "element", elementId: matchedIds[0] });
|
|
1876
|
+
} else if (matchedIds.length > 1) {
|
|
1877
|
+
setSelection({ kind: "elements", elementIds: matchedIds });
|
|
1878
|
+
} else {
|
|
1879
|
+
setSelection({ kind: "none" });
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1641
1882
|
}
|
|
1883
|
+
render2();
|
|
1642
1884
|
}
|
|
1885
|
+
dragState = null;
|
|
1886
|
+
rotateState = null;
|
|
1643
1887
|
}
|
|
1644
|
-
function
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
if (baseEl.id === excludeElementId) continue;
|
|
1651
|
-
if (baseEl.type === "line" || baseEl.type === "arrow") continue;
|
|
1652
|
-
const state = snap2.get(baseEl.id);
|
|
1653
|
-
if (!state || !state.visible) continue;
|
|
1654
|
-
const points = getAnchorPoints(baseEl, state);
|
|
1655
|
-
allAnchors.push(...points);
|
|
1656
|
-
}
|
|
1657
|
-
const nearest = findNearestAnchor({ x, y }, allAnchors, SNAP_RADIUS);
|
|
1658
|
-
if (!nearest) return null;
|
|
1659
|
-
return {
|
|
1660
|
-
elementId: nearest.elementId,
|
|
1661
|
-
anchor: nearest.anchor,
|
|
1662
|
-
x: nearest.x,
|
|
1663
|
-
y: nearest.y
|
|
1664
|
-
};
|
|
1888
|
+
function requestCanvasRender() {
|
|
1889
|
+
if (canvasRenderFrame !== null) return;
|
|
1890
|
+
canvasRenderFrame = requestAnimationFrame(() => {
|
|
1891
|
+
canvasRenderFrame = null;
|
|
1892
|
+
render2();
|
|
1893
|
+
});
|
|
1665
1894
|
}
|
|
1666
|
-
function
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
const
|
|
1672
|
-
const
|
|
1673
|
-
const
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1895
|
+
function finishElementDraw(draw) {
|
|
1896
|
+
if (draw.tool === "image") {
|
|
1897
|
+
document.getElementById("studio-image-file")?.click();
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
const dragged = isShapeDrag(draw.start, draw.current);
|
|
1901
|
+
const bounds = dragBounds(draw.start, draw.current);
|
|
1902
|
+
const center = dragged ? { x: bounds.centerX, y: bounds.centerY } : draw.start;
|
|
1903
|
+
const element = makeDefaultElement(
|
|
1904
|
+
draw.tool,
|
|
1905
|
+
uniqueElementId(draw.tool),
|
|
1906
|
+
center.x,
|
|
1907
|
+
center.y,
|
|
1908
|
+
draw.polygonSides
|
|
1909
|
+
);
|
|
1910
|
+
if (!element) return;
|
|
1911
|
+
if (dragged && element.type === "rect") {
|
|
1912
|
+
addElement({
|
|
1913
|
+
...element,
|
|
1914
|
+
x: bounds.x,
|
|
1915
|
+
y: bounds.y,
|
|
1916
|
+
width: Math.max(1, bounds.width),
|
|
1917
|
+
height: Math.max(1, bounds.height)
|
|
1918
|
+
});
|
|
1919
|
+
} else if (dragged && element.type === "circle") {
|
|
1920
|
+
addElement({
|
|
1921
|
+
...element,
|
|
1922
|
+
cx: bounds.centerX,
|
|
1923
|
+
cy: bounds.centerY,
|
|
1924
|
+
r: Math.max(2, bounds.distance / 2)
|
|
1925
|
+
});
|
|
1926
|
+
} else if (dragged && element.type === "polygon") {
|
|
1927
|
+
addElement({
|
|
1928
|
+
...element,
|
|
1929
|
+
points: regularPolygonPoints(
|
|
1930
|
+
{ x: bounds.centerX, y: bounds.centerY },
|
|
1931
|
+
Math.max(2, bounds.distance / 2),
|
|
1932
|
+
draw.polygonSides
|
|
1933
|
+
)
|
|
1934
|
+
});
|
|
1935
|
+
} else {
|
|
1936
|
+
addElement(element);
|
|
1937
|
+
}
|
|
1690
1938
|
}
|
|
1691
|
-
function
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1939
|
+
function applyMove(baseEl, state, x, y, polygonOriginal) {
|
|
1940
|
+
if (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "text") {
|
|
1941
|
+
setElementValueAtTime(baseEl.id, { x, y });
|
|
1942
|
+
} else if (baseEl.type === "circle") {
|
|
1943
|
+
setElementValueAtTime(baseEl.id, { cx: x, cy: y });
|
|
1944
|
+
} else if ((baseEl.type === "line" || baseEl.type === "arrow") && typeof state.x1 === "number" && typeof state.y1 === "number" && typeof state.x2 === "number" && typeof state.y2 === "number") {
|
|
1945
|
+
const dx = x - state.x1;
|
|
1946
|
+
const dy = y - state.y1;
|
|
1947
|
+
setElementValueAtTime(baseEl.id, {
|
|
1948
|
+
x1: x,
|
|
1949
|
+
y1: y,
|
|
1950
|
+
x2: state.x2 + dx,
|
|
1951
|
+
y2: state.y2 + dy
|
|
1952
|
+
});
|
|
1953
|
+
} else if (baseEl.type === "path") {
|
|
1954
|
+
setElementValueAtTime(baseEl.id, { x, y });
|
|
1955
|
+
} else if (baseEl.type === "polygon") {
|
|
1956
|
+
const orig = polygonOriginal ?? dragState?.originalPolygonPoints;
|
|
1957
|
+
if (!orig) return;
|
|
1958
|
+
const startFirst = firstPolygonPoint(orig);
|
|
1959
|
+
if (!startFirst) return;
|
|
1960
|
+
const dx = x - startFirst.x;
|
|
1961
|
+
const dy = y - startFirst.y;
|
|
1962
|
+
const newPoints = shiftPolygonPoints(orig, dx, dy);
|
|
1963
|
+
setElementValueAtTime(baseEl.id, { points: newPoints });
|
|
1964
|
+
} else if (baseEl.type === "group") {
|
|
1965
|
+
const dx = x - baseEl.x;
|
|
1966
|
+
const dy = y - baseEl.y;
|
|
1967
|
+
moveGroupBy(baseEl.id, dx, dy);
|
|
1968
|
+
}
|
|
1697
1969
|
}
|
|
1698
|
-
function
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
const r = state;
|
|
1702
|
-
const cx = r.x + r.width / 2;
|
|
1703
|
-
const cy = r.y + r.height / 2;
|
|
1704
|
-
const g = makeG(baseEl.id, rotation, cx, cy);
|
|
1705
|
-
g.innerHTML = `
|
|
1706
|
-
<rect x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}" rx="${r.cornerRadius}"
|
|
1707
|
-
fill="${r.fill}" stroke="${r.stroke}" stroke-width="${r.strokeWidth}" />
|
|
1708
|
-
${r.label ? `<text x="${cx}" y="${cy + 5}" text-anchor="middle"
|
|
1709
|
-
font-size="${r.labelSize}" font-weight="600" fill="${r.labelColor}">${escapeXml(r.label)}</text>` : ""}
|
|
1710
|
-
${r.subtitle ? `<text x="${cx}" y="${cy + r.labelSize + 8}"
|
|
1711
|
-
text-anchor="middle" font-size="10" fill="${r.labelColor}" opacity="0.7">${escapeXml(r.subtitle)}</text>` : ""}
|
|
1712
|
-
`;
|
|
1713
|
-
return g;
|
|
1970
|
+
function readPositionAnchor(baseEl, state) {
|
|
1971
|
+
if (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "text") {
|
|
1972
|
+
return { x: state.x, y: state.y };
|
|
1714
1973
|
}
|
|
1715
|
-
if (baseEl.type === "circle")
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
<circle cx="${c.cx}" cy="${c.cy}" r="${c.r}" fill="${c.fill}" stroke="${c.stroke}"
|
|
1720
|
-
stroke-width="${c.strokeWidth}" />
|
|
1721
|
-
${c.label ? `<text x="${c.cx}" y="${c.cy + 5}" text-anchor="middle"
|
|
1722
|
-
font-size="${c.labelSize}" font-weight="600" fill="${c.labelColor}">${escapeXml(c.label)}</text>` : ""}
|
|
1723
|
-
`;
|
|
1724
|
-
return g;
|
|
1974
|
+
if (baseEl.type === "circle")
|
|
1975
|
+
return { x: state.cx, y: state.cy };
|
|
1976
|
+
if ((baseEl.type === "line" || baseEl.type === "arrow") && typeof state.x1 === "number") {
|
|
1977
|
+
return { x: state.x1, y: state.y1 };
|
|
1725
1978
|
}
|
|
1726
|
-
if (baseEl.type === "
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
g.style.color = l.stroke;
|
|
1732
|
-
const dash = l.strokeDasharray ? `stroke-dasharray="${l.strokeDasharray}"` : "";
|
|
1733
|
-
const mStart = markerUrlFor(l.headStart, "start");
|
|
1734
|
-
const mEnd = markerUrlFor(l.headEnd, "end");
|
|
1735
|
-
const mAttr = `${mStart ? `marker-start="${mStart}"` : ""} ${mEnd ? `marker-end="${mEnd}"` : ""}`;
|
|
1736
|
-
g.innerHTML = `<line x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}"
|
|
1737
|
-
stroke="currentColor" stroke-width="${l.strokeWidth}" ${dash} ${mAttr} />`;
|
|
1738
|
-
return g;
|
|
1979
|
+
if (baseEl.type === "path")
|
|
1980
|
+
return { x: state.x ?? 0, y: state.y ?? 0 };
|
|
1981
|
+
if (baseEl.type === "polygon") {
|
|
1982
|
+
const first = firstPolygonPoint(String(state.points ?? ""));
|
|
1983
|
+
return first;
|
|
1739
1984
|
}
|
|
1740
|
-
if (baseEl.type === "
|
|
1741
|
-
|
|
1742
|
-
const coords = resolveArrowCoords(a, snap2, byId);
|
|
1743
|
-
if (!coords) return null;
|
|
1744
|
-
const g = makeG(baseEl.id, 0, 0, 0);
|
|
1745
|
-
g.style.color = a.stroke;
|
|
1746
|
-
const dash = a.strokeDasharray ? `stroke-dasharray="${a.strokeDasharray}"` : "";
|
|
1747
|
-
const midX = (coords.x1 + coords.x2) / 2;
|
|
1748
|
-
const midY = (coords.y1 + coords.y2) / 2;
|
|
1749
|
-
const isCurved = (a.curvature || 0) !== 0;
|
|
1750
|
-
const mStart = markerUrlFor(a.headStart ?? "none", "start");
|
|
1751
|
-
const mEnd = markerUrlFor(a.headEnd ?? "arrow", "end");
|
|
1752
|
-
const mAttr = `${mStart ? `marker-start="${mStart}"` : ""} ${mEnd ? `marker-end="${mEnd}"` : ""}`;
|
|
1753
|
-
let pathHtml;
|
|
1754
|
-
if (isCurved) {
|
|
1755
|
-
const dx = coords.x2 - coords.x1;
|
|
1756
|
-
const dy = coords.y2 - coords.y1;
|
|
1757
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
1758
|
-
const nx = -dy / len;
|
|
1759
|
-
const ny = dx / len;
|
|
1760
|
-
const cpx = midX + nx * (a.curvature ?? 0);
|
|
1761
|
-
const cpy = midY + ny * (a.curvature ?? 0);
|
|
1762
|
-
pathHtml = `<path d="M ${coords.x1} ${coords.y1} Q ${cpx} ${cpy} ${coords.x2} ${coords.y2}" fill="none" stroke="currentColor" stroke-width="${a.strokeWidth}" ${dash} ${mAttr} />`;
|
|
1763
|
-
} else {
|
|
1764
|
-
pathHtml = `<line x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}" stroke="currentColor" stroke-width="${a.strokeWidth}" ${dash} ${mAttr} />`;
|
|
1765
|
-
}
|
|
1766
|
-
g.innerHTML = `
|
|
1767
|
-
${pathHtml}
|
|
1768
|
-
${a.label ? `<g>
|
|
1769
|
-
<rect x="${midX - 80}" y="${midY - 11}" width="160" height="22" rx="4" fill="white" stroke="currentColor" stroke-opacity="0.4"/>
|
|
1770
|
-
<text x="${midX}" y="${midY + 4}" text-anchor="middle" font-size="12" font-family="ui-monospace, monospace" fill="${a.labelColor}">${escapeXml(a.label)}</text>
|
|
1771
|
-
</g>` : ""}
|
|
1772
|
-
`;
|
|
1773
|
-
return g;
|
|
1985
|
+
if (baseEl.type === "group") {
|
|
1986
|
+
return { x: baseEl.x, y: baseEl.y };
|
|
1774
1987
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1988
|
+
return null;
|
|
1989
|
+
}
|
|
1990
|
+
function elementBBox(baseEl, state) {
|
|
1991
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
1992
|
+
return {
|
|
1993
|
+
x: state.x,
|
|
1994
|
+
y: state.y,
|
|
1995
|
+
w: state.width,
|
|
1996
|
+
h: state.height
|
|
1997
|
+
};
|
|
1781
1998
|
}
|
|
1782
|
-
if (baseEl.type === "
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
height="${i.height}" preserveAspectRatio="${i.preserveAspectRatio}" opacity="${i.opacity}" />` : `<rect x="${i.x}" y="${i.y}" width="${i.width}" height="${i.height}" fill="none"
|
|
1791
|
-
stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.4" />`;
|
|
1792
|
-
return g;
|
|
1999
|
+
if (baseEl.type === "circle") {
|
|
2000
|
+
const r = state.r;
|
|
2001
|
+
return {
|
|
2002
|
+
x: state.cx - r,
|
|
2003
|
+
y: state.cy - r,
|
|
2004
|
+
w: r * 2,
|
|
2005
|
+
h: r * 2
|
|
2006
|
+
};
|
|
1793
2007
|
}
|
|
1794
|
-
if (baseEl.type === "
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2008
|
+
if (baseEl.type === "text") {
|
|
2009
|
+
const bbox = textBBoxOnCanvas(canvasEl, baseEl.id);
|
|
2010
|
+
if (bbox) return bbox;
|
|
2011
|
+
return {
|
|
2012
|
+
x: state.x,
|
|
2013
|
+
y: state.y,
|
|
2014
|
+
w: 100,
|
|
2015
|
+
h: state.fontSize ?? 16
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
if (baseEl.type === "line" || baseEl.type === "arrow") {
|
|
2019
|
+
const coords = baseEl.type === "line" ? resolveLineCoords(
|
|
2020
|
+
state,
|
|
2021
|
+
getCurrentSnapshot(),
|
|
2022
|
+
/* @__PURE__ */ new Map()
|
|
2023
|
+
) : resolveArrowCoords(
|
|
2024
|
+
state,
|
|
2025
|
+
getCurrentSnapshot(),
|
|
2026
|
+
/* @__PURE__ */ new Map()
|
|
2027
|
+
);
|
|
2028
|
+
if (!coords) return null;
|
|
2029
|
+
return {
|
|
2030
|
+
x: Math.min(coords.x1, coords.x2),
|
|
2031
|
+
y: Math.min(coords.y1, coords.y2),
|
|
2032
|
+
w: Math.abs(coords.x2 - coords.x1),
|
|
2033
|
+
h: Math.abs(coords.y2 - coords.y1)
|
|
2034
|
+
};
|
|
1801
2035
|
}
|
|
1802
2036
|
if (baseEl.type === "polygon") {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
return
|
|
2037
|
+
return polygonBoundingBox(String(state.points ?? ""));
|
|
2038
|
+
}
|
|
2039
|
+
if (baseEl.type === "path") {
|
|
2040
|
+
return pathBBoxOnCanvas(canvasEl, baseEl.id);
|
|
1807
2041
|
}
|
|
1808
2042
|
return null;
|
|
1809
2043
|
}
|
|
1810
|
-
function
|
|
1811
|
-
|
|
1812
|
-
}
|
|
1813
|
-
function hidePreview2() {
|
|
1814
|
-
hidePreview(canvasEl);
|
|
2044
|
+
function intersectsMarquee(elBbox, marquee) {
|
|
2045
|
+
return elBbox.x < marquee.x + marquee.w && elBbox.x + elBbox.w > marquee.x && elBbox.y < marquee.y + marquee.h && elBbox.y + elBbox.h > marquee.y;
|
|
1815
2046
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
searchEl?.addEventListener("keydown", (e) => {
|
|
1838
|
-
if (e.key === "Escape" && searchEl) {
|
|
1839
|
-
searchEl.value = "";
|
|
1840
|
-
render2();
|
|
2047
|
+
function handleResizeMove(e) {
|
|
2048
|
+
if (!resizeState || !canvasEl) return;
|
|
2049
|
+
const def = getDef();
|
|
2050
|
+
if (!def) return;
|
|
2051
|
+
const baseEl = def.elements.find((x) => x.id === resizeState.elementId);
|
|
2052
|
+
if (!baseEl) return;
|
|
2053
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
2054
|
+
const sx = def.canvas.width / rect.width;
|
|
2055
|
+
const sy = def.canvas.height / rect.height;
|
|
2056
|
+
const dx = (e.clientX - resizeState.startMouseX) * sx;
|
|
2057
|
+
const dy = (e.clientY - resizeState.startMouseY) * sy;
|
|
2058
|
+
const h = resizeState.handle;
|
|
2059
|
+
const uniform = e.shiftKey;
|
|
2060
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
2061
|
+
let newX = resizeState.startX;
|
|
2062
|
+
let newY = resizeState.startY;
|
|
2063
|
+
let newW = resizeState.startW;
|
|
2064
|
+
let newH = resizeState.startH;
|
|
2065
|
+
if (h.includes("w")) {
|
|
2066
|
+
newX = resizeState.startX + dx;
|
|
2067
|
+
newW = resizeState.startW - dx;
|
|
1841
2068
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2069
|
+
if (h.includes("e")) {
|
|
2070
|
+
newW = resizeState.startW + dx;
|
|
2071
|
+
}
|
|
2072
|
+
if (h.includes("n")) {
|
|
2073
|
+
newY = resizeState.startY + dy;
|
|
2074
|
+
newH = resizeState.startH - dy;
|
|
2075
|
+
}
|
|
2076
|
+
if (h.includes("s")) {
|
|
2077
|
+
newH = resizeState.startH + dy;
|
|
2078
|
+
}
|
|
2079
|
+
if (uniform && (h === "nw" || h === "ne" || h === "se" || h === "sw")) {
|
|
2080
|
+
const aspect = resizeState.aspect;
|
|
2081
|
+
const fromW = Math.abs(newW);
|
|
2082
|
+
const fromH = Math.abs(newH);
|
|
2083
|
+
if (fromW / aspect > fromH) {
|
|
2084
|
+
const targetH = Math.max(1, Math.round(fromW / aspect));
|
|
2085
|
+
if (h.includes("n"))
|
|
2086
|
+
newY = resizeState.startY + resizeState.startH - targetH;
|
|
2087
|
+
newH = targetH;
|
|
2088
|
+
} else {
|
|
2089
|
+
const targetW = Math.max(1, Math.round(fromH * aspect));
|
|
2090
|
+
if (h.includes("w"))
|
|
2091
|
+
newX = resizeState.startX + resizeState.startW - targetW;
|
|
2092
|
+
newW = targetW;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (newW < 0) {
|
|
2096
|
+
newX = newX + newW;
|
|
2097
|
+
newW = -newW;
|
|
2098
|
+
}
|
|
2099
|
+
if (newH < 0) {
|
|
2100
|
+
newY = newY + newH;
|
|
2101
|
+
newH = -newH;
|
|
2102
|
+
}
|
|
2103
|
+
newW = Math.max(2, Math.round(newW));
|
|
2104
|
+
newH = Math.max(2, Math.round(newH));
|
|
2105
|
+
setElementValueAtTime(baseEl.id, {
|
|
2106
|
+
x: Math.round(newX),
|
|
2107
|
+
y: Math.round(newY),
|
|
2108
|
+
width: newW,
|
|
2109
|
+
height: newH
|
|
2110
|
+
});
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (baseEl.type === "circle") {
|
|
2114
|
+
const cx = resizeState.startCx ?? 0;
|
|
2115
|
+
const cy = resizeState.startCy ?? 0;
|
|
2116
|
+
const r0 = resizeState.startR ?? 1;
|
|
2117
|
+
let r1;
|
|
2118
|
+
if (h === "e") r1 = Math.max(2, r0 + dx);
|
|
2119
|
+
else if (h === "w") r1 = Math.max(2, r0 - dx);
|
|
2120
|
+
else if (h === "s") r1 = Math.max(2, r0 + dy);
|
|
2121
|
+
else if (h === "n") r1 = Math.max(2, r0 - dy);
|
|
2122
|
+
else {
|
|
2123
|
+
const ddx = h.includes("e") ? dx : -dx;
|
|
2124
|
+
const ddy = h.includes("s") ? dy : -dy;
|
|
2125
|
+
const dd = uniform ? Math.max(ddx, ddy) : (ddx + ddy) / 2;
|
|
2126
|
+
r1 = Math.max(2, r0 + dd);
|
|
2127
|
+
}
|
|
2128
|
+
setElementValueAtTime(baseEl.id, { r: Math.round(r1), cx, cy });
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
if (baseEl.type === "text") {
|
|
2132
|
+
const startFs = resizeState.startFontSize ?? 16;
|
|
2133
|
+
const startH0 = Math.max(1, resizeState.startH);
|
|
2134
|
+
const newH = h.includes("n") ? Math.max(1, startH0 - dy) : Math.max(1, startH0 + dy);
|
|
2135
|
+
const ratio = newH / startH0;
|
|
2136
|
+
const newFontSize = Math.max(6, Math.round(startFs * ratio));
|
|
2137
|
+
setElementValueAtTime(baseEl.id, { fontSize: newFontSize });
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
1858
2140
|
}
|
|
1859
2141
|
function render2() {
|
|
1860
|
-
if (!
|
|
2142
|
+
if (!canvasEl) return;
|
|
1861
2143
|
const def = getDef();
|
|
1862
|
-
if (!def
|
|
1863
|
-
|
|
2144
|
+
if (!def) {
|
|
2145
|
+
canvasEl.innerHTML = "";
|
|
2146
|
+
canvasEl.removeAttribute("viewBox");
|
|
2147
|
+
canvasEl.style.width = "0";
|
|
2148
|
+
canvasEl.style.height = "0";
|
|
1864
2149
|
return;
|
|
1865
2150
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2151
|
+
canvasEl.setAttribute(
|
|
2152
|
+
"viewBox",
|
|
2153
|
+
`0 0 ${def.canvas.width} ${def.canvas.height}`
|
|
2154
|
+
);
|
|
2155
|
+
canvasEl.style.width = def.canvas.width * canvasZoom + "px";
|
|
2156
|
+
canvasEl.style.height = def.canvas.height * canvasZoom + "px";
|
|
2157
|
+
canvasEl.style.backgroundColor = def.canvas.background;
|
|
2158
|
+
canvasEl.innerHTML = `<defs>${HEAD_MARKER_DEFS}</defs>`;
|
|
2159
|
+
const snap2 = getCurrentSnapshot();
|
|
2160
|
+
const elementsById = /* @__PURE__ */ new Map();
|
|
2161
|
+
for (const el of def.elements) elementsById.set(el.id, el);
|
|
2162
|
+
for (const el of def.elements) {
|
|
2163
|
+
const state = snap2.get(el.id);
|
|
2164
|
+
if (!state) continue;
|
|
2165
|
+
const g = renderElement(el, state, snap2, elementsById);
|
|
2166
|
+
if (g) {
|
|
2167
|
+
if (!state.visible) g.setAttribute("opacity", "0.25");
|
|
2168
|
+
canvasEl.appendChild(g);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const selection = getSelection();
|
|
2172
|
+
if (selection.kind === "element") {
|
|
2173
|
+
const selEl = elementsById.get(selection.elementId);
|
|
2174
|
+
if (selEl && isGroup(selEl)) {
|
|
2175
|
+
const groupOutline = renderGroupOutline(selection.elementId);
|
|
2176
|
+
if (groupOutline) canvasEl.appendChild(groupOutline);
|
|
2177
|
+
} else {
|
|
2178
|
+
const outline = renderSelectionOutline(
|
|
2179
|
+
canvasEl,
|
|
2180
|
+
selection.elementId,
|
|
2181
|
+
snap2,
|
|
2182
|
+
elementsById
|
|
2183
|
+
);
|
|
2184
|
+
if (outline) canvasEl.appendChild(outline);
|
|
2185
|
+
const selectMode = getActiveTool() === "select";
|
|
2186
|
+
const handle = selectMode ? renderRotationHandle(
|
|
2187
|
+
canvasEl,
|
|
2188
|
+
selection.elementId,
|
|
2189
|
+
snap2,
|
|
2190
|
+
elementsById
|
|
2191
|
+
) : null;
|
|
2192
|
+
if (handle) canvasEl.appendChild(handle);
|
|
2193
|
+
const endpoints = selectMode ? renderLineEndpointHandles(selection.elementId, snap2, elementsById) : null;
|
|
2194
|
+
if (endpoints) canvasEl.appendChild(endpoints);
|
|
2195
|
+
const vertices = selectMode ? renderPolygonVertexHandles(selection.elementId, snap2, elementsById) : null;
|
|
2196
|
+
if (vertices) canvasEl.appendChild(vertices);
|
|
2197
|
+
const resize = selectMode ? renderResizeHandles(canvasEl, selection.elementId, snap2, elementsById) : null;
|
|
2198
|
+
if (resize) canvasEl.appendChild(resize);
|
|
2199
|
+
}
|
|
2200
|
+
} else if (selection.kind === "elements") {
|
|
2201
|
+
for (const elId of selection.elementIds) {
|
|
2202
|
+
const outline = renderSelectionOutline(
|
|
2203
|
+
canvasEl,
|
|
2204
|
+
elId,
|
|
2205
|
+
snap2,
|
|
2206
|
+
elementsById
|
|
2207
|
+
);
|
|
2208
|
+
if (outline) canvasEl.appendChild(outline);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
if (endpointDragState) {
|
|
2212
|
+
const guides = renderSnapTargets(
|
|
2213
|
+
endpointDragState.elementId,
|
|
2214
|
+
snap2,
|
|
2215
|
+
elementsById,
|
|
2216
|
+
endpointDragState.snapTarget
|
|
2217
|
+
);
|
|
2218
|
+
if (guides) canvasEl.appendChild(guides);
|
|
2219
|
+
}
|
|
2220
|
+
const activeTool2 = getActiveTool();
|
|
2221
|
+
if (hoveredElementId && (activeTool2 === "line" || activeTool2 === "arrow") && (!connectState || hoveredElementId !== connectState.fromId)) {
|
|
2222
|
+
const anchors = renderAnchorDots(hoveredElementId, snap2, elementsById);
|
|
2223
|
+
if (anchors) canvasEl.appendChild(anchors);
|
|
1892
2224
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
setSelection(toggleSelectionFor(sel, id));
|
|
1897
|
-
return;
|
|
2225
|
+
if (connectState && (activeTool2 === "line" || activeTool2 === "arrow") && connectState.fromId !== hoveredElementId) {
|
|
2226
|
+
const anchors = renderAnchorDots(connectState.fromId, snap2, elementsById);
|
|
2227
|
+
if (anchors) canvasEl.appendChild(anchors);
|
|
1898
2228
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
".
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2229
|
+
if (connectState) {
|
|
2230
|
+
const tempLine = document.createElementNS(SVG_NS, "line");
|
|
2231
|
+
tempLine.setAttribute("x1", String(connectState.startX));
|
|
2232
|
+
tempLine.setAttribute("y1", String(connectState.startY));
|
|
2233
|
+
tempLine.setAttribute("x2", String(connectState.currentX));
|
|
2234
|
+
tempLine.setAttribute("y2", String(connectState.currentY));
|
|
2235
|
+
tempLine.setAttribute("stroke", "#6366f1");
|
|
2236
|
+
tempLine.setAttribute("stroke-width", "2");
|
|
2237
|
+
tempLine.setAttribute("stroke-dasharray", "5 3");
|
|
2238
|
+
tempLine.setAttribute("marker-end", "url(#studio-arrow)");
|
|
2239
|
+
tempLine.style.pointerEvents = "none";
|
|
2240
|
+
canvasEl.appendChild(tempLine);
|
|
2241
|
+
}
|
|
2242
|
+
if (toolDrawState) {
|
|
2243
|
+
const preview = document.createElementNS(SVG_NS, "line");
|
|
2244
|
+
preview.setAttribute("x1", String(toolDrawState.startX));
|
|
2245
|
+
preview.setAttribute("y1", String(toolDrawState.startY));
|
|
2246
|
+
preview.setAttribute("x2", String(toolDrawState.currentX));
|
|
2247
|
+
preview.setAttribute("y2", String(toolDrawState.currentY));
|
|
2248
|
+
preview.setAttribute("stroke", "#6366f1");
|
|
2249
|
+
preview.setAttribute("stroke-width", "2");
|
|
2250
|
+
preview.setAttribute("stroke-dasharray", "5 3");
|
|
2251
|
+
preview.style.pointerEvents = "none";
|
|
2252
|
+
canvasEl.appendChild(preview);
|
|
2253
|
+
}
|
|
2254
|
+
if (elementDrawState) {
|
|
2255
|
+
const bounds = dragBounds(elementDrawState.start, elementDrawState.current);
|
|
2256
|
+
const preview = document.createElementNS(
|
|
2257
|
+
SVG_NS,
|
|
2258
|
+
elementDrawState.tool === "circle" ? "circle" : elementDrawState.tool === "polygon" ? "polygon" : "rect"
|
|
2259
|
+
);
|
|
2260
|
+
if (elementDrawState.tool === "circle") {
|
|
2261
|
+
preview.setAttribute("cx", String(bounds.centerX));
|
|
2262
|
+
preview.setAttribute("cy", String(bounds.centerY));
|
|
2263
|
+
preview.setAttribute("r", String(Math.max(2, bounds.distance / 2)));
|
|
2264
|
+
} else if (elementDrawState.tool === "polygon") {
|
|
2265
|
+
preview.setAttribute(
|
|
2266
|
+
"points",
|
|
2267
|
+
regularPolygonPoints(
|
|
2268
|
+
{ x: bounds.centerX, y: bounds.centerY },
|
|
2269
|
+
Math.max(2, bounds.distance / 2),
|
|
2270
|
+
elementDrawState.polygonSides
|
|
2271
|
+
)
|
|
2272
|
+
);
|
|
2273
|
+
} else {
|
|
2274
|
+
preview.setAttribute("x", String(bounds.x));
|
|
2275
|
+
preview.setAttribute("y", String(bounds.y));
|
|
2276
|
+
preview.setAttribute("width", String(Math.max(1, bounds.width)));
|
|
2277
|
+
preview.setAttribute("height", String(Math.max(1, bounds.height)));
|
|
2278
|
+
}
|
|
2279
|
+
preview.setAttribute("fill", "rgba(99, 102, 241, 0.12)");
|
|
2280
|
+
preview.setAttribute("stroke", "#6366f1");
|
|
2281
|
+
preview.setAttribute("stroke-width", "2");
|
|
2282
|
+
preview.setAttribute("stroke-dasharray", "5 3");
|
|
2283
|
+
preview.style.pointerEvents = "none";
|
|
2284
|
+
canvasEl.appendChild(preview);
|
|
2285
|
+
}
|
|
2286
|
+
if (pathDraftState) {
|
|
2287
|
+
const points = [
|
|
2288
|
+
...pathDraftState.points,
|
|
2289
|
+
...pathDraftState.current ? [pathDraftState.current] : []
|
|
2290
|
+
];
|
|
2291
|
+
const preview = document.createElementNS(SVG_NS, "polyline");
|
|
2292
|
+
preview.setAttribute(
|
|
2293
|
+
"points",
|
|
2294
|
+
points.map((point) => `${point.x},${point.y}`).join(" ")
|
|
2295
|
+
);
|
|
2296
|
+
preview.setAttribute("fill", "none");
|
|
2297
|
+
preview.setAttribute("stroke", "#6366f1");
|
|
2298
|
+
preview.setAttribute("stroke-width", "2");
|
|
2299
|
+
preview.setAttribute("stroke-dasharray", "5 3");
|
|
2300
|
+
preview.style.pointerEvents = "none";
|
|
2301
|
+
canvasEl.appendChild(preview);
|
|
2302
|
+
for (const point of pathDraftState.points) {
|
|
2303
|
+
const anchor = document.createElementNS(SVG_NS, "circle");
|
|
2304
|
+
anchor.setAttribute("cx", String(point.x));
|
|
2305
|
+
anchor.setAttribute("cy", String(point.y));
|
|
2306
|
+
anchor.setAttribute("r", "4");
|
|
2307
|
+
anchor.setAttribute("fill", "#fff");
|
|
2308
|
+
anchor.setAttribute("stroke", "#6366f1");
|
|
2309
|
+
anchor.setAttribute("stroke-width", "2");
|
|
2310
|
+
anchor.style.pointerEvents = "none";
|
|
2311
|
+
canvasEl.appendChild(anchor);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
if (marqueeState) {
|
|
2315
|
+
const mx = Math.min(marqueeState.startX, marqueeState.currentX);
|
|
2316
|
+
const my = Math.min(marqueeState.startY, marqueeState.currentY);
|
|
2317
|
+
const mw = Math.abs(marqueeState.currentX - marqueeState.startX);
|
|
2318
|
+
const mh = Math.abs(marqueeState.currentY - marqueeState.startY);
|
|
2319
|
+
if (mw > 0 || mh > 0) {
|
|
2320
|
+
const marqueeRect = document.createElementNS(SVG_NS, "rect");
|
|
2321
|
+
marqueeRect.setAttribute("x", String(mx));
|
|
2322
|
+
marqueeRect.setAttribute("y", String(my));
|
|
2323
|
+
marqueeRect.setAttribute("width", String(mw));
|
|
2324
|
+
marqueeRect.setAttribute("height", String(mh));
|
|
2325
|
+
marqueeRect.setAttribute("fill", "rgba(99, 102, 241, 0.08)");
|
|
2326
|
+
marqueeRect.setAttribute("stroke", "#6366f1");
|
|
2327
|
+
marqueeRect.setAttribute("stroke-width", "1");
|
|
2328
|
+
marqueeRect.setAttribute("stroke-dasharray", "4 3");
|
|
2329
|
+
marqueeRect.style.pointerEvents = "none";
|
|
2330
|
+
canvasEl.appendChild(marqueeRect);
|
|
2331
|
+
}
|
|
1910
2332
|
}
|
|
1911
|
-
li.classList.add("is-dragging");
|
|
1912
|
-
}
|
|
1913
|
-
function onDragOver(e) {
|
|
1914
|
-
if (!dragSourceId) return;
|
|
1915
|
-
e.preventDefault();
|
|
1916
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
1917
|
-
const li = e.target.closest(
|
|
1918
|
-
".studio-element-list-item"
|
|
1919
|
-
);
|
|
1920
|
-
listEl?.querySelectorAll(".is-drop-above, .is-drop-below").forEach((n) => n.classList.remove("is-drop-above", "is-drop-below"));
|
|
1921
|
-
if (!li) return;
|
|
1922
|
-
const rect = li.getBoundingClientRect();
|
|
1923
|
-
const upper = e.clientY < rect.top + rect.height / 2;
|
|
1924
|
-
li.classList.add(upper ? "is-drop-above" : "is-drop-below");
|
|
1925
|
-
}
|
|
1926
|
-
function onDragLeave(e) {
|
|
1927
|
-
const li = e.target.closest(
|
|
1928
|
-
".studio-element-list-item"
|
|
1929
|
-
);
|
|
1930
|
-
li?.classList.remove("is-drop-above", "is-drop-below");
|
|
1931
|
-
}
|
|
1932
|
-
function onDrop(e) {
|
|
1933
|
-
if (!dragSourceId) return;
|
|
1934
|
-
e.preventDefault();
|
|
1935
|
-
const li = e.target.closest(
|
|
1936
|
-
".studio-element-list-item"
|
|
1937
|
-
);
|
|
1938
|
-
if (!li) return;
|
|
1939
|
-
const targetId = li.dataset.elemId ?? "";
|
|
1940
|
-
if (!targetId || targetId === dragSourceId) return;
|
|
1941
|
-
const rect = li.getBoundingClientRect();
|
|
1942
|
-
const upper = e.clientY < rect.top + rect.height / 2;
|
|
1943
|
-
const positionInVisualList = upper ? "before" : "after";
|
|
1944
|
-
const positionInDefArray = positionInVisualList === "before" ? "after" : "before";
|
|
1945
|
-
reorderElement(dragSourceId, targetId, positionInDefArray);
|
|
1946
|
-
dragSourceId = null;
|
|
1947
|
-
}
|
|
1948
|
-
function onDragEnd() {
|
|
1949
|
-
dragSourceId = null;
|
|
1950
|
-
listEl?.querySelectorAll(".is-drop-above, .is-drop-below, .is-dragging").forEach(
|
|
1951
|
-
(n) => n.classList.remove("is-drop-above", "is-drop-below", "is-dragging")
|
|
1952
|
-
);
|
|
1953
2333
|
}
|
|
1954
|
-
function
|
|
1955
|
-
const target = e.target;
|
|
1956
|
-
const btn = target.closest("[data-add-element]");
|
|
1957
|
-
if (!btn) return;
|
|
1958
|
-
const type = btn.dataset.addElement;
|
|
1959
|
-
if (!type) return;
|
|
2334
|
+
function findSnapTarget(x, y, excludeElementId) {
|
|
1960
2335
|
const def = getDef();
|
|
1961
|
-
if (!def) return;
|
|
1962
|
-
const
|
|
1963
|
-
const
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
y: cy - 40,
|
|
2081
|
-
d: "M 0 0 L 80 0 L 40 80 Z",
|
|
2082
|
-
fill: "#a5b4fc",
|
|
2083
|
-
stroke: "#6366f1",
|
|
2084
|
-
strokeWidth: 2,
|
|
2085
|
-
opacity: 1
|
|
2086
|
-
};
|
|
2087
|
-
case "polygon": {
|
|
2088
|
-
const r = 40;
|
|
2089
|
-
const sides = 6;
|
|
2090
|
-
const pts = Array.from({ length: sides }, (_, i) => {
|
|
2091
|
-
const a = i * Math.PI * 2 / sides - Math.PI / 2;
|
|
2092
|
-
return `${(cx + r * Math.cos(a)).toFixed(1)},${(cy + r * Math.sin(a)).toFixed(1)}`;
|
|
2093
|
-
}).join(" ");
|
|
2094
|
-
return {
|
|
2095
|
-
type: "polygon",
|
|
2096
|
-
id,
|
|
2097
|
-
rotation: 0,
|
|
2098
|
-
appearances: [],
|
|
2099
|
-
tracks: [],
|
|
2100
|
-
points: pts,
|
|
2101
|
-
fill: "#a5b4fc",
|
|
2102
|
-
stroke: "#6366f1",
|
|
2103
|
-
strokeWidth: 1.5,
|
|
2104
|
-
opacity: 1
|
|
2105
|
-
};
|
|
2336
|
+
if (!def) return null;
|
|
2337
|
+
const snap2 = getCurrentSnapshot();
|
|
2338
|
+
const allAnchors = [];
|
|
2339
|
+
for (const baseEl of def.elements) {
|
|
2340
|
+
if (baseEl.id === excludeElementId) continue;
|
|
2341
|
+
if (baseEl.type === "line" || baseEl.type === "arrow") continue;
|
|
2342
|
+
const state = snap2.get(baseEl.id);
|
|
2343
|
+
if (!state || !state.visible) continue;
|
|
2344
|
+
const points = getAnchorPoints(baseEl, state);
|
|
2345
|
+
allAnchors.push(...points);
|
|
2346
|
+
}
|
|
2347
|
+
const nearest = findNearestAnchor({ x, y }, allAnchors, SNAP_RADIUS);
|
|
2348
|
+
if (!nearest) return null;
|
|
2349
|
+
return {
|
|
2350
|
+
elementId: nearest.elementId,
|
|
2351
|
+
anchor: nearest.anchor,
|
|
2352
|
+
x: nearest.x,
|
|
2353
|
+
y: nearest.y
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function renderGroupOutline(groupId) {
|
|
2357
|
+
const box = groupBbox(groupId);
|
|
2358
|
+
if (!box) return null;
|
|
2359
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2360
|
+
g.setAttribute("data-elem-id", groupId);
|
|
2361
|
+
const pad = 6;
|
|
2362
|
+
const x = box.x - pad;
|
|
2363
|
+
const y = box.y - pad;
|
|
2364
|
+
const w = box.w + pad * 2;
|
|
2365
|
+
const h = box.h + pad * 2;
|
|
2366
|
+
const thickness = 8;
|
|
2367
|
+
g.innerHTML = `
|
|
2368
|
+
<rect x="${x}" y="${y}" width="${w}" height="${h}"
|
|
2369
|
+
fill="none" stroke="var(--color-accent)" stroke-width="2" stroke-dasharray="6 4" rx="4" pointer-events="none" />
|
|
2370
|
+
<rect x="${x - thickness}" y="${y - thickness}" width="${w + thickness * 2}" height="${thickness * 2}"
|
|
2371
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2372
|
+
<rect x="${x - thickness}" y="${y + h - thickness}" width="${w + thickness * 2}" height="${thickness * 2}"
|
|
2373
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2374
|
+
<rect x="${x - thickness}" y="${y}" width="${thickness * 2}" height="${h}"
|
|
2375
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2376
|
+
<rect x="${x + w - thickness}" y="${y}" width="${thickness * 2}" height="${h}"
|
|
2377
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2378
|
+
`;
|
|
2379
|
+
return g;
|
|
2380
|
+
}
|
|
2381
|
+
function makeG(elementId, rotation, cx, cy) {
|
|
2382
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2383
|
+
g.setAttribute("data-elem-id", elementId);
|
|
2384
|
+
g.classList.add("element-handle");
|
|
2385
|
+
if (rotation) g.setAttribute("transform", `rotate(${rotation} ${cx} ${cy})`);
|
|
2386
|
+
return g;
|
|
2387
|
+
}
|
|
2388
|
+
function renderElement(baseEl, state, snap2, byId) {
|
|
2389
|
+
const rotation = state.rotation || 0;
|
|
2390
|
+
if (baseEl.type === "rect") {
|
|
2391
|
+
const r = state;
|
|
2392
|
+
const cx = r.x + r.width / 2;
|
|
2393
|
+
const cy = r.y + r.height / 2;
|
|
2394
|
+
const g = makeG(baseEl.id, rotation, cx, cy);
|
|
2395
|
+
g.innerHTML = `
|
|
2396
|
+
<rect x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}" rx="${r.cornerRadius}"
|
|
2397
|
+
fill="${r.fill}" stroke="${r.stroke}" stroke-width="${r.strokeWidth}" />
|
|
2398
|
+
${r.label ? `<text x="${cx}" y="${cy + 5}" text-anchor="middle"
|
|
2399
|
+
font-size="${r.labelSize}" font-weight="600" fill="${r.labelColor}">${escapeXml(r.label)}</text>` : ""}
|
|
2400
|
+
${r.subtitle ? `<text x="${cx}" y="${cy + r.labelSize + 8}"
|
|
2401
|
+
text-anchor="middle" font-size="10" fill="${r.labelColor}" opacity="0.7">${escapeXml(r.subtitle)}</text>` : ""}
|
|
2402
|
+
`;
|
|
2403
|
+
return g;
|
|
2404
|
+
}
|
|
2405
|
+
if (baseEl.type === "circle") {
|
|
2406
|
+
const c = state;
|
|
2407
|
+
const g = makeG(baseEl.id, rotation, c.cx, c.cy);
|
|
2408
|
+
g.innerHTML = `
|
|
2409
|
+
<circle cx="${c.cx}" cy="${c.cy}" r="${c.r}" fill="${c.fill}" stroke="${c.stroke}"
|
|
2410
|
+
stroke-width="${c.strokeWidth}" />
|
|
2411
|
+
${c.label ? `<text x="${c.cx}" y="${c.cy + 5}" text-anchor="middle"
|
|
2412
|
+
font-size="${c.labelSize}" font-weight="600" fill="${c.labelColor}">${escapeXml(c.label)}</text>` : ""}
|
|
2413
|
+
`;
|
|
2414
|
+
return g;
|
|
2415
|
+
}
|
|
2416
|
+
if (baseEl.type === "line") {
|
|
2417
|
+
const l = state;
|
|
2418
|
+
const coords = resolveLineCoords(l, snap2, byId);
|
|
2419
|
+
if (!coords) return null;
|
|
2420
|
+
const g = makeG(baseEl.id, 0, 0, 0);
|
|
2421
|
+
g.style.color = l.stroke;
|
|
2422
|
+
const dash = l.strokeDasharray ? `stroke-dasharray="${l.strokeDasharray}"` : "";
|
|
2423
|
+
const mStart = markerUrlFor(l.headStart, "start");
|
|
2424
|
+
const mEnd = markerUrlFor(l.headEnd, "end");
|
|
2425
|
+
const mAttr = `${mStart ? `marker-start="${mStart}"` : ""} ${mEnd ? `marker-end="${mEnd}"` : ""}`;
|
|
2426
|
+
g.innerHTML = `<line x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}"
|
|
2427
|
+
stroke="currentColor" stroke-width="${l.strokeWidth}" ${dash} ${mAttr} />`;
|
|
2428
|
+
return g;
|
|
2429
|
+
}
|
|
2430
|
+
if (baseEl.type === "arrow") {
|
|
2431
|
+
const a = state;
|
|
2432
|
+
const coords = resolveArrowCoords(a, snap2, byId);
|
|
2433
|
+
if (!coords) return null;
|
|
2434
|
+
const g = makeG(baseEl.id, 0, 0, 0);
|
|
2435
|
+
g.style.color = a.stroke;
|
|
2436
|
+
const dash = a.strokeDasharray ? `stroke-dasharray="${a.strokeDasharray}"` : "";
|
|
2437
|
+
const midX = (coords.x1 + coords.x2) / 2;
|
|
2438
|
+
const midY = (coords.y1 + coords.y2) / 2;
|
|
2439
|
+
const isCurved = (a.curvature || 0) !== 0;
|
|
2440
|
+
const mStart = markerUrlFor(a.headStart ?? "none", "start");
|
|
2441
|
+
const mEnd = markerUrlFor(a.headEnd ?? "arrow", "end");
|
|
2442
|
+
const mAttr = `${mStart ? `marker-start="${mStart}"` : ""} ${mEnd ? `marker-end="${mEnd}"` : ""}`;
|
|
2443
|
+
let pathHtml;
|
|
2444
|
+
if (isCurved) {
|
|
2445
|
+
const dx = coords.x2 - coords.x1;
|
|
2446
|
+
const dy = coords.y2 - coords.y1;
|
|
2447
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
2448
|
+
const nx = -dy / len;
|
|
2449
|
+
const ny = dx / len;
|
|
2450
|
+
const cpx = midX + nx * (a.curvature ?? 0);
|
|
2451
|
+
const cpy = midY + ny * (a.curvature ?? 0);
|
|
2452
|
+
pathHtml = `<path d="M ${coords.x1} ${coords.y1} Q ${cpx} ${cpy} ${coords.x2} ${coords.y2}" fill="none" stroke="currentColor" stroke-width="${a.strokeWidth}" ${dash} ${mAttr} />`;
|
|
2453
|
+
} else {
|
|
2454
|
+
pathHtml = `<line x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}" stroke="currentColor" stroke-width="${a.strokeWidth}" ${dash} ${mAttr} />`;
|
|
2106
2455
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2456
|
+
g.innerHTML = `
|
|
2457
|
+
${pathHtml}
|
|
2458
|
+
${a.label ? `<g>
|
|
2459
|
+
<rect x="${midX - 80}" y="${midY - 11}" width="160" height="22" rx="4" fill="white" stroke="currentColor" stroke-opacity="0.4"/>
|
|
2460
|
+
<text x="${midX}" y="${midY + 4}" text-anchor="middle" font-size="12" font-family="ui-monospace, monospace" fill="${a.labelColor}">${escapeXml(a.label)}</text>
|
|
2461
|
+
</g>` : ""}
|
|
2462
|
+
`;
|
|
2463
|
+
return g;
|
|
2464
|
+
}
|
|
2465
|
+
if (baseEl.type === "text") {
|
|
2466
|
+
const t = state;
|
|
2467
|
+
const g = makeG(baseEl.id, rotation, t.x, t.y);
|
|
2468
|
+
g.innerHTML = `<text x="${t.x}" y="${t.y}" font-size="${t.fontSize}" font-weight="${t.fontWeight}"
|
|
2469
|
+
fill="${t.color}" text-anchor="${t.textAnchor}">${escapeXml(t.content)}</text>`;
|
|
2470
|
+
return g;
|
|
2471
|
+
}
|
|
2472
|
+
if (baseEl.type === "image") {
|
|
2473
|
+
const i = state;
|
|
2474
|
+
const cx = i.x + i.width / 2;
|
|
2475
|
+
const cy = i.y + i.height / 2;
|
|
2476
|
+
const g = makeG(baseEl.id, rotation, cx, cy);
|
|
2477
|
+
const resolved = resolveAsset(i.assetId, getDef()?.assets ?? {});
|
|
2478
|
+
const href = resolved.status === "resolved" ? resolved.href ?? "" : "";
|
|
2479
|
+
g.innerHTML = href ? `<image href="${escapeXml(href)}" x="${i.x}" y="${i.y}" width="${i.width}"
|
|
2480
|
+
height="${i.height}" preserveAspectRatio="${i.preserveAspectRatio}" opacity="${i.opacity}" />` : `<rect x="${i.x}" y="${i.y}" width="${i.width}" height="${i.height}" fill="none"
|
|
2481
|
+
stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.4" />`;
|
|
2482
|
+
return g;
|
|
2483
|
+
}
|
|
2484
|
+
if (baseEl.type === "path") {
|
|
2485
|
+
const p = state;
|
|
2486
|
+
const g = makeG(baseEl.id, rotation, p.x, p.y);
|
|
2487
|
+
const dash = p.strokeDasharray ? `stroke-dasharray="${p.strokeDasharray}"` : "";
|
|
2488
|
+
const xform = p.x || p.y ? `transform="translate(${p.x} ${p.y})"` : "";
|
|
2489
|
+
g.innerHTML = `<path d="${escapeXml(p.d)}" fill="${p.fill}" stroke="${p.stroke}" stroke-width="${p.strokeWidth}" ${dash} opacity="${p.opacity}" ${xform} pointer-events="all" />`;
|
|
2490
|
+
return g;
|
|
2491
|
+
}
|
|
2492
|
+
if (baseEl.type === "polygon") {
|
|
2493
|
+
const pg = state;
|
|
2494
|
+
const g = makeG(baseEl.id, rotation, 0, 0);
|
|
2495
|
+
g.innerHTML = `<polygon points="${escapeXml(pg.points)}" fill="${pg.fill}" stroke="${pg.stroke}" stroke-width="${pg.strokeWidth}" opacity="${pg.opacity}" pointer-events="all" />`;
|
|
2496
|
+
return g;
|
|
2109
2497
|
}
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
function showPreview2(def, options) {
|
|
2501
|
+
showPreview(canvasEl, def, options);
|
|
2502
|
+
}
|
|
2503
|
+
function hidePreview2() {
|
|
2504
|
+
hidePreview(canvasEl);
|
|
2110
2505
|
}
|
|
2111
2506
|
|
|
2112
2507
|
// src/legacy/studio-focus.ts
|
|
@@ -2303,6 +2698,16 @@ function distributeSelected(kind) {
|
|
|
2303
2698
|
var panelEl = null;
|
|
2304
2699
|
var closedSections = /* @__PURE__ */ new Set();
|
|
2305
2700
|
var isComposingFallback = false;
|
|
2701
|
+
var DEFAULT_LOCALES = ["ko", "en"];
|
|
2702
|
+
function parseLocales(value) {
|
|
2703
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2704
|
+
return value.split(",").map((locale) => locale.trim()).filter((locale) => {
|
|
2705
|
+
const key = locale.toLowerCase();
|
|
2706
|
+
if (!locale || seen.has(key)) return false;
|
|
2707
|
+
seen.add(key);
|
|
2708
|
+
return true;
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2306
2711
|
function initProperties(root) {
|
|
2307
2712
|
panelEl = root;
|
|
2308
2713
|
subscribe(render3);
|
|
@@ -2416,6 +2821,11 @@ function renderInner() {
|
|
|
2416
2821
|
const metaBody = [
|
|
2417
2822
|
textField("title", "meta.title", def.title),
|
|
2418
2823
|
textField("description", "meta.description", def.description),
|
|
2824
|
+
textField(
|
|
2825
|
+
"\uBB38\uC11C \uC5B8\uC5B4 (\uC27C\uD45C\uB85C \uAD6C\uBD84)",
|
|
2826
|
+
"meta.locales",
|
|
2827
|
+
(def.locales ?? DEFAULT_LOCALES).join(", ")
|
|
2828
|
+
),
|
|
2419
2829
|
numberField("duration (ms)", "meta.duration", def.duration, 100),
|
|
2420
2830
|
numberField("canvas.width", "canvas.width", def.canvas.width),
|
|
2421
2831
|
numberField("canvas.height", "canvas.height", def.canvas.height),
|
|
@@ -2438,6 +2848,17 @@ function renderInner() {
|
|
|
2438
2848
|
"\uBAA9\uCC28 \uD45C\uC2DC (chapter list)",
|
|
2439
2849
|
"settings.showChapterList",
|
|
2440
2850
|
def.settings.showChapterList ?? false
|
|
2851
|
+
),
|
|
2852
|
+
selectField(
|
|
2853
|
+
"\uBAA9\uCC28 \uC704\uCE58",
|
|
2854
|
+
"settings.chapterListPosition",
|
|
2855
|
+
def.settings.chapterListPosition ?? "right",
|
|
2856
|
+
[
|
|
2857
|
+
{ value: "left", label: "\uC88C\uCE21" },
|
|
2858
|
+
{ value: "right", label: "\uC6B0\uCE21" },
|
|
2859
|
+
{ value: "top", label: "\uC0C1\uB2E8" },
|
|
2860
|
+
{ value: "bottom", label: "\uD558\uB2E8" }
|
|
2861
|
+
]
|
|
2441
2862
|
)
|
|
2442
2863
|
].join("");
|
|
2443
2864
|
panelEl.innerHTML = `
|
|
@@ -2564,7 +2985,7 @@ function renderElementForm(def, el) {
|
|
|
2564
2985
|
`;
|
|
2565
2986
|
return;
|
|
2566
2987
|
}
|
|
2567
|
-
const baseFields = renderBaseFields(el);
|
|
2988
|
+
const baseFields = renderBaseFields(def, el);
|
|
2568
2989
|
const appearances = renderAppearances(def, el);
|
|
2569
2990
|
const tracks = renderTracks(el);
|
|
2570
2991
|
const baseHeader = `<span class="studio-props-header-title">${escapeHtml2(el.id)}</span><span class="studio-props-header-type">${escapeHtml2(el.type)}</span>`;
|
|
@@ -2581,7 +3002,7 @@ function renderElementForm(def, el) {
|
|
|
2581
3002
|
</div>
|
|
2582
3003
|
`;
|
|
2583
3004
|
}
|
|
2584
|
-
function renderBaseFields(el) {
|
|
3005
|
+
function renderBaseFields(def, el) {
|
|
2585
3006
|
const numberFields = [];
|
|
2586
3007
|
const colorFields = [];
|
|
2587
3008
|
const textFields = [];
|
|
@@ -2612,6 +3033,14 @@ function renderBaseFields(el) {
|
|
|
2612
3033
|
numberFields.push({ label: "cy", key: "cy", value: e.cy });
|
|
2613
3034
|
numberFields.push({ label: "r", key: "r", value: e.r });
|
|
2614
3035
|
}
|
|
3036
|
+
if (el.type === "polygon") {
|
|
3037
|
+
const pointCount = String(e.points ?? "").trim().split(/\s+/).filter(Boolean).length;
|
|
3038
|
+
numberFields.push({
|
|
3039
|
+
label: "\uBCC0 \uAC1C\uC218",
|
|
3040
|
+
key: "polygonSides",
|
|
3041
|
+
value: pointCount
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
2615
3044
|
if (el.type === "line" || el.type === "arrow") {
|
|
2616
3045
|
if (typeof e.x1 === "number") {
|
|
2617
3046
|
numberFields.push({ label: "x1", key: "x1", value: e.x1 });
|
|
@@ -2686,8 +3115,30 @@ function renderBaseFields(el) {
|
|
|
2686
3115
|
value: e.rotation,
|
|
2687
3116
|
step: 5
|
|
2688
3117
|
});
|
|
3118
|
+
const localizedTextFields = (() => {
|
|
3119
|
+
if (el.type !== "text") return [];
|
|
3120
|
+
const localized = el;
|
|
3121
|
+
const documentLocales = def.locales ?? DEFAULT_LOCALES;
|
|
3122
|
+
const locales = localized.locales ?? documentLocales;
|
|
3123
|
+
return [
|
|
3124
|
+
textField(
|
|
3125
|
+
"\uC774 \uC694\uC18C\uC758 \uC5B8\uC5B4 (\uBE44\uC6B0\uBA74 \uBB38\uC11C \uC124\uC815 \uC0AC\uC6A9)",
|
|
3126
|
+
"el.locales",
|
|
3127
|
+
localized.locales?.join(", ") ?? ""
|
|
3128
|
+
),
|
|
3129
|
+
`<p class="studio-props-empty studio-i18n-hint">\uAE30\uBCF8 \uBB38\uAD6C\uB294 content\uC785\uB2C8\uB2E4. \uBC88\uC5ED\uC774 \uC5C6\uAC70\uB098 \uD604\uC7AC \uC5B8\uC5B4\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC73C\uBA74 \uAE30\uBCF8 \uBB38\uAD6C\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4.</p>`,
|
|
3130
|
+
...locales.map(
|
|
3131
|
+
(locale) => textField(
|
|
3132
|
+
`${locale} \uBC88\uC5ED`,
|
|
3133
|
+
`el.translation.${locale}`,
|
|
3134
|
+
localized.translations?.[locale] ?? ""
|
|
3135
|
+
)
|
|
3136
|
+
)
|
|
3137
|
+
];
|
|
3138
|
+
})();
|
|
2689
3139
|
const html = [
|
|
2690
3140
|
...textFields.map((f) => textField(f.label, `el.${f.key}`, f.value)),
|
|
3141
|
+
...localizedTextFields,
|
|
2691
3142
|
...numberFields.map(
|
|
2692
3143
|
(f) => numberField(f.label, `el.${f.key}`, f.value, f.step)
|
|
2693
3144
|
),
|
|
@@ -2757,12 +3208,25 @@ function onInput(e) {
|
|
|
2757
3208
|
if (!key) return;
|
|
2758
3209
|
if (target.type === "text" && e.isComposing) return;
|
|
2759
3210
|
if (target.type === "text" && isComposingFallback) return;
|
|
3211
|
+
if (target.type === "color") {
|
|
3212
|
+
const textInput = target.parentElement?.querySelector(
|
|
3213
|
+
`input[type="text"][data-prop-key="${CSS.escape(key)}"]`
|
|
3214
|
+
);
|
|
3215
|
+
if (textInput) textInput.value = target.value;
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
2760
3218
|
let value = target.value;
|
|
2761
3219
|
if (target.type === "number") value = Number(target.value);
|
|
2762
3220
|
else if (target.type === "checkbox") value = target.checked;
|
|
2763
3221
|
apply(key, value);
|
|
2764
3222
|
}
|
|
2765
3223
|
function onChange(e) {
|
|
3224
|
+
const target = e.target;
|
|
3225
|
+
if (target instanceof HTMLInputElement && target.type === "color") {
|
|
3226
|
+
const key = target.dataset.propKey;
|
|
3227
|
+
if (key) apply(key, target.value);
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
2766
3230
|
onInput(e);
|
|
2767
3231
|
}
|
|
2768
3232
|
function apply(key, value) {
|
|
@@ -2772,7 +3236,10 @@ function apply(key, value) {
|
|
|
2772
3236
|
if (key === "meta.title") updateMeta({ title: String(value) });
|
|
2773
3237
|
else if (key === "meta.description")
|
|
2774
3238
|
updateMeta({ description: String(value) });
|
|
2775
|
-
else if (key === "meta.
|
|
3239
|
+
else if (key === "meta.locales") {
|
|
3240
|
+
const locales = parseLocales(String(value));
|
|
3241
|
+
if (locales.length > 0) updateLocales(locales);
|
|
3242
|
+
} else if (key === "meta.duration") updateDuration(Number(value));
|
|
2776
3243
|
else if (key === "canvas.width") updateCanvas({ width: Number(value) });
|
|
2777
3244
|
else if (key === "canvas.height") updateCanvas({ height: Number(value) });
|
|
2778
3245
|
else if (key === "canvas.background")
|
|
@@ -2784,11 +3251,49 @@ function apply(key, value) {
|
|
|
2784
3251
|
updateSettings({ showCaption: Boolean(value) });
|
|
2785
3252
|
else if (key === "settings.showChapterList")
|
|
2786
3253
|
updateSettings({ showChapterList: Boolean(value) });
|
|
3254
|
+
else if (key === "settings.chapterListPosition")
|
|
3255
|
+
updateSettings({
|
|
3256
|
+
chapterListPosition: String(value)
|
|
3257
|
+
});
|
|
2787
3258
|
else if (key.startsWith("el.") && sel.kind === "element") {
|
|
2788
3259
|
const prop = key.slice(3);
|
|
2789
3260
|
const time = getCurrentTime();
|
|
2790
3261
|
const el = def.elements.find((e) => e.id === sel.elementId);
|
|
2791
3262
|
if (!el) return;
|
|
3263
|
+
if (prop === "locales" && el.type === "text") {
|
|
3264
|
+
const locales = parseLocales(String(value));
|
|
3265
|
+
updateElementBase(sel.elementId, {
|
|
3266
|
+
locales: locales.length > 0 ? locales : void 0
|
|
3267
|
+
});
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
if (prop.startsWith("translation.") && el.type === "text") {
|
|
3271
|
+
const locale = prop.slice("translation.".length);
|
|
3272
|
+
const localized = el;
|
|
3273
|
+
const translations = { ...localized.translations ?? {} };
|
|
3274
|
+
if (String(value)) translations[locale] = String(value);
|
|
3275
|
+
else delete translations[locale];
|
|
3276
|
+
updateElementBase(sel.elementId, { translations });
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
if (prop === "polygonSides" && el.type === "polygon") {
|
|
3280
|
+
const points = el.points.trim().split(/\s+/).map((point) => point.split(",").map(Number)).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
|
|
3281
|
+
if (points.length < 3) return;
|
|
3282
|
+
const minX = Math.min(...points.map(([x]) => x));
|
|
3283
|
+
const maxX = Math.max(...points.map(([x]) => x));
|
|
3284
|
+
const minY = Math.min(...points.map(([, y]) => y));
|
|
3285
|
+
const maxY = Math.max(...points.map(([, y]) => y));
|
|
3286
|
+
const cx = (minX + maxX) / 2;
|
|
3287
|
+
const cy = (minY + maxY) / 2;
|
|
3288
|
+
const radius = Math.max(4, Math.max(maxX - minX, maxY - minY) / 2);
|
|
3289
|
+
const sides = Math.max(3, Math.min(24, Math.round(Number(value)) || 3));
|
|
3290
|
+
const next = Array.from({ length: sides }, (_, index) => {
|
|
3291
|
+
const angle = index * Math.PI * 2 / sides - Math.PI / 2;
|
|
3292
|
+
return `${(cx + radius * Math.cos(angle)).toFixed(1)},${(cy + radius * Math.sin(angle)).toFixed(1)}`;
|
|
3293
|
+
}).join(" ");
|
|
3294
|
+
updateElementBase(sel.elementId, { points: next });
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
2792
3297
|
const hasTrack = el.tracks.some((t) => t.property === prop);
|
|
2793
3298
|
if (hasTrack) {
|
|
2794
3299
|
setTrackKeyframe(sel.elementId, prop, time, value);
|
|
@@ -2959,6 +3464,20 @@ try {
|
|
|
2959
3464
|
} catch {
|
|
2960
3465
|
}
|
|
2961
3466
|
var dragMode = null;
|
|
3467
|
+
var boundDocuments = /* @__PURE__ */ new WeakMap();
|
|
3468
|
+
function bindTimelinePointerDocument(doc) {
|
|
3469
|
+
const existing = boundDocuments.get(doc);
|
|
3470
|
+
if (existing) return existing;
|
|
3471
|
+
doc.addEventListener("mousemove", onMouseMove2);
|
|
3472
|
+
doc.addEventListener("mouseup", onMouseUp2);
|
|
3473
|
+
const cleanup = () => {
|
|
3474
|
+
doc.removeEventListener("mousemove", onMouseMove2);
|
|
3475
|
+
doc.removeEventListener("mouseup", onMouseUp2);
|
|
3476
|
+
boundDocuments.delete(doc);
|
|
3477
|
+
};
|
|
3478
|
+
boundDocuments.set(doc, cleanup);
|
|
3479
|
+
return cleanup;
|
|
3480
|
+
}
|
|
2962
3481
|
function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
|
|
2963
3482
|
tracksEl = tracksRoot;
|
|
2964
3483
|
elTracksEl = elementTracks;
|
|
@@ -2968,8 +3487,7 @@ function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
|
|
|
2968
3487
|
tracksRoot.addEventListener("mousedown", onMouseDown2);
|
|
2969
3488
|
elementTracks.addEventListener("click", onElTracksClick);
|
|
2970
3489
|
elementTracks.addEventListener("mousedown", onMouseDown2);
|
|
2971
|
-
document
|
|
2972
|
-
document.addEventListener("mouseup", onMouseUp2);
|
|
3490
|
+
bindTimelinePointerDocument(document);
|
|
2973
3491
|
addBtn?.addEventListener("click", () => {
|
|
2974
3492
|
const id = uniqueChapterId();
|
|
2975
3493
|
const def = getDef();
|
|
@@ -3029,9 +3547,15 @@ function mirrorScroll(source, target) {
|
|
|
3029
3547
|
}
|
|
3030
3548
|
function showDragTooltip(clientX, clientY, text) {
|
|
3031
3549
|
if (!dragTooltipEl) {
|
|
3032
|
-
|
|
3550
|
+
const ownerDocument = tracksEl?.ownerDocument ?? document;
|
|
3551
|
+
dragTooltipEl = ownerDocument.createElement("div");
|
|
3033
3552
|
dragTooltipEl.className = "studio-tl-drag-tooltip";
|
|
3034
|
-
|
|
3553
|
+
ownerDocument.body.appendChild(dragTooltipEl);
|
|
3554
|
+
} else if (tracksEl && dragTooltipEl.ownerDocument !== tracksEl.ownerDocument) {
|
|
3555
|
+
dragTooltipEl.remove();
|
|
3556
|
+
dragTooltipEl = null;
|
|
3557
|
+
showDragTooltip(clientX, clientY, text);
|
|
3558
|
+
return;
|
|
3035
3559
|
}
|
|
3036
3560
|
dragTooltipEl.textContent = text;
|
|
3037
3561
|
dragTooltipEl.style.left = `${clientX + 14}px`;
|
|
@@ -3110,7 +3634,20 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
3110
3634
|
elTracksEl.innerHTML = '<p class="studio-tl-empty">\uC694\uC18C \uC5C6\uC74C</p>';
|
|
3111
3635
|
return;
|
|
3112
3636
|
}
|
|
3637
|
+
const byId = new Map(elements.map((element) => [element.id, element]));
|
|
3638
|
+
const depthOf = (element) => {
|
|
3639
|
+
let depth = 0;
|
|
3640
|
+
let parentId = element.parentId;
|
|
3641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3642
|
+
while (parentId && !seen.has(parentId)) {
|
|
3643
|
+
seen.add(parentId);
|
|
3644
|
+
depth += 1;
|
|
3645
|
+
parentId = byId.get(parentId)?.parentId;
|
|
3646
|
+
}
|
|
3647
|
+
return depth;
|
|
3648
|
+
};
|
|
3113
3649
|
const rows = elements.map((el) => {
|
|
3650
|
+
const depth = depthOf(el);
|
|
3114
3651
|
const isSel = sel.kind === "element" && sel.elementId === el.id || sel.kind === "elements" && sel.elementIds.includes(el.id);
|
|
3115
3652
|
const appearanceBars = el.appearances.map((ap, idx) => {
|
|
3116
3653
|
const left = timeToPx(ap.start);
|
|
@@ -3127,8 +3664,8 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
3127
3664
|
(kf) => `<div class="studio-tl-keyframe" style="left:${timeToPx(kf.time)}px" data-kf-elem-id="${escapeHtml3(el.id)}" data-kf-prop="${escapeHtml3(kf.prop)}" data-kf-time="${kf.time}" title="${escapeHtml3(kf.prop)} @ ${kf.time}ms (\uB4DC\uB798\uADF8\uB85C \uC774\uB3D9)">\u25C6</div>`
|
|
3128
3665
|
).join("");
|
|
3129
3666
|
return `
|
|
3130
|
-
<div class="studio-tl-row studio-tl-element-row ${isSel ? "is-selected" : ""}" data-elem-id="${escapeHtml3(el.id)}">
|
|
3131
|
-
<div class="studio-tl-gutter studio-tl-element-label" title="${escapeHtml3(el.id)}">${escapeHtml3(gutterLabel(el))}</div>
|
|
3667
|
+
<div class="studio-tl-row studio-tl-element-row ${isSel ? "is-selected" : ""} ${el.type === "group" ? "is-group" : ""}" data-elem-id="${escapeHtml3(el.id)}">
|
|
3668
|
+
<div class="studio-tl-gutter studio-tl-element-label" style="--studio-tree-depth:${depth}" title="${escapeHtml3(el.id)}">${el.type === "group" ? "\u25BE " : depth > 0 ? "\u21B3 " : ""}${escapeHtml3(gutterLabel(el))}</div>
|
|
3132
3669
|
<div class="studio-tl-body" style="width:${totalPx}px">
|
|
3133
3670
|
<div class="studio-tl-element-track" data-tl-area="elements">
|
|
3134
3671
|
${appearanceBars}
|
|
@@ -14852,10 +15389,10 @@ async function uploadAndInsertImage(file, host) {
|
|
|
14852
15389
|
}
|
|
14853
15390
|
try {
|
|
14854
15391
|
host.setStatus("\uC774\uBBF8\uC9C0 \uCC98\uB9AC \uC911\u2026");
|
|
14855
|
-
const
|
|
15392
|
+
const source = host.resolveImage ? await host.resolveImage(file) : await readAsDataUrl(file);
|
|
14856
15393
|
const cx = def.canvas.width / 2;
|
|
14857
15394
|
const cy = def.canvas.height / 2;
|
|
14858
|
-
const tempImg = await loadImageSize(
|
|
15395
|
+
const tempImg = await loadImageSize(source);
|
|
14859
15396
|
const maxDim = Math.min(def.canvas.width, def.canvas.height) * 0.5;
|
|
14860
15397
|
let w = tempImg?.w ?? 200;
|
|
14861
15398
|
let h = tempImg?.h ?? 200;
|
|
@@ -14875,7 +15412,7 @@ async function uploadAndInsertImage(file, host) {
|
|
|
14875
15412
|
y: Math.round(cy - h / 2),
|
|
14876
15413
|
width: w,
|
|
14877
15414
|
height: h,
|
|
14878
|
-
assetId: registerDataUriAsset(
|
|
15415
|
+
assetId: source.startsWith("data:") ? registerDataUriAsset(source) : registerExternalAsset(source),
|
|
14879
15416
|
preserveAspectRatio: "xMidYMid meet",
|
|
14880
15417
|
opacity: 1
|
|
14881
15418
|
});
|
|
@@ -14944,7 +15481,12 @@ function escapeHtml5(s) {
|
|
|
14944
15481
|
function startDraft() {
|
|
14945
15482
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
14946
15483
|
const id = `draft-${stamp}`;
|
|
14947
|
-
const def = animationDocumentSchema.parse({
|
|
15484
|
+
const def = animationDocumentSchema.parse({
|
|
15485
|
+
clothoVersion: 1,
|
|
15486
|
+
id,
|
|
15487
|
+
title: "",
|
|
15488
|
+
description: ""
|
|
15489
|
+
});
|
|
14948
15490
|
setDraft(def);
|
|
14949
15491
|
}
|
|
14950
15492
|
async function openLibrary(ui) {
|
|
@@ -15372,6 +15914,9 @@ function queryUi() {
|
|
|
15372
15914
|
const idDisplay = $("studio-id-display");
|
|
15373
15915
|
const status = $("studio-status");
|
|
15374
15916
|
const saveBtn = $("studio-save");
|
|
15917
|
+
const exportBtn = $("studio-export-json");
|
|
15918
|
+
const importBtn = $("studio-import-json");
|
|
15919
|
+
const importFileInput = $("studio-import-file");
|
|
15375
15920
|
const deleteBtn = $("studio-delete");
|
|
15376
15921
|
const newBtn = $("studio-new");
|
|
15377
15922
|
const openBtn = $("studio-open");
|
|
@@ -15379,6 +15924,8 @@ function queryUi() {
|
|
|
15379
15924
|
const restartBtn = $("studio-restart");
|
|
15380
15925
|
const speedInput = $("studio-speed");
|
|
15381
15926
|
const speedValue = $("studio-speed-value");
|
|
15927
|
+
const previewLoopInput = $("studio-preview-loop");
|
|
15928
|
+
const detachTimelineBtn = $("studio-detach-timeline");
|
|
15382
15929
|
const canvas = document.getElementById(
|
|
15383
15930
|
"studio-canvas"
|
|
15384
15931
|
);
|
|
@@ -15404,7 +15951,7 @@ function queryUi() {
|
|
|
15404
15951
|
const undoBtn = $("studio-undo");
|
|
15405
15952
|
const redoBtn = $("studio-redo");
|
|
15406
15953
|
const gridToggleBtn = $("studio-grid-toggle");
|
|
15407
|
-
if (!titleInput || !idDisplay || !status || !saveBtn || !deleteBtn || !newBtn || !openBtn || !playBtn || !restartBtn || !speedInput || !speedValue || !canvas || !elementList || !toolsRoot || !propsRoot || !timelineTracks || !elementTracks || !addStepBtn || !libraryDialog || !libraryList || !newDialog || !newIdInput || !newTitleInput || !newCreateBtn || !newError || !canvasWidthInput || !canvasHeightInput || !imageUploadBtn || !imageFileInput || !helpBtn || !helpDialog || !undoBtn || !redoBtn || !gridToggleBtn) {
|
|
15954
|
+
if (!titleInput || !idDisplay || !status || !saveBtn || !exportBtn || !importBtn || !importFileInput || !deleteBtn || !newBtn || !openBtn || !playBtn || !restartBtn || !speedInput || !speedValue || !previewLoopInput || !detachTimelineBtn || !canvas || !elementList || !toolsRoot || !propsRoot || !timelineTracks || !elementTracks || !addStepBtn || !libraryDialog || !libraryList || !newDialog || !newIdInput || !newTitleInput || !newCreateBtn || !newError || !canvasWidthInput || !canvasHeightInput || !imageUploadBtn || !imageFileInput || !helpBtn || !helpDialog || !undoBtn || !redoBtn || !gridToggleBtn) {
|
|
15408
15955
|
return null;
|
|
15409
15956
|
}
|
|
15410
15957
|
return {
|
|
@@ -15413,6 +15960,9 @@ function queryUi() {
|
|
|
15413
15960
|
idDisplay,
|
|
15414
15961
|
status,
|
|
15415
15962
|
saveBtn,
|
|
15963
|
+
exportBtn,
|
|
15964
|
+
importBtn,
|
|
15965
|
+
importFileInput,
|
|
15416
15966
|
deleteBtn,
|
|
15417
15967
|
newBtn,
|
|
15418
15968
|
openBtn,
|
|
@@ -15420,6 +15970,8 @@ function queryUi() {
|
|
|
15420
15970
|
restartBtn,
|
|
15421
15971
|
speedInput,
|
|
15422
15972
|
speedValue,
|
|
15973
|
+
previewLoopInput,
|
|
15974
|
+
detachTimelineBtn,
|
|
15423
15975
|
canvas,
|
|
15424
15976
|
elementList,
|
|
15425
15977
|
toolsRoot,
|
|
@@ -15455,39 +16007,59 @@ function startInlineTextEdit(canvas, el) {
|
|
|
15455
16007
|
"foreignObject"
|
|
15456
16008
|
);
|
|
15457
16009
|
fo.setAttribute("data-inline-text-editor", "");
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
const
|
|
16010
|
+
const width = Math.max(80, (el.content?.length ?? 5) * fontSize * 0.7 + 20);
|
|
16011
|
+
const anchor = el.textAnchor ?? "start";
|
|
16012
|
+
const x = anchor === "middle" ? el.x - width / 2 : anchor === "end" ? el.x - width : el.x;
|
|
16013
|
+
fo.setAttribute("x", String(x));
|
|
16014
|
+
fo.setAttribute("y", String(el.y - fontSize * 1.15));
|
|
15461
16015
|
fo.setAttribute("width", String(width));
|
|
15462
|
-
fo.setAttribute("height", String(fontSize * 1.
|
|
15463
|
-
const
|
|
15464
|
-
|
|
15465
|
-
|
|
15466
|
-
|
|
16016
|
+
fo.setAttribute("height", String(fontSize * 1.8));
|
|
16017
|
+
const renderedText = Array.from(
|
|
16018
|
+
canvas.querySelectorAll("[data-elem-id]")
|
|
16019
|
+
).find((node) => node.dataset.elemId === el.id);
|
|
16020
|
+
const previousVisibility = renderedText?.style.visibility ?? "";
|
|
16021
|
+
if (renderedText) renderedText.style.visibility = "hidden";
|
|
16022
|
+
const editor = document.createElement("div");
|
|
16023
|
+
editor.contentEditable = "true";
|
|
16024
|
+
editor.spellcheck = false;
|
|
16025
|
+
editor.textContent = el.content ?? "";
|
|
16026
|
+
editor.setAttribute("role", "textbox");
|
|
16027
|
+
editor.setAttribute("aria-label", "\uD14D\uC2A4\uD2B8 \uB0B4\uC6A9 \uD3B8\uC9D1");
|
|
16028
|
+
Object.assign(editor.style, {
|
|
15467
16029
|
width: "100%",
|
|
15468
16030
|
height: "100%",
|
|
15469
16031
|
boxSizing: "border-box",
|
|
15470
|
-
border: "
|
|
15471
|
-
|
|
15472
|
-
background: "
|
|
15473
|
-
padding: "0
|
|
16032
|
+
border: "0",
|
|
16033
|
+
borderBottom: "2px solid #6366f1",
|
|
16034
|
+
background: "color-mix(in srgb, Canvas 82%, transparent)",
|
|
16035
|
+
padding: "0 2px",
|
|
15474
16036
|
font: `${el.fontWeight ?? 400} ${fontSize}px ui-sans-serif,system-ui,sans-serif`,
|
|
15475
16037
|
color: el.color ?? "#0f172a",
|
|
15476
16038
|
outline: "none",
|
|
15477
|
-
|
|
16039
|
+
whiteSpace: "pre",
|
|
16040
|
+
overflow: "hidden"
|
|
15478
16041
|
});
|
|
15479
|
-
fo.appendChild(
|
|
16042
|
+
fo.appendChild(editor);
|
|
15480
16043
|
canvas.appendChild(fo);
|
|
15481
16044
|
requestAnimationFrame(() => {
|
|
15482
|
-
|
|
15483
|
-
|
|
16045
|
+
editor.focus();
|
|
16046
|
+
const range = document.createRange();
|
|
16047
|
+
range.selectNodeContents(editor);
|
|
16048
|
+
range.collapse(false);
|
|
16049
|
+
const selection = window.getSelection();
|
|
16050
|
+
selection?.removeAllRanges();
|
|
16051
|
+
selection?.addRange(range);
|
|
15484
16052
|
});
|
|
15485
16053
|
let settled = false;
|
|
16054
|
+
const restoreRenderedText = () => {
|
|
16055
|
+
if (renderedText) renderedText.style.visibility = previousVisibility;
|
|
16056
|
+
};
|
|
15486
16057
|
const commit = () => {
|
|
15487
16058
|
if (settled) return;
|
|
15488
16059
|
settled = true;
|
|
15489
|
-
const newContent =
|
|
16060
|
+
const newContent = editor.textContent ?? "";
|
|
15490
16061
|
fo.remove();
|
|
16062
|
+
restoreRenderedText();
|
|
15491
16063
|
if (newContent !== el.content) {
|
|
15492
16064
|
updateElementBase(el.id, { content: newContent });
|
|
15493
16065
|
}
|
|
@@ -15496,16 +16068,17 @@ function startInlineTextEdit(canvas, el) {
|
|
|
15496
16068
|
if (settled) return;
|
|
15497
16069
|
settled = true;
|
|
15498
16070
|
fo.remove();
|
|
16071
|
+
restoreRenderedText();
|
|
15499
16072
|
};
|
|
15500
16073
|
let isComposingFallback2 = false;
|
|
15501
|
-
|
|
16074
|
+
editor.addEventListener("compositionstart", () => {
|
|
15502
16075
|
isComposingFallback2 = true;
|
|
15503
16076
|
});
|
|
15504
|
-
|
|
16077
|
+
editor.addEventListener("compositionend", () => {
|
|
15505
16078
|
isComposingFallback2 = false;
|
|
15506
16079
|
});
|
|
15507
|
-
|
|
15508
|
-
|
|
16080
|
+
editor.addEventListener("blur", commit);
|
|
16081
|
+
editor.addEventListener("keydown", (e) => {
|
|
15509
16082
|
if (e.isComposing || isComposingFallback2) return;
|
|
15510
16083
|
if (e.key === "Enter") {
|
|
15511
16084
|
e.preventDefault();
|
|
@@ -15642,7 +16215,12 @@ function togglePlay(ui) {
|
|
|
15642
16215
|
if (playState === "playing") {
|
|
15643
16216
|
stopPreview(ui);
|
|
15644
16217
|
} else {
|
|
15645
|
-
showPreview2(def
|
|
16218
|
+
showPreview2(def, {
|
|
16219
|
+
loop: ui.previewLoopInput.checked,
|
|
16220
|
+
speed: Number(ui.speedInput.value),
|
|
16221
|
+
onTime: setCurrentTime,
|
|
16222
|
+
onEnd: () => stopPreview(ui)
|
|
16223
|
+
});
|
|
15646
16224
|
playState = "playing";
|
|
15647
16225
|
ui.playBtn.textContent = "\u23F9 Stop";
|
|
15648
16226
|
}
|
|
@@ -15832,9 +16410,12 @@ function bindKeyboardShortcuts(ui) {
|
|
|
15832
16410
|
const inText = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLElement && e.target.isContentEditable;
|
|
15833
16411
|
const mod = e.metaKey || e.ctrlKey;
|
|
15834
16412
|
if (e.key === "Escape") {
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
16413
|
+
if (!inText) {
|
|
16414
|
+
setActiveTool("select");
|
|
16415
|
+
const sel = getSelection();
|
|
16416
|
+
if (sel.kind !== "none") {
|
|
16417
|
+
setSelection({ kind: "none" });
|
|
16418
|
+
}
|
|
15838
16419
|
}
|
|
15839
16420
|
return;
|
|
15840
16421
|
}
|
|
@@ -15899,6 +16480,16 @@ function bindKeyboardShortcuts(ui) {
|
|
|
15899
16480
|
if (selectAdjacentElement(e.shiftKey ? -1 : 1)) e.preventDefault();
|
|
15900
16481
|
return;
|
|
15901
16482
|
}
|
|
16483
|
+
const shortcutTool = toolForShortcut(e.key, {
|
|
16484
|
+
isEditing: inText,
|
|
16485
|
+
modifierKey: mod,
|
|
16486
|
+
altKey: e.altKey
|
|
16487
|
+
});
|
|
16488
|
+
if (shortcutTool) {
|
|
16489
|
+
e.preventDefault();
|
|
16490
|
+
setActiveTool(shortcutTool);
|
|
16491
|
+
return;
|
|
16492
|
+
}
|
|
15902
16493
|
if (!inText && !mod && (e.key === "g" || e.key === "G")) {
|
|
15903
16494
|
e.preventDefault();
|
|
15904
16495
|
setGridEnabled(!isGridEnabled());
|
|
@@ -16002,8 +16593,176 @@ function bindKeyboardShortcuts(ui) {
|
|
|
16002
16593
|
});
|
|
16003
16594
|
}
|
|
16004
16595
|
|
|
16596
|
+
// src/legacy/popup-document.ts
|
|
16597
|
+
function prepareStudioPopupDocument(popupDocument, sourceDocument, sourceApp, options) {
|
|
16598
|
+
popupDocument.open();
|
|
16599
|
+
popupDocument.write(
|
|
16600
|
+
`<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml8(options.title)}</title></head><body class="editor-active ${escapeHtml8(options.bodyClass)}"></body></html>`
|
|
16601
|
+
);
|
|
16602
|
+
popupDocument.close();
|
|
16603
|
+
const base = popupDocument.createElement("base");
|
|
16604
|
+
base.href = sourceDocument.baseURI;
|
|
16605
|
+
popupDocument.head.prepend(base);
|
|
16606
|
+
for (const source of sourceDocument.querySelectorAll(
|
|
16607
|
+
'link[rel="stylesheet"], style'
|
|
16608
|
+
)) {
|
|
16609
|
+
popupDocument.head.appendChild(source.cloneNode(true));
|
|
16610
|
+
}
|
|
16611
|
+
const computed = getComputedStyle(sourceApp);
|
|
16612
|
+
const target = popupDocument.documentElement.style;
|
|
16613
|
+
for (let index = 0; index < computed.length; index += 1) {
|
|
16614
|
+
const property = computed.item(index);
|
|
16615
|
+
if (property.startsWith("--")) {
|
|
16616
|
+
target.setProperty(property, computed.getPropertyValue(property));
|
|
16617
|
+
}
|
|
16618
|
+
}
|
|
16619
|
+
}
|
|
16620
|
+
function escapeHtml8(value) {
|
|
16621
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
16622
|
+
}
|
|
16623
|
+
|
|
16624
|
+
// src/legacy/timeline-popout.ts
|
|
16625
|
+
var POPUP_FEATURES = "popup=yes,width=1200,height=620";
|
|
16626
|
+
function setupTimelinePopout(ui, _togglePlayback) {
|
|
16627
|
+
const timeline = ui.detachTimelineBtn.closest(
|
|
16628
|
+
".studio-timeline-wrap"
|
|
16629
|
+
);
|
|
16630
|
+
if (!timeline) return;
|
|
16631
|
+
let popup = null;
|
|
16632
|
+
let placeholder = null;
|
|
16633
|
+
let closeTimer = null;
|
|
16634
|
+
let unbindPopupPointerEvents = null;
|
|
16635
|
+
let restoring = false;
|
|
16636
|
+
const restoreTimeline = (closePopup) => {
|
|
16637
|
+
if (restoring) return;
|
|
16638
|
+
restoring = true;
|
|
16639
|
+
if (placeholder?.parentNode) {
|
|
16640
|
+
placeholder.parentNode.insertBefore(timeline, placeholder.nextSibling);
|
|
16641
|
+
placeholder.remove();
|
|
16642
|
+
}
|
|
16643
|
+
placeholder = null;
|
|
16644
|
+
unbindPopupPointerEvents?.();
|
|
16645
|
+
unbindPopupPointerEvents = null;
|
|
16646
|
+
if (closeTimer !== null) window.clearInterval(closeTimer);
|
|
16647
|
+
closeTimer = null;
|
|
16648
|
+
ui.app.classList.remove("is-timeline-detached");
|
|
16649
|
+
ui.detachTimelineBtn.setAttribute("aria-pressed", "false");
|
|
16650
|
+
ui.detachTimelineBtn.textContent = "\u25A3 \uD0C0\uC784\uB77C\uC778 \uBD84\uB9AC";
|
|
16651
|
+
const windowToClose = popup;
|
|
16652
|
+
popup = null;
|
|
16653
|
+
if (closePopup && windowToClose && !windowToClose.closed) {
|
|
16654
|
+
windowToClose.close();
|
|
16655
|
+
}
|
|
16656
|
+
restoring = false;
|
|
16657
|
+
};
|
|
16658
|
+
ui.detachTimelineBtn.addEventListener("click", () => {
|
|
16659
|
+
if (popup && !popup.closed) {
|
|
16660
|
+
restoreTimeline(true);
|
|
16661
|
+
return;
|
|
16662
|
+
}
|
|
16663
|
+
const opened = window.open("", "_blank", POPUP_FEATURES);
|
|
16664
|
+
if (!opened) {
|
|
16665
|
+
ui.status.textContent = "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C popup\uC744 \uD5C8\uC6A9\uD574\uC8FC\uC138\uC694.";
|
|
16666
|
+
return;
|
|
16667
|
+
}
|
|
16668
|
+
popup = opened;
|
|
16669
|
+
placeholder = document.createComment("clotho-timeline-home");
|
|
16670
|
+
timeline.before(placeholder);
|
|
16671
|
+
prepareStudioPopupDocument(opened.document, document, ui.app, {
|
|
16672
|
+
title: "Clotho Timeline",
|
|
16673
|
+
bodyClass: "studio-timeline-popout"
|
|
16674
|
+
});
|
|
16675
|
+
opened.document.body.appendChild(timeline);
|
|
16676
|
+
unbindPopupPointerEvents = bindTimelinePointerDocument(opened.document);
|
|
16677
|
+
opened.addEventListener("beforeunload", () => restoreTimeline(false), {
|
|
16678
|
+
once: true
|
|
16679
|
+
});
|
|
16680
|
+
closeTimer = window.setInterval(() => {
|
|
16681
|
+
if (!popup || popup.closed) restoreTimeline(false);
|
|
16682
|
+
}, 500);
|
|
16683
|
+
ui.app.classList.add("is-timeline-detached");
|
|
16684
|
+
ui.detachTimelineBtn.setAttribute("aria-pressed", "true");
|
|
16685
|
+
ui.detachTimelineBtn.textContent = "\u2199 \uD3B8\uC9D1\uAE30\uC5D0 \uB2E4\uC2DC \uD569\uCE58\uAE30";
|
|
16686
|
+
opened.focus();
|
|
16687
|
+
});
|
|
16688
|
+
}
|
|
16689
|
+
|
|
16690
|
+
// src/legacy/player-popout.ts
|
|
16691
|
+
function setupPlayerPopout(ui) {
|
|
16692
|
+
const button = document.getElementById(
|
|
16693
|
+
"studio-player-preview"
|
|
16694
|
+
);
|
|
16695
|
+
if (!button) return;
|
|
16696
|
+
let popup = null;
|
|
16697
|
+
let playerHandle = null;
|
|
16698
|
+
let unsubscribe = null;
|
|
16699
|
+
let renderedDocument = getDef();
|
|
16700
|
+
const cleanup = () => {
|
|
16701
|
+
playerHandle?.destroy();
|
|
16702
|
+
playerHandle = null;
|
|
16703
|
+
unsubscribe?.();
|
|
16704
|
+
unsubscribe = null;
|
|
16705
|
+
popup = null;
|
|
16706
|
+
button.setAttribute("aria-pressed", "false");
|
|
16707
|
+
};
|
|
16708
|
+
const render5 = async () => {
|
|
16709
|
+
const targetWindow = popup;
|
|
16710
|
+
const def = getDef();
|
|
16711
|
+
if (!targetWindow || targetWindow.closed || !def) return;
|
|
16712
|
+
const container = targetWindow.document.getElementById(
|
|
16713
|
+
"clotho-player-preview"
|
|
16714
|
+
);
|
|
16715
|
+
if (!container) return;
|
|
16716
|
+
playerHandle?.destroy();
|
|
16717
|
+
container.replaceChildren();
|
|
16718
|
+
const { mountPlayer, koreanStrings } = await import('@kokoa/clotho/dom');
|
|
16719
|
+
if (!popup || popup !== targetWindow || targetWindow.closed) return;
|
|
16720
|
+
playerHandle = mountPlayer(container, def, {
|
|
16721
|
+
strings: koreanStrings,
|
|
16722
|
+
player: {
|
|
16723
|
+
autoplay: def.settings.autoplay,
|
|
16724
|
+
loop: def.settings.loop
|
|
16725
|
+
}
|
|
16726
|
+
});
|
|
16727
|
+
renderedDocument = def;
|
|
16728
|
+
};
|
|
16729
|
+
button.addEventListener("click", () => {
|
|
16730
|
+
if (popup && !popup.closed) {
|
|
16731
|
+
popup.focus();
|
|
16732
|
+
return;
|
|
16733
|
+
}
|
|
16734
|
+
const def = getDef();
|
|
16735
|
+
if (!def) {
|
|
16736
|
+
ui.status.textContent = "\uBA3C\uC800 \uC560\uB2C8\uBA54\uC774\uC158\uC744 \uC5F4\uAC70\uB098 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694.";
|
|
16737
|
+
return;
|
|
16738
|
+
}
|
|
16739
|
+
const opened = window.open("", "_blank", "popup=yes,width=1100,height=760");
|
|
16740
|
+
if (!opened) {
|
|
16741
|
+
ui.status.textContent = "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C popup\uC744 \uD5C8\uC6A9\uD574\uC8FC\uC138\uC694.";
|
|
16742
|
+
return;
|
|
16743
|
+
}
|
|
16744
|
+
popup = opened;
|
|
16745
|
+
prepareStudioPopupDocument(opened.document, document, ui.app, {
|
|
16746
|
+
title: `${def.title || def.id} \xB7 \uC2E4\uC81C \uBBF8\uB9AC\uBCF4\uAE30`,
|
|
16747
|
+
bodyClass: "studio-player-popout"
|
|
16748
|
+
});
|
|
16749
|
+
const container = opened.document.createElement("main");
|
|
16750
|
+
container.id = "clotho-player-preview";
|
|
16751
|
+
opened.document.body.appendChild(container);
|
|
16752
|
+
button.setAttribute("aria-pressed", "true");
|
|
16753
|
+
renderedDocument = def;
|
|
16754
|
+
void render5();
|
|
16755
|
+
unsubscribe = subscribe(() => {
|
|
16756
|
+
const next = getDef();
|
|
16757
|
+
if (next !== renderedDocument) void render5();
|
|
16758
|
+
});
|
|
16759
|
+
opened.addEventListener("beforeunload", cleanup, { once: true });
|
|
16760
|
+
opened.focus();
|
|
16761
|
+
});
|
|
16762
|
+
}
|
|
16763
|
+
|
|
16005
16764
|
// src/legacy/main.ts
|
|
16006
|
-
function initStudio(opts) {
|
|
16765
|
+
function initStudio(opts = {}) {
|
|
16007
16766
|
const ui = queryUi();
|
|
16008
16767
|
if (!ui) {
|
|
16009
16768
|
console.error("[studio] missing required elements");
|
|
@@ -16011,6 +16770,9 @@ function initStudio(opts) {
|
|
|
16011
16770
|
}
|
|
16012
16771
|
document.body.classList.add("editor-active");
|
|
16013
16772
|
document.documentElement.classList.add("editor-active");
|
|
16773
|
+
const editorTitle = document.getElementById("studio-editor-title");
|
|
16774
|
+
if (editorTitle)
|
|
16775
|
+
editorTitle.textContent = opts.editorTitle ?? "Clotho Editor";
|
|
16014
16776
|
initGrid();
|
|
16015
16777
|
reflectGridUi(ui);
|
|
16016
16778
|
subscribeGrid(() => reflectGridUi(ui));
|
|
@@ -16040,7 +16802,8 @@ function initStudio(opts) {
|
|
|
16040
16802
|
});
|
|
16041
16803
|
const imageHost = {
|
|
16042
16804
|
app: ui.app,
|
|
16043
|
-
setStatus: (text, kind) => setStatus(ui, text, kind)
|
|
16805
|
+
setStatus: (text, kind) => setStatus(ui, text, kind),
|
|
16806
|
+
resolveImage: opts.resolveImage
|
|
16044
16807
|
};
|
|
16045
16808
|
ui.imageUploadBtn.addEventListener("click", () => ui.imageFileInput.click());
|
|
16046
16809
|
ui.imageFileInput.addEventListener("change", () => {
|
|
@@ -16141,6 +16904,29 @@ function initStudio(opts) {
|
|
|
16141
16904
|
{ id: "help", label: "\u2328 \uB2E8\uCD95\uD0A4 \uBCF4\uAE30", hint: "? / Shift+/", run: openHelp }
|
|
16142
16905
|
]);
|
|
16143
16906
|
ui.saveBtn.addEventListener("click", () => void saveCurrent(ui));
|
|
16907
|
+
ui.exportBtn.addEventListener("click", () => {
|
|
16908
|
+
const def = getDef();
|
|
16909
|
+
if (!def) return;
|
|
16910
|
+
downloadAnimationJson(def);
|
|
16911
|
+
setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uB0B4\uB824\uBC1B\uC558\uC2B5\uB2C8\uB2E4.`, "ok");
|
|
16912
|
+
});
|
|
16913
|
+
ui.importBtn.addEventListener("click", () => ui.importFileInput.click());
|
|
16914
|
+
ui.importFileInput.addEventListener("change", () => {
|
|
16915
|
+
const file = ui.importFileInput.files?.[0];
|
|
16916
|
+
ui.importFileInput.value = "";
|
|
16917
|
+
if (!file) return;
|
|
16918
|
+
void file.text().then((text) => animationDocumentSchema.parse(JSON.parse(text))).then((def) => importAnimation(def)).then((def) => {
|
|
16919
|
+
setDef(def);
|
|
16920
|
+
markClean();
|
|
16921
|
+
setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.`, "ok");
|
|
16922
|
+
}).catch((error) => {
|
|
16923
|
+
setStatus(
|
|
16924
|
+
ui,
|
|
16925
|
+
`JSON \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`,
|
|
16926
|
+
"error"
|
|
16927
|
+
);
|
|
16928
|
+
});
|
|
16929
|
+
});
|
|
16144
16930
|
ui.deleteBtn.addEventListener("click", () => void deleteCurrent(ui));
|
|
16145
16931
|
ui.undoBtn.addEventListener("click", () => undo());
|
|
16146
16932
|
ui.redoBtn.addEventListener("click", () => redo());
|
|
@@ -16157,7 +16943,20 @@ function initStudio(opts) {
|
|
|
16157
16943
|
});
|
|
16158
16944
|
ui.speedInput.addEventListener("input", () => {
|
|
16159
16945
|
ui.speedValue.textContent = Number(ui.speedInput.value).toFixed(2) + "x";
|
|
16946
|
+
if (isPlaying()) {
|
|
16947
|
+
stopPreview(ui);
|
|
16948
|
+
togglePlay(ui);
|
|
16949
|
+
}
|
|
16160
16950
|
});
|
|
16951
|
+
ui.previewLoopInput.addEventListener("change", () => {
|
|
16952
|
+
updateSettings({ loop: ui.previewLoopInput.checked });
|
|
16953
|
+
if (isPlaying()) {
|
|
16954
|
+
stopPreview(ui);
|
|
16955
|
+
togglePlay(ui);
|
|
16956
|
+
}
|
|
16957
|
+
});
|
|
16958
|
+
setupTimelinePopout(ui);
|
|
16959
|
+
setupPlayerPopout(ui);
|
|
16161
16960
|
ui.newCreateBtn.addEventListener("click", () => void createNew(ui));
|
|
16162
16961
|
document.addEventListener("click", (e) => {
|
|
16163
16962
|
const closer = e.target.closest(
|
|
@@ -16169,11 +16968,15 @@ function initStudio(opts) {
|
|
|
16169
16968
|
dialog.close();
|
|
16170
16969
|
if (dialog === ui.newDialog) dialog.removeAttribute("data-mode");
|
|
16171
16970
|
});
|
|
16971
|
+
document.addEventListener("click", (e) => {
|
|
16972
|
+
const dialog = e.target;
|
|
16973
|
+
if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close();
|
|
16974
|
+
});
|
|
16172
16975
|
bindKeyboardShortcuts(ui);
|
|
16173
16976
|
window.addEventListener("beforeunload", (e) => {
|
|
16174
16977
|
if (isDirty()) e.preventDefault();
|
|
16175
16978
|
});
|
|
16176
|
-
if (opts
|
|
16979
|
+
if (opts.initialId) {
|
|
16177
16980
|
void loadAnimation2(ui, opts.initialId);
|
|
16178
16981
|
} else if (!getDef()) {
|
|
16179
16982
|
startDraft();
|
|
@@ -16187,6 +16990,9 @@ function reflectState(ui) {
|
|
|
16187
16990
|
ui.redoBtn.disabled = !canRedo();
|
|
16188
16991
|
const def = getDef();
|
|
16189
16992
|
if (def) {
|
|
16993
|
+
ui.titleInput.disabled = false;
|
|
16994
|
+
ui.canvasWidthInput.disabled = false;
|
|
16995
|
+
ui.canvasHeightInput.disabled = false;
|
|
16190
16996
|
if (document.activeElement !== ui.titleInput) {
|
|
16191
16997
|
ui.titleInput.value = def.title;
|
|
16192
16998
|
}
|
|
@@ -16198,17 +17004,25 @@ function reflectState(ui) {
|
|
|
16198
17004
|
}
|
|
16199
17005
|
ui.idDisplay.textContent = isDraft() ? "Draft" : def.id ?? "";
|
|
16200
17006
|
ui.saveBtn.disabled = false;
|
|
17007
|
+
ui.exportBtn.disabled = false;
|
|
16201
17008
|
ui.deleteBtn.disabled = isDraft();
|
|
17009
|
+
if (document.activeElement !== ui.previewLoopInput) {
|
|
17010
|
+
ui.previewLoopInput.checked = def.settings.loop;
|
|
17011
|
+
}
|
|
16202
17012
|
} else {
|
|
17013
|
+
ui.titleInput.disabled = true;
|
|
17014
|
+
ui.canvasWidthInput.disabled = true;
|
|
17015
|
+
ui.canvasHeightInput.disabled = true;
|
|
16203
17016
|
ui.titleInput.value = "";
|
|
16204
17017
|
ui.canvasWidthInput.value = "";
|
|
16205
17018
|
ui.canvasHeightInput.value = "";
|
|
16206
17019
|
ui.idDisplay.textContent = "";
|
|
16207
17020
|
ui.saveBtn.disabled = true;
|
|
17021
|
+
ui.exportBtn.disabled = true;
|
|
16208
17022
|
ui.deleteBtn.disabled = true;
|
|
16209
17023
|
}
|
|
16210
17024
|
}
|
|
16211
17025
|
|
|
16212
17026
|
export { getVisibleElementIds, initStudio };
|
|
16213
|
-
//# sourceMappingURL=main-
|
|
16214
|
-
//# sourceMappingURL=main-
|
|
17027
|
+
//# sourceMappingURL=main-NCPWDB2U.js.map
|
|
17028
|
+
//# sourceMappingURL=main-NCPWDB2U.js.map
|