@kokoa/clotho-editor 0.1.1 → 0.1.4

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