@widgetic/canvas 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canvas/Canvas.svelte +763 -143
- package/dist/canvas/Canvas.svelte.d.ts +33 -2
- package/dist/canvas/CanvasToolbar.svelte +5 -0
- package/dist/canvas/PanZoomPanel.svelte +15 -2
- package/dist/canvas/props-panel/PropsPanel.svelte +132 -1
- package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +6 -0
- package/package.json +1 -1
|
@@ -213,6 +213,9 @@
|
|
|
213
213
|
// Callback: fires when the user requests a screenshot refresh for the linked widget.
|
|
214
214
|
export let onRefreshScreenshot: ((widgetId: string) => void) | null = null;
|
|
215
215
|
|
|
216
|
+
/** Host persists canvases.primary_widget_id after Canvas paints the red stroke. */
|
|
217
|
+
export let onSetPrimaryWidget: ((widgetId: string) => void) | null = null;
|
|
218
|
+
|
|
216
219
|
// Widget name resolver: given a widgetId, returns the widget name (or null).
|
|
217
220
|
// Used by PropsPanel to display the linked widget's name.
|
|
218
221
|
export let getWidgetName: ((widgetId: string) => string | null) | null = null;
|
|
@@ -305,7 +308,7 @@
|
|
|
305
308
|
let activeEditingTextbox: Textbox | null = null;
|
|
306
309
|
|
|
307
310
|
const MIN_DRAW_DISTANCE = 4;
|
|
308
|
-
const MIN_TEXTBOX_WIDTH =
|
|
311
|
+
const MIN_TEXTBOX_WIDTH = 15;
|
|
309
312
|
|
|
310
313
|
// Clipboard for copy/paste functionality
|
|
311
314
|
let clipboard: any = null;
|
|
@@ -361,6 +364,237 @@
|
|
|
361
364
|
return `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`;
|
|
362
365
|
}
|
|
363
366
|
|
|
367
|
+
/** Regular Fabric Group currently in double-click edit mode (children selectable). */
|
|
368
|
+
let activeEditingGroup: any = null;
|
|
369
|
+
let isExitingGroupEdit = false;
|
|
370
|
+
let isRedirectingGroupSelection = false;
|
|
371
|
+
|
|
372
|
+
function isRegularGroup(obj: any): boolean {
|
|
373
|
+
if (!obj || isFrameShape(obj)) return false;
|
|
374
|
+
if ((obj as any)._isWidgetImage) return false;
|
|
375
|
+
return String(obj.type || '').toLowerCase() === 'group';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const GROUP_IDLE_STROKE = '#64748b';
|
|
379
|
+
const GROUP_IDLE_DASH = [8, 5];
|
|
380
|
+
const GROUP_IDLE_STROKE_WIDTH = 1.5;
|
|
381
|
+
/** Hide the idle dashed outline during toDataURL / convert screenshots. */
|
|
382
|
+
let skipGroupIdleOutline = false;
|
|
383
|
+
|
|
384
|
+
/** Stamp top-level paint order before ActiveSelection swallows children. */
|
|
385
|
+
function stampTopLevelPaintOrder() {
|
|
386
|
+
if (!canvas) return;
|
|
387
|
+
canvas.getObjects().forEach((obj: any, i: number) => {
|
|
388
|
+
const t = String(obj.type || '').toLowerCase();
|
|
389
|
+
if (t === 'activeselection' || t === 'activeSelection') return;
|
|
390
|
+
obj._canvasPaintOrder = i;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isGroupActivelySelected(group: any): boolean {
|
|
395
|
+
if (!canvas || !group) return false;
|
|
396
|
+
const active = canvas.getActiveObject() as any;
|
|
397
|
+
if (!active) return false;
|
|
398
|
+
if (active === group) return true;
|
|
399
|
+
const type = String(active.type || '').toLowerCase();
|
|
400
|
+
if ((type === 'activeselection' || type === 'activeSelection') && typeof active.getObjects === 'function') {
|
|
401
|
+
return active.getObjects().includes(group);
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Group chrome (fill/stroke behind-or-around children) + idle dashed outline.
|
|
407
|
+
* Idle dashes are skipped while the group is selected and during screenshots. */
|
|
408
|
+
function applyGroupIdleOutlineRender(group: any) {
|
|
409
|
+
if (!isRegularGroup(group) || (group as any)._hasGroupOutlineRender) return;
|
|
410
|
+
(group as any)._hasGroupOutlineRender = true;
|
|
411
|
+
const originalDrawObject = group.drawObject.bind(group);
|
|
412
|
+
group.drawObject = function (ctx: CanvasRenderingContext2D, forClipping?: boolean, context?: any) {
|
|
413
|
+
const w = this.width;
|
|
414
|
+
const h = this.height;
|
|
415
|
+
const scale = Math.max(this.scaleX || 1, this.scaleY || 1);
|
|
416
|
+
const groupFill = (this as any)._groupFill;
|
|
417
|
+
const fillOpacity = (this as any)._groupFillOpacity ?? 1;
|
|
418
|
+
if (w && h && groupFill && groupFill !== '' && groupFill !== 'transparent') {
|
|
419
|
+
ctx.save();
|
|
420
|
+
ctx.fillStyle = fillOpacity < 1 ? colorToRgba(groupFill, fillOpacity) : groupFill;
|
|
421
|
+
ctx.fillRect(-w / 2, -h / 2, w, h);
|
|
422
|
+
ctx.restore();
|
|
423
|
+
}
|
|
424
|
+
originalDrawObject(ctx, forClipping, context);
|
|
425
|
+
if (forClipping || !w || !h) return;
|
|
426
|
+
|
|
427
|
+
const groupStroke = (this as any)._groupStroke;
|
|
428
|
+
const groupStrokeWidth = (this as any)._groupStrokeWidth ?? 0;
|
|
429
|
+
if (groupStroke && groupStrokeWidth > 0) {
|
|
430
|
+
ctx.save();
|
|
431
|
+
ctx.strokeStyle = groupStroke;
|
|
432
|
+
ctx.lineWidth = groupStrokeWidth / scale;
|
|
433
|
+
const dash = (this as any)._groupStrokeDashArray;
|
|
434
|
+
ctx.setLineDash(Array.isArray(dash) && dash.length ? dash : []);
|
|
435
|
+
ctx.beginPath();
|
|
436
|
+
ctx.rect(-w / 2, -h / 2, w, h);
|
|
437
|
+
ctx.stroke();
|
|
438
|
+
ctx.restore();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (skipGroupIdleOutline || isGroupActivelySelected(this)) return;
|
|
443
|
+
ctx.save();
|
|
444
|
+
ctx.strokeStyle = GROUP_IDLE_STROKE;
|
|
445
|
+
ctx.lineWidth = GROUP_IDLE_STROKE_WIDTH / scale;
|
|
446
|
+
ctx.setLineDash(GROUP_IDLE_DASH);
|
|
447
|
+
ctx.beginPath();
|
|
448
|
+
ctx.rect(-w / 2, -h / 2, w, h);
|
|
449
|
+
ctx.stroke();
|
|
450
|
+
ctx.restore();
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Regular Groups stay uncached so children remain visible. `interactive` is
|
|
455
|
+
* false by default (click selects the group). Double-click enters edit mode. */
|
|
456
|
+
function applyInteractiveGroupFlags(group: any) {
|
|
457
|
+
if (!group || isFrameShape(group)) return;
|
|
458
|
+
if (!isRegularGroup(group)) return;
|
|
459
|
+
group.subTargetCheck = true;
|
|
460
|
+
group.objectCaching = false;
|
|
461
|
+
if (!(group as any)._groupEditMode) {
|
|
462
|
+
group.interactive = false;
|
|
463
|
+
}
|
|
464
|
+
applyGroupIdleOutlineRender(group);
|
|
465
|
+
group.getObjects?.()?.forEach((child: any) => applyInteractiveGroupFlags(child));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function snapshotGroupChildrenWorld(group: any): Array<{ obj: any; world: number[] }> {
|
|
469
|
+
return (group.getObjects?.() || []).map((obj: any) => ({
|
|
470
|
+
obj,
|
|
471
|
+
world: obj.calcTransformMatrix().slice(),
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function copyRegularGroupIdentity(fromGroup: any, toGroup: any) {
|
|
476
|
+
toGroup._objectId = fromGroup._objectId;
|
|
477
|
+
toGroup._customType = fromGroup._customType || 'group';
|
|
478
|
+
toGroup._groupFill = fromGroup._groupFill;
|
|
479
|
+
toGroup._groupStroke = fromGroup._groupStroke;
|
|
480
|
+
toGroup._groupStrokeWidth = fromGroup._groupStrokeWidth;
|
|
481
|
+
toGroup._groupFillOpacity = fromGroup._groupFillOpacity;
|
|
482
|
+
toGroup._groupStrokeDashArray = fromGroup._groupStrokeDashArray;
|
|
483
|
+
toGroup._canvasPaintOrder = fromGroup._canvasPaintOrder;
|
|
484
|
+
toGroup.set({ fill: '', stroke: '', strokeWidth: 0, backgroundColor: '' });
|
|
485
|
+
applyInteractiveGroupFlags(toGroup);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Leave children in canvas-absolute (world) space without FitContentLayout
|
|
490
|
+
* re-offsetting them. `removeAll()` would apply the *old* group matrix again.
|
|
491
|
+
*/
|
|
492
|
+
function detachGroupChildrenAtWorld(
|
|
493
|
+
group: any,
|
|
494
|
+
snapshots: Array<{ obj: any; world: number[] }>,
|
|
495
|
+
) {
|
|
496
|
+
const applyTransformToObject = (fabricUtil as any).applyTransformToObject;
|
|
497
|
+
group._activeObjects = [];
|
|
498
|
+
for (const { obj, world } of snapshots) {
|
|
499
|
+
if (typeof group._watchObject === 'function') {
|
|
500
|
+
group._watchObject(false, obj);
|
|
501
|
+
}
|
|
502
|
+
// Unset parent first — applyTransformToObject writes the object's
|
|
503
|
+
// own matrix; if `group` is still set, world = group * own (double).
|
|
504
|
+
obj._set?.('group', undefined);
|
|
505
|
+
obj._set?.('parent', undefined);
|
|
506
|
+
obj.group = undefined;
|
|
507
|
+
obj.parent = undefined;
|
|
508
|
+
applyTransformToObject(obj, world);
|
|
509
|
+
obj.setCoords?.();
|
|
510
|
+
}
|
|
511
|
+
group._objects = [];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Ungroup at current world positions, then Group() again (same path as
|
|
516
|
+
* onGroupSelection). Avoids the interactive-exit snap-back.
|
|
517
|
+
*/
|
|
518
|
+
function rebuildRegularGroupFromWorld(
|
|
519
|
+
group: any,
|
|
520
|
+
snapshots = snapshotGroupChildrenWorld(group),
|
|
521
|
+
): any {
|
|
522
|
+
if (!canvas || !group) return group;
|
|
523
|
+
if (!snapshots.length) return group;
|
|
524
|
+
const children = snapshots.map((entry) => entry.obj);
|
|
525
|
+
if (group.group) {
|
|
526
|
+
canvasLog('Canvas: skip group rebuild inside parent', group.group._customType);
|
|
527
|
+
return group;
|
|
528
|
+
}
|
|
529
|
+
const stackIndex = canvas.getObjects().indexOf(group);
|
|
530
|
+
|
|
531
|
+
detachGroupChildrenAtWorld(group, snapshots);
|
|
532
|
+
canvas.remove(group);
|
|
533
|
+
|
|
534
|
+
const newGroup = new Group(children, {
|
|
535
|
+
subTargetCheck: true,
|
|
536
|
+
interactive: false,
|
|
537
|
+
objectCaching: false,
|
|
538
|
+
fill: '',
|
|
539
|
+
stroke: '',
|
|
540
|
+
strokeWidth: 0,
|
|
541
|
+
backgroundColor: '',
|
|
542
|
+
});
|
|
543
|
+
copyRegularGroupIdentity(group, newGroup);
|
|
544
|
+
canvas.add(newGroup);
|
|
545
|
+
if (stackIndex >= 0 && typeof (canvas as any).moveObjectTo === 'function') {
|
|
546
|
+
(canvas as any).moveObjectTo(newGroup, stackIndex);
|
|
547
|
+
}
|
|
548
|
+
stampTopLevelPaintOrder();
|
|
549
|
+
canvasLog('Canvas: rebuilt group after edit', {
|
|
550
|
+
children: children.length,
|
|
551
|
+
objectId: newGroup._objectId,
|
|
552
|
+
});
|
|
553
|
+
return newGroup;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function exitGroupEdit(reselectGroup = false) {
|
|
557
|
+
const group = activeEditingGroup;
|
|
558
|
+
if (!group || isExitingGroupEdit) return;
|
|
559
|
+
isExitingGroupEdit = true;
|
|
560
|
+
// World matrices BEFORE discard — deselect remaps against the old box.
|
|
561
|
+
const snapshots = snapshotGroupChildrenWorld(group);
|
|
562
|
+
// Clear before discard: selection:cleared would otherwise re-enter
|
|
563
|
+
// exitGroupEdit and snapshot already-remapped (snapped) coords.
|
|
564
|
+
activeEditingGroup = null;
|
|
565
|
+
(group as any)._groupEditMode = false;
|
|
566
|
+
try {
|
|
567
|
+
if (canvas) {
|
|
568
|
+
const active = canvas.getActiveObject();
|
|
569
|
+
if (active && active !== group) {
|
|
570
|
+
canvas.discardActiveObject();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
group.interactive = false;
|
|
574
|
+
const rebuilt = rebuildRegularGroupFromWorld(group, snapshots);
|
|
575
|
+
canvasLog('Canvas: exited group edit');
|
|
576
|
+
if (reselectGroup && canvas && rebuilt) {
|
|
577
|
+
canvas.setActiveObject(rebuilt);
|
|
578
|
+
}
|
|
579
|
+
canvas?.requestRenderAll();
|
|
580
|
+
} finally {
|
|
581
|
+
isExitingGroupEdit = false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function enterGroupEdit(group: any) {
|
|
586
|
+
if (!group || !isRegularGroup(group)) return;
|
|
587
|
+
if (activeEditingGroup && activeEditingGroup !== group) {
|
|
588
|
+
exitGroupEdit(false);
|
|
589
|
+
}
|
|
590
|
+
(group as any)._groupEditMode = true;
|
|
591
|
+
group.interactive = true;
|
|
592
|
+
group.subTargetCheck = true;
|
|
593
|
+
group.objectCaching = false;
|
|
594
|
+
activeEditingGroup = group;
|
|
595
|
+
canvasLog('Canvas: entered group edit', { children: group.getObjects?.()?.length });
|
|
596
|
+
}
|
|
597
|
+
|
|
364
598
|
// Text settings
|
|
365
599
|
let textBoxPlaceholder = 'Type here';
|
|
366
600
|
let textFontSize = 20;
|
|
@@ -399,6 +633,18 @@
|
|
|
399
633
|
scaled: fabric.IEventHandler<Event>;
|
|
400
634
|
} | null = null;
|
|
401
635
|
|
|
636
|
+
/**
|
|
637
|
+
* Pending multi-selection conversions, keyed by the selection's _objectId.
|
|
638
|
+
* convertObjectToWidget snapshots children + bounds here; the later
|
|
639
|
+
* replaceObjectWithWidgetImage consumes the entry. Needed because Fabric 6
|
|
640
|
+
* ActiveSelection empties itself on deselect (onDeselect → removeAll), so
|
|
641
|
+
* the selection state cannot be relied on after the convert click.
|
|
642
|
+
*/
|
|
643
|
+
const pendingSelectionConversions = new Map<
|
|
644
|
+
string,
|
|
645
|
+
{ children: any[]; bounds: { left: number; top: number; width: number; height: number } }
|
|
646
|
+
>();
|
|
647
|
+
|
|
402
648
|
/**
|
|
403
649
|
* HELPER FUNCTIONS
|
|
404
650
|
*/
|
|
@@ -406,45 +652,49 @@
|
|
|
406
652
|
// Capture screenshot of selected object(s)
|
|
407
653
|
async function captureObjectScreenshot(target: any): Promise<string> {
|
|
408
654
|
if (!canvas) return '';
|
|
409
|
-
|
|
655
|
+
|
|
656
|
+
// Fabric 6 hazard: ActiveSelection.onDeselect() calls removeAll(), which
|
|
657
|
+
// EMPTIES the selection. Read everything we need from the selection
|
|
658
|
+
// BEFORE any discardActiveObject(), and never discard mid-capture.
|
|
659
|
+
const isMultiSelection =
|
|
660
|
+
target.type === 'activeSelection' || target.type === 'activeselection';
|
|
661
|
+
const selectionChildren = isMultiSelection ? target.getObjects() : [];
|
|
662
|
+
const selectionBounds = target.getBoundingRect();
|
|
663
|
+
|
|
664
|
+
skipGroupIdleOutline = true;
|
|
410
665
|
try {
|
|
411
|
-
// Deselect object temporarily to capture without selection controls
|
|
412
|
-
const activeObject = canvas.getActiveObject();
|
|
413
|
-
canvas.discardActiveObject();
|
|
414
|
-
canvas.renderAll();
|
|
415
|
-
|
|
416
666
|
let dataUrl = '';
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// Adjust position relative to group center
|
|
432
|
-
cloned.set({
|
|
433
|
-
left: (tempCanvas.width / 2) + (obj.left - target.left),
|
|
434
|
-
top: (tempCanvas.height / 2) + (obj.top - target.top)
|
|
435
|
-
});
|
|
436
|
-
tempCanvas.add(cloned);
|
|
667
|
+
|
|
668
|
+
if (isMultiSelection) {
|
|
669
|
+
// Multi-selection: capture on the REAL canvas — hide everything
|
|
670
|
+
// outside the selection, render, crop the selection's bounds.
|
|
671
|
+
// Do NOT discard the selection: in Fabric 6 discarding empties it
|
|
672
|
+
// (onDeselect → removeAll) and the children would vanish. Selection
|
|
673
|
+
// controls live on the upper canvas, so toDataURL never shows them.
|
|
674
|
+
const childSet = new Set(selectionChildren);
|
|
675
|
+
const isHidden: Array<[any, boolean]> = [];
|
|
676
|
+
for (const o of canvas.getObjects()) {
|
|
677
|
+
if (!childSet.has(o) && o.visible) {
|
|
678
|
+
o.visible = false;
|
|
679
|
+
isHidden.push([o, true]);
|
|
680
|
+
}
|
|
437
681
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
dataUrl = tempCanvas.toDataURL({
|
|
682
|
+
canvas.renderAll();
|
|
683
|
+
dataUrl = canvas.toDataURL({
|
|
441
684
|
format: 'png',
|
|
442
|
-
multiplier: 2
|
|
685
|
+
multiplier: 2,
|
|
686
|
+
left: Math.floor(selectionBounds.left),
|
|
687
|
+
top: Math.floor(selectionBounds.top),
|
|
688
|
+
width: Math.max(1, Math.ceil(selectionBounds.width)),
|
|
689
|
+
height: Math.max(1, Math.ceil(selectionBounds.height))
|
|
443
690
|
});
|
|
444
|
-
|
|
691
|
+
for (const [o, wasVisible] of isHidden) {
|
|
692
|
+
if (wasVisible) o.visible = true;
|
|
693
|
+
}
|
|
694
|
+
canvas.requestRenderAll();
|
|
445
695
|
} else {
|
|
446
|
-
// Single object
|
|
447
|
-
//
|
|
696
|
+
// Single object — toDataURL exports only the object with
|
|
697
|
+
// transparency preserved, no deselect needed either.
|
|
448
698
|
dataUrl = target.toDataURL({
|
|
449
699
|
format: 'png',
|
|
450
700
|
multiplier: 2,
|
|
@@ -452,19 +702,16 @@
|
|
|
452
702
|
withoutShadow: false
|
|
453
703
|
});
|
|
454
704
|
}
|
|
455
|
-
|
|
456
|
-
canvasLog('Capture completed for:', target.type);
|
|
457
|
-
|
|
458
|
-
// Restore selection
|
|
459
|
-
if (activeObject) {
|
|
460
|
-
canvas.setActiveObject(activeObject);
|
|
461
|
-
canvas.renderAll();
|
|
462
|
-
}
|
|
463
|
-
|
|
705
|
+
|
|
706
|
+
canvasLog('Capture completed for:', target.type, '| dataUrl length:', dataUrl.length);
|
|
707
|
+
|
|
464
708
|
return dataUrl;
|
|
465
709
|
} catch (e) {
|
|
466
710
|
console.error('Error capturing screenshot:', e);
|
|
467
711
|
return '';
|
|
712
|
+
} finally {
|
|
713
|
+
skipGroupIdleOutline = false;
|
|
714
|
+
canvas.requestRenderAll();
|
|
468
715
|
}
|
|
469
716
|
}
|
|
470
717
|
|
|
@@ -539,8 +786,33 @@
|
|
|
539
786
|
const isFrame = isFrameShape(obj);
|
|
540
787
|
const children = isFrame ? getFrameChildren(obj) : [];
|
|
541
788
|
const bounds = obj.getBoundingRect();
|
|
789
|
+
const isMultiSelection =
|
|
790
|
+
obj.type === 'activeSelection' || obj.type === 'activeselection';
|
|
791
|
+
|
|
792
|
+
// A multi-selection has no stable _objectId (it is a transient container).
|
|
793
|
+
// Allocate one so the host's replace step can resolve what to remove —
|
|
794
|
+
// without it the payload carried an invented obj_<timestamp> id that
|
|
795
|
+
// matched nothing and the original sketch shapes stayed on the canvas.
|
|
796
|
+
if (isMultiSelection && !(obj as any)._objectId) {
|
|
797
|
+
(obj as any)._objectId = generateObjectId();
|
|
798
|
+
}
|
|
542
799
|
|
|
543
800
|
try {
|
|
801
|
+
// Snapshot multi-selection children + bounds BEFORE anything can
|
|
802
|
+
// deselect the selection (onDeselect empties it in Fabric 6).
|
|
803
|
+
// replaceObjectWithWidgetImage consumes this by selection id.
|
|
804
|
+
if (isMultiSelection) {
|
|
805
|
+
pendingSelectionConversions.set((obj as any)._objectId, {
|
|
806
|
+
children: [...obj.getObjects()],
|
|
807
|
+
bounds: {
|
|
808
|
+
left: bounds.left,
|
|
809
|
+
top: bounds.top,
|
|
810
|
+
width: bounds.width,
|
|
811
|
+
height: bounds.height
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
544
816
|
const screenshot = await captureObjectScreenshot(obj);
|
|
545
817
|
|
|
546
818
|
const objectName = isFrame
|
|
@@ -579,8 +851,6 @@
|
|
|
579
851
|
ShapeTypes: payload.metadata.shapeTypes,
|
|
580
852
|
});
|
|
581
853
|
|
|
582
|
-
obj._isConverting = true;
|
|
583
|
-
selectedObject = selectedObject;
|
|
584
854
|
onConvertToWidget(payload);
|
|
585
855
|
} catch (err) {
|
|
586
856
|
console.error('Canvas: convertObjectToWidget failed', err);
|
|
@@ -595,6 +865,16 @@
|
|
|
595
865
|
selectedObject = null;
|
|
596
866
|
}
|
|
597
867
|
|
|
868
|
+
/** Same as Ctrl+D / context Duplicate — clones selection (widget shapes notify host). */
|
|
869
|
+
export function duplicateSelectedObjects() {
|
|
870
|
+
return _duplicateSelected();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Same as Delete key / context Delete. */
|
|
874
|
+
export function deleteSelectedObjects() {
|
|
875
|
+
return _deleteActiveObjects();
|
|
876
|
+
}
|
|
877
|
+
|
|
598
878
|
/** Re-link a cloned object to a new widget (called by host after duplicateWidget API).
|
|
599
879
|
* Selects the object on canvas so Props Panel updates with the correct widget info. */
|
|
600
880
|
/** Mark the currently selected object as "duplicating in progress" so PropsPanel shows a loader. */
|
|
@@ -652,32 +932,77 @@
|
|
|
652
932
|
): Promise<string | null> {
|
|
653
933
|
if (!canvas) return null;
|
|
654
934
|
|
|
655
|
-
|
|
656
|
-
|
|
935
|
+
// Convert registry — convertObjectToWidget snapshots the selection's
|
|
936
|
+
// children + bounds under the selection's _objectId BEFORE anything can
|
|
937
|
+
// deselect it (Fabric 6 ActiveSelection.onDeselect() → removeAll() empties
|
|
938
|
+
// the selection, so live lookups are unreliable by replace time).
|
|
939
|
+
const pendingConversion = pendingSelectionConversions.get(objectId);
|
|
940
|
+
pendingSelectionConversions.delete(objectId);
|
|
941
|
+
|
|
942
|
+
// Resolve a single (non-selection) source object by id, if any.
|
|
943
|
+
let obj = canvas.getObjects().find((o: any) => o._objectId === objectId);
|
|
944
|
+
let selectionChildren: any[] = [];
|
|
945
|
+
if (pendingConversion) {
|
|
946
|
+
// Registry path: still-present children are removed below.
|
|
947
|
+
selectionChildren = pendingConversion.children.filter((c: any) =>
|
|
948
|
+
canvas!.getObjects().includes(c)
|
|
949
|
+
);
|
|
950
|
+
} else if (!obj) {
|
|
951
|
+
const active = canvas.getActiveObject();
|
|
952
|
+
if (
|
|
953
|
+
active &&
|
|
954
|
+
(active.type === 'activeSelection' || active.type === 'activeselection') &&
|
|
955
|
+
(active as any)._objectId === objectId
|
|
956
|
+
) {
|
|
957
|
+
selectionChildren = (active as any).getObjects();
|
|
958
|
+
} else {
|
|
959
|
+
// Last resort: a top-level object whose children match (group)
|
|
960
|
+
const byChild = canvas.getObjects().find((o: any) =>
|
|
961
|
+
typeof o.getObjects === 'function' &&
|
|
962
|
+
o.getObjects().some((c: any) => c._objectId === objectId)
|
|
963
|
+
);
|
|
964
|
+
obj = byChild;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (!obj && selectionChildren.length === 0) {
|
|
657
968
|
canvasWarn('Canvas: replaceObjectWithWidgetImage — object not found', objectId);
|
|
658
969
|
return null;
|
|
659
970
|
}
|
|
660
971
|
|
|
661
972
|
try {
|
|
662
|
-
const
|
|
663
|
-
const
|
|
973
|
+
const isFrame = obj ? isFrameShape(obj) : false;
|
|
974
|
+
const bounds = pendingConversion?.bounds ??
|
|
975
|
+
(obj
|
|
976
|
+
? obj.getBoundingRect()
|
|
977
|
+
: (() => {
|
|
978
|
+
const a = canvas.getActiveObject() as any;
|
|
979
|
+
return a ? a.getBoundingRect() : { left: 0, top: 0, width: 200, height: 200 };
|
|
980
|
+
})());
|
|
664
981
|
|
|
665
982
|
// Preserve original visual properties before removing
|
|
666
|
-
const originalStroke = (obj as any)
|
|
667
|
-
const originalStrokeWidth = (obj as any)
|
|
668
|
-
const originalStrokeDashArray = (obj as any)
|
|
669
|
-
const originalRx = (obj as any)
|
|
670
|
-
const originalRy = (obj as any)
|
|
671
|
-
const originalOpacity = (obj as any)
|
|
672
|
-
const originalFill = (obj as any)
|
|
673
|
-
|
|
674
|
-
if (isFrame) {
|
|
675
|
-
const children = getFrameChildren(obj);
|
|
676
|
-
const bg = getFrameBackground(obj);
|
|
983
|
+
const originalStroke = (obj as any)?.stroke || '#94a3b8';
|
|
984
|
+
const originalStrokeWidth = (obj as any)?.strokeWidth ?? 2;
|
|
985
|
+
const originalStrokeDashArray = (obj as any)?.strokeDashArray || null;
|
|
986
|
+
const originalRx = (obj as any)?.rx || 0;
|
|
987
|
+
const originalRy = (obj as any)?.ry || 0;
|
|
988
|
+
const originalOpacity = (obj as any)?.opacity ?? 1;
|
|
989
|
+
const originalFill = (obj as any)?.fill || null;
|
|
990
|
+
|
|
991
|
+
if (isFrame && obj) {
|
|
992
|
+
const children = getFrameChildren(obj as any);
|
|
993
|
+
const bg = getFrameBackground(obj as any);
|
|
677
994
|
for (const child of children) canvas.remove(child);
|
|
678
995
|
if (bg) canvas.remove(bg);
|
|
679
996
|
}
|
|
680
|
-
|
|
997
|
+
// Multi-selection: remove every child shape so the sketch is absorbed
|
|
998
|
+
// into the single WidgetShape (F4 — no duplicate shapes left).
|
|
999
|
+
if (selectionChildren.length > 0) {
|
|
1000
|
+
// Deselect first so Fabric doesn't re-add children on discard.
|
|
1001
|
+
// After this point nothing reads from the (now emptied) selection.
|
|
1002
|
+
canvas.discardActiveObject();
|
|
1003
|
+
for (const child of selectionChildren) canvas.remove(child);
|
|
1004
|
+
}
|
|
1005
|
+
if (obj) canvas.remove(obj);
|
|
681
1006
|
|
|
682
1007
|
const widgetImage = createImagePlaceholder({
|
|
683
1008
|
left: bounds.left,
|
|
@@ -708,12 +1033,16 @@
|
|
|
708
1033
|
|
|
709
1034
|
canvas.add(widgetImage);
|
|
710
1035
|
|
|
1036
|
+
// Set image fields SYNCHRONOUSLY: an autosave that runs before the
|
|
1037
|
+
// <img> loads must still persist the screenshot URL (previously the
|
|
1038
|
+
// JSON saved a bare placeholder and the sketch was lost on reload).
|
|
1039
|
+
widgetImage._imageUrl = screenshotDataUrl;
|
|
1040
|
+
widgetImage._hasImage = true;
|
|
1041
|
+
widgetImage._originalFilename = 'widget-screenshot.png';
|
|
1042
|
+
|
|
711
1043
|
const img = new Image();
|
|
712
1044
|
img.onload = () => {
|
|
713
1045
|
widgetImage._imageElement = img;
|
|
714
|
-
widgetImage._hasImage = true;
|
|
715
|
-
widgetImage._imageUrl = screenshotDataUrl;
|
|
716
|
-
widgetImage._originalFilename = 'widget-screenshot.png';
|
|
717
1046
|
widgetImage._originalAspectRatio = img.width / img.height;
|
|
718
1047
|
widgetImage.dirty = true;
|
|
719
1048
|
canvas.requestRenderAll();
|
|
@@ -786,6 +1115,40 @@
|
|
|
786
1115
|
return false;
|
|
787
1116
|
}
|
|
788
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Screen-space box of a widget shape plus the canvas element viewport.
|
|
1120
|
+
* Used to park Widget Details left/right of the shape.
|
|
1121
|
+
*/
|
|
1122
|
+
export function getWidgetShapeScreenRect(widgetId: string): {
|
|
1123
|
+
left: number;
|
|
1124
|
+
top: number;
|
|
1125
|
+
width: number;
|
|
1126
|
+
height: number;
|
|
1127
|
+
viewportLeft: number;
|
|
1128
|
+
viewportTop: number;
|
|
1129
|
+
viewportWidth: number;
|
|
1130
|
+
viewportHeight: number;
|
|
1131
|
+
} | null {
|
|
1132
|
+
if (!canvas || !canvasEl || !widgetId) return null;
|
|
1133
|
+
const obj = canvas.getObjects().find((o: any) => o._widgetId === widgetId);
|
|
1134
|
+
if (!obj) {
|
|
1135
|
+
canvasLog('Canvas: getWidgetShapeScreenRect — no shape for', widgetId);
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
const bbox = obj.getBoundingRect();
|
|
1139
|
+
const canvasRect = canvasEl.getBoundingClientRect();
|
|
1140
|
+
return {
|
|
1141
|
+
left: canvasRect.left + bbox.left,
|
|
1142
|
+
top: canvasRect.top + bbox.top,
|
|
1143
|
+
width: bbox.width,
|
|
1144
|
+
height: bbox.height,
|
|
1145
|
+
viewportLeft: canvasRect.left,
|
|
1146
|
+
viewportTop: canvasRect.top,
|
|
1147
|
+
viewportWidth: canvasRect.width,
|
|
1148
|
+
viewportHeight: canvasRect.height,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
789
1152
|
/**
|
|
790
1153
|
* Get the current image data URL of a widget shape on the canvas.
|
|
791
1154
|
* Returns the base64 data URL or null if the shape is not found or has no image.
|
|
@@ -1917,6 +2280,15 @@
|
|
|
1917
2280
|
if ((obj as any)._customType === 'image') {
|
|
1918
2281
|
// Upgrade old Rect-based image placeholders to ImageShape (backward compat)
|
|
1919
2282
|
upgradeToImageShape(obj);
|
|
2283
|
+
if ((obj as any)._isWidgetImage) {
|
|
2284
|
+
if ((obj as any)._isPrimaryWidget) {
|
|
2285
|
+
obj.set({
|
|
2286
|
+
stroke: PRIMARY_WIDGET_STROKE,
|
|
2287
|
+
strokeWidth: PRIMARY_WIDGET_STROKE_WIDTH,
|
|
2288
|
+
strokeDashArray: null,
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1920
2292
|
canvasLog('Canvas: object:added — upgraded image shape for', (obj as any)._objectId || 'image');
|
|
1921
2293
|
}
|
|
1922
2294
|
|
|
@@ -2054,7 +2426,7 @@
|
|
|
2054
2426
|
// Textboxes use backgroundColor for their box fill.
|
|
2055
2427
|
// Default to white so the box fill opacity slider is visible.
|
|
2056
2428
|
obj.set({ backgroundColor: '#ffffff' });
|
|
2057
|
-
} else {
|
|
2429
|
+
} else if (String(obj.type || '').toLowerCase() !== 'group') {
|
|
2058
2430
|
obj.set({
|
|
2059
2431
|
fill: WIREFRAME_FILL,
|
|
2060
2432
|
stroke: WIREFRAME_STROKE,
|
|
@@ -2065,8 +2437,11 @@
|
|
|
2065
2437
|
canvasLog('Canvas: Wireframe defaults applied to new object', obj._objectId);
|
|
2066
2438
|
}
|
|
2067
2439
|
}
|
|
2440
|
+
stampTopLevelPaintOrder();
|
|
2068
2441
|
});
|
|
2069
2442
|
|
|
2443
|
+
canvas.on('mouse:down:before', stampTopLevelPaintOrder);
|
|
2444
|
+
|
|
2070
2445
|
// ── Proportional resize lock: handled via canvas.uniformScaling in before:transform ──
|
|
2071
2446
|
// Edge handles (ml/mr/mt/mb) are hidden for _lockAspectRatio shapes (see object:added).
|
|
2072
2447
|
// canvas.uniformScaling=true is set in before:transform for the drag duration, which
|
|
@@ -3012,6 +3387,13 @@
|
|
|
3012
3387
|
if (activeTool !== 'select') {
|
|
3013
3388
|
activateSelectTool();
|
|
3014
3389
|
}
|
|
3390
|
+
|
|
3391
|
+
// Escape while inside a group exits edit mode and keeps the group selected
|
|
3392
|
+
if (activeEditingGroup) {
|
|
3393
|
+
exitGroupEdit(true);
|
|
3394
|
+
canvasLog('Canvas: Escape - Exited group edit');
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3015
3397
|
|
|
3016
3398
|
// Deselect all objects
|
|
3017
3399
|
canvas.discardActiveObject();
|
|
@@ -3042,6 +3424,11 @@
|
|
|
3042
3424
|
'_textPadding',
|
|
3043
3425
|
// Text stroke properties
|
|
3044
3426
|
'_textStrokeColor', '_textStrokeWidth',
|
|
3427
|
+
// Group chrome (fill/stroke around children, like Frame)
|
|
3428
|
+
'_groupFill', '_groupStroke', '_groupStrokeWidth',
|
|
3429
|
+
'_groupFillOpacity', '_groupStrokeDashArray',
|
|
3430
|
+
'_canvasPaintOrder',
|
|
3431
|
+
'_isPrimaryWidget',
|
|
3045
3432
|
];
|
|
3046
3433
|
|
|
3047
3434
|
// Apply custom Image render to a cloned object
|
|
@@ -4084,6 +4471,28 @@
|
|
|
4084
4471
|
canvas.setActiveObject(activeObj.group);
|
|
4085
4472
|
return activeObj.group;
|
|
4086
4473
|
}
|
|
4474
|
+
|
|
4475
|
+
// Regular group children: only selectable in double-click edit mode.
|
|
4476
|
+
// Otherwise promote the click to the parent group (tldraw-like).
|
|
4477
|
+
const parent = activeObj.group;
|
|
4478
|
+
if (parent && isRegularGroup(parent)) {
|
|
4479
|
+
if (activeEditingGroup === parent && parent.interactive) {
|
|
4480
|
+
return activeObj;
|
|
4481
|
+
}
|
|
4482
|
+
if (!isRedirectingGroupSelection) {
|
|
4483
|
+
isRedirectingGroupSelection = true;
|
|
4484
|
+
try {
|
|
4485
|
+
canvas.setActiveObject(parent);
|
|
4486
|
+
} finally {
|
|
4487
|
+
isRedirectingGroupSelection = false;
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
return parent;
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
if (activeEditingGroup && activeObj !== activeEditingGroup) {
|
|
4494
|
+
exitGroupEdit(false);
|
|
4495
|
+
}
|
|
4087
4496
|
|
|
4088
4497
|
return activeObj;
|
|
4089
4498
|
};
|
|
@@ -4170,6 +4579,9 @@
|
|
|
4170
4579
|
|
|
4171
4580
|
const handleSelectionCleared = () => {
|
|
4172
4581
|
selectedObject = null;
|
|
4582
|
+
if (activeEditingGroup) {
|
|
4583
|
+
exitGroupEdit(false);
|
|
4584
|
+
}
|
|
4173
4585
|
canvasLog('Canvas: Selection cleared');
|
|
4174
4586
|
if (onNonWidgetSelected) {
|
|
4175
4587
|
onNonWidgetSelected();
|
|
@@ -4200,6 +4612,27 @@
|
|
|
4200
4612
|
return;
|
|
4201
4613
|
}
|
|
4202
4614
|
|
|
4615
|
+
// Regular Group: double-click enters edit mode (tldraw-like).
|
|
4616
|
+
// Single click always selects the group; children are only
|
|
4617
|
+
// selectable after this enter.
|
|
4618
|
+
if (
|
|
4619
|
+
String(target.type || '').toLowerCase() === 'group' &&
|
|
4620
|
+
!isFrameShape(target) &&
|
|
4621
|
+
!(target as any)._isWidgetImage
|
|
4622
|
+
) {
|
|
4623
|
+
enterGroupEdit(target);
|
|
4624
|
+
const sub = Array.isArray(e.subTargets) ? e.subTargets[0] : null;
|
|
4625
|
+
if (sub && sub !== target) {
|
|
4626
|
+
canvas.setActiveObject(sub);
|
|
4627
|
+
}
|
|
4628
|
+
canvas.requestRenderAll();
|
|
4629
|
+
canvasLog('Canvas: entered group via double-click', {
|
|
4630
|
+
children: target.getObjects?.()?.length,
|
|
4631
|
+
subType: sub?.type,
|
|
4632
|
+
});
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4203
4636
|
// Widget Image (linked to a widget): open WidgetDetails panel
|
|
4204
4637
|
// Must be checked BEFORE generic WidgetShape — widget shapes also have _customType === 'image'
|
|
4205
4638
|
if (onWidgetImageClick && (target as any)?._isWidgetImage && (target as any)?._widgetId) {
|
|
@@ -5061,6 +5494,9 @@
|
|
|
5061
5494
|
if (canvas && (canvas as any)._frameInteriorMouseDownHandler) {
|
|
5062
5495
|
canvas.off('mouse:down:before', (canvas as any)._frameInteriorMouseDownHandler);
|
|
5063
5496
|
}
|
|
5497
|
+
if (canvas) {
|
|
5498
|
+
canvas.off('mouse:down:before', stampTopLevelPaintOrder);
|
|
5499
|
+
}
|
|
5064
5500
|
if (canvas && (canvas as any)._frameInteriorMouseUpHandler) {
|
|
5065
5501
|
canvas.off('mouse:up', (canvas as any)._frameInteriorMouseUpHandler);
|
|
5066
5502
|
}
|
|
@@ -5203,13 +5639,25 @@
|
|
|
5203
5639
|
};
|
|
5204
5640
|
};
|
|
5205
5641
|
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5642
|
+
/** Width that fits the current default/placeholder text on one line.
|
|
5643
|
+
* measureText is the wrap width; padding is drawn outside that, so only a
|
|
5644
|
+
* small extra is needed (avoid double-counting create padding). */
|
|
5645
|
+
const measureDefaultTextboxWidth = () => {
|
|
5646
|
+
const text = textBoxPlaceholder || '';
|
|
5647
|
+
const extra = 2;
|
|
5648
|
+
if (!text) return MIN_TEXTBOX_WIDTH;
|
|
5649
|
+
try {
|
|
5650
|
+
const fabricCtx = canvas && typeof canvas.getContext === 'function' ? canvas.getContext() : null;
|
|
5651
|
+
const ctx = fabricCtx ?? document.createElement('canvas').getContext('2d');
|
|
5652
|
+
if (ctx) {
|
|
5653
|
+
ctx.font = `${textFontSize}px ${textFontFamily}`;
|
|
5654
|
+
const measured = ctx.measureText(text).width;
|
|
5655
|
+
return Math.max(MIN_TEXTBOX_WIDTH, Math.ceil(measured + extra));
|
|
5656
|
+
}
|
|
5657
|
+
} catch {
|
|
5658
|
+
/* fall through to heuristic */
|
|
5209
5659
|
}
|
|
5210
|
-
|
|
5211
|
-
const estimatedWidth = textBoxPlaceholder.length * textFontSize * 0.6;
|
|
5212
|
-
return Math.max(MIN_TEXTBOX_WIDTH, estimatedWidth);
|
|
5660
|
+
return Math.max(MIN_TEXTBOX_WIDTH, Math.ceil(text.length * textFontSize * 0.6 + extra));
|
|
5213
5661
|
};
|
|
5214
5662
|
|
|
5215
5663
|
const createArrowGeometry = (
|
|
@@ -6238,7 +6686,7 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
6238
6686
|
previewObject = new Textbox(textBoxPlaceholder, {
|
|
6239
6687
|
left: drawingStartPoint.x,
|
|
6240
6688
|
top: drawingStartPoint.y,
|
|
6241
|
-
width:
|
|
6689
|
+
width: measureDefaultTextboxWidth(),
|
|
6242
6690
|
fontSize: textFontSize,
|
|
6243
6691
|
fontFamily: textFontFamily,
|
|
6244
6692
|
fill: textFillColor,
|
|
@@ -6262,7 +6710,7 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
6262
6710
|
}
|
|
6263
6711
|
}
|
|
6264
6712
|
|
|
6265
|
-
const minWidth =
|
|
6713
|
+
const minWidth = MIN_TEXTBOX_WIDTH;
|
|
6266
6714
|
const desiredWidth = Math.max(width, minWidth);
|
|
6267
6715
|
const left = Math.min(pointer.x, drawingStartPoint.x);
|
|
6268
6716
|
const top = Math.min(pointer.y, drawingStartPoint.y);
|
|
@@ -6290,8 +6738,9 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
6290
6738
|
// On quick click (no drag), use the estimated placeholder width
|
|
6291
6739
|
// so the text fits nicely without wrapping.
|
|
6292
6740
|
const width = Math.abs(pointer.x - drawingStartPoint.x);
|
|
6293
|
-
const
|
|
6294
|
-
|
|
6741
|
+
const textboxWidth = width < MIN_DRAW_DISTANCE
|
|
6742
|
+
? measureDefaultTextboxWidth()
|
|
6743
|
+
: Math.max(width, MIN_TEXTBOX_WIDTH);
|
|
6295
6744
|
|
|
6296
6745
|
// Calculate position (support drag in any direction)
|
|
6297
6746
|
const left = Math.min(pointer.x, drawingStartPoint.x);
|
|
@@ -6787,8 +7236,61 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
6787
7236
|
}
|
|
6788
7237
|
|
|
6789
7238
|
/** No visible Fabric stroke on widget shapes — preview image may supply its own border. */
|
|
7239
|
+
const PRIMARY_WIDGET_STROKE = '#DC2626';
|
|
7240
|
+
const PRIMARY_WIDGET_STROKE_WIDTH = 2;
|
|
7241
|
+
|
|
7242
|
+
/** Red stroke marks the canvas primary widget; others stay borderless. */
|
|
7243
|
+
function applyPrimaryWidgetStrokes(primaryWidgetId: string | null) {
|
|
7244
|
+
if (!canvas) return;
|
|
7245
|
+
for (const obj of canvas.getObjects()) {
|
|
7246
|
+
const anyObj = obj as any;
|
|
7247
|
+
if (!anyObj._isWidgetImage) continue;
|
|
7248
|
+
const isPrimary = !!(primaryWidgetId && anyObj._widgetId === primaryWidgetId);
|
|
7249
|
+
anyObj._isPrimaryWidget = isPrimary;
|
|
7250
|
+
if (isPrimary) {
|
|
7251
|
+
obj.set({
|
|
7252
|
+
stroke: PRIMARY_WIDGET_STROKE,
|
|
7253
|
+
strokeWidth: PRIMARY_WIDGET_STROKE_WIDTH,
|
|
7254
|
+
strokeDashArray: null,
|
|
7255
|
+
});
|
|
7256
|
+
} else {
|
|
7257
|
+
obj.set({ stroke: 'transparent', strokeWidth: 0, strokeDashArray: null });
|
|
7258
|
+
}
|
|
7259
|
+
obj.dirty = true;
|
|
7260
|
+
}
|
|
7261
|
+
canvas.requestRenderAll();
|
|
7262
|
+
canvasLog('Canvas: applied primary widget stroke', primaryWidgetId);
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
export function setPrimaryWidgetShape(widgetId: string): boolean {
|
|
7266
|
+
if (!widgetId) return false;
|
|
7267
|
+
applyPrimaryWidgetStrokes(widgetId);
|
|
7268
|
+
selectedObject = selectedObject;
|
|
7269
|
+
return true;
|
|
7270
|
+
}
|
|
7271
|
+
|
|
7272
|
+
function handleSetPrimaryFromPanel(widgetId: string) {
|
|
7273
|
+
setPrimaryWidgetShape(widgetId);
|
|
7274
|
+
onSetPrimaryWidget?.(widgetId);
|
|
7275
|
+
}
|
|
7276
|
+
|
|
7277
|
+
export function getPrimaryWidgetId(): string | null {
|
|
7278
|
+
if (!canvas) return null;
|
|
7279
|
+
const primary = canvas.getObjects().find((o: any) => o._isWidgetImage && o._isPrimaryWidget);
|
|
7280
|
+
return (primary as any)?._widgetId ?? null;
|
|
7281
|
+
}
|
|
7282
|
+
|
|
7283
|
+
/** No visible Fabric stroke on widget shapes unless this one is primary. */
|
|
6790
7284
|
function clearWidgetShapeDefaultBorder(obj: any): void {
|
|
6791
|
-
obj.
|
|
7285
|
+
if (obj._isPrimaryWidget) {
|
|
7286
|
+
obj.set({
|
|
7287
|
+
stroke: PRIMARY_WIDGET_STROKE,
|
|
7288
|
+
strokeWidth: PRIMARY_WIDGET_STROKE_WIDTH,
|
|
7289
|
+
strokeDashArray: null,
|
|
7290
|
+
});
|
|
7291
|
+
} else {
|
|
7292
|
+
obj.set({ stroke: 'transparent', strokeWidth: 0, strokeDashArray: null });
|
|
7293
|
+
}
|
|
6792
7294
|
obj.dirty = true;
|
|
6793
7295
|
}
|
|
6794
7296
|
|
|
@@ -7567,63 +8069,76 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
7567
8069
|
* They are used to test the canvas and the fabric.js library.
|
|
7568
8070
|
* */
|
|
7569
8071
|
// Group selection method
|
|
7570
|
-
export function onGroupSelection() {
|
|
8072
|
+
export function onGroupSelection(): string | null {
|
|
7571
8073
|
if (!canvas) {
|
|
7572
|
-
return;
|
|
8074
|
+
return null;
|
|
7573
8075
|
}
|
|
7574
8076
|
|
|
7575
8077
|
const activeObject = canvas.getActiveObject();
|
|
7576
8078
|
if (!activeObject) {
|
|
7577
|
-
return;
|
|
8079
|
+
return null;
|
|
7578
8080
|
}
|
|
7579
8081
|
|
|
7580
8082
|
// check if the active object is an active selection
|
|
7581
|
-
if (activeObject.type !== 'activeSelection' &&
|
|
8083
|
+
if (activeObject.type !== 'activeSelection' &&
|
|
7582
8084
|
activeObject.type !== 'activeselection') {
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
8085
|
+
// Already a single object (e.g. re-convert of an existing group) —
|
|
8086
|
+
// return its id so the host can replace it later.
|
|
8087
|
+
const single = activeObject as any;
|
|
8088
|
+
if (!single._objectId) single._objectId = generateObjectId();
|
|
8089
|
+
return single._objectId as string;
|
|
8090
|
+
}
|
|
8091
|
+
|
|
8092
|
+
// Copy — getObjects() returns the live _objects array.
|
|
8093
|
+
// Sort by canvas paint order (bottom → top) so grouping does not reverse z-index.
|
|
8094
|
+
const objects = [...activeObject.getObjects()].sort((a: any, b: any) => {
|
|
8095
|
+
const ia = a._canvasPaintOrder;
|
|
8096
|
+
const ib = b._canvasPaintOrder;
|
|
8097
|
+
if (typeof ia === 'number' && typeof ib === 'number') return ia - ib;
|
|
8098
|
+
return 0;
|
|
8099
|
+
});
|
|
7588
8100
|
if (objects.length === 0) {
|
|
7589
|
-
return;
|
|
8101
|
+
return null;
|
|
7590
8102
|
}
|
|
7591
8103
|
|
|
7592
|
-
//
|
|
8104
|
+
// Fabric 6: ActiveSelection.onDeselect() → removeAll() applies the
|
|
8105
|
+
// selection transform so children return to canvas-absolute coords.
|
|
8106
|
+
// This MUST run while objects are still on the canvas. Removing them
|
|
8107
|
+
// first, then discarding, double-applies the transform — children
|
|
8108
|
+
// collapse behind the bounding rect (the Sep 2025 grouping bug).
|
|
8109
|
+
canvas.discardActiveObject();
|
|
8110
|
+
|
|
7593
8111
|
objects.forEach((obj: any) => {
|
|
7594
8112
|
canvas.remove(obj);
|
|
8113
|
+
if (!obj._objectId) obj._objectId = generateObjectId();
|
|
7595
8114
|
});
|
|
7596
8115
|
|
|
7597
|
-
//
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
// Create group with all objects
|
|
8116
|
+
// Group constructor + FitContentLayout converts canvas-absolute coords
|
|
8117
|
+
// into group-local and places the group at the former selection bounds.
|
|
7601
8118
|
const group = new Group(objects, {
|
|
7602
|
-
|
|
8119
|
+
subTargetCheck: true,
|
|
8120
|
+
interactive: false,
|
|
8121
|
+
objectCaching: false,
|
|
8122
|
+
fill: '',
|
|
8123
|
+
stroke: '',
|
|
8124
|
+
strokeWidth: 0,
|
|
8125
|
+
backgroundColor: '',
|
|
7603
8126
|
});
|
|
8127
|
+
applyInteractiveGroupFlags(group);
|
|
8128
|
+
(group as any)._objectId = generateObjectId();
|
|
8129
|
+
(group as any)._customType = 'group';
|
|
8130
|
+
group.set({ fill: '', stroke: '', strokeWidth: 0, backgroundColor: '' });
|
|
7604
8131
|
|
|
7605
|
-
// Disable object caching if group contains children with custom render overrides.
|
|
7606
|
-
// Frame labels are drawn outside the bounding box (clipped by cache).
|
|
7607
|
-
// Image placeholders use manual transforms that conflict with cache context.
|
|
7608
|
-
const hasCustomRenderChild = objects.some((obj: any) =>
|
|
7609
|
-
isFrameShape(obj) || (obj as any)._customType === 'image'
|
|
7610
|
-
);
|
|
7611
|
-
if (hasCustomRenderChild) {
|
|
7612
|
-
group.objectCaching = false;
|
|
7613
|
-
}
|
|
7614
|
-
|
|
7615
|
-
// add the group to the canvas
|
|
7616
8132
|
canvas.add(group);
|
|
7617
|
-
|
|
7618
|
-
// set the group as the active object
|
|
7619
8133
|
canvas.setActiveObject(group);
|
|
7620
|
-
|
|
7621
|
-
// request a render all
|
|
7622
8134
|
canvas.requestRenderAll();
|
|
7623
8135
|
takeHistorySnapshot();
|
|
7624
8136
|
|
|
7625
|
-
|
|
7626
|
-
|
|
8137
|
+
canvasLog('Group selection created', {
|
|
8138
|
+
objectCount: objects.length,
|
|
8139
|
+
objectId: (group as any)._objectId,
|
|
8140
|
+
});
|
|
8141
|
+
return (group as any)._objectId as string;
|
|
7627
8142
|
}
|
|
7628
8143
|
|
|
7629
8144
|
// Ungroup selection method - using native Fabric.js approach
|
|
@@ -7651,6 +8166,9 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
7651
8166
|
}
|
|
7652
8167
|
|
|
7653
8168
|
const group = activeObject as Group;
|
|
8169
|
+
if (activeEditingGroup === group) {
|
|
8170
|
+
exitGroupEdit(false);
|
|
8171
|
+
}
|
|
7654
8172
|
|
|
7655
8173
|
canvasLog('Ungroup: Starting with', group.getObjects().length, 'objects');
|
|
7656
8174
|
|
|
@@ -7769,6 +8287,10 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
7769
8287
|
'_widgetId',
|
|
7770
8288
|
// Widget Image: converted widget placeholder on canvas
|
|
7771
8289
|
'_isWidgetImage',
|
|
8290
|
+
'_groupFill', '_groupStroke', '_groupStrokeWidth',
|
|
8291
|
+
'_groupFillOpacity', '_groupStrokeDashArray',
|
|
8292
|
+
'_canvasPaintOrder',
|
|
8293
|
+
'_isPrimaryWidget',
|
|
7772
8294
|
];
|
|
7773
8295
|
|
|
7774
8296
|
// ── History: capture / restore ───────────────────────────────────────────
|
|
@@ -7830,8 +8352,8 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
7830
8352
|
}, autosaveDelayMs);
|
|
7831
8353
|
}
|
|
7832
8354
|
|
|
7833
|
-
/** Force an immediate save (used by Cmd+S
|
|
7834
|
-
function saveNow() {
|
|
8355
|
+
/** Force an immediate save (used by Cmd+S, the save button, and host post-replace flushes). */
|
|
8356
|
+
export function saveNow() {
|
|
7835
8357
|
if (_autoSaveTimer) clearTimeout(_autoSaveTimer);
|
|
7836
8358
|
_autoSaveTimer = null;
|
|
7837
8359
|
const json = saveCanvas();
|
|
@@ -7880,6 +8402,7 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
7880
8402
|
syncTextboxFabricPadding(obj);
|
|
7881
8403
|
}
|
|
7882
8404
|
}
|
|
8405
|
+
applyInteractiveGroupFlags(obj);
|
|
7883
8406
|
} catch (reviverErr) {
|
|
7884
8407
|
canvasWarn('Canvas: restoreHistorySnapshot reviver error', obj?.type, reviverErr);
|
|
7885
8408
|
}
|
|
@@ -7896,6 +8419,13 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
7896
8419
|
});
|
|
7897
8420
|
} else if (obj._customType === 'image' || obj._hasImage || obj._imageUrl) {
|
|
7898
8421
|
upgradeToImageShape(obj);
|
|
8422
|
+
} else {
|
|
8423
|
+
applyInteractiveGroupFlags(obj);
|
|
8424
|
+
obj.getObjects?.()?.forEach((child: any) => {
|
|
8425
|
+
if (child._customType === 'image' || child._hasImage || child._imageUrl) {
|
|
8426
|
+
upgradeToImageShape(child);
|
|
8427
|
+
}
|
|
8428
|
+
});
|
|
7899
8429
|
}
|
|
7900
8430
|
});
|
|
7901
8431
|
canvas.discardActiveObject();
|
|
@@ -8021,6 +8551,7 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
8021
8551
|
syncTextboxFabricPadding(obj);
|
|
8022
8552
|
}
|
|
8023
8553
|
}
|
|
8554
|
+
applyInteractiveGroupFlags(obj);
|
|
8024
8555
|
} catch (reviverErr) {
|
|
8025
8556
|
canvasWarn('Canvas: loadCanvas reviver error for object', obj?.type, reviverErr);
|
|
8026
8557
|
}
|
|
@@ -8051,6 +8582,9 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
8051
8582
|
};
|
|
8052
8583
|
img.src = obj._imageUrl;
|
|
8053
8584
|
}
|
|
8585
|
+
} else if (String(obj.type || '').toLowerCase() === 'group') {
|
|
8586
|
+
applyInteractiveGroupFlags(obj);
|
|
8587
|
+
(obj.getObjects?.() || []).forEach(restoreObject);
|
|
8054
8588
|
}
|
|
8055
8589
|
};
|
|
8056
8590
|
allObjects.forEach(restoreObject);
|
|
@@ -8090,7 +8624,8 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
8090
8624
|
const spatialOrphans = canvas.getObjects().filter((obj: any) =>
|
|
8091
8625
|
!isFrameShape(obj) &&
|
|
8092
8626
|
!(obj as any)._isFrameBackground &&
|
|
8093
|
-
!obj.group // not already inside any group
|
|
8627
|
+
!obj.group && // not already inside any group
|
|
8628
|
+
String(obj.type || '').toLowerCase() !== 'group'
|
|
8094
8629
|
);
|
|
8095
8630
|
for (const obj of spatialOrphans) {
|
|
8096
8631
|
const center = obj.getCenterPoint();
|
|
@@ -8430,7 +8965,6 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
8430
8965
|
}
|
|
8431
8966
|
|
|
8432
8967
|
// If it's a plain group or activeSelection (NOT a Frame), apply to all children.
|
|
8433
|
-
// Frames are caught above and returned early — this guard prevents accidental Group-path execution.
|
|
8434
8968
|
if (!isFrameShape(selectedObject) && (selectedObject.type === 'group' || selectedObject.type === 'activeSelection' || selectedObject.type === 'activeselection')) {
|
|
8435
8969
|
const group = selectedObject as Group;
|
|
8436
8970
|
group.getObjects().forEach((obj: any) => {
|
|
@@ -8473,6 +9007,74 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
8473
9007
|
selectedObject = temp;
|
|
8474
9008
|
canvasLog('Canvas: Updated fill color to', color);
|
|
8475
9009
|
}
|
|
9010
|
+
|
|
9011
|
+
function refreshSelectedObjectUi() {
|
|
9012
|
+
const temp = selectedObject;
|
|
9013
|
+
selectedObject = null;
|
|
9014
|
+
selectedObject = temp;
|
|
9015
|
+
}
|
|
9016
|
+
|
|
9017
|
+
/** Group box chrome (behind/around children). Does not paint children. */
|
|
9018
|
+
function updateGroupFill(color: string) {
|
|
9019
|
+
if (!canvas || !isRegularGroup(selectedObject)) return;
|
|
9020
|
+
(selectedObject as any)._groupFill = color;
|
|
9021
|
+
selectedObject.dirty = true;
|
|
9022
|
+
canvas.requestRenderAll();
|
|
9023
|
+
refreshSelectedObjectUi();
|
|
9024
|
+
canvasLog('Canvas: Updated group chrome fill to', color);
|
|
9025
|
+
}
|
|
9026
|
+
|
|
9027
|
+
function updateGroupFillOpacity(opacityPercent: number) {
|
|
9028
|
+
if (!canvas || !isRegularGroup(selectedObject)) return;
|
|
9029
|
+
(selectedObject as any)._groupFillOpacity = opacityPercent / 100;
|
|
9030
|
+
selectedObject.dirty = true;
|
|
9031
|
+
canvas.requestRenderAll();
|
|
9032
|
+
refreshSelectedObjectUi();
|
|
9033
|
+
canvasLog('Canvas: Updated group chrome fill opacity to', opacityPercent + '%');
|
|
9034
|
+
}
|
|
9035
|
+
|
|
9036
|
+
function updateGroupStroke(color: string) {
|
|
9037
|
+
if (!canvas || !isRegularGroup(selectedObject)) return;
|
|
9038
|
+
(selectedObject as any)._groupStroke = color;
|
|
9039
|
+
if (!(selectedObject as any)._groupStrokeWidth) {
|
|
9040
|
+
(selectedObject as any)._groupStrokeWidth = 2;
|
|
9041
|
+
}
|
|
9042
|
+
selectedObject.dirty = true;
|
|
9043
|
+
canvas.requestRenderAll();
|
|
9044
|
+
refreshSelectedObjectUi();
|
|
9045
|
+
canvasLog('Canvas: Updated group chrome stroke to', color);
|
|
9046
|
+
}
|
|
9047
|
+
|
|
9048
|
+
function updateGroupStrokeWidth(width: number) {
|
|
9049
|
+
if (!canvas || !isRegularGroup(selectedObject)) return;
|
|
9050
|
+
(selectedObject as any)._groupStrokeWidth = width;
|
|
9051
|
+
if (width > 0 && !(selectedObject as any)._groupStroke) {
|
|
9052
|
+
(selectedObject as any)._groupStroke = '#000000';
|
|
9053
|
+
}
|
|
9054
|
+
selectedObject.dirty = true;
|
|
9055
|
+
canvas.requestRenderAll();
|
|
9056
|
+
refreshSelectedObjectUi();
|
|
9057
|
+
debouncedHistorySnapshot();
|
|
9058
|
+
canvasLog('Canvas: Updated group chrome stroke width to', width);
|
|
9059
|
+
}
|
|
9060
|
+
|
|
9061
|
+
function updateGroupStrokeType(type: 'solid' | 'dashed' | 'dotted') {
|
|
9062
|
+
if (!canvas || !isRegularGroup(selectedObject)) return;
|
|
9063
|
+
let dashArray: number[] | null = null;
|
|
9064
|
+
if (type === 'dashed') dashArray = [10, 5];
|
|
9065
|
+
else if (type === 'dotted') dashArray = [2, 4];
|
|
9066
|
+
(selectedObject as any)._groupStrokeDashArray = dashArray;
|
|
9067
|
+
if (!(selectedObject as any)._groupStroke) {
|
|
9068
|
+
(selectedObject as any)._groupStroke = '#000000';
|
|
9069
|
+
}
|
|
9070
|
+
if (!(selectedObject as any)._groupStrokeWidth) {
|
|
9071
|
+
(selectedObject as any)._groupStrokeWidth = 2;
|
|
9072
|
+
}
|
|
9073
|
+
selectedObject.dirty = true;
|
|
9074
|
+
canvas.requestRenderAll();
|
|
9075
|
+
refreshSelectedObjectUi();
|
|
9076
|
+
canvasLog('Canvas: Updated group chrome stroke type to', type);
|
|
9077
|
+
}
|
|
8476
9078
|
|
|
8477
9079
|
// Helper to convert any color to rgba with specified alpha
|
|
8478
9080
|
function colorToRgba(color: string, alpha: number): string {
|
|
@@ -8567,7 +9169,6 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
8567
9169
|
}
|
|
8568
9170
|
// Set on container too for UI reflection
|
|
8569
9171
|
selectedObject._fillOpacity = alpha;
|
|
8570
|
-
// If it's a regular group or activeSelection, apply to all children
|
|
8571
9172
|
} else if (selectedObject.type === 'group' || selectedObject.type === 'activeSelection' || selectedObject.type === 'activeselection') {
|
|
8572
9173
|
const group = selectedObject as Group;
|
|
8573
9174
|
group.getObjects().forEach((obj: any) => {
|
|
@@ -9196,48 +9797,61 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
9196
9797
|
canvasLog('Canvas: Reordered in frame', { direction, from: idx, to: newIdx });
|
|
9197
9798
|
}
|
|
9198
9799
|
|
|
9199
|
-
function
|
|
9800
|
+
function reorderInGroup(group: any, obj: any, direction: 'front' | 'forward' | 'backward' | 'back') {
|
|
9801
|
+
const objects = group._objects as any[];
|
|
9802
|
+
const idx = objects.indexOf(obj);
|
|
9803
|
+
if (idx === -1) return;
|
|
9804
|
+
const minIndex = 0;
|
|
9805
|
+
const maxIndex = objects.length - 1;
|
|
9806
|
+
objects.splice(idx, 1);
|
|
9807
|
+
let newIdx: number;
|
|
9808
|
+
switch (direction) {
|
|
9809
|
+
case 'front': newIdx = maxIndex; break;
|
|
9810
|
+
case 'forward': newIdx = Math.min(idx + 1, maxIndex); break;
|
|
9811
|
+
case 'backward': newIdx = Math.max(idx - 1, minIndex); break;
|
|
9812
|
+
case 'back': newIdx = minIndex; break;
|
|
9813
|
+
}
|
|
9814
|
+
objects.splice(newIdx, 0, obj);
|
|
9815
|
+
group.dirty = true;
|
|
9816
|
+
group.objectCaching = false;
|
|
9817
|
+
canvasLog('Canvas: Reordered in group', { direction, from: idx, to: newIdx });
|
|
9818
|
+
}
|
|
9819
|
+
|
|
9820
|
+
function applyLayerOrder(direction: 'front' | 'forward' | 'backward' | 'back') {
|
|
9200
9821
|
if (!selectedObject || !canvas) return;
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9822
|
+
const parent = selectedObject.group;
|
|
9823
|
+
if (parent && isFrameShape(parent)) {
|
|
9824
|
+
reorderInFrame(parent, selectedObject, direction);
|
|
9825
|
+
} else if (parent && isRegularGroup(parent)) {
|
|
9826
|
+
reorderInGroup(parent, selectedObject, direction);
|
|
9827
|
+
} else if (direction === 'front') {
|
|
9204
9828
|
canvas.bringObjectToFront(selectedObject);
|
|
9829
|
+
} else if (direction === 'forward') {
|
|
9830
|
+
canvas.bringObjectForward(selectedObject);
|
|
9831
|
+
} else if (direction === 'backward') {
|
|
9832
|
+
canvas.sendObjectBackwards(selectedObject);
|
|
9833
|
+
} else {
|
|
9834
|
+
canvas.sendObjectToBack(selectedObject);
|
|
9205
9835
|
}
|
|
9836
|
+
stampTopLevelPaintOrder();
|
|
9206
9837
|
canvas.requestRenderAll();
|
|
9207
9838
|
takeHistorySnapshot();
|
|
9208
9839
|
}
|
|
9840
|
+
|
|
9841
|
+
function bringToFront() {
|
|
9842
|
+
applyLayerOrder('front');
|
|
9843
|
+
}
|
|
9209
9844
|
|
|
9210
9845
|
function bringForward() {
|
|
9211
|
-
|
|
9212
|
-
if (selectedObject.group && isFrameShape(selectedObject.group)) {
|
|
9213
|
-
reorderInFrame(selectedObject.group, selectedObject, 'forward');
|
|
9214
|
-
} else {
|
|
9215
|
-
canvas.bringObjectForward(selectedObject);
|
|
9216
|
-
}
|
|
9217
|
-
canvas.requestRenderAll();
|
|
9218
|
-
takeHistorySnapshot();
|
|
9846
|
+
applyLayerOrder('forward');
|
|
9219
9847
|
}
|
|
9220
9848
|
|
|
9221
9849
|
function sendBackward() {
|
|
9222
|
-
|
|
9223
|
-
if (selectedObject.group && isFrameShape(selectedObject.group)) {
|
|
9224
|
-
reorderInFrame(selectedObject.group, selectedObject, 'backward');
|
|
9225
|
-
} else {
|
|
9226
|
-
canvas.sendObjectBackwards(selectedObject);
|
|
9227
|
-
}
|
|
9228
|
-
canvas.requestRenderAll();
|
|
9229
|
-
takeHistorySnapshot();
|
|
9850
|
+
applyLayerOrder('backward');
|
|
9230
9851
|
}
|
|
9231
9852
|
|
|
9232
9853
|
function sendToBack() {
|
|
9233
|
-
|
|
9234
|
-
if (selectedObject.group && isFrameShape(selectedObject.group)) {
|
|
9235
|
-
reorderInFrame(selectedObject.group, selectedObject, 'back');
|
|
9236
|
-
} else {
|
|
9237
|
-
canvas.sendObjectToBack(selectedObject);
|
|
9238
|
-
}
|
|
9239
|
-
canvas.requestRenderAll();
|
|
9240
|
-
takeHistorySnapshot();
|
|
9854
|
+
applyLayerOrder('back');
|
|
9241
9855
|
}
|
|
9242
9856
|
|
|
9243
9857
|
/**
|
|
@@ -10552,6 +11166,11 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
10552
11166
|
{wireframeMode}
|
|
10553
11167
|
onFillChange={updateSelectedFill}
|
|
10554
11168
|
onFillOpacityChange={updateSelectedFillOpacity}
|
|
11169
|
+
onGroupFillChange={updateGroupFill}
|
|
11170
|
+
onGroupFillOpacityChange={updateGroupFillOpacity}
|
|
11171
|
+
onGroupStrokeChange={updateGroupStroke}
|
|
11172
|
+
onGroupStrokeWidthChange={updateGroupStrokeWidth}
|
|
11173
|
+
onGroupStrokeTypeChange={updateGroupStrokeType}
|
|
10555
11174
|
onBackgroundColorChange={updateSelectedBackgroundColor}
|
|
10556
11175
|
onBackgroundOpacityChange={updateSelectedBackgroundOpacity}
|
|
10557
11176
|
onStrokeChange={updateSelectedStroke}
|
|
@@ -10589,6 +11208,7 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
10589
11208
|
onWidgetClick={onWidgetImageClick}
|
|
10590
11209
|
{onWidgetRename}
|
|
10591
11210
|
{onRefreshScreenshot}
|
|
11211
|
+
onSetAsPrimary={handleSetPrimaryFromPanel}
|
|
10592
11212
|
{getWidgetName}
|
|
10593
11213
|
{isWidgetKnown}
|
|
10594
11214
|
onRemoveOrphanedShape={() => { _deleteActiveObjects(); }}
|
|
@@ -10673,6 +11293,6 @@ function checkAndAddNewShapeToFrame(shape: any) {
|
|
|
10673
11293
|
max-w-[calc(100%-1rem)] w-11/12
|
|
10674
11294
|
-->
|
|
10675
11295
|
|
|
10676
|
-
<style>/*! tailwindcss v4.
|
|
11296
|
+
<style>/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
|
|
10677
11297
|
button{cursor:pointer}.canvas-container{-webkit-user-select:none;user-select:none}.canvas-container input,.canvas-container textarea{-webkit-user-select:text;user-select:text}.canvas-loader-spinner{border:3px solid #e5e7eb;border-top-color:#374151;border-radius:50%;width:36px;height:36px;animation:.8s linear infinite canvas-spin}@keyframes canvas-spin{to{transform:rotate(360deg)}}
|
|
10678
11298
|
</style>
|