@kokoa/clotho-editor 0.1.0 → 0.1.3
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 +73 -13
- package/dist/{chunk-UE32Q6U5.js → chunk-Z2K5HFHI.js} +176 -102
- package/dist/chunk-Z2K5HFHI.js.map +1 -0
- package/dist/clotho-editor.css +145 -18
- package/dist/index.d.ts +38 -2
- package/dist/index.js +128 -17
- package/dist/index.js.map +1 -1
- package/dist/{main-SWHHYTLM.js → main-5YXLTYPQ.js} +1742 -985
- package/dist/main-5YXLTYPQ.js.map +1 -0
- package/docs/PORTING.md +16 -33
- package/package.json +19 -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, updateDuration, updateAppearance, updateEffect, childIdsOf } from './chunk-Z2K5HFHI.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,422 +431,817 @@ 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
|
-
|
|
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
|
+
fontSize: 18,
|
|
725
|
+
fontWeight: 400,
|
|
726
|
+
color: "#18181b",
|
|
727
|
+
textAnchor: "middle"
|
|
728
|
+
};
|
|
729
|
+
case "image":
|
|
730
|
+
return {
|
|
731
|
+
type: "image",
|
|
732
|
+
id,
|
|
733
|
+
rotation: 0,
|
|
734
|
+
appearances: [],
|
|
735
|
+
tracks: [],
|
|
736
|
+
x: cx - 50,
|
|
737
|
+
y: cy - 50,
|
|
738
|
+
width: 100,
|
|
739
|
+
height: 100,
|
|
740
|
+
assetId: registerExternalAsset(placeholderImageUrl()),
|
|
741
|
+
preserveAspectRatio: "xMidYMid meet",
|
|
742
|
+
opacity: 1
|
|
743
|
+
};
|
|
744
|
+
case "path":
|
|
745
|
+
return {
|
|
746
|
+
type: "path",
|
|
747
|
+
id,
|
|
748
|
+
rotation: 0,
|
|
749
|
+
appearances: [],
|
|
750
|
+
tracks: [],
|
|
751
|
+
x: cx - 40,
|
|
752
|
+
y: cy - 40,
|
|
753
|
+
d: "M 0 0 L 80 0 L 40 80 Z",
|
|
754
|
+
fill: "#a5b4fc",
|
|
755
|
+
stroke: "#6366f1",
|
|
756
|
+
strokeWidth: 2,
|
|
757
|
+
opacity: 1
|
|
758
|
+
};
|
|
759
|
+
case "polygon": {
|
|
760
|
+
const r = 40;
|
|
761
|
+
const sides = Math.max(3, Math.min(24, Math.round(polygonSides) || 6));
|
|
762
|
+
const pts = Array.from({ length: sides }, (_, i) => {
|
|
763
|
+
const a = i * Math.PI * 2 / sides - Math.PI / 2;
|
|
764
|
+
return `${(cx + r * Math.cos(a)).toFixed(1)},${(cy + r * Math.sin(a)).toFixed(1)}`;
|
|
765
|
+
}).join(" ");
|
|
766
|
+
return {
|
|
767
|
+
type: "polygon",
|
|
768
|
+
id,
|
|
769
|
+
rotation: 0,
|
|
770
|
+
appearances: [],
|
|
771
|
+
tracks: [],
|
|
772
|
+
points: pts,
|
|
773
|
+
fill: "#a5b4fc",
|
|
774
|
+
stroke: "#6366f1",
|
|
775
|
+
strokeWidth: 1.5,
|
|
776
|
+
opacity: 1
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
default:
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/legacy/drawing-interactions.ts
|
|
785
|
+
function dragBounds(start, end) {
|
|
786
|
+
const width = Math.abs(end.x - start.x);
|
|
787
|
+
const height = Math.abs(end.y - start.y);
|
|
788
|
+
return {
|
|
789
|
+
x: Math.min(start.x, end.x),
|
|
790
|
+
y: Math.min(start.y, end.y),
|
|
791
|
+
width,
|
|
792
|
+
height,
|
|
793
|
+
centerX: (start.x + end.x) / 2,
|
|
794
|
+
centerY: (start.y + end.y) / 2,
|
|
795
|
+
distance: Math.hypot(end.x - start.x, end.y - start.y)
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function isShapeDrag(start, end) {
|
|
799
|
+
return dragBounds(start, end).distance >= 4;
|
|
800
|
+
}
|
|
801
|
+
function regularPolygonPoints(center, radius, requestedSides) {
|
|
802
|
+
const sides = Math.max(3, Math.min(24, Math.round(requestedSides) || 6));
|
|
803
|
+
return Array.from({ length: sides }, (_, index) => {
|
|
804
|
+
const angle = index * Math.PI * 2 / sides - Math.PI / 2;
|
|
805
|
+
return `${(center.x + radius * Math.cos(angle)).toFixed(1)},${(center.y + radius * Math.sin(angle)).toFixed(1)}`;
|
|
806
|
+
}).join(" ");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/legacy/canvas-handles.ts
|
|
810
|
+
function renderResizeHandles(canvasEl2, elId, snap2, byId) {
|
|
811
|
+
const baseEl = byId.get(elId);
|
|
812
|
+
const state = snap2.get(elId);
|
|
813
|
+
if (!baseEl || !state) return null;
|
|
814
|
+
let box;
|
|
815
|
+
let handles = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
816
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
817
|
+
box = {
|
|
818
|
+
x: state.x,
|
|
819
|
+
y: state.y,
|
|
820
|
+
w: state.width,
|
|
821
|
+
h: state.height
|
|
822
|
+
};
|
|
823
|
+
} else if (baseEl.type === "circle") {
|
|
824
|
+
const r = state.r;
|
|
825
|
+
box = {
|
|
826
|
+
x: state.cx - r,
|
|
827
|
+
y: state.cy - r,
|
|
828
|
+
w: r * 2,
|
|
829
|
+
h: r * 2
|
|
830
|
+
};
|
|
831
|
+
handles = ["nw", "ne", "se", "sw"];
|
|
832
|
+
} else if (baseEl.type === "text") {
|
|
833
|
+
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
834
|
+
if (!bbox) return null;
|
|
835
|
+
box = bbox;
|
|
836
|
+
handles = ["se"];
|
|
837
|
+
} else {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
const positions = {
|
|
841
|
+
nw: { x: box.x, y: box.y, cursor: "nwse-resize" },
|
|
842
|
+
n: { x: box.x + box.w / 2, y: box.y, cursor: "ns-resize" },
|
|
843
|
+
ne: { x: box.x + box.w, y: box.y, cursor: "nesw-resize" },
|
|
844
|
+
e: { x: box.x + box.w, y: box.y + box.h / 2, cursor: "ew-resize" },
|
|
845
|
+
se: { x: box.x + box.w, y: box.y + box.h, cursor: "nwse-resize" },
|
|
846
|
+
s: { x: box.x + box.w / 2, y: box.y + box.h, cursor: "ns-resize" },
|
|
847
|
+
sw: { x: box.x, y: box.y + box.h, cursor: "nesw-resize" },
|
|
848
|
+
w: { x: box.x, y: box.y + box.h / 2, cursor: "ew-resize" }
|
|
849
|
+
};
|
|
850
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
851
|
+
const rotation = state.rotation || 0;
|
|
852
|
+
if (rotation && (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "circle")) {
|
|
853
|
+
const cx = box.x + box.w / 2;
|
|
854
|
+
const cy = box.y + box.h / 2;
|
|
855
|
+
g.setAttribute("transform", `rotate(${rotation} ${cx} ${cy})`);
|
|
856
|
+
}
|
|
857
|
+
let html = "";
|
|
858
|
+
for (const h of handles) {
|
|
859
|
+
const p = positions[h];
|
|
860
|
+
html += `<rect x="${p.x - 5}" y="${p.y - 5}" width="10" height="10"
|
|
861
|
+
fill="white" stroke="var(--color-accent)" stroke-width="2" rx="2"
|
|
862
|
+
data-resize-handle="${h}" data-elem-id="${escapeXml(elId)}" style="cursor: ${p.cursor}" />`;
|
|
863
|
+
}
|
|
864
|
+
g.innerHTML = html;
|
|
865
|
+
return g;
|
|
866
|
+
}
|
|
867
|
+
function renderRotationHandle(canvasEl2, elId, snap2, byId) {
|
|
868
|
+
const baseEl = byId.get(elId);
|
|
869
|
+
const state = snap2.get(elId);
|
|
870
|
+
if (!baseEl || !state) return null;
|
|
871
|
+
const center = centerOfElement(baseEl, state);
|
|
872
|
+
if (!center) return null;
|
|
873
|
+
let topY = null;
|
|
874
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
875
|
+
topY = state.y - 24;
|
|
876
|
+
} else if (baseEl.type === "circle") {
|
|
877
|
+
topY = state.cy - state.r - 24;
|
|
878
|
+
} else if (baseEl.type === "text") {
|
|
879
|
+
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
880
|
+
if (bbox) topY = bbox.y - 24;
|
|
881
|
+
} else if (baseEl.type === "polygon") {
|
|
882
|
+
const bbox = polygonBoundingBox(String(state.points ?? ""));
|
|
883
|
+
if (bbox) topY = bbox.y - 24;
|
|
884
|
+
} else if (baseEl.type === "path") {
|
|
885
|
+
const bbox = pathBBoxOnCanvas(canvasEl2, elId);
|
|
886
|
+
if (bbox) topY = bbox.y - 24;
|
|
887
|
+
}
|
|
888
|
+
if (topY === null) return null;
|
|
889
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
890
|
+
const rotation = state.rotation || 0;
|
|
891
|
+
if (rotation)
|
|
892
|
+
g.setAttribute("transform", `rotate(${rotation} ${center.x} ${center.y})`);
|
|
893
|
+
g.innerHTML = `
|
|
894
|
+
<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" />
|
|
895
|
+
<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" />
|
|
896
|
+
<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>
|
|
897
|
+
`;
|
|
898
|
+
return g;
|
|
899
|
+
}
|
|
900
|
+
function renderAnchorDots(elId, snap2, byId) {
|
|
901
|
+
const baseEl = byId.get(elId);
|
|
902
|
+
const state = snap2.get(elId);
|
|
903
|
+
if (!baseEl || !state) return null;
|
|
904
|
+
if (baseEl.type !== "rect" && baseEl.type !== "circle" && baseEl.type !== "image")
|
|
905
|
+
return null;
|
|
906
|
+
const points = anchorPointsOf(baseEl, state);
|
|
907
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
908
|
+
for (const p of points) {
|
|
909
|
+
g.innerHTML += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="white" stroke="#6366f1" stroke-width="2"
|
|
910
|
+
data-anchor-handle data-elem-id="${escapeXml(elId)}" data-anchor="${p.anchor}" style="cursor: crosshair" />`;
|
|
911
|
+
}
|
|
912
|
+
return g;
|
|
913
|
+
}
|
|
914
|
+
function renderLineEndpointHandles(elId, snap2, byId) {
|
|
915
|
+
const baseEl = byId.get(elId);
|
|
916
|
+
const state = snap2.get(elId);
|
|
917
|
+
if (!baseEl || !state) return null;
|
|
918
|
+
if (baseEl.type !== "line" && baseEl.type !== "arrow") return null;
|
|
919
|
+
const coords = baseEl.type === "line" ? resolveLineCoords(state, snap2, byId) : resolveArrowCoords(state, snap2, byId);
|
|
920
|
+
if (!coords) return null;
|
|
921
|
+
const midX = (coords.x1 + coords.x2) / 2;
|
|
922
|
+
const midY = (coords.y1 + coords.y2) / 2;
|
|
923
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
924
|
+
g.innerHTML = `
|
|
925
|
+
<rect x="${coords.x1 - 6}" y="${coords.y1 - 6}" width="12" height="12" rx="2"
|
|
926
|
+
fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
927
|
+
data-endpoint-handle="start" data-elem-id="${escapeXml(elId)}" style="cursor: grab" />
|
|
928
|
+
<rect x="${coords.x2 - 6}" y="${coords.y2 - 6}" width="12" height="12" rx="2"
|
|
929
|
+
fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
930
|
+
data-endpoint-handle="end" data-elem-id="${escapeXml(elId)}" style="cursor: grab" />
|
|
931
|
+
<circle cx="${midX}" cy="${midY}" r="6"
|
|
932
|
+
fill="var(--color-accent)" stroke="white" stroke-width="2"
|
|
933
|
+
data-line-mid-handle data-elem-id="${escapeXml(elId)}" style="cursor: move" />
|
|
934
|
+
`;
|
|
935
|
+
return g;
|
|
936
|
+
}
|
|
937
|
+
function renderPolygonVertexHandles(elId, snap2, byId) {
|
|
938
|
+
const baseEl = byId.get(elId);
|
|
939
|
+
const state = snap2.get(elId);
|
|
940
|
+
if (!baseEl || !state) return null;
|
|
941
|
+
if (baseEl.type !== "polygon") return null;
|
|
942
|
+
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
943
|
+
const parsed = [];
|
|
944
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
945
|
+
const [x, y] = points[i].split(",").map(Number);
|
|
946
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
947
|
+
parsed.push({ x, y, i });
|
|
948
|
+
}
|
|
949
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
950
|
+
let html = "";
|
|
951
|
+
if (parsed.length >= 2) {
|
|
952
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
953
|
+
const a = parsed[i];
|
|
954
|
+
const b = parsed[(i + 1) % parsed.length];
|
|
955
|
+
const mx = (a.x + b.x) / 2;
|
|
956
|
+
const my = (a.y + b.y) / 2;
|
|
957
|
+
html += `<g data-vertex-add data-elem-id="${escapeXml(elId)}" data-after-index="${a.i}" style="cursor: copy">
|
|
958
|
+
<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" />
|
|
959
|
+
<text x="${mx}" y="${my + 3}" text-anchor="middle" font-size="10" fill="var(--color-accent)" font-weight="700" pointer-events="none">+</text>
|
|
960
|
+
</g>`;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
for (const p of parsed) {
|
|
964
|
+
const removable = parsed.length > 3;
|
|
965
|
+
html += `<circle cx="${p.x}" cy="${p.y}" r="6" fill="white" stroke="var(--color-accent)" stroke-width="2"
|
|
966
|
+
data-vertex-handle data-elem-id="${escapeXml(elId)}" data-vertex-index="${p.i}"
|
|
967
|
+
style="cursor: grab" >
|
|
968
|
+
<title>${removable ? "\uB4DC\uB798\uADF8=\uC774\uB3D9, \uC6B0\uD074\uB9AD=\uC0AD\uC81C" : "\uB4DC\uB798\uADF8=\uC774\uB3D9"}</title>
|
|
969
|
+
</circle>`;
|
|
970
|
+
}
|
|
971
|
+
g.innerHTML = html;
|
|
972
|
+
return g;
|
|
973
|
+
}
|
|
974
|
+
function renderSnapTargets(draggingElementId, snap2, byId, activeSnapTarget) {
|
|
975
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
976
|
+
let html = "";
|
|
977
|
+
for (const baseEl of byId.values()) {
|
|
978
|
+
if (baseEl.id === draggingElementId) continue;
|
|
979
|
+
if (baseEl.type === "line" || baseEl.type === "arrow") continue;
|
|
980
|
+
const state = snap2.get(baseEl.id);
|
|
981
|
+
if (!state || !state.visible) continue;
|
|
982
|
+
const points = getAnchorPoints(baseEl, state);
|
|
983
|
+
for (const p of points) {
|
|
984
|
+
const isActive = activeSnapTarget?.elementId === baseEl.id && activeSnapTarget?.anchor === p.anchor;
|
|
985
|
+
if (isActive) {
|
|
986
|
+
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" />`;
|
|
987
|
+
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" />`;
|
|
988
|
+
} else {
|
|
989
|
+
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" />`;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
g.innerHTML = html;
|
|
994
|
+
return g;
|
|
995
|
+
}
|
|
996
|
+
function renderSelectionOutline(canvasEl2, elId, snap2, byId) {
|
|
997
|
+
const baseEl = byId.get(elId);
|
|
998
|
+
const state = snap2.get(elId);
|
|
999
|
+
if (!baseEl || !state) return null;
|
|
1000
|
+
let box = null;
|
|
1001
|
+
if (baseEl.type === "rect" || baseEl.type === "image") {
|
|
1002
|
+
box = {
|
|
1003
|
+
x: state.x - 4,
|
|
1004
|
+
y: state.y - 4,
|
|
1005
|
+
w: state.width + 8,
|
|
1006
|
+
h: state.height + 8
|
|
1007
|
+
};
|
|
1008
|
+
} else if (baseEl.type === "circle") {
|
|
1009
|
+
const r = state.r;
|
|
1010
|
+
box = {
|
|
1011
|
+
x: state.cx - r - 4,
|
|
1012
|
+
y: state.cy - r - 4,
|
|
1013
|
+
w: r * 2 + 8,
|
|
1014
|
+
h: r * 2 + 8
|
|
1015
|
+
};
|
|
1016
|
+
} else if (baseEl.type === "text") {
|
|
1017
|
+
const bbox = textBBoxOnCanvas(canvasEl2, elId);
|
|
1018
|
+
if (bbox)
|
|
1019
|
+
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
1020
|
+
} else if (baseEl.type === "line" || baseEl.type === "arrow") {
|
|
1021
|
+
const coords = baseEl.type === "line" ? resolveLineCoords(state, snap2, byId) : resolveArrowCoords(state, snap2, byId);
|
|
1022
|
+
if (!coords) return null;
|
|
1023
|
+
box = {
|
|
1024
|
+
x: Math.min(coords.x1, coords.x2) - 4,
|
|
1025
|
+
y: Math.min(coords.y1, coords.y2) - 4,
|
|
1026
|
+
w: Math.abs(coords.x2 - coords.x1) + 8,
|
|
1027
|
+
h: Math.abs(coords.y2 - coords.y1) + 8
|
|
1028
|
+
};
|
|
1029
|
+
} else if (baseEl.type === "polygon") {
|
|
1030
|
+
const bbox = polygonBoundingBox(String(state.points ?? ""));
|
|
1031
|
+
if (bbox)
|
|
1032
|
+
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
1033
|
+
} else if (baseEl.type === "path") {
|
|
1034
|
+
const bbox = pathBBoxOnCanvas(canvasEl2, elId);
|
|
1035
|
+
if (bbox)
|
|
1036
|
+
box = { x: bbox.x - 4, y: bbox.y - 4, w: bbox.w + 8, h: bbox.h + 8 };
|
|
1037
|
+
}
|
|
1038
|
+
if (!box) return null;
|
|
1039
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
1040
|
+
rect.setAttribute("x", String(box.x));
|
|
1041
|
+
rect.setAttribute("y", String(box.y));
|
|
1042
|
+
rect.setAttribute("width", String(box.w));
|
|
1043
|
+
rect.setAttribute("height", String(box.h));
|
|
1044
|
+
rect.classList.add("element-selected-outline");
|
|
1045
|
+
return rect;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/legacy/canvas-preview.ts
|
|
1049
|
+
var previewRoot = null;
|
|
1050
|
+
var previewHandle = null;
|
|
1051
|
+
var unsubscribePreview = null;
|
|
1052
|
+
async function mountPreview(host, doc, options) {
|
|
1053
|
+
const { mountStage } = await import('@kokoa/clotho/dom');
|
|
1054
|
+
previewHandle = mountStage(host, doc, {
|
|
1055
|
+
player: {
|
|
1056
|
+
autoplay: true,
|
|
1057
|
+
loop: options.loop,
|
|
1058
|
+
speed: options.speed,
|
|
1059
|
+
onEnd: options.onEnd
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
unsubscribePreview = previewHandle.player.subscribe((state) => {
|
|
1063
|
+
options.onTime(state.time);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
function showPreview(canvasEl2, def, options) {
|
|
1067
|
+
if (!canvasEl2) return;
|
|
1068
|
+
const parent = canvasEl2.parentElement;
|
|
1069
|
+
if (!parent) return;
|
|
1070
|
+
hidePreview(canvasEl2);
|
|
1071
|
+
canvasEl2.style.visibility = "hidden";
|
|
1072
|
+
previewRoot = document.createElement("div");
|
|
1073
|
+
const bg = def.canvas.background && def.canvas.background !== "transparent" ? def.canvas.background : "transparent";
|
|
1074
|
+
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;`;
|
|
1075
|
+
parent.style.position = "relative";
|
|
1076
|
+
parent.appendChild(previewRoot);
|
|
1077
|
+
void mountPreview(previewRoot, def, options);
|
|
1078
|
+
}
|
|
1079
|
+
function hidePreview(canvasEl2) {
|
|
1080
|
+
if (canvasEl2) canvasEl2.style.visibility = "";
|
|
1081
|
+
unsubscribePreview?.();
|
|
1082
|
+
unsubscribePreview = null;
|
|
1083
|
+
previewHandle?.destroy();
|
|
1084
|
+
previewHandle = null;
|
|
1085
|
+
if (previewRoot) {
|
|
1086
|
+
previewRoot.remove();
|
|
1087
|
+
previewRoot = null;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/legacy/canvas.ts
|
|
1092
|
+
var SNAP_RADIUS = 12;
|
|
1093
|
+
var HEAD_MARKER_DEFS = `
|
|
1094
|
+
<marker id="studio-h-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
1095
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1096
|
+
</marker>
|
|
1097
|
+
<marker id="studio-h-arrow-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
1098
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1099
|
+
</marker>
|
|
1100
|
+
<marker id="studio-h-triangle" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
1101
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1102
|
+
</marker>
|
|
1103
|
+
<marker id="studio-h-triangle-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
|
1104
|
+
<path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor" />
|
|
1105
|
+
</marker>
|
|
1106
|
+
<marker id="studio-h-triangle-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto">
|
|
1107
|
+
<path d="M 0 0 L 10 5 L 0 10" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
1108
|
+
</marker>
|
|
1109
|
+
<marker id="studio-h-triangle-open-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="9" markerHeight="9" orient="auto-start-reverse">
|
|
1110
|
+
<path d="M 0 0 L 10 5 L 0 10" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
1111
|
+
</marker>
|
|
1112
|
+
<marker id="studio-h-circle" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6">
|
|
1113
|
+
<circle cx="5" cy="5" r="4" fill="currentColor" />
|
|
1114
|
+
</marker>
|
|
1115
|
+
<marker id="studio-h-circle-open" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="7" markerHeight="7">
|
|
1116
|
+
<circle cx="5" cy="5" r="4" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
1117
|
+
</marker>
|
|
1118
|
+
<marker id="studio-h-diamond" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
1119
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="currentColor" />
|
|
1120
|
+
</marker>
|
|
1121
|
+
<marker id="studio-h-diamond-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
1122
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="currentColor" />
|
|
1123
|
+
</marker>
|
|
1124
|
+
<marker id="studio-h-diamond-open" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
|
|
1125
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
1126
|
+
</marker>
|
|
1127
|
+
<marker id="studio-h-diamond-open-start" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
|
|
1128
|
+
<path d="M 0 5 L 5 0 L 10 5 L 5 10 z" fill="white" stroke="currentColor" stroke-width="1.5" />
|
|
1129
|
+
</marker>
|
|
1130
|
+
<marker id="studio-h-bar" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="10" orient="auto">
|
|
1131
|
+
<rect x="4" y="0" width="2" height="10" fill="currentColor" />
|
|
1132
|
+
</marker>
|
|
1133
|
+
`;
|
|
1134
|
+
function markerUrlFor(head, end) {
|
|
1135
|
+
if (!head || head === "none") return "";
|
|
1136
|
+
const isOpenCircle = head === "circle-open";
|
|
1137
|
+
const isCircle = head === "circle" || isOpenCircle;
|
|
1138
|
+
if (isCircle) return `url(#studio-h-${head})`;
|
|
1139
|
+
const idBase = `studio-h-${head}`;
|
|
1140
|
+
return end === "start" ? `url(#${idBase}-start)` : `url(#${idBase})`;
|
|
1141
|
+
}
|
|
1142
|
+
var dragState = null;
|
|
1143
|
+
var rotateState = null;
|
|
1144
|
+
var connectState = null;
|
|
1145
|
+
var endpointDragState = null;
|
|
1146
|
+
var vertexDragState = null;
|
|
1147
|
+
var resizeState = null;
|
|
1148
|
+
var marqueeState = null;
|
|
1149
|
+
var marqueeJustFinished = false;
|
|
1150
|
+
var toolJustFinished = false;
|
|
1151
|
+
var canvasEl = null;
|
|
1152
|
+
var hoveredElementId = null;
|
|
1153
|
+
var canvasZoom = 1;
|
|
1154
|
+
var toolDrawState = null;
|
|
1155
|
+
var elementDrawState = null;
|
|
1156
|
+
var canvasRenderFrame = null;
|
|
1157
|
+
var pathDraftState = null;
|
|
1158
|
+
function initCanvas(root) {
|
|
1159
|
+
canvasEl = root;
|
|
1160
|
+
root.addEventListener("click", onCanvasClick);
|
|
1161
|
+
root.addEventListener("dblclick", onPathDoubleClick);
|
|
1162
|
+
root.addEventListener("mousedown", onMouseDown);
|
|
1163
|
+
root.addEventListener("mouseover", onMouseOver);
|
|
1164
|
+
root.addEventListener("mouseleave", onMouseLeave);
|
|
1165
|
+
root.addEventListener("contextmenu", onContextMenu);
|
|
1166
|
+
root.addEventListener("wheel", onCanvasWheel, { passive: false });
|
|
1167
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
1168
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
1169
|
+
document.addEventListener("keydown", onPathKeyDown);
|
|
1170
|
+
subscribe(render2);
|
|
1171
|
+
render2();
|
|
1172
|
+
}
|
|
1173
|
+
function onCanvasWheel(e) {
|
|
1174
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
1175
|
+
e.preventDefault();
|
|
1176
|
+
const wrap = canvasEl?.closest(".studio-canvas-wrap");
|
|
1177
|
+
if (!wrap) {
|
|
1178
|
+
canvasZoom = Math.max(
|
|
1179
|
+
0.1,
|
|
1180
|
+
Math.min(5, canvasZoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))
|
|
1181
|
+
);
|
|
1182
|
+
render2();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const wrapRect = wrap.getBoundingClientRect();
|
|
1186
|
+
const mxInWrap = e.clientX - wrapRect.left + wrap.scrollLeft;
|
|
1187
|
+
const myInWrap = e.clientY - wrapRect.top + wrap.scrollTop;
|
|
1188
|
+
const old = canvasZoom;
|
|
1189
|
+
canvasZoom = Math.max(
|
|
1190
|
+
0.1,
|
|
1191
|
+
Math.min(5, canvasZoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))
|
|
1192
|
+
);
|
|
1193
|
+
const ratio = canvasZoom / old;
|
|
1194
|
+
render2();
|
|
1195
|
+
wrap.scrollLeft = mxInWrap * ratio - (e.clientX - wrapRect.left);
|
|
1196
|
+
wrap.scrollTop = myInWrap * ratio - (e.clientY - wrapRect.top);
|
|
1197
|
+
}
|
|
1198
|
+
function onContextMenu(e) {
|
|
1199
|
+
const target = e.target;
|
|
1200
|
+
const vertexEl = target?.closest("[data-vertex-handle]");
|
|
1201
|
+
if (!vertexEl) return;
|
|
1202
|
+
const id = vertexEl.dataset.elemId ?? "";
|
|
1203
|
+
const idx = Number(vertexEl.dataset.vertexIndex ?? "-1");
|
|
1204
|
+
const def = getDef();
|
|
1205
|
+
if (!def || idx < 0) return;
|
|
1206
|
+
const el = def.elements.find((x) => x.id === id);
|
|
1207
|
+
if (!el || el.type !== "polygon") return;
|
|
1208
|
+
const snap2 = getCurrentSnapshot();
|
|
1209
|
+
const state = snap2.get(id);
|
|
1210
|
+
if (!state) return;
|
|
1211
|
+
const points = String(state.points ?? "").trim().split(/\s+/);
|
|
1212
|
+
if (points.length <= 3) return;
|
|
1213
|
+
e.preventDefault();
|
|
1214
|
+
points.splice(idx, 1);
|
|
1215
|
+
setElementValueAtTime(id, { points: points.join(" ") });
|
|
1216
|
+
}
|
|
1217
|
+
function onMouseOver(e) {
|
|
1218
|
+
const id = findElementId(e.target);
|
|
1219
|
+
if (id !== hoveredElementId) {
|
|
1220
|
+
hoveredElementId = id;
|
|
1221
|
+
render2();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function onMouseLeave() {
|
|
1225
|
+
if (hoveredElementId !== null) {
|
|
1226
|
+
hoveredElementId = null;
|
|
1227
|
+
render2();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function svgPoint(clientX, clientY) {
|
|
1231
|
+
if (!canvasEl) return null;
|
|
1232
|
+
const def = getDef();
|
|
1233
|
+
if (!def) return null;
|
|
1234
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
1235
|
+
const sx = def.canvas.width / rect.width;
|
|
1236
|
+
const sy = def.canvas.height / rect.height;
|
|
1237
|
+
return snapPoint({
|
|
1238
|
+
x: (clientX - rect.left) * sx,
|
|
1239
|
+
y: (clientY - rect.top) * sy
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
function findElementId(target) {
|
|
1243
|
+
let node = target;
|
|
1244
|
+
while (node && node !== canvasEl) {
|
|
838
1245
|
const id = node.getAttribute?.("data-elem-id");
|
|
839
1246
|
if (id) return id;
|
|
840
1247
|
node = node.parentElement;
|
|
@@ -847,6 +1254,10 @@ function onCanvasClick(e) {
|
|
|
847
1254
|
marqueeJustFinished = false;
|
|
848
1255
|
return;
|
|
849
1256
|
}
|
|
1257
|
+
if (toolJustFinished) {
|
|
1258
|
+
toolJustFinished = false;
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
850
1261
|
const target = e.target;
|
|
851
1262
|
const addVertexHandle = target?.closest("[data-vertex-add]");
|
|
852
1263
|
if (addVertexHandle) {
|
|
@@ -878,11 +1289,8 @@ function onCanvasClick(e) {
|
|
|
878
1289
|
const id = findElementId(e.target);
|
|
879
1290
|
if (id) {
|
|
880
1291
|
const resolvedId = e.altKey ? id : findContainingGroup(id)?.id ?? id;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
} else {
|
|
884
|
-
setSelection({ kind: "element", elementId: resolvedId });
|
|
885
|
-
}
|
|
1292
|
+
setActiveTool("select");
|
|
1293
|
+
setSelection(nextPointerSelection(getSelection(), resolvedId, e));
|
|
886
1294
|
} else if (e.target === canvasEl) setSelection({ kind: "none" });
|
|
887
1295
|
}
|
|
888
1296
|
function onMouseDown(e) {
|
|
@@ -890,13 +1298,66 @@ function onMouseDown(e) {
|
|
|
890
1298
|
const target = e.target;
|
|
891
1299
|
const def = getDef();
|
|
892
1300
|
if (!def) return;
|
|
1301
|
+
const activeTool2 = getActiveTool();
|
|
1302
|
+
if (activeTool2 !== "select" && e.target === canvasEl) {
|
|
1303
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1304
|
+
if (!point) return;
|
|
1305
|
+
e.preventDefault();
|
|
1306
|
+
if (activeTool2 === "path") {
|
|
1307
|
+
if (!pathDraftState) {
|
|
1308
|
+
pathDraftState = { points: [point], current: point };
|
|
1309
|
+
} else {
|
|
1310
|
+
pathDraftState.points.push(point);
|
|
1311
|
+
pathDraftState.current = point;
|
|
1312
|
+
}
|
|
1313
|
+
render2();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (activeTool2 === "line" || activeTool2 === "arrow") {
|
|
1317
|
+
toolDrawState = {
|
|
1318
|
+
tool: activeTool2,
|
|
1319
|
+
startX: point.x,
|
|
1320
|
+
startY: point.y,
|
|
1321
|
+
currentX: point.x,
|
|
1322
|
+
currentY: point.y
|
|
1323
|
+
};
|
|
1324
|
+
render2();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const sidesInput = document.getElementById(
|
|
1328
|
+
"studio-polygon-sides"
|
|
1329
|
+
);
|
|
1330
|
+
if (activeTool2 === "rect" || activeTool2 === "circle" || activeTool2 === "polygon" || activeTool2 === "text" || activeTool2 === "image") {
|
|
1331
|
+
elementDrawState = {
|
|
1332
|
+
tool: activeTool2,
|
|
1333
|
+
start: point,
|
|
1334
|
+
current: point,
|
|
1335
|
+
polygonSides: Number(sidesInput?.value ?? 6)
|
|
1336
|
+
};
|
|
1337
|
+
requestCanvasRender();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (activeTool2 !== "select" && !((activeTool2 === "line" || activeTool2 === "arrow") && target?.closest("[data-anchor-handle]"))) {
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (shouldDeferElementPointerDown(e, {
|
|
1345
|
+
isElement: Boolean(findElementId(e.target)),
|
|
1346
|
+
isHandle: Boolean(
|
|
1347
|
+
target?.closest(
|
|
1348
|
+
"[data-rotate-handle], [data-anchor-handle], [data-resize-handle], [data-endpoint-handle], [data-line-mid-handle], [data-vertex-handle], [data-vertex-add]"
|
|
1349
|
+
)
|
|
1350
|
+
)
|
|
1351
|
+
})) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
893
1354
|
if (target?.closest("[data-vertex-add]")) {
|
|
894
1355
|
e.preventDefault();
|
|
895
1356
|
e.stopPropagation();
|
|
896
1357
|
return;
|
|
897
1358
|
}
|
|
898
1359
|
const resizeHandle = target?.closest("[data-resize-handle]");
|
|
899
|
-
if (resizeHandle) {
|
|
1360
|
+
if (resizeHandle && activeTool2 === "select") {
|
|
900
1361
|
e.preventDefault();
|
|
901
1362
|
const id2 = resizeHandle.dataset.elemId ?? "";
|
|
902
1363
|
const handle = resizeHandle.dataset.resizeHandle;
|
|
@@ -938,7 +1399,7 @@ function onMouseDown(e) {
|
|
|
938
1399
|
}
|
|
939
1400
|
resizeState = rs;
|
|
940
1401
|
setSelection({ kind: "element", elementId: id2 });
|
|
941
|
-
|
|
1402
|
+
render2();
|
|
942
1403
|
return;
|
|
943
1404
|
}
|
|
944
1405
|
const vertexHandle = target?.closest("[data-vertex-handle]");
|
|
@@ -955,7 +1416,7 @@ function onMouseDown(e) {
|
|
|
955
1416
|
originalPoints: String(elState2.points ?? "")
|
|
956
1417
|
};
|
|
957
1418
|
setSelection({ kind: "element", elementId: id2 });
|
|
958
|
-
|
|
1419
|
+
render2();
|
|
959
1420
|
return;
|
|
960
1421
|
}
|
|
961
1422
|
const endpointHandle = target?.closest("[data-endpoint-handle]");
|
|
@@ -973,7 +1434,7 @@ function onMouseDown(e) {
|
|
|
973
1434
|
snapTarget: null
|
|
974
1435
|
};
|
|
975
1436
|
setSelection({ kind: "element", elementId: id2 });
|
|
976
|
-
|
|
1437
|
+
render2();
|
|
977
1438
|
return;
|
|
978
1439
|
}
|
|
979
1440
|
const midHandle = target?.closest("[data-line-mid-handle]");
|
|
@@ -1022,7 +1483,7 @@ function onMouseDown(e) {
|
|
|
1022
1483
|
return;
|
|
1023
1484
|
}
|
|
1024
1485
|
const anchorHandle = target?.closest("[data-anchor-handle]");
|
|
1025
|
-
if (anchorHandle) {
|
|
1486
|
+
if (anchorHandle && (activeTool2 === "line" || activeTool2 === "arrow")) {
|
|
1026
1487
|
e.preventDefault();
|
|
1027
1488
|
const fromId = anchorHandle.dataset.elemId ?? "";
|
|
1028
1489
|
const fromAnchor = anchorHandle.dataset.anchor ?? "auto";
|
|
@@ -1036,7 +1497,7 @@ function onMouseDown(e) {
|
|
|
1036
1497
|
currentX: pt.x,
|
|
1037
1498
|
currentY: pt.y
|
|
1038
1499
|
};
|
|
1039
|
-
|
|
1500
|
+
render2();
|
|
1040
1501
|
return;
|
|
1041
1502
|
}
|
|
1042
1503
|
const rawId = findElementId(e.target);
|
|
@@ -1099,6 +1560,29 @@ function collectDragExtras(anchorId, def, snap2) {
|
|
|
1099
1560
|
return extras;
|
|
1100
1561
|
}
|
|
1101
1562
|
function onMouseMove(e) {
|
|
1563
|
+
if (pathDraftState && getActiveTool() === "path") {
|
|
1564
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1565
|
+
if (point) {
|
|
1566
|
+
pathDraftState.current = point;
|
|
1567
|
+
render2();
|
|
1568
|
+
}
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (elementDrawState) {
|
|
1572
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1573
|
+
if (!point) return;
|
|
1574
|
+
elementDrawState.current = point;
|
|
1575
|
+
requestCanvasRender();
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (toolDrawState) {
|
|
1579
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1580
|
+
if (!point) return;
|
|
1581
|
+
toolDrawState.currentX = point.x;
|
|
1582
|
+
toolDrawState.currentY = point.y;
|
|
1583
|
+
render2();
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1102
1586
|
if (resizeState) {
|
|
1103
1587
|
handleResizeMove(e);
|
|
1104
1588
|
return;
|
|
@@ -1168,7 +1652,7 @@ function onMouseMove(e) {
|
|
|
1168
1652
|
}
|
|
1169
1653
|
}
|
|
1170
1654
|
}
|
|
1171
|
-
|
|
1655
|
+
render2();
|
|
1172
1656
|
return;
|
|
1173
1657
|
}
|
|
1174
1658
|
if (rotateState) {
|
|
@@ -1186,7 +1670,7 @@ function onMouseMove(e) {
|
|
|
1186
1670
|
if (!pt) return;
|
|
1187
1671
|
connectState.currentX = pt.x;
|
|
1188
1672
|
connectState.currentY = pt.y;
|
|
1189
|
-
|
|
1673
|
+
render2();
|
|
1190
1674
|
return;
|
|
1191
1675
|
}
|
|
1192
1676
|
if (marqueeState) {
|
|
@@ -1194,7 +1678,7 @@ function onMouseMove(e) {
|
|
|
1194
1678
|
if (!pt) return;
|
|
1195
1679
|
marqueeState.currentX = pt.x;
|
|
1196
1680
|
marqueeState.currentY = pt.y;
|
|
1197
|
-
|
|
1681
|
+
render2();
|
|
1198
1682
|
return;
|
|
1199
1683
|
}
|
|
1200
1684
|
if (!dragState || !canvasEl) return;
|
|
@@ -1226,18 +1710,104 @@ function onMouseMove(e) {
|
|
|
1226
1710
|
);
|
|
1227
1711
|
}
|
|
1228
1712
|
}
|
|
1713
|
+
function onPathDoubleClick(e) {
|
|
1714
|
+
if (!pathDraftState || getActiveTool() !== "path") return;
|
|
1715
|
+
e.preventDefault();
|
|
1716
|
+
const points = pathDraftState.points;
|
|
1717
|
+
if (points.length >= 2 && Math.hypot(
|
|
1718
|
+
points.at(-1).x - points.at(-2).x,
|
|
1719
|
+
points.at(-1).y - points.at(-2).y
|
|
1720
|
+
) < 2) {
|
|
1721
|
+
points.pop();
|
|
1722
|
+
}
|
|
1723
|
+
finishPathDraft();
|
|
1724
|
+
}
|
|
1725
|
+
function onPathKeyDown(e) {
|
|
1726
|
+
if (!pathDraftState || getActiveTool() !== "path") return;
|
|
1727
|
+
const target = e.target;
|
|
1728
|
+
const isEditing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
|
|
1729
|
+
if (isEditing) return;
|
|
1730
|
+
if (e.key === "Enter") {
|
|
1731
|
+
e.preventDefault();
|
|
1732
|
+
finishPathDraft();
|
|
1733
|
+
} else if (e.key === "Escape") {
|
|
1734
|
+
e.preventDefault();
|
|
1735
|
+
pathDraftState = null;
|
|
1736
|
+
render2();
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function finishPathDraft() {
|
|
1740
|
+
const draft = pathDraftState;
|
|
1741
|
+
pathDraftState = null;
|
|
1742
|
+
if (!draft || draft.points.length < 2) {
|
|
1743
|
+
render2();
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const origin = draft.points[0];
|
|
1747
|
+
const element = makeDefaultElement(
|
|
1748
|
+
"path",
|
|
1749
|
+
uniqueElementId("path"),
|
|
1750
|
+
origin.x,
|
|
1751
|
+
origin.y
|
|
1752
|
+
);
|
|
1753
|
+
if (element?.type === "path") {
|
|
1754
|
+
const d = draft.points.map((point, index) => {
|
|
1755
|
+
const command = index === 0 ? "M" : "L";
|
|
1756
|
+
return `${command} ${(point.x - origin.x).toFixed(1)} ${(point.y - origin.y).toFixed(1)}`;
|
|
1757
|
+
}).join(" ");
|
|
1758
|
+
addElement({ ...element, x: origin.x, y: origin.y, d });
|
|
1759
|
+
}
|
|
1760
|
+
render2();
|
|
1761
|
+
}
|
|
1229
1762
|
function onMouseUp(e) {
|
|
1763
|
+
if (elementDrawState) {
|
|
1764
|
+
const draw = elementDrawState;
|
|
1765
|
+
const point = svgPoint(e.clientX, e.clientY);
|
|
1766
|
+
if (point) draw.current = point;
|
|
1767
|
+
elementDrawState = null;
|
|
1768
|
+
if (canvasRenderFrame !== null) {
|
|
1769
|
+
cancelAnimationFrame(canvasRenderFrame);
|
|
1770
|
+
canvasRenderFrame = null;
|
|
1771
|
+
}
|
|
1772
|
+
toolJustFinished = true;
|
|
1773
|
+
finishElementDraw(draw);
|
|
1774
|
+
render2();
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (toolDrawState) {
|
|
1778
|
+
const draw = toolDrawState;
|
|
1779
|
+
toolDrawState = null;
|
|
1780
|
+
if (Math.hypot(draw.currentX - draw.startX, draw.currentY - draw.startY) >= 4) {
|
|
1781
|
+
const element = makeDefaultElement(
|
|
1782
|
+
draw.tool,
|
|
1783
|
+
uniqueElementId(draw.tool),
|
|
1784
|
+
0,
|
|
1785
|
+
0
|
|
1786
|
+
);
|
|
1787
|
+
if (element && (element.type === "line" || element.type === "arrow")) {
|
|
1788
|
+
addElement({
|
|
1789
|
+
...element,
|
|
1790
|
+
x1: draw.startX,
|
|
1791
|
+
y1: draw.startY,
|
|
1792
|
+
x2: draw.currentX,
|
|
1793
|
+
y2: draw.currentY
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
render2();
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1230
1800
|
if (resizeState) {
|
|
1231
1801
|
resizeState = null;
|
|
1232
|
-
|
|
1802
|
+
render2();
|
|
1233
1803
|
}
|
|
1234
1804
|
if (vertexDragState) {
|
|
1235
1805
|
vertexDragState = null;
|
|
1236
|
-
|
|
1806
|
+
render2();
|
|
1237
1807
|
}
|
|
1238
1808
|
if (endpointDragState) {
|
|
1239
1809
|
endpointDragState = null;
|
|
1240
|
-
|
|
1810
|
+
render2();
|
|
1241
1811
|
}
|
|
1242
1812
|
if (connectState) {
|
|
1243
1813
|
const target = e.target;
|
|
@@ -1247,10 +1817,9 @@ function onMouseUp(e) {
|
|
|
1247
1817
|
if (elemId && elemId !== connectState.fromId) {
|
|
1248
1818
|
const def = getDef();
|
|
1249
1819
|
if (def) {
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
id: newId,
|
|
1820
|
+
const connectionType = getActiveTool() === "line" ? "line" : "arrow";
|
|
1821
|
+
const common = {
|
|
1822
|
+
id: uniqueElementId(connectionType),
|
|
1254
1823
|
rotation: 0,
|
|
1255
1824
|
appearances: [],
|
|
1256
1825
|
tracks: [],
|
|
@@ -1260,17 +1829,25 @@ function onMouseUp(e) {
|
|
|
1260
1829
|
toAnchor,
|
|
1261
1830
|
stroke: "#6366f1",
|
|
1262
1831
|
strokeWidth: 2,
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1832
|
+
headStart: "none"
|
|
1833
|
+
};
|
|
1834
|
+
if (connectionType === "line") {
|
|
1835
|
+
addElement({ ...common, type: "line", headEnd: "none" });
|
|
1836
|
+
} else {
|
|
1837
|
+
addElement({
|
|
1838
|
+
...common,
|
|
1839
|
+
type: "arrow",
|
|
1840
|
+
curvature: 0,
|
|
1841
|
+
labelColor: "#0b0b0f",
|
|
1842
|
+
labelOffsetX: 0,
|
|
1843
|
+
labelOffsetY: 4,
|
|
1844
|
+
headEnd: "arrow"
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1270
1847
|
}
|
|
1271
1848
|
}
|
|
1272
1849
|
connectState = null;
|
|
1273
|
-
|
|
1850
|
+
render2();
|
|
1274
1851
|
}
|
|
1275
1852
|
if (marqueeState) {
|
|
1276
1853
|
const mx = Math.min(marqueeState.startX, marqueeState.currentX);
|
|
@@ -1302,11 +1879,62 @@ function onMouseUp(e) {
|
|
|
1302
1879
|
}
|
|
1303
1880
|
}
|
|
1304
1881
|
}
|
|
1305
|
-
|
|
1882
|
+
render2();
|
|
1306
1883
|
}
|
|
1307
1884
|
dragState = null;
|
|
1308
1885
|
rotateState = null;
|
|
1309
1886
|
}
|
|
1887
|
+
function requestCanvasRender() {
|
|
1888
|
+
if (canvasRenderFrame !== null) return;
|
|
1889
|
+
canvasRenderFrame = requestAnimationFrame(() => {
|
|
1890
|
+
canvasRenderFrame = null;
|
|
1891
|
+
render2();
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function finishElementDraw(draw) {
|
|
1895
|
+
if (draw.tool === "image") {
|
|
1896
|
+
document.getElementById("studio-image-file")?.click();
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const dragged = isShapeDrag(draw.start, draw.current);
|
|
1900
|
+
const bounds = dragBounds(draw.start, draw.current);
|
|
1901
|
+
const center = dragged ? { x: bounds.centerX, y: bounds.centerY } : draw.start;
|
|
1902
|
+
const element = makeDefaultElement(
|
|
1903
|
+
draw.tool,
|
|
1904
|
+
uniqueElementId(draw.tool),
|
|
1905
|
+
center.x,
|
|
1906
|
+
center.y,
|
|
1907
|
+
draw.polygonSides
|
|
1908
|
+
);
|
|
1909
|
+
if (!element) return;
|
|
1910
|
+
if (dragged && element.type === "rect") {
|
|
1911
|
+
addElement({
|
|
1912
|
+
...element,
|
|
1913
|
+
x: bounds.x,
|
|
1914
|
+
y: bounds.y,
|
|
1915
|
+
width: Math.max(1, bounds.width),
|
|
1916
|
+
height: Math.max(1, bounds.height)
|
|
1917
|
+
});
|
|
1918
|
+
} else if (dragged && element.type === "circle") {
|
|
1919
|
+
addElement({
|
|
1920
|
+
...element,
|
|
1921
|
+
cx: bounds.centerX,
|
|
1922
|
+
cy: bounds.centerY,
|
|
1923
|
+
r: Math.max(2, bounds.distance / 2)
|
|
1924
|
+
});
|
|
1925
|
+
} else if (dragged && element.type === "polygon") {
|
|
1926
|
+
addElement({
|
|
1927
|
+
...element,
|
|
1928
|
+
points: regularPolygonPoints(
|
|
1929
|
+
{ x: bounds.centerX, y: bounds.centerY },
|
|
1930
|
+
Math.max(2, bounds.distance / 2),
|
|
1931
|
+
draw.polygonSides
|
|
1932
|
+
)
|
|
1933
|
+
});
|
|
1934
|
+
} else {
|
|
1935
|
+
addElement(element);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1310
1938
|
function applyMove(baseEl, state, x, y, polygonOriginal) {
|
|
1311
1939
|
if (baseEl.type === "rect" || baseEl.type === "image" || baseEl.type === "text") {
|
|
1312
1940
|
setElementValueAtTime(baseEl.id, { x, y });
|
|
@@ -1509,7 +2137,7 @@ function handleResizeMove(e) {
|
|
|
1509
2137
|
return;
|
|
1510
2138
|
}
|
|
1511
2139
|
}
|
|
1512
|
-
function
|
|
2140
|
+
function render2() {
|
|
1513
2141
|
if (!canvasEl) return;
|
|
1514
2142
|
const def = getDef();
|
|
1515
2143
|
if (!def) {
|
|
@@ -1552,32 +2180,20 @@ function render() {
|
|
|
1552
2180
|
snap2,
|
|
1553
2181
|
elementsById
|
|
1554
2182
|
);
|
|
1555
|
-
if (outline) canvasEl.appendChild(outline);
|
|
1556
|
-
const
|
|
1557
|
-
|
|
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(
|
|
2183
|
+
if (outline) canvasEl.appendChild(outline);
|
|
2184
|
+
const selectMode = getActiveTool() === "select";
|
|
2185
|
+
const handle = selectMode ? renderRotationHandle(
|
|
1576
2186
|
canvasEl,
|
|
1577
2187
|
selection.elementId,
|
|
1578
2188
|
snap2,
|
|
1579
2189
|
elementsById
|
|
1580
|
-
);
|
|
2190
|
+
) : null;
|
|
2191
|
+
if (handle) canvasEl.appendChild(handle);
|
|
2192
|
+
const endpoints = selectMode ? renderLineEndpointHandles(selection.elementId, snap2, elementsById) : null;
|
|
2193
|
+
if (endpoints) canvasEl.appendChild(endpoints);
|
|
2194
|
+
const vertices = selectMode ? renderPolygonVertexHandles(selection.elementId, snap2, elementsById) : null;
|
|
2195
|
+
if (vertices) canvasEl.appendChild(vertices);
|
|
2196
|
+
const resize = selectMode ? renderResizeHandles(canvasEl, selection.elementId, snap2, elementsById) : null;
|
|
1581
2197
|
if (resize) canvasEl.appendChild(resize);
|
|
1582
2198
|
}
|
|
1583
2199
|
} else if (selection.kind === "elements") {
|
|
@@ -1600,513 +2216,291 @@ function render() {
|
|
|
1600
2216
|
);
|
|
1601
2217
|
if (guides) canvasEl.appendChild(guides);
|
|
1602
2218
|
}
|
|
1603
|
-
|
|
2219
|
+
const activeTool2 = getActiveTool();
|
|
2220
|
+
if (hoveredElementId && (activeTool2 === "line" || activeTool2 === "arrow") && (!connectState || hoveredElementId !== connectState.fromId)) {
|
|
1604
2221
|
const anchors = renderAnchorDots(hoveredElementId, snap2, elementsById);
|
|
1605
2222
|
if (anchors) canvasEl.appendChild(anchors);
|
|
1606
2223
|
}
|
|
1607
|
-
if (connectState && connectState.fromId !== hoveredElementId) {
|
|
2224
|
+
if (connectState && (activeTool2 === "line" || activeTool2 === "arrow") && connectState.fromId !== hoveredElementId) {
|
|
1608
2225
|
const anchors = renderAnchorDots(connectState.fromId, snap2, elementsById);
|
|
1609
2226
|
if (anchors) canvasEl.appendChild(anchors);
|
|
1610
2227
|
}
|
|
1611
2228
|
if (connectState) {
|
|
1612
|
-
const tempLine = document.createElementNS(SVG_NS, "line");
|
|
1613
|
-
tempLine.setAttribute("x1", String(connectState.startX));
|
|
1614
|
-
tempLine.setAttribute("y1", String(connectState.startY));
|
|
1615
|
-
tempLine.setAttribute("x2", String(connectState.currentX));
|
|
1616
|
-
tempLine.setAttribute("y2", String(connectState.currentY));
|
|
1617
|
-
tempLine.setAttribute("stroke", "#6366f1");
|
|
1618
|
-
tempLine.setAttribute("stroke-width", "2");
|
|
1619
|
-
tempLine.setAttribute("stroke-dasharray", "5 3");
|
|
1620
|
-
tempLine.setAttribute("marker-end", "url(#studio-arrow)");
|
|
1621
|
-
tempLine.style.pointerEvents = "none";
|
|
1622
|
-
canvasEl.appendChild(tempLine);
|
|
1623
|
-
}
|
|
1624
|
-
if (marqueeState) {
|
|
1625
|
-
const mx = Math.min(marqueeState.startX, marqueeState.currentX);
|
|
1626
|
-
const my = Math.min(marqueeState.startY, marqueeState.currentY);
|
|
1627
|
-
const mw = Math.abs(marqueeState.currentX - marqueeState.startX);
|
|
1628
|
-
const mh = Math.abs(marqueeState.currentY - marqueeState.startY);
|
|
1629
|
-
if (mw > 0 || mh > 0) {
|
|
1630
|
-
const marqueeRect = document.createElementNS(SVG_NS, "rect");
|
|
1631
|
-
marqueeRect.setAttribute("x", String(mx));
|
|
1632
|
-
marqueeRect.setAttribute("y", String(my));
|
|
1633
|
-
marqueeRect.setAttribute("width", String(mw));
|
|
1634
|
-
marqueeRect.setAttribute("height", String(mh));
|
|
1635
|
-
marqueeRect.setAttribute("fill", "rgba(99, 102, 241, 0.08)");
|
|
1636
|
-
marqueeRect.setAttribute("stroke", "#6366f1");
|
|
1637
|
-
marqueeRect.setAttribute("stroke-width", "1");
|
|
1638
|
-
marqueeRect.setAttribute("stroke-dasharray", "4 3");
|
|
1639
|
-
marqueeRect.style.pointerEvents = "none";
|
|
1640
|
-
canvasEl.appendChild(marqueeRect);
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
function findSnapTarget(x, y, excludeElementId) {
|
|
1645
|
-
const def = getDef();
|
|
1646
|
-
if (!def) return null;
|
|
1647
|
-
const snap2 = getCurrentSnapshot();
|
|
1648
|
-
const allAnchors = [];
|
|
1649
|
-
for (const baseEl of def.elements) {
|
|
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
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
function renderGroupOutline(groupId) {
|
|
1667
|
-
const box = groupBbox(groupId);
|
|
1668
|
-
if (!box) return null;
|
|
1669
|
-
const g = document.createElementNS(SVG_NS, "g");
|
|
1670
|
-
g.setAttribute("data-elem-id", groupId);
|
|
1671
|
-
const pad = 6;
|
|
1672
|
-
const x = box.x - pad;
|
|
1673
|
-
const y = box.y - pad;
|
|
1674
|
-
const w = box.w + pad * 2;
|
|
1675
|
-
const h = box.h + pad * 2;
|
|
1676
|
-
const thickness = 8;
|
|
1677
|
-
g.innerHTML = `
|
|
1678
|
-
<rect x="${x}" y="${y}" width="${w}" height="${h}"
|
|
1679
|
-
fill="none" stroke="var(--color-accent)" stroke-width="2" stroke-dasharray="6 4" rx="4" pointer-events="none" />
|
|
1680
|
-
<rect x="${x - thickness}" y="${y - thickness}" width="${w + thickness * 2}" height="${thickness * 2}"
|
|
1681
|
-
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
1682
|
-
<rect x="${x - thickness}" y="${y + h - thickness}" width="${w + thickness * 2}" height="${thickness * 2}"
|
|
1683
|
-
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
1684
|
-
<rect x="${x - thickness}" y="${y}" width="${thickness * 2}" height="${h}"
|
|
1685
|
-
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
1686
|
-
<rect x="${x + w - thickness}" y="${y}" width="${thickness * 2}" height="${h}"
|
|
1687
|
-
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
1688
|
-
`;
|
|
1689
|
-
return g;
|
|
1690
|
-
}
|
|
1691
|
-
function makeG(elementId, rotation, cx, cy) {
|
|
1692
|
-
const g = document.createElementNS(SVG_NS, "g");
|
|
1693
|
-
g.setAttribute("data-elem-id", elementId);
|
|
1694
|
-
g.classList.add("element-handle");
|
|
1695
|
-
if (rotation) g.setAttribute("transform", `rotate(${rotation} ${cx} ${cy})`);
|
|
1696
|
-
return g;
|
|
1697
|
-
}
|
|
1698
|
-
function renderElement(baseEl, state, snap2, byId) {
|
|
1699
|
-
const rotation = state.rotation || 0;
|
|
1700
|
-
if (baseEl.type === "rect") {
|
|
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;
|
|
1714
|
-
}
|
|
1715
|
-
if (baseEl.type === "circle") {
|
|
1716
|
-
const c = state;
|
|
1717
|
-
const g = makeG(baseEl.id, rotation, c.cx, c.cy);
|
|
1718
|
-
g.innerHTML = `
|
|
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;
|
|
1725
|
-
}
|
|
1726
|
-
if (baseEl.type === "line") {
|
|
1727
|
-
const l = state;
|
|
1728
|
-
const coords = resolveLineCoords(l, snap2, byId);
|
|
1729
|
-
if (!coords) return null;
|
|
1730
|
-
const g = makeG(baseEl.id, 0, 0, 0);
|
|
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;
|
|
1739
|
-
}
|
|
1740
|
-
if (baseEl.type === "arrow") {
|
|
1741
|
-
const a = state;
|
|
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;
|
|
1774
|
-
}
|
|
1775
|
-
if (baseEl.type === "text") {
|
|
1776
|
-
const t = state;
|
|
1777
|
-
const g = makeG(baseEl.id, rotation, t.x, t.y);
|
|
1778
|
-
g.innerHTML = `<text x="${t.x}" y="${t.y}" font-size="${t.fontSize}" font-weight="${t.fontWeight}"
|
|
1779
|
-
fill="${t.color}" text-anchor="${t.textAnchor}">${escapeXml(t.content)}</text>`;
|
|
1780
|
-
return g;
|
|
1781
|
-
}
|
|
1782
|
-
if (baseEl.type === "image") {
|
|
1783
|
-
const i = state;
|
|
1784
|
-
const cx = i.x + i.width / 2;
|
|
1785
|
-
const cy = i.y + i.height / 2;
|
|
1786
|
-
const g = makeG(baseEl.id, rotation, cx, cy);
|
|
1787
|
-
const resolved = resolveAsset(i.assetId, getDef()?.assets ?? {});
|
|
1788
|
-
const href = resolved.status === "resolved" ? resolved.href ?? "" : "";
|
|
1789
|
-
g.innerHTML = href ? `<image href="${escapeXml(href)}" x="${i.x}" y="${i.y}" width="${i.width}"
|
|
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;
|
|
1793
|
-
}
|
|
1794
|
-
if (baseEl.type === "path") {
|
|
1795
|
-
const p = state;
|
|
1796
|
-
const g = makeG(baseEl.id, rotation, p.x, p.y);
|
|
1797
|
-
const dash = p.strokeDasharray ? `stroke-dasharray="${p.strokeDasharray}"` : "";
|
|
1798
|
-
const xform = p.x || p.y ? `transform="translate(${p.x} ${p.y})"` : "";
|
|
1799
|
-
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" />`;
|
|
1800
|
-
return g;
|
|
1801
|
-
}
|
|
1802
|
-
if (baseEl.type === "polygon") {
|
|
1803
|
-
const pg = state;
|
|
1804
|
-
const g = makeG(baseEl.id, rotation, 0, 0);
|
|
1805
|
-
g.innerHTML = `<polygon points="${escapeXml(pg.points)}" fill="${pg.fill}" stroke="${pg.stroke}" stroke-width="${pg.strokeWidth}" opacity="${pg.opacity}" pointer-events="all" />`;
|
|
1806
|
-
return g;
|
|
1807
|
-
}
|
|
1808
|
-
return null;
|
|
1809
|
-
}
|
|
1810
|
-
function showPreview2(def) {
|
|
1811
|
-
showPreview(canvasEl, def);
|
|
1812
|
-
}
|
|
1813
|
-
function hidePreview2() {
|
|
1814
|
-
hidePreview(canvasEl);
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
// src/legacy/element-list.ts
|
|
1818
|
-
var listEl = null;
|
|
1819
|
-
var toolsRootEl = null;
|
|
1820
|
-
var searchEl = null;
|
|
1821
|
-
var dragSourceId = null;
|
|
1822
|
-
function initElementList(root, toolsRoot) {
|
|
1823
|
-
listEl = root;
|
|
1824
|
-
toolsRootEl = toolsRoot;
|
|
1825
|
-
searchEl = document.getElementById(
|
|
1826
|
-
"studio-element-search"
|
|
1827
|
-
);
|
|
1828
|
-
subscribe(render2);
|
|
1829
|
-
root.addEventListener("click", onClick);
|
|
1830
|
-
root.addEventListener("dragstart", onDragStart);
|
|
1831
|
-
root.addEventListener("dragover", onDragOver);
|
|
1832
|
-
root.addEventListener("dragleave", onDragLeave);
|
|
1833
|
-
root.addEventListener("drop", onDrop);
|
|
1834
|
-
root.addEventListener("dragend", onDragEnd);
|
|
1835
|
-
toolsRootEl?.addEventListener("click", onToolsClick);
|
|
1836
|
-
searchEl?.addEventListener("input", render2);
|
|
1837
|
-
searchEl?.addEventListener("keydown", (e) => {
|
|
1838
|
-
if (e.key === "Escape" && searchEl) {
|
|
1839
|
-
searchEl.value = "";
|
|
1840
|
-
render2();
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
render2();
|
|
1844
|
-
}
|
|
1845
|
-
function escapeHtml(s) {
|
|
1846
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1847
|
-
}
|
|
1848
|
-
function friendlyElementLabel(el) {
|
|
1849
|
-
const e = el;
|
|
1850
|
-
const visible = e.name?.trim() || e.label?.trim() || e.content?.trim();
|
|
1851
|
-
return visible ? visible : el.id;
|
|
1852
|
-
}
|
|
1853
|
-
function matchesSearch(el, query) {
|
|
1854
|
-
if (!query) return true;
|
|
1855
|
-
const e = el;
|
|
1856
|
-
const haystack = [el.id, el.type, e.name, e.label, e.content].filter(Boolean).join(" ").toLowerCase();
|
|
1857
|
-
return haystack.includes(query);
|
|
1858
|
-
}
|
|
1859
|
-
function render2() {
|
|
1860
|
-
if (!listEl) return;
|
|
1861
|
-
const def = getDef();
|
|
1862
|
-
if (!def || def.elements.length === 0) {
|
|
1863
|
-
listEl.innerHTML = '<li style="color: var(--color-fg-muted); padding: 0.4rem;">\uC694\uC18C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</li>';
|
|
1864
|
-
return;
|
|
2229
|
+
const tempLine = document.createElementNS(SVG_NS, "line");
|
|
2230
|
+
tempLine.setAttribute("x1", String(connectState.startX));
|
|
2231
|
+
tempLine.setAttribute("y1", String(connectState.startY));
|
|
2232
|
+
tempLine.setAttribute("x2", String(connectState.currentX));
|
|
2233
|
+
tempLine.setAttribute("y2", String(connectState.currentY));
|
|
2234
|
+
tempLine.setAttribute("stroke", "#6366f1");
|
|
2235
|
+
tempLine.setAttribute("stroke-width", "2");
|
|
2236
|
+
tempLine.setAttribute("stroke-dasharray", "5 3");
|
|
2237
|
+
tempLine.setAttribute("marker-end", "url(#studio-arrow)");
|
|
2238
|
+
tempLine.style.pointerEvents = "none";
|
|
2239
|
+
canvasEl.appendChild(tempLine);
|
|
1865
2240
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
return `<li class="studio-element-list-item ${isSel ? "is-selected" : ""}" data-elem-id="${escapeHtml(el.id)}" draggable="true">
|
|
1878
|
-
<span class="studio-element-grip" aria-hidden="true">\u22EE\u22EE</span>
|
|
1879
|
-
<span class="studio-element-list-item-label">${escapeHtml(label)} <span class="studio-element-list-item-type">${escapeHtml(el.type)}</span>${subId}</span>
|
|
1880
|
-
<button type="button" class="studio-element-list-delete" data-delete title="\uC0AD\uC81C">\u2715</button>
|
|
1881
|
-
</li>`;
|
|
1882
|
-
}).join("");
|
|
1883
|
-
}
|
|
1884
|
-
function onClick(e) {
|
|
1885
|
-
const target = e.target;
|
|
1886
|
-
const li = target.closest(".studio-element-list-item");
|
|
1887
|
-
if (!li) return;
|
|
1888
|
-
const id = li.dataset.elemId ?? "";
|
|
1889
|
-
if (target.closest("[data-delete]")) {
|
|
1890
|
-
deleteElement(id);
|
|
1891
|
-
return;
|
|
2241
|
+
if (toolDrawState) {
|
|
2242
|
+
const preview = document.createElementNS(SVG_NS, "line");
|
|
2243
|
+
preview.setAttribute("x1", String(toolDrawState.startX));
|
|
2244
|
+
preview.setAttribute("y1", String(toolDrawState.startY));
|
|
2245
|
+
preview.setAttribute("x2", String(toolDrawState.currentX));
|
|
2246
|
+
preview.setAttribute("y2", String(toolDrawState.currentY));
|
|
2247
|
+
preview.setAttribute("stroke", "#6366f1");
|
|
2248
|
+
preview.setAttribute("stroke-width", "2");
|
|
2249
|
+
preview.setAttribute("stroke-dasharray", "5 3");
|
|
2250
|
+
preview.style.pointerEvents = "none";
|
|
2251
|
+
canvasEl.appendChild(preview);
|
|
1892
2252
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
2253
|
+
if (elementDrawState) {
|
|
2254
|
+
const bounds = dragBounds(elementDrawState.start, elementDrawState.current);
|
|
2255
|
+
const preview = document.createElementNS(
|
|
2256
|
+
SVG_NS,
|
|
2257
|
+
elementDrawState.tool === "circle" ? "circle" : elementDrawState.tool === "polygon" ? "polygon" : "rect"
|
|
2258
|
+
);
|
|
2259
|
+
if (elementDrawState.tool === "circle") {
|
|
2260
|
+
preview.setAttribute("cx", String(bounds.centerX));
|
|
2261
|
+
preview.setAttribute("cy", String(bounds.centerY));
|
|
2262
|
+
preview.setAttribute("r", String(Math.max(2, bounds.distance / 2)));
|
|
2263
|
+
} else if (elementDrawState.tool === "polygon") {
|
|
2264
|
+
preview.setAttribute(
|
|
2265
|
+
"points",
|
|
2266
|
+
regularPolygonPoints(
|
|
2267
|
+
{ x: bounds.centerX, y: bounds.centerY },
|
|
2268
|
+
Math.max(2, bounds.distance / 2),
|
|
2269
|
+
elementDrawState.polygonSides
|
|
2270
|
+
)
|
|
2271
|
+
);
|
|
2272
|
+
} else {
|
|
2273
|
+
preview.setAttribute("x", String(bounds.x));
|
|
2274
|
+
preview.setAttribute("y", String(bounds.y));
|
|
2275
|
+
preview.setAttribute("width", String(Math.max(1, bounds.width)));
|
|
2276
|
+
preview.setAttribute("height", String(Math.max(1, bounds.height)));
|
|
2277
|
+
}
|
|
2278
|
+
preview.setAttribute("fill", "rgba(99, 102, 241, 0.12)");
|
|
2279
|
+
preview.setAttribute("stroke", "#6366f1");
|
|
2280
|
+
preview.setAttribute("stroke-width", "2");
|
|
2281
|
+
preview.setAttribute("stroke-dasharray", "5 3");
|
|
2282
|
+
preview.style.pointerEvents = "none";
|
|
2283
|
+
canvasEl.appendChild(preview);
|
|
1898
2284
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2285
|
+
if (pathDraftState) {
|
|
2286
|
+
const points = [
|
|
2287
|
+
...pathDraftState.points,
|
|
2288
|
+
...pathDraftState.current ? [pathDraftState.current] : []
|
|
2289
|
+
];
|
|
2290
|
+
const preview = document.createElementNS(SVG_NS, "polyline");
|
|
2291
|
+
preview.setAttribute(
|
|
2292
|
+
"points",
|
|
2293
|
+
points.map((point) => `${point.x},${point.y}`).join(" ")
|
|
2294
|
+
);
|
|
2295
|
+
preview.setAttribute("fill", "none");
|
|
2296
|
+
preview.setAttribute("stroke", "#6366f1");
|
|
2297
|
+
preview.setAttribute("stroke-width", "2");
|
|
2298
|
+
preview.setAttribute("stroke-dasharray", "5 3");
|
|
2299
|
+
preview.style.pointerEvents = "none";
|
|
2300
|
+
canvasEl.appendChild(preview);
|
|
2301
|
+
for (const point of pathDraftState.points) {
|
|
2302
|
+
const anchor = document.createElementNS(SVG_NS, "circle");
|
|
2303
|
+
anchor.setAttribute("cx", String(point.x));
|
|
2304
|
+
anchor.setAttribute("cy", String(point.y));
|
|
2305
|
+
anchor.setAttribute("r", "4");
|
|
2306
|
+
anchor.setAttribute("fill", "#fff");
|
|
2307
|
+
anchor.setAttribute("stroke", "#6366f1");
|
|
2308
|
+
anchor.setAttribute("stroke-width", "2");
|
|
2309
|
+
anchor.style.pointerEvents = "none";
|
|
2310
|
+
canvasEl.appendChild(anchor);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (marqueeState) {
|
|
2314
|
+
const mx = Math.min(marqueeState.startX, marqueeState.currentX);
|
|
2315
|
+
const my = Math.min(marqueeState.startY, marqueeState.currentY);
|
|
2316
|
+
const mw = Math.abs(marqueeState.currentX - marqueeState.startX);
|
|
2317
|
+
const mh = Math.abs(marqueeState.currentY - marqueeState.startY);
|
|
2318
|
+
if (mw > 0 || mh > 0) {
|
|
2319
|
+
const marqueeRect = document.createElementNS(SVG_NS, "rect");
|
|
2320
|
+
marqueeRect.setAttribute("x", String(mx));
|
|
2321
|
+
marqueeRect.setAttribute("y", String(my));
|
|
2322
|
+
marqueeRect.setAttribute("width", String(mw));
|
|
2323
|
+
marqueeRect.setAttribute("height", String(mh));
|
|
2324
|
+
marqueeRect.setAttribute("fill", "rgba(99, 102, 241, 0.08)");
|
|
2325
|
+
marqueeRect.setAttribute("stroke", "#6366f1");
|
|
2326
|
+
marqueeRect.setAttribute("stroke-width", "1");
|
|
2327
|
+
marqueeRect.setAttribute("stroke-dasharray", "4 3");
|
|
2328
|
+
marqueeRect.style.pointerEvents = "none";
|
|
2329
|
+
canvasEl.appendChild(marqueeRect);
|
|
2330
|
+
}
|
|
1910
2331
|
}
|
|
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
2332
|
}
|
|
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;
|
|
2333
|
+
function findSnapTarget(x, y, excludeElementId) {
|
|
1960
2334
|
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
|
-
};
|
|
2335
|
+
if (!def) return null;
|
|
2336
|
+
const snap2 = getCurrentSnapshot();
|
|
2337
|
+
const allAnchors = [];
|
|
2338
|
+
for (const baseEl of def.elements) {
|
|
2339
|
+
if (baseEl.id === excludeElementId) continue;
|
|
2340
|
+
if (baseEl.type === "line" || baseEl.type === "arrow") continue;
|
|
2341
|
+
const state = snap2.get(baseEl.id);
|
|
2342
|
+
if (!state || !state.visible) continue;
|
|
2343
|
+
const points = getAnchorPoints(baseEl, state);
|
|
2344
|
+
allAnchors.push(...points);
|
|
2345
|
+
}
|
|
2346
|
+
const nearest = findNearestAnchor({ x, y }, allAnchors, SNAP_RADIUS);
|
|
2347
|
+
if (!nearest) return null;
|
|
2348
|
+
return {
|
|
2349
|
+
elementId: nearest.elementId,
|
|
2350
|
+
anchor: nearest.anchor,
|
|
2351
|
+
x: nearest.x,
|
|
2352
|
+
y: nearest.y
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
function renderGroupOutline(groupId) {
|
|
2356
|
+
const box = groupBbox(groupId);
|
|
2357
|
+
if (!box) return null;
|
|
2358
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2359
|
+
g.setAttribute("data-elem-id", groupId);
|
|
2360
|
+
const pad = 6;
|
|
2361
|
+
const x = box.x - pad;
|
|
2362
|
+
const y = box.y - pad;
|
|
2363
|
+
const w = box.w + pad * 2;
|
|
2364
|
+
const h = box.h + pad * 2;
|
|
2365
|
+
const thickness = 8;
|
|
2366
|
+
g.innerHTML = `
|
|
2367
|
+
<rect x="${x}" y="${y}" width="${w}" height="${h}"
|
|
2368
|
+
fill="none" stroke="var(--color-accent)" stroke-width="2" stroke-dasharray="6 4" rx="4" pointer-events="none" />
|
|
2369
|
+
<rect x="${x - thickness}" y="${y - thickness}" width="${w + thickness * 2}" height="${thickness * 2}"
|
|
2370
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2371
|
+
<rect x="${x - thickness}" y="${y + h - thickness}" width="${w + thickness * 2}" height="${thickness * 2}"
|
|
2372
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2373
|
+
<rect x="${x - thickness}" y="${y}" width="${thickness * 2}" height="${h}"
|
|
2374
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2375
|
+
<rect x="${x + w - thickness}" y="${y}" width="${thickness * 2}" height="${h}"
|
|
2376
|
+
fill="rgba(0,0,0,0.0001)" style="cursor: move" pointer-events="all" />
|
|
2377
|
+
`;
|
|
2378
|
+
return g;
|
|
2379
|
+
}
|
|
2380
|
+
function makeG(elementId, rotation, cx, cy) {
|
|
2381
|
+
const g = document.createElementNS(SVG_NS, "g");
|
|
2382
|
+
g.setAttribute("data-elem-id", elementId);
|
|
2383
|
+
g.classList.add("element-handle");
|
|
2384
|
+
if (rotation) g.setAttribute("transform", `rotate(${rotation} ${cx} ${cy})`);
|
|
2385
|
+
return g;
|
|
2386
|
+
}
|
|
2387
|
+
function renderElement(baseEl, state, snap2, byId) {
|
|
2388
|
+
const rotation = state.rotation || 0;
|
|
2389
|
+
if (baseEl.type === "rect") {
|
|
2390
|
+
const r = state;
|
|
2391
|
+
const cx = r.x + r.width / 2;
|
|
2392
|
+
const cy = r.y + r.height / 2;
|
|
2393
|
+
const g = makeG(baseEl.id, rotation, cx, cy);
|
|
2394
|
+
g.innerHTML = `
|
|
2395
|
+
<rect x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}" rx="${r.cornerRadius}"
|
|
2396
|
+
fill="${r.fill}" stroke="${r.stroke}" stroke-width="${r.strokeWidth}" />
|
|
2397
|
+
${r.label ? `<text x="${cx}" y="${cy + 5}" text-anchor="middle"
|
|
2398
|
+
font-size="${r.labelSize}" font-weight="600" fill="${r.labelColor}">${escapeXml(r.label)}</text>` : ""}
|
|
2399
|
+
${r.subtitle ? `<text x="${cx}" y="${cy + r.labelSize + 8}"
|
|
2400
|
+
text-anchor="middle" font-size="10" fill="${r.labelColor}" opacity="0.7">${escapeXml(r.subtitle)}</text>` : ""}
|
|
2401
|
+
`;
|
|
2402
|
+
return g;
|
|
2403
|
+
}
|
|
2404
|
+
if (baseEl.type === "circle") {
|
|
2405
|
+
const c = state;
|
|
2406
|
+
const g = makeG(baseEl.id, rotation, c.cx, c.cy);
|
|
2407
|
+
g.innerHTML = `
|
|
2408
|
+
<circle cx="${c.cx}" cy="${c.cy}" r="${c.r}" fill="${c.fill}" stroke="${c.stroke}"
|
|
2409
|
+
stroke-width="${c.strokeWidth}" />
|
|
2410
|
+
${c.label ? `<text x="${c.cx}" y="${c.cy + 5}" text-anchor="middle"
|
|
2411
|
+
font-size="${c.labelSize}" font-weight="600" fill="${c.labelColor}">${escapeXml(c.label)}</text>` : ""}
|
|
2412
|
+
`;
|
|
2413
|
+
return g;
|
|
2414
|
+
}
|
|
2415
|
+
if (baseEl.type === "line") {
|
|
2416
|
+
const l = state;
|
|
2417
|
+
const coords = resolveLineCoords(l, snap2, byId);
|
|
2418
|
+
if (!coords) return null;
|
|
2419
|
+
const g = makeG(baseEl.id, 0, 0, 0);
|
|
2420
|
+
g.style.color = l.stroke;
|
|
2421
|
+
const dash = l.strokeDasharray ? `stroke-dasharray="${l.strokeDasharray}"` : "";
|
|
2422
|
+
const mStart = markerUrlFor(l.headStart, "start");
|
|
2423
|
+
const mEnd = markerUrlFor(l.headEnd, "end");
|
|
2424
|
+
const mAttr = `${mStart ? `marker-start="${mStart}"` : ""} ${mEnd ? `marker-end="${mEnd}"` : ""}`;
|
|
2425
|
+
g.innerHTML = `<line x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}"
|
|
2426
|
+
stroke="currentColor" stroke-width="${l.strokeWidth}" ${dash} ${mAttr} />`;
|
|
2427
|
+
return g;
|
|
2428
|
+
}
|
|
2429
|
+
if (baseEl.type === "arrow") {
|
|
2430
|
+
const a = state;
|
|
2431
|
+
const coords = resolveArrowCoords(a, snap2, byId);
|
|
2432
|
+
if (!coords) return null;
|
|
2433
|
+
const g = makeG(baseEl.id, 0, 0, 0);
|
|
2434
|
+
g.style.color = a.stroke;
|
|
2435
|
+
const dash = a.strokeDasharray ? `stroke-dasharray="${a.strokeDasharray}"` : "";
|
|
2436
|
+
const midX = (coords.x1 + coords.x2) / 2;
|
|
2437
|
+
const midY = (coords.y1 + coords.y2) / 2;
|
|
2438
|
+
const isCurved = (a.curvature || 0) !== 0;
|
|
2439
|
+
const mStart = markerUrlFor(a.headStart ?? "none", "start");
|
|
2440
|
+
const mEnd = markerUrlFor(a.headEnd ?? "arrow", "end");
|
|
2441
|
+
const mAttr = `${mStart ? `marker-start="${mStart}"` : ""} ${mEnd ? `marker-end="${mEnd}"` : ""}`;
|
|
2442
|
+
let pathHtml;
|
|
2443
|
+
if (isCurved) {
|
|
2444
|
+
const dx = coords.x2 - coords.x1;
|
|
2445
|
+
const dy = coords.y2 - coords.y1;
|
|
2446
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
2447
|
+
const nx = -dy / len;
|
|
2448
|
+
const ny = dx / len;
|
|
2449
|
+
const cpx = midX + nx * (a.curvature ?? 0);
|
|
2450
|
+
const cpy = midY + ny * (a.curvature ?? 0);
|
|
2451
|
+
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} />`;
|
|
2452
|
+
} else {
|
|
2453
|
+
pathHtml = `<line x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}" stroke="currentColor" stroke-width="${a.strokeWidth}" ${dash} ${mAttr} />`;
|
|
2106
2454
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2455
|
+
g.innerHTML = `
|
|
2456
|
+
${pathHtml}
|
|
2457
|
+
${a.label ? `<g>
|
|
2458
|
+
<rect x="${midX - 80}" y="${midY - 11}" width="160" height="22" rx="4" fill="white" stroke="currentColor" stroke-opacity="0.4"/>
|
|
2459
|
+
<text x="${midX}" y="${midY + 4}" text-anchor="middle" font-size="12" font-family="ui-monospace, monospace" fill="${a.labelColor}">${escapeXml(a.label)}</text>
|
|
2460
|
+
</g>` : ""}
|
|
2461
|
+
`;
|
|
2462
|
+
return g;
|
|
2109
2463
|
}
|
|
2464
|
+
if (baseEl.type === "text") {
|
|
2465
|
+
const t = state;
|
|
2466
|
+
const g = makeG(baseEl.id, rotation, t.x, t.y);
|
|
2467
|
+
g.innerHTML = `<text x="${t.x}" y="${t.y}" font-size="${t.fontSize}" font-weight="${t.fontWeight}"
|
|
2468
|
+
fill="${t.color}" text-anchor="${t.textAnchor}">${escapeXml(t.content)}</text>`;
|
|
2469
|
+
return g;
|
|
2470
|
+
}
|
|
2471
|
+
if (baseEl.type === "image") {
|
|
2472
|
+
const i = state;
|
|
2473
|
+
const cx = i.x + i.width / 2;
|
|
2474
|
+
const cy = i.y + i.height / 2;
|
|
2475
|
+
const g = makeG(baseEl.id, rotation, cx, cy);
|
|
2476
|
+
const resolved = resolveAsset(i.assetId, getDef()?.assets ?? {});
|
|
2477
|
+
const href = resolved.status === "resolved" ? resolved.href ?? "" : "";
|
|
2478
|
+
g.innerHTML = href ? `<image href="${escapeXml(href)}" x="${i.x}" y="${i.y}" width="${i.width}"
|
|
2479
|
+
height="${i.height}" preserveAspectRatio="${i.preserveAspectRatio}" opacity="${i.opacity}" />` : `<rect x="${i.x}" y="${i.y}" width="${i.width}" height="${i.height}" fill="none"
|
|
2480
|
+
stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.4" />`;
|
|
2481
|
+
return g;
|
|
2482
|
+
}
|
|
2483
|
+
if (baseEl.type === "path") {
|
|
2484
|
+
const p = state;
|
|
2485
|
+
const g = makeG(baseEl.id, rotation, p.x, p.y);
|
|
2486
|
+
const dash = p.strokeDasharray ? `stroke-dasharray="${p.strokeDasharray}"` : "";
|
|
2487
|
+
const xform = p.x || p.y ? `transform="translate(${p.x} ${p.y})"` : "";
|
|
2488
|
+
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" />`;
|
|
2489
|
+
return g;
|
|
2490
|
+
}
|
|
2491
|
+
if (baseEl.type === "polygon") {
|
|
2492
|
+
const pg = state;
|
|
2493
|
+
const g = makeG(baseEl.id, rotation, 0, 0);
|
|
2494
|
+
g.innerHTML = `<polygon points="${escapeXml(pg.points)}" fill="${pg.fill}" stroke="${pg.stroke}" stroke-width="${pg.strokeWidth}" opacity="${pg.opacity}" pointer-events="all" />`;
|
|
2495
|
+
return g;
|
|
2496
|
+
}
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
function showPreview2(def, options) {
|
|
2500
|
+
showPreview(canvasEl, def, options);
|
|
2501
|
+
}
|
|
2502
|
+
function hidePreview2() {
|
|
2503
|
+
hidePreview(canvasEl);
|
|
2110
2504
|
}
|
|
2111
2505
|
|
|
2112
2506
|
// src/legacy/studio-focus.ts
|
|
@@ -2438,6 +2832,17 @@ function renderInner() {
|
|
|
2438
2832
|
"\uBAA9\uCC28 \uD45C\uC2DC (chapter list)",
|
|
2439
2833
|
"settings.showChapterList",
|
|
2440
2834
|
def.settings.showChapterList ?? false
|
|
2835
|
+
),
|
|
2836
|
+
selectField(
|
|
2837
|
+
"\uBAA9\uCC28 \uC704\uCE58",
|
|
2838
|
+
"settings.chapterListPosition",
|
|
2839
|
+
def.settings.chapterListPosition ?? "right",
|
|
2840
|
+
[
|
|
2841
|
+
{ value: "left", label: "\uC88C\uCE21" },
|
|
2842
|
+
{ value: "right", label: "\uC6B0\uCE21" },
|
|
2843
|
+
{ value: "top", label: "\uC0C1\uB2E8" },
|
|
2844
|
+
{ value: "bottom", label: "\uD558\uB2E8" }
|
|
2845
|
+
]
|
|
2441
2846
|
)
|
|
2442
2847
|
].join("");
|
|
2443
2848
|
panelEl.innerHTML = `
|
|
@@ -2612,6 +3017,14 @@ function renderBaseFields(el) {
|
|
|
2612
3017
|
numberFields.push({ label: "cy", key: "cy", value: e.cy });
|
|
2613
3018
|
numberFields.push({ label: "r", key: "r", value: e.r });
|
|
2614
3019
|
}
|
|
3020
|
+
if (el.type === "polygon") {
|
|
3021
|
+
const pointCount = String(e.points ?? "").trim().split(/\s+/).filter(Boolean).length;
|
|
3022
|
+
numberFields.push({
|
|
3023
|
+
label: "\uBCC0 \uAC1C\uC218",
|
|
3024
|
+
key: "polygonSides",
|
|
3025
|
+
value: pointCount
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
2615
3028
|
if (el.type === "line" || el.type === "arrow") {
|
|
2616
3029
|
if (typeof e.x1 === "number") {
|
|
2617
3030
|
numberFields.push({ label: "x1", key: "x1", value: e.x1 });
|
|
@@ -2757,12 +3170,25 @@ function onInput(e) {
|
|
|
2757
3170
|
if (!key) return;
|
|
2758
3171
|
if (target.type === "text" && e.isComposing) return;
|
|
2759
3172
|
if (target.type === "text" && isComposingFallback) return;
|
|
3173
|
+
if (target.type === "color") {
|
|
3174
|
+
const textInput = target.parentElement?.querySelector(
|
|
3175
|
+
`input[type="text"][data-prop-key="${CSS.escape(key)}"]`
|
|
3176
|
+
);
|
|
3177
|
+
if (textInput) textInput.value = target.value;
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
2760
3180
|
let value = target.value;
|
|
2761
3181
|
if (target.type === "number") value = Number(target.value);
|
|
2762
3182
|
else if (target.type === "checkbox") value = target.checked;
|
|
2763
3183
|
apply(key, value);
|
|
2764
3184
|
}
|
|
2765
3185
|
function onChange(e) {
|
|
3186
|
+
const target = e.target;
|
|
3187
|
+
if (target instanceof HTMLInputElement && target.type === "color") {
|
|
3188
|
+
const key = target.dataset.propKey;
|
|
3189
|
+
if (key) apply(key, target.value);
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
2766
3192
|
onInput(e);
|
|
2767
3193
|
}
|
|
2768
3194
|
function apply(key, value) {
|
|
@@ -2784,11 +3210,33 @@ function apply(key, value) {
|
|
|
2784
3210
|
updateSettings({ showCaption: Boolean(value) });
|
|
2785
3211
|
else if (key === "settings.showChapterList")
|
|
2786
3212
|
updateSettings({ showChapterList: Boolean(value) });
|
|
3213
|
+
else if (key === "settings.chapterListPosition")
|
|
3214
|
+
updateSettings({
|
|
3215
|
+
chapterListPosition: String(value)
|
|
3216
|
+
});
|
|
2787
3217
|
else if (key.startsWith("el.") && sel.kind === "element") {
|
|
2788
3218
|
const prop = key.slice(3);
|
|
2789
3219
|
const time = getCurrentTime();
|
|
2790
3220
|
const el = def.elements.find((e) => e.id === sel.elementId);
|
|
2791
3221
|
if (!el) return;
|
|
3222
|
+
if (prop === "polygonSides" && el.type === "polygon") {
|
|
3223
|
+
const points = el.points.trim().split(/\s+/).map((point) => point.split(",").map(Number)).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
|
|
3224
|
+
if (points.length < 3) return;
|
|
3225
|
+
const minX = Math.min(...points.map(([x]) => x));
|
|
3226
|
+
const maxX = Math.max(...points.map(([x]) => x));
|
|
3227
|
+
const minY = Math.min(...points.map(([, y]) => y));
|
|
3228
|
+
const maxY = Math.max(...points.map(([, y]) => y));
|
|
3229
|
+
const cx = (minX + maxX) / 2;
|
|
3230
|
+
const cy = (minY + maxY) / 2;
|
|
3231
|
+
const radius = Math.max(4, Math.max(maxX - minX, maxY - minY) / 2);
|
|
3232
|
+
const sides = Math.max(3, Math.min(24, Math.round(Number(value)) || 3));
|
|
3233
|
+
const next = Array.from({ length: sides }, (_, index) => {
|
|
3234
|
+
const angle = index * Math.PI * 2 / sides - Math.PI / 2;
|
|
3235
|
+
return `${(cx + radius * Math.cos(angle)).toFixed(1)},${(cy + radius * Math.sin(angle)).toFixed(1)}`;
|
|
3236
|
+
}).join(" ");
|
|
3237
|
+
updateElementBase(sel.elementId, { points: next });
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
2792
3240
|
const hasTrack = el.tracks.some((t) => t.property === prop);
|
|
2793
3241
|
if (hasTrack) {
|
|
2794
3242
|
setTrackKeyframe(sel.elementId, prop, time, value);
|
|
@@ -2959,6 +3407,20 @@ try {
|
|
|
2959
3407
|
} catch {
|
|
2960
3408
|
}
|
|
2961
3409
|
var dragMode = null;
|
|
3410
|
+
var boundDocuments = /* @__PURE__ */ new WeakMap();
|
|
3411
|
+
function bindTimelinePointerDocument(doc) {
|
|
3412
|
+
const existing = boundDocuments.get(doc);
|
|
3413
|
+
if (existing) return existing;
|
|
3414
|
+
doc.addEventListener("mousemove", onMouseMove2);
|
|
3415
|
+
doc.addEventListener("mouseup", onMouseUp2);
|
|
3416
|
+
const cleanup = () => {
|
|
3417
|
+
doc.removeEventListener("mousemove", onMouseMove2);
|
|
3418
|
+
doc.removeEventListener("mouseup", onMouseUp2);
|
|
3419
|
+
boundDocuments.delete(doc);
|
|
3420
|
+
};
|
|
3421
|
+
boundDocuments.set(doc, cleanup);
|
|
3422
|
+
return cleanup;
|
|
3423
|
+
}
|
|
2962
3424
|
function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
|
|
2963
3425
|
tracksEl = tracksRoot;
|
|
2964
3426
|
elTracksEl = elementTracks;
|
|
@@ -2968,8 +3430,7 @@ function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
|
|
|
2968
3430
|
tracksRoot.addEventListener("mousedown", onMouseDown2);
|
|
2969
3431
|
elementTracks.addEventListener("click", onElTracksClick);
|
|
2970
3432
|
elementTracks.addEventListener("mousedown", onMouseDown2);
|
|
2971
|
-
document
|
|
2972
|
-
document.addEventListener("mouseup", onMouseUp2);
|
|
3433
|
+
bindTimelinePointerDocument(document);
|
|
2973
3434
|
addBtn?.addEventListener("click", () => {
|
|
2974
3435
|
const id = uniqueChapterId();
|
|
2975
3436
|
const def = getDef();
|
|
@@ -3029,9 +3490,15 @@ function mirrorScroll(source, target) {
|
|
|
3029
3490
|
}
|
|
3030
3491
|
function showDragTooltip(clientX, clientY, text) {
|
|
3031
3492
|
if (!dragTooltipEl) {
|
|
3032
|
-
|
|
3493
|
+
const ownerDocument = tracksEl?.ownerDocument ?? document;
|
|
3494
|
+
dragTooltipEl = ownerDocument.createElement("div");
|
|
3033
3495
|
dragTooltipEl.className = "studio-tl-drag-tooltip";
|
|
3034
|
-
|
|
3496
|
+
ownerDocument.body.appendChild(dragTooltipEl);
|
|
3497
|
+
} else if (tracksEl && dragTooltipEl.ownerDocument !== tracksEl.ownerDocument) {
|
|
3498
|
+
dragTooltipEl.remove();
|
|
3499
|
+
dragTooltipEl = null;
|
|
3500
|
+
showDragTooltip(clientX, clientY, text);
|
|
3501
|
+
return;
|
|
3035
3502
|
}
|
|
3036
3503
|
dragTooltipEl.textContent = text;
|
|
3037
3504
|
dragTooltipEl.style.left = `${clientX + 14}px`;
|
|
@@ -3110,7 +3577,20 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
3110
3577
|
elTracksEl.innerHTML = '<p class="studio-tl-empty">\uC694\uC18C \uC5C6\uC74C</p>';
|
|
3111
3578
|
return;
|
|
3112
3579
|
}
|
|
3580
|
+
const byId = new Map(elements.map((element) => [element.id, element]));
|
|
3581
|
+
const depthOf = (element) => {
|
|
3582
|
+
let depth = 0;
|
|
3583
|
+
let parentId = element.parentId;
|
|
3584
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3585
|
+
while (parentId && !seen.has(parentId)) {
|
|
3586
|
+
seen.add(parentId);
|
|
3587
|
+
depth += 1;
|
|
3588
|
+
parentId = byId.get(parentId)?.parentId;
|
|
3589
|
+
}
|
|
3590
|
+
return depth;
|
|
3591
|
+
};
|
|
3113
3592
|
const rows = elements.map((el) => {
|
|
3593
|
+
const depth = depthOf(el);
|
|
3114
3594
|
const isSel = sel.kind === "element" && sel.elementId === el.id || sel.kind === "elements" && sel.elementIds.includes(el.id);
|
|
3115
3595
|
const appearanceBars = el.appearances.map((ap, idx) => {
|
|
3116
3596
|
const left = timeToPx(ap.start);
|
|
@@ -3127,8 +3607,8 @@ function renderElementTracks(elements, currentTime, totalPx, sel) {
|
|
|
3127
3607
|
(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
3608
|
).join("");
|
|
3129
3609
|
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>
|
|
3610
|
+
<div class="studio-tl-row studio-tl-element-row ${isSel ? "is-selected" : ""} ${el.type === "group" ? "is-group" : ""}" data-elem-id="${escapeHtml3(el.id)}">
|
|
3611
|
+
<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
3612
|
<div class="studio-tl-body" style="width:${totalPx}px">
|
|
3133
3613
|
<div class="studio-tl-element-track" data-tl-area="elements">
|
|
3134
3614
|
${appearanceBars}
|
|
@@ -14852,10 +15332,10 @@ async function uploadAndInsertImage(file, host) {
|
|
|
14852
15332
|
}
|
|
14853
15333
|
try {
|
|
14854
15334
|
host.setStatus("\uC774\uBBF8\uC9C0 \uCC98\uB9AC \uC911\u2026");
|
|
14855
|
-
const
|
|
15335
|
+
const source = host.resolveImage ? await host.resolveImage(file) : await readAsDataUrl(file);
|
|
14856
15336
|
const cx = def.canvas.width / 2;
|
|
14857
15337
|
const cy = def.canvas.height / 2;
|
|
14858
|
-
const tempImg = await loadImageSize(
|
|
15338
|
+
const tempImg = await loadImageSize(source);
|
|
14859
15339
|
const maxDim = Math.min(def.canvas.width, def.canvas.height) * 0.5;
|
|
14860
15340
|
let w = tempImg?.w ?? 200;
|
|
14861
15341
|
let h = tempImg?.h ?? 200;
|
|
@@ -14875,7 +15355,7 @@ async function uploadAndInsertImage(file, host) {
|
|
|
14875
15355
|
y: Math.round(cy - h / 2),
|
|
14876
15356
|
width: w,
|
|
14877
15357
|
height: h,
|
|
14878
|
-
assetId: registerDataUriAsset(
|
|
15358
|
+
assetId: source.startsWith("data:") ? registerDataUriAsset(source) : registerExternalAsset(source),
|
|
14879
15359
|
preserveAspectRatio: "xMidYMid meet",
|
|
14880
15360
|
opacity: 1
|
|
14881
15361
|
});
|
|
@@ -14944,7 +15424,12 @@ function escapeHtml5(s) {
|
|
|
14944
15424
|
function startDraft() {
|
|
14945
15425
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
14946
15426
|
const id = `draft-${stamp}`;
|
|
14947
|
-
const def = animationDocumentSchema.parse({
|
|
15427
|
+
const def = animationDocumentSchema.parse({
|
|
15428
|
+
clothoVersion: 1,
|
|
15429
|
+
id,
|
|
15430
|
+
title: "",
|
|
15431
|
+
description: ""
|
|
15432
|
+
});
|
|
14948
15433
|
setDraft(def);
|
|
14949
15434
|
}
|
|
14950
15435
|
async function openLibrary(ui) {
|
|
@@ -15372,6 +15857,9 @@ function queryUi() {
|
|
|
15372
15857
|
const idDisplay = $("studio-id-display");
|
|
15373
15858
|
const status = $("studio-status");
|
|
15374
15859
|
const saveBtn = $("studio-save");
|
|
15860
|
+
const exportBtn = $("studio-export-json");
|
|
15861
|
+
const importBtn = $("studio-import-json");
|
|
15862
|
+
const importFileInput = $("studio-import-file");
|
|
15375
15863
|
const deleteBtn = $("studio-delete");
|
|
15376
15864
|
const newBtn = $("studio-new");
|
|
15377
15865
|
const openBtn = $("studio-open");
|
|
@@ -15379,6 +15867,8 @@ function queryUi() {
|
|
|
15379
15867
|
const restartBtn = $("studio-restart");
|
|
15380
15868
|
const speedInput = $("studio-speed");
|
|
15381
15869
|
const speedValue = $("studio-speed-value");
|
|
15870
|
+
const previewLoopInput = $("studio-preview-loop");
|
|
15871
|
+
const detachTimelineBtn = $("studio-detach-timeline");
|
|
15382
15872
|
const canvas = document.getElementById(
|
|
15383
15873
|
"studio-canvas"
|
|
15384
15874
|
);
|
|
@@ -15404,7 +15894,7 @@ function queryUi() {
|
|
|
15404
15894
|
const undoBtn = $("studio-undo");
|
|
15405
15895
|
const redoBtn = $("studio-redo");
|
|
15406
15896
|
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) {
|
|
15897
|
+
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
15898
|
return null;
|
|
15409
15899
|
}
|
|
15410
15900
|
return {
|
|
@@ -15413,6 +15903,9 @@ function queryUi() {
|
|
|
15413
15903
|
idDisplay,
|
|
15414
15904
|
status,
|
|
15415
15905
|
saveBtn,
|
|
15906
|
+
exportBtn,
|
|
15907
|
+
importBtn,
|
|
15908
|
+
importFileInput,
|
|
15416
15909
|
deleteBtn,
|
|
15417
15910
|
newBtn,
|
|
15418
15911
|
openBtn,
|
|
@@ -15420,6 +15913,8 @@ function queryUi() {
|
|
|
15420
15913
|
restartBtn,
|
|
15421
15914
|
speedInput,
|
|
15422
15915
|
speedValue,
|
|
15916
|
+
previewLoopInput,
|
|
15917
|
+
detachTimelineBtn,
|
|
15423
15918
|
canvas,
|
|
15424
15919
|
elementList,
|
|
15425
15920
|
toolsRoot,
|
|
@@ -15455,39 +15950,59 @@ function startInlineTextEdit(canvas, el) {
|
|
|
15455
15950
|
"foreignObject"
|
|
15456
15951
|
);
|
|
15457
15952
|
fo.setAttribute("data-inline-text-editor", "");
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
const
|
|
15953
|
+
const width = Math.max(80, (el.content?.length ?? 5) * fontSize * 0.7 + 20);
|
|
15954
|
+
const anchor = el.textAnchor ?? "start";
|
|
15955
|
+
const x = anchor === "middle" ? el.x - width / 2 : anchor === "end" ? el.x - width : el.x;
|
|
15956
|
+
fo.setAttribute("x", String(x));
|
|
15957
|
+
fo.setAttribute("y", String(el.y - fontSize * 1.15));
|
|
15461
15958
|
fo.setAttribute("width", String(width));
|
|
15462
|
-
fo.setAttribute("height", String(fontSize * 1.
|
|
15463
|
-
const
|
|
15464
|
-
|
|
15465
|
-
|
|
15466
|
-
|
|
15959
|
+
fo.setAttribute("height", String(fontSize * 1.8));
|
|
15960
|
+
const renderedText = Array.from(
|
|
15961
|
+
canvas.querySelectorAll("[data-elem-id]")
|
|
15962
|
+
).find((node) => node.dataset.elemId === el.id);
|
|
15963
|
+
const previousVisibility = renderedText?.style.visibility ?? "";
|
|
15964
|
+
if (renderedText) renderedText.style.visibility = "hidden";
|
|
15965
|
+
const editor = document.createElement("div");
|
|
15966
|
+
editor.contentEditable = "true";
|
|
15967
|
+
editor.spellcheck = false;
|
|
15968
|
+
editor.textContent = el.content ?? "";
|
|
15969
|
+
editor.setAttribute("role", "textbox");
|
|
15970
|
+
editor.setAttribute("aria-label", "\uD14D\uC2A4\uD2B8 \uB0B4\uC6A9 \uD3B8\uC9D1");
|
|
15971
|
+
Object.assign(editor.style, {
|
|
15467
15972
|
width: "100%",
|
|
15468
15973
|
height: "100%",
|
|
15469
15974
|
boxSizing: "border-box",
|
|
15470
|
-
border: "
|
|
15471
|
-
|
|
15472
|
-
background: "
|
|
15473
|
-
padding: "0
|
|
15975
|
+
border: "0",
|
|
15976
|
+
borderBottom: "2px solid #6366f1",
|
|
15977
|
+
background: "color-mix(in srgb, Canvas 82%, transparent)",
|
|
15978
|
+
padding: "0 2px",
|
|
15474
15979
|
font: `${el.fontWeight ?? 400} ${fontSize}px ui-sans-serif,system-ui,sans-serif`,
|
|
15475
15980
|
color: el.color ?? "#0f172a",
|
|
15476
15981
|
outline: "none",
|
|
15477
|
-
|
|
15982
|
+
whiteSpace: "pre",
|
|
15983
|
+
overflow: "hidden"
|
|
15478
15984
|
});
|
|
15479
|
-
fo.appendChild(
|
|
15985
|
+
fo.appendChild(editor);
|
|
15480
15986
|
canvas.appendChild(fo);
|
|
15481
15987
|
requestAnimationFrame(() => {
|
|
15482
|
-
|
|
15483
|
-
|
|
15988
|
+
editor.focus();
|
|
15989
|
+
const range = document.createRange();
|
|
15990
|
+
range.selectNodeContents(editor);
|
|
15991
|
+
range.collapse(false);
|
|
15992
|
+
const selection = window.getSelection();
|
|
15993
|
+
selection?.removeAllRanges();
|
|
15994
|
+
selection?.addRange(range);
|
|
15484
15995
|
});
|
|
15485
15996
|
let settled = false;
|
|
15997
|
+
const restoreRenderedText = () => {
|
|
15998
|
+
if (renderedText) renderedText.style.visibility = previousVisibility;
|
|
15999
|
+
};
|
|
15486
16000
|
const commit = () => {
|
|
15487
16001
|
if (settled) return;
|
|
15488
16002
|
settled = true;
|
|
15489
|
-
const newContent =
|
|
16003
|
+
const newContent = editor.textContent ?? "";
|
|
15490
16004
|
fo.remove();
|
|
16005
|
+
restoreRenderedText();
|
|
15491
16006
|
if (newContent !== el.content) {
|
|
15492
16007
|
updateElementBase(el.id, { content: newContent });
|
|
15493
16008
|
}
|
|
@@ -15496,16 +16011,17 @@ function startInlineTextEdit(canvas, el) {
|
|
|
15496
16011
|
if (settled) return;
|
|
15497
16012
|
settled = true;
|
|
15498
16013
|
fo.remove();
|
|
16014
|
+
restoreRenderedText();
|
|
15499
16015
|
};
|
|
15500
16016
|
let isComposingFallback2 = false;
|
|
15501
|
-
|
|
16017
|
+
editor.addEventListener("compositionstart", () => {
|
|
15502
16018
|
isComposingFallback2 = true;
|
|
15503
16019
|
});
|
|
15504
|
-
|
|
16020
|
+
editor.addEventListener("compositionend", () => {
|
|
15505
16021
|
isComposingFallback2 = false;
|
|
15506
16022
|
});
|
|
15507
|
-
|
|
15508
|
-
|
|
16023
|
+
editor.addEventListener("blur", commit);
|
|
16024
|
+
editor.addEventListener("keydown", (e) => {
|
|
15509
16025
|
if (e.isComposing || isComposingFallback2) return;
|
|
15510
16026
|
if (e.key === "Enter") {
|
|
15511
16027
|
e.preventDefault();
|
|
@@ -15642,7 +16158,12 @@ function togglePlay(ui) {
|
|
|
15642
16158
|
if (playState === "playing") {
|
|
15643
16159
|
stopPreview(ui);
|
|
15644
16160
|
} else {
|
|
15645
|
-
showPreview2(def
|
|
16161
|
+
showPreview2(def, {
|
|
16162
|
+
loop: ui.previewLoopInput.checked,
|
|
16163
|
+
speed: Number(ui.speedInput.value),
|
|
16164
|
+
onTime: setCurrentTime,
|
|
16165
|
+
onEnd: () => stopPreview(ui)
|
|
16166
|
+
});
|
|
15646
16167
|
playState = "playing";
|
|
15647
16168
|
ui.playBtn.textContent = "\u23F9 Stop";
|
|
15648
16169
|
}
|
|
@@ -15832,9 +16353,12 @@ function bindKeyboardShortcuts(ui) {
|
|
|
15832
16353
|
const inText = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLElement && e.target.isContentEditable;
|
|
15833
16354
|
const mod = e.metaKey || e.ctrlKey;
|
|
15834
16355
|
if (e.key === "Escape") {
|
|
15835
|
-
|
|
15836
|
-
|
|
15837
|
-
|
|
16356
|
+
if (!inText) {
|
|
16357
|
+
setActiveTool("select");
|
|
16358
|
+
const sel = getSelection();
|
|
16359
|
+
if (sel.kind !== "none") {
|
|
16360
|
+
setSelection({ kind: "none" });
|
|
16361
|
+
}
|
|
15838
16362
|
}
|
|
15839
16363
|
return;
|
|
15840
16364
|
}
|
|
@@ -15899,6 +16423,16 @@ function bindKeyboardShortcuts(ui) {
|
|
|
15899
16423
|
if (selectAdjacentElement(e.shiftKey ? -1 : 1)) e.preventDefault();
|
|
15900
16424
|
return;
|
|
15901
16425
|
}
|
|
16426
|
+
const shortcutTool = toolForShortcut(e.key, {
|
|
16427
|
+
isEditing: inText,
|
|
16428
|
+
modifierKey: mod,
|
|
16429
|
+
altKey: e.altKey
|
|
16430
|
+
});
|
|
16431
|
+
if (shortcutTool) {
|
|
16432
|
+
e.preventDefault();
|
|
16433
|
+
setActiveTool(shortcutTool);
|
|
16434
|
+
return;
|
|
16435
|
+
}
|
|
15902
16436
|
if (!inText && !mod && (e.key === "g" || e.key === "G")) {
|
|
15903
16437
|
e.preventDefault();
|
|
15904
16438
|
setGridEnabled(!isGridEnabled());
|
|
@@ -16002,8 +16536,176 @@ function bindKeyboardShortcuts(ui) {
|
|
|
16002
16536
|
});
|
|
16003
16537
|
}
|
|
16004
16538
|
|
|
16539
|
+
// src/legacy/popup-document.ts
|
|
16540
|
+
function prepareStudioPopupDocument(popupDocument, sourceDocument, sourceApp, options) {
|
|
16541
|
+
popupDocument.open();
|
|
16542
|
+
popupDocument.write(
|
|
16543
|
+
`<!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>`
|
|
16544
|
+
);
|
|
16545
|
+
popupDocument.close();
|
|
16546
|
+
const base = popupDocument.createElement("base");
|
|
16547
|
+
base.href = sourceDocument.baseURI;
|
|
16548
|
+
popupDocument.head.prepend(base);
|
|
16549
|
+
for (const source of sourceDocument.querySelectorAll(
|
|
16550
|
+
'link[rel="stylesheet"], style'
|
|
16551
|
+
)) {
|
|
16552
|
+
popupDocument.head.appendChild(source.cloneNode(true));
|
|
16553
|
+
}
|
|
16554
|
+
const computed = getComputedStyle(sourceApp);
|
|
16555
|
+
const target = popupDocument.documentElement.style;
|
|
16556
|
+
for (let index = 0; index < computed.length; index += 1) {
|
|
16557
|
+
const property = computed.item(index);
|
|
16558
|
+
if (property.startsWith("--")) {
|
|
16559
|
+
target.setProperty(property, computed.getPropertyValue(property));
|
|
16560
|
+
}
|
|
16561
|
+
}
|
|
16562
|
+
}
|
|
16563
|
+
function escapeHtml8(value) {
|
|
16564
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
16565
|
+
}
|
|
16566
|
+
|
|
16567
|
+
// src/legacy/timeline-popout.ts
|
|
16568
|
+
var POPUP_FEATURES = "popup=yes,width=1200,height=620";
|
|
16569
|
+
function setupTimelinePopout(ui, _togglePlayback) {
|
|
16570
|
+
const timeline = ui.detachTimelineBtn.closest(
|
|
16571
|
+
".studio-timeline-wrap"
|
|
16572
|
+
);
|
|
16573
|
+
if (!timeline) return;
|
|
16574
|
+
let popup = null;
|
|
16575
|
+
let placeholder = null;
|
|
16576
|
+
let closeTimer = null;
|
|
16577
|
+
let unbindPopupPointerEvents = null;
|
|
16578
|
+
let restoring = false;
|
|
16579
|
+
const restoreTimeline = (closePopup) => {
|
|
16580
|
+
if (restoring) return;
|
|
16581
|
+
restoring = true;
|
|
16582
|
+
if (placeholder?.parentNode) {
|
|
16583
|
+
placeholder.parentNode.insertBefore(timeline, placeholder.nextSibling);
|
|
16584
|
+
placeholder.remove();
|
|
16585
|
+
}
|
|
16586
|
+
placeholder = null;
|
|
16587
|
+
unbindPopupPointerEvents?.();
|
|
16588
|
+
unbindPopupPointerEvents = null;
|
|
16589
|
+
if (closeTimer !== null) window.clearInterval(closeTimer);
|
|
16590
|
+
closeTimer = null;
|
|
16591
|
+
ui.app.classList.remove("is-timeline-detached");
|
|
16592
|
+
ui.detachTimelineBtn.setAttribute("aria-pressed", "false");
|
|
16593
|
+
ui.detachTimelineBtn.textContent = "\u25A3 \uD0C0\uC784\uB77C\uC778 \uBD84\uB9AC";
|
|
16594
|
+
const windowToClose = popup;
|
|
16595
|
+
popup = null;
|
|
16596
|
+
if (closePopup && windowToClose && !windowToClose.closed) {
|
|
16597
|
+
windowToClose.close();
|
|
16598
|
+
}
|
|
16599
|
+
restoring = false;
|
|
16600
|
+
};
|
|
16601
|
+
ui.detachTimelineBtn.addEventListener("click", () => {
|
|
16602
|
+
if (popup && !popup.closed) {
|
|
16603
|
+
restoreTimeline(true);
|
|
16604
|
+
return;
|
|
16605
|
+
}
|
|
16606
|
+
const opened = window.open("", "_blank", POPUP_FEATURES);
|
|
16607
|
+
if (!opened) {
|
|
16608
|
+
ui.status.textContent = "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C popup\uC744 \uD5C8\uC6A9\uD574\uC8FC\uC138\uC694.";
|
|
16609
|
+
return;
|
|
16610
|
+
}
|
|
16611
|
+
popup = opened;
|
|
16612
|
+
placeholder = document.createComment("clotho-timeline-home");
|
|
16613
|
+
timeline.before(placeholder);
|
|
16614
|
+
prepareStudioPopupDocument(opened.document, document, ui.app, {
|
|
16615
|
+
title: "Clotho Timeline",
|
|
16616
|
+
bodyClass: "studio-timeline-popout"
|
|
16617
|
+
});
|
|
16618
|
+
opened.document.body.appendChild(timeline);
|
|
16619
|
+
unbindPopupPointerEvents = bindTimelinePointerDocument(opened.document);
|
|
16620
|
+
opened.addEventListener("beforeunload", () => restoreTimeline(false), {
|
|
16621
|
+
once: true
|
|
16622
|
+
});
|
|
16623
|
+
closeTimer = window.setInterval(() => {
|
|
16624
|
+
if (!popup || popup.closed) restoreTimeline(false);
|
|
16625
|
+
}, 500);
|
|
16626
|
+
ui.app.classList.add("is-timeline-detached");
|
|
16627
|
+
ui.detachTimelineBtn.setAttribute("aria-pressed", "true");
|
|
16628
|
+
ui.detachTimelineBtn.textContent = "\u2199 \uD3B8\uC9D1\uAE30\uC5D0 \uB2E4\uC2DC \uD569\uCE58\uAE30";
|
|
16629
|
+
opened.focus();
|
|
16630
|
+
});
|
|
16631
|
+
}
|
|
16632
|
+
|
|
16633
|
+
// src/legacy/player-popout.ts
|
|
16634
|
+
function setupPlayerPopout(ui) {
|
|
16635
|
+
const button = document.getElementById(
|
|
16636
|
+
"studio-player-preview"
|
|
16637
|
+
);
|
|
16638
|
+
if (!button) return;
|
|
16639
|
+
let popup = null;
|
|
16640
|
+
let playerHandle = null;
|
|
16641
|
+
let unsubscribe = null;
|
|
16642
|
+
let renderedDocument = getDef();
|
|
16643
|
+
const cleanup = () => {
|
|
16644
|
+
playerHandle?.destroy();
|
|
16645
|
+
playerHandle = null;
|
|
16646
|
+
unsubscribe?.();
|
|
16647
|
+
unsubscribe = null;
|
|
16648
|
+
popup = null;
|
|
16649
|
+
button.setAttribute("aria-pressed", "false");
|
|
16650
|
+
};
|
|
16651
|
+
const render5 = async () => {
|
|
16652
|
+
const targetWindow = popup;
|
|
16653
|
+
const def = getDef();
|
|
16654
|
+
if (!targetWindow || targetWindow.closed || !def) return;
|
|
16655
|
+
const container = targetWindow.document.getElementById(
|
|
16656
|
+
"clotho-player-preview"
|
|
16657
|
+
);
|
|
16658
|
+
if (!container) return;
|
|
16659
|
+
playerHandle?.destroy();
|
|
16660
|
+
container.replaceChildren();
|
|
16661
|
+
const { mountPlayer, koreanStrings } = await import('@kokoa/clotho/dom');
|
|
16662
|
+
if (!popup || popup !== targetWindow || targetWindow.closed) return;
|
|
16663
|
+
playerHandle = mountPlayer(container, def, {
|
|
16664
|
+
strings: koreanStrings,
|
|
16665
|
+
player: {
|
|
16666
|
+
autoplay: def.settings.autoplay,
|
|
16667
|
+
loop: def.settings.loop
|
|
16668
|
+
}
|
|
16669
|
+
});
|
|
16670
|
+
renderedDocument = def;
|
|
16671
|
+
};
|
|
16672
|
+
button.addEventListener("click", () => {
|
|
16673
|
+
if (popup && !popup.closed) {
|
|
16674
|
+
popup.focus();
|
|
16675
|
+
return;
|
|
16676
|
+
}
|
|
16677
|
+
const def = getDef();
|
|
16678
|
+
if (!def) {
|
|
16679
|
+
ui.status.textContent = "\uBA3C\uC800 \uC560\uB2C8\uBA54\uC774\uC158\uC744 \uC5F4\uAC70\uB098 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694.";
|
|
16680
|
+
return;
|
|
16681
|
+
}
|
|
16682
|
+
const opened = window.open("", "_blank", "popup=yes,width=1100,height=760");
|
|
16683
|
+
if (!opened) {
|
|
16684
|
+
ui.status.textContent = "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C popup\uC744 \uD5C8\uC6A9\uD574\uC8FC\uC138\uC694.";
|
|
16685
|
+
return;
|
|
16686
|
+
}
|
|
16687
|
+
popup = opened;
|
|
16688
|
+
prepareStudioPopupDocument(opened.document, document, ui.app, {
|
|
16689
|
+
title: `${def.title || def.id} \xB7 \uC2E4\uC81C \uBBF8\uB9AC\uBCF4\uAE30`,
|
|
16690
|
+
bodyClass: "studio-player-popout"
|
|
16691
|
+
});
|
|
16692
|
+
const container = opened.document.createElement("main");
|
|
16693
|
+
container.id = "clotho-player-preview";
|
|
16694
|
+
opened.document.body.appendChild(container);
|
|
16695
|
+
button.setAttribute("aria-pressed", "true");
|
|
16696
|
+
renderedDocument = def;
|
|
16697
|
+
void render5();
|
|
16698
|
+
unsubscribe = subscribe(() => {
|
|
16699
|
+
const next = getDef();
|
|
16700
|
+
if (next !== renderedDocument) void render5();
|
|
16701
|
+
});
|
|
16702
|
+
opened.addEventListener("beforeunload", cleanup, { once: true });
|
|
16703
|
+
opened.focus();
|
|
16704
|
+
});
|
|
16705
|
+
}
|
|
16706
|
+
|
|
16005
16707
|
// src/legacy/main.ts
|
|
16006
|
-
function initStudio(opts) {
|
|
16708
|
+
function initStudio(opts = {}) {
|
|
16007
16709
|
const ui = queryUi();
|
|
16008
16710
|
if (!ui) {
|
|
16009
16711
|
console.error("[studio] missing required elements");
|
|
@@ -16011,6 +16713,9 @@ function initStudio(opts) {
|
|
|
16011
16713
|
}
|
|
16012
16714
|
document.body.classList.add("editor-active");
|
|
16013
16715
|
document.documentElement.classList.add("editor-active");
|
|
16716
|
+
const editorTitle = document.getElementById("studio-editor-title");
|
|
16717
|
+
if (editorTitle)
|
|
16718
|
+
editorTitle.textContent = opts.editorTitle ?? "Clotho Editor";
|
|
16014
16719
|
initGrid();
|
|
16015
16720
|
reflectGridUi(ui);
|
|
16016
16721
|
subscribeGrid(() => reflectGridUi(ui));
|
|
@@ -16040,7 +16745,8 @@ function initStudio(opts) {
|
|
|
16040
16745
|
});
|
|
16041
16746
|
const imageHost = {
|
|
16042
16747
|
app: ui.app,
|
|
16043
|
-
setStatus: (text, kind) => setStatus(ui, text, kind)
|
|
16748
|
+
setStatus: (text, kind) => setStatus(ui, text, kind),
|
|
16749
|
+
resolveImage: opts.resolveImage
|
|
16044
16750
|
};
|
|
16045
16751
|
ui.imageUploadBtn.addEventListener("click", () => ui.imageFileInput.click());
|
|
16046
16752
|
ui.imageFileInput.addEventListener("change", () => {
|
|
@@ -16141,6 +16847,29 @@ function initStudio(opts) {
|
|
|
16141
16847
|
{ id: "help", label: "\u2328 \uB2E8\uCD95\uD0A4 \uBCF4\uAE30", hint: "? / Shift+/", run: openHelp }
|
|
16142
16848
|
]);
|
|
16143
16849
|
ui.saveBtn.addEventListener("click", () => void saveCurrent(ui));
|
|
16850
|
+
ui.exportBtn.addEventListener("click", () => {
|
|
16851
|
+
const def = getDef();
|
|
16852
|
+
if (!def) return;
|
|
16853
|
+
downloadAnimationJson(def);
|
|
16854
|
+
setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uB0B4\uB824\uBC1B\uC558\uC2B5\uB2C8\uB2E4.`, "ok");
|
|
16855
|
+
});
|
|
16856
|
+
ui.importBtn.addEventListener("click", () => ui.importFileInput.click());
|
|
16857
|
+
ui.importFileInput.addEventListener("change", () => {
|
|
16858
|
+
const file = ui.importFileInput.files?.[0];
|
|
16859
|
+
ui.importFileInput.value = "";
|
|
16860
|
+
if (!file) return;
|
|
16861
|
+
void file.text().then((text) => animationDocumentSchema.parse(JSON.parse(text))).then((def) => importAnimation(def)).then((def) => {
|
|
16862
|
+
setDef(def);
|
|
16863
|
+
markClean();
|
|
16864
|
+
setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.`, "ok");
|
|
16865
|
+
}).catch((error) => {
|
|
16866
|
+
setStatus(
|
|
16867
|
+
ui,
|
|
16868
|
+
`JSON \uAC00\uC838\uC624\uAE30 \uC2E4\uD328: ${error instanceof Error ? error.message : String(error)}`,
|
|
16869
|
+
"error"
|
|
16870
|
+
);
|
|
16871
|
+
});
|
|
16872
|
+
});
|
|
16144
16873
|
ui.deleteBtn.addEventListener("click", () => void deleteCurrent(ui));
|
|
16145
16874
|
ui.undoBtn.addEventListener("click", () => undo());
|
|
16146
16875
|
ui.redoBtn.addEventListener("click", () => redo());
|
|
@@ -16157,7 +16886,20 @@ function initStudio(opts) {
|
|
|
16157
16886
|
});
|
|
16158
16887
|
ui.speedInput.addEventListener("input", () => {
|
|
16159
16888
|
ui.speedValue.textContent = Number(ui.speedInput.value).toFixed(2) + "x";
|
|
16889
|
+
if (isPlaying()) {
|
|
16890
|
+
stopPreview(ui);
|
|
16891
|
+
togglePlay(ui);
|
|
16892
|
+
}
|
|
16160
16893
|
});
|
|
16894
|
+
ui.previewLoopInput.addEventListener("change", () => {
|
|
16895
|
+
updateSettings({ loop: ui.previewLoopInput.checked });
|
|
16896
|
+
if (isPlaying()) {
|
|
16897
|
+
stopPreview(ui);
|
|
16898
|
+
togglePlay(ui);
|
|
16899
|
+
}
|
|
16900
|
+
});
|
|
16901
|
+
setupTimelinePopout(ui);
|
|
16902
|
+
setupPlayerPopout(ui);
|
|
16161
16903
|
ui.newCreateBtn.addEventListener("click", () => void createNew(ui));
|
|
16162
16904
|
document.addEventListener("click", (e) => {
|
|
16163
16905
|
const closer = e.target.closest(
|
|
@@ -16169,11 +16911,15 @@ function initStudio(opts) {
|
|
|
16169
16911
|
dialog.close();
|
|
16170
16912
|
if (dialog === ui.newDialog) dialog.removeAttribute("data-mode");
|
|
16171
16913
|
});
|
|
16914
|
+
document.addEventListener("click", (e) => {
|
|
16915
|
+
const dialog = e.target;
|
|
16916
|
+
if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close();
|
|
16917
|
+
});
|
|
16172
16918
|
bindKeyboardShortcuts(ui);
|
|
16173
16919
|
window.addEventListener("beforeunload", (e) => {
|
|
16174
16920
|
if (isDirty()) e.preventDefault();
|
|
16175
16921
|
});
|
|
16176
|
-
if (opts
|
|
16922
|
+
if (opts.initialId) {
|
|
16177
16923
|
void loadAnimation2(ui, opts.initialId);
|
|
16178
16924
|
} else if (!getDef()) {
|
|
16179
16925
|
startDraft();
|
|
@@ -16187,6 +16933,9 @@ function reflectState(ui) {
|
|
|
16187
16933
|
ui.redoBtn.disabled = !canRedo();
|
|
16188
16934
|
const def = getDef();
|
|
16189
16935
|
if (def) {
|
|
16936
|
+
ui.titleInput.disabled = false;
|
|
16937
|
+
ui.canvasWidthInput.disabled = false;
|
|
16938
|
+
ui.canvasHeightInput.disabled = false;
|
|
16190
16939
|
if (document.activeElement !== ui.titleInput) {
|
|
16191
16940
|
ui.titleInput.value = def.title;
|
|
16192
16941
|
}
|
|
@@ -16198,17 +16947,25 @@ function reflectState(ui) {
|
|
|
16198
16947
|
}
|
|
16199
16948
|
ui.idDisplay.textContent = isDraft() ? "Draft" : def.id ?? "";
|
|
16200
16949
|
ui.saveBtn.disabled = false;
|
|
16950
|
+
ui.exportBtn.disabled = false;
|
|
16201
16951
|
ui.deleteBtn.disabled = isDraft();
|
|
16952
|
+
if (document.activeElement !== ui.previewLoopInput) {
|
|
16953
|
+
ui.previewLoopInput.checked = def.settings.loop;
|
|
16954
|
+
}
|
|
16202
16955
|
} else {
|
|
16956
|
+
ui.titleInput.disabled = true;
|
|
16957
|
+
ui.canvasWidthInput.disabled = true;
|
|
16958
|
+
ui.canvasHeightInput.disabled = true;
|
|
16203
16959
|
ui.titleInput.value = "";
|
|
16204
16960
|
ui.canvasWidthInput.value = "";
|
|
16205
16961
|
ui.canvasHeightInput.value = "";
|
|
16206
16962
|
ui.idDisplay.textContent = "";
|
|
16207
16963
|
ui.saveBtn.disabled = true;
|
|
16964
|
+
ui.exportBtn.disabled = true;
|
|
16208
16965
|
ui.deleteBtn.disabled = true;
|
|
16209
16966
|
}
|
|
16210
16967
|
}
|
|
16211
16968
|
|
|
16212
16969
|
export { getVisibleElementIds, initStudio };
|
|
16213
|
-
//# sourceMappingURL=main-
|
|
16214
|
-
//# sourceMappingURL=main-
|
|
16970
|
+
//# sourceMappingURL=main-5YXLTYPQ.js.map
|
|
16971
|
+
//# sourceMappingURL=main-5YXLTYPQ.js.map
|