@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.1
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/EF_FRAMEGEN.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +16 -6
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +129 -56
- package/dist/elements/EFThumbnailStrip.js +605 -359
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +233 -25
- package/dist/elements/EFTimegroup.js +865 -144
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +154 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +171 -28
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +7 -1
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +178 -0
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +833 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/preview/workers/encoderWorkerInline.js +103 -0
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +71 -67
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.94.0/helpers/decorate.js";
|
|
2
|
+
import { TWMixin } from "../gui/TWMixin2.js";
|
|
3
|
+
import { EFTargetable } from "../elements/TargetController.js";
|
|
4
|
+
import { panZoomTransformContext } from "../gui/panZoomTransformContext.js";
|
|
5
|
+
import { SelectionController } from "./selection/SelectionController.js";
|
|
6
|
+
import { selectionContext } from "./selection/selectionContext.js";
|
|
7
|
+
import { getElementBounds } from "./getElementBounds.js";
|
|
8
|
+
import "./overlays/SelectionOverlay.js";
|
|
9
|
+
import { canvasToScreen, screenToCanvas } from "./coordinateTransform.js";
|
|
10
|
+
import "../gui/EFOverlayLayer.js";
|
|
11
|
+
import { getRotatedBoundingBox, parseRotationFromTransform } from "../gui/transformCalculations.js";
|
|
12
|
+
import "../gui/EFTransformHandles.js";
|
|
13
|
+
import { findRootTemporal } from "../elements/findRootTemporal.js";
|
|
14
|
+
import { consume, provide } from "@lit/context";
|
|
15
|
+
import { LitElement, css, html } from "lit";
|
|
16
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
17
|
+
|
|
18
|
+
//#region src/canvas/EFCanvas.ts
|
|
19
|
+
let EFCanvas = class EFCanvas$1 extends EFTargetable(TWMixin(LitElement)) {
|
|
20
|
+
static {
|
|
21
|
+
this.styles = [css`
|
|
22
|
+
:host {
|
|
23
|
+
display: block;
|
|
24
|
+
position: relative;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
}
|
|
28
|
+
.canvas-content {
|
|
29
|
+
position: relative;
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: 100%;
|
|
32
|
+
}
|
|
33
|
+
`];
|
|
34
|
+
}
|
|
35
|
+
constructor() {
|
|
36
|
+
super();
|
|
37
|
+
this.elementIdAttribute = "data-element-id";
|
|
38
|
+
this.enableTransformHandles = true;
|
|
39
|
+
this.elementRegistry = /* @__PURE__ */ new Map();
|
|
40
|
+
this.elementMetadata = /* @__PURE__ */ new Map();
|
|
41
|
+
this.overlayLayer = null;
|
|
42
|
+
this.selectionOverlay = null;
|
|
43
|
+
this.transformHandlesMap = /* @__PURE__ */ new Map();
|
|
44
|
+
this.overlayRafId = null;
|
|
45
|
+
this.isDragging = false;
|
|
46
|
+
this.dragStarted = false;
|
|
47
|
+
this.dragStartPos = null;
|
|
48
|
+
this.dragStartCanvasPos = null;
|
|
49
|
+
this.dragStartElementPositions = /* @__PURE__ */ new Map();
|
|
50
|
+
this.draggedElementId = null;
|
|
51
|
+
this.capturedPointerId = null;
|
|
52
|
+
this.DRAG_THRESHOLD = 5;
|
|
53
|
+
this.isBoxSelecting = false;
|
|
54
|
+
this.boxSelectStart = null;
|
|
55
|
+
this.boxSelectModifierKeys = false;
|
|
56
|
+
this.emptySpaceClickPos = null;
|
|
57
|
+
this.elementHoverHandlers = /* @__PURE__ */ new Map();
|
|
58
|
+
this._activeRootTemporal = null;
|
|
59
|
+
this.highlightedElement = null;
|
|
60
|
+
this.handlePointerDown = (e) => {
|
|
61
|
+
if (e.button !== 0) return;
|
|
62
|
+
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
63
|
+
let topmostElementId = null;
|
|
64
|
+
for (const el of elementsAtPoint) {
|
|
65
|
+
if (el.tagName === "EF-OVERLAY-LAYER" || el.tagName === "EF-TRANSFORM-HANDLES" || el.closest("ef-overlay-layer") || el.closest("ef-transform-handles")) continue;
|
|
66
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
67
|
+
if (el === this) continue;
|
|
68
|
+
if (!this.contains(el)) continue;
|
|
69
|
+
try {
|
|
70
|
+
this.tryRegisterElement(el);
|
|
71
|
+
const elementId = el.id || el.getAttribute(this.elementIdAttribute);
|
|
72
|
+
if (elementId && this.elementRegistry.has(elementId)) {
|
|
73
|
+
topmostElementId = elementId;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
let current = el.parentElement;
|
|
78
|
+
while (current && current !== this) {
|
|
79
|
+
try {
|
|
80
|
+
this.tryRegisterElement(current);
|
|
81
|
+
const elementId = current.id || current.getAttribute(this.elementIdAttribute);
|
|
82
|
+
if (elementId && this.elementRegistry.has(elementId)) {
|
|
83
|
+
topmostElementId = elementId;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
current = current.parentElement;
|
|
88
|
+
}
|
|
89
|
+
if (topmostElementId) break;
|
|
90
|
+
}
|
|
91
|
+
if (topmostElementId) {
|
|
92
|
+
const elementId = topmostElementId;
|
|
93
|
+
const isSelected = this.selectionController.getModel().selectedIds.has(elementId);
|
|
94
|
+
if (e.shiftKey) {
|
|
95
|
+
this.selectionController.selectionContext.addToSelection(elementId);
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
} else if (e.ctrlKey || e.metaKey) {
|
|
98
|
+
this.selectionController.selectionContext.toggle(elementId);
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
e.stopPropagation();
|
|
101
|
+
} else {
|
|
102
|
+
if (!isSelected) this.selectionController.selectionContext.select(elementId);
|
|
103
|
+
const selectedIds = Array.from(this.selectionController.getModel().selectedIds);
|
|
104
|
+
for (const id of selectedIds) {
|
|
105
|
+
this.updateElementMetadata(id);
|
|
106
|
+
const metadata = this.elementMetadata.get(id);
|
|
107
|
+
if (metadata) this.dragStartElementPositions.set(id, {
|
|
108
|
+
x: metadata.x,
|
|
109
|
+
y: metadata.y
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
this.isDragging = false;
|
|
113
|
+
this.dragStarted = false;
|
|
114
|
+
this.dragStartPos = {
|
|
115
|
+
x: e.clientX,
|
|
116
|
+
y: e.clientY
|
|
117
|
+
};
|
|
118
|
+
this.draggedElementId = elementId;
|
|
119
|
+
this.capturedPointerId = e.pointerId;
|
|
120
|
+
try {
|
|
121
|
+
this.setPointerCapture(e.pointerId);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.warn("[EFCanvas] Failed to capture pointer:", err);
|
|
124
|
+
}
|
|
125
|
+
const canvasRect = (this.shadowRoot?.querySelector(".canvas-content"))?.getBoundingClientRect() || this.getBoundingClientRect();
|
|
126
|
+
this.dragStartCanvasPos = screenToCanvas(e.clientX, e.clientY, canvasRect, this.panZoomTransform);
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
const canvasRect = (this.shadowRoot?.querySelector(".canvas-content"))?.getBoundingClientRect() || this.getBoundingClientRect();
|
|
131
|
+
const canvasPos = screenToCanvas(e.clientX, e.clientY, canvasRect, this.panZoomTransform);
|
|
132
|
+
this.boxSelectModifierKeys = e.shiftKey || e.ctrlKey || e.metaKey;
|
|
133
|
+
this.isBoxSelecting = true;
|
|
134
|
+
this.boxSelectStart = canvasPos;
|
|
135
|
+
this.selectionController.selectionContext.startBoxSelect(canvasPos.x, canvasPos.y);
|
|
136
|
+
this.capturedPointerId = e.pointerId;
|
|
137
|
+
try {
|
|
138
|
+
this.setPointerCapture(e.pointerId);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn("[EFCanvas] Failed to capture pointer:", err);
|
|
141
|
+
}
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
e.stopPropagation();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
this.handlePointerMove = (e) => {
|
|
147
|
+
if (e.buttons === 0) {
|
|
148
|
+
if (this.capturedPointerId !== null) {
|
|
149
|
+
try {
|
|
150
|
+
this.releasePointerCapture(e.pointerId);
|
|
151
|
+
} catch (err) {}
|
|
152
|
+
this.capturedPointerId = null;
|
|
153
|
+
}
|
|
154
|
+
if (this.isDragging || this.dragStarted || this.draggedElementId !== null) {
|
|
155
|
+
this.isDragging = false;
|
|
156
|
+
this.dragStarted = false;
|
|
157
|
+
this.dragStartPos = null;
|
|
158
|
+
this.dragStartCanvasPos = null;
|
|
159
|
+
this.dragStartElementPositions.clear();
|
|
160
|
+
this.draggedElementId = null;
|
|
161
|
+
}
|
|
162
|
+
if (this.isBoxSelecting) {
|
|
163
|
+
this.isBoxSelecting = false;
|
|
164
|
+
this.boxSelectStart = null;
|
|
165
|
+
this.boxSelectModifierKeys = false;
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const canvasRect = (this.shadowRoot?.querySelector(".canvas-content"))?.getBoundingClientRect() || this.getBoundingClientRect();
|
|
170
|
+
if (!this.dragStarted && this.draggedElementId && this.dragStartPos && this.dragStartCanvasPos && this.dragStartElementPositions.size > 0) if (Math.sqrt(Math.pow(e.clientX - this.dragStartPos.x, 2) + Math.pow(e.clientY - this.dragStartPos.y, 2)) >= this.DRAG_THRESHOLD) {
|
|
171
|
+
this.dragStarted = true;
|
|
172
|
+
this.isDragging = true;
|
|
173
|
+
} else return;
|
|
174
|
+
if (this.isDragging && this.dragStarted && this.dragStartCanvasPos && this.dragStartElementPositions.size > 0) {
|
|
175
|
+
const canvasPos = screenToCanvas(e.clientX, e.clientY, canvasRect, this.panZoomTransform);
|
|
176
|
+
const deltaX = canvasPos.x - this.dragStartCanvasPos.x;
|
|
177
|
+
const deltaY = canvasPos.y - this.dragStartCanvasPos.y;
|
|
178
|
+
for (const [elementId, startPos] of this.dragStartElementPositions.entries()) {
|
|
179
|
+
const newX = startPos.x + deltaX;
|
|
180
|
+
const newY = startPos.y + deltaY;
|
|
181
|
+
this.updateElementPosition(elementId, newX, newY);
|
|
182
|
+
}
|
|
183
|
+
e.stopPropagation();
|
|
184
|
+
} else if (this.isBoxSelecting && this.boxSelectStart) {
|
|
185
|
+
const canvasPos = screenToCanvas(e.clientX, e.clientY, canvasRect, this.panZoomTransform);
|
|
186
|
+
this.selectionController.selectionContext.updateBoxSelect(canvasPos.x, canvasPos.y);
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
this.handlePointerUp = (e) => {
|
|
191
|
+
const wasDragging = this.isDragging || this.dragStarted;
|
|
192
|
+
const wasBoxSelecting = this.isBoxSelecting;
|
|
193
|
+
if (this.capturedPointerId !== null) {
|
|
194
|
+
try {
|
|
195
|
+
this.releasePointerCapture(e.pointerId);
|
|
196
|
+
} catch (err) {}
|
|
197
|
+
this.capturedPointerId = null;
|
|
198
|
+
}
|
|
199
|
+
if (this.draggedElementId !== null || this.dragStartPos !== null) {
|
|
200
|
+
this.isDragging = false;
|
|
201
|
+
this.dragStarted = false;
|
|
202
|
+
this.dragStartPos = null;
|
|
203
|
+
this.dragStartCanvasPos = null;
|
|
204
|
+
this.dragStartElementPositions.clear();
|
|
205
|
+
this.draggedElementId = null;
|
|
206
|
+
}
|
|
207
|
+
if (this.isBoxSelecting) {
|
|
208
|
+
this.isBoxSelecting = false;
|
|
209
|
+
this.selectionController.selectionContext.endBoxSelect((bounds) => this.hitTest(bounds), this.boxSelectModifierKeys);
|
|
210
|
+
this.boxSelectStart = null;
|
|
211
|
+
this.boxSelectModifierKeys = false;
|
|
212
|
+
}
|
|
213
|
+
if (this.emptySpaceClickPos) {
|
|
214
|
+
if (!wasDragging && !wasBoxSelecting) {
|
|
215
|
+
if (!(Math.abs(e.clientX - this.emptySpaceClickPos.x) > 2 || Math.abs(e.clientY - this.emptySpaceClickPos.y) > 2)) this.selectionController.selectionContext.clear();
|
|
216
|
+
}
|
|
217
|
+
this.emptySpaceClickPos = null;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
this.lastMultiSelectionRotation = null;
|
|
221
|
+
this.multiSelectionRotationCenter = null;
|
|
222
|
+
this.multiSelectionResizeInitial = null;
|
|
223
|
+
this.selectionController = new SelectionController(this);
|
|
224
|
+
this.selectionController.setHitTest((bounds) => this.hitTest(bounds));
|
|
225
|
+
this.selectionContext = this.selectionController.selectionContext;
|
|
226
|
+
}
|
|
227
|
+
connectedCallback() {
|
|
228
|
+
super.connectedCallback();
|
|
229
|
+
this.setupElementTracking();
|
|
230
|
+
this.setupOverlayLayer();
|
|
231
|
+
this.setupSelectionOverlay();
|
|
232
|
+
this.setupSelectionListener();
|
|
233
|
+
this.addEventListener("pointerdown", this.handlePointerDown);
|
|
234
|
+
this.addEventListener("pointermove", this.handlePointerMove);
|
|
235
|
+
this.addEventListener("pointerup", this.handlePointerUp);
|
|
236
|
+
this.addEventListener("pointercancel", this.handlePointerUp);
|
|
237
|
+
this.startOverlayRafLoop();
|
|
238
|
+
}
|
|
239
|
+
disconnectedCallback() {
|
|
240
|
+
super.disconnectedCallback();
|
|
241
|
+
this.stopOverlayRafLoop();
|
|
242
|
+
this.cleanupOverlayLayer();
|
|
243
|
+
this.cleanupSelectionOverlay();
|
|
244
|
+
this.cleanupTransformHandles();
|
|
245
|
+
this.removeSelectionListener();
|
|
246
|
+
this.removeEventListener("pointerdown", this.handlePointerDown);
|
|
247
|
+
this.removeEventListener("pointermove", this.handlePointerMove);
|
|
248
|
+
this.removeEventListener("pointerup", this.handlePointerUp);
|
|
249
|
+
this.removeEventListener("pointercancel", this.handlePointerUp);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Setup mutation observer to track element additions/removals.
|
|
253
|
+
*/
|
|
254
|
+
setupElementTracking() {
|
|
255
|
+
new MutationObserver((mutations) => {
|
|
256
|
+
for (const mutation of mutations) {
|
|
257
|
+
for (const node of Array.from(mutation.addedNodes)) if (node instanceof HTMLElement) this.tryRegisterElement(node);
|
|
258
|
+
for (const node of Array.from(mutation.removedNodes)) if (node instanceof HTMLElement) this.unregisterElement(node);
|
|
259
|
+
}
|
|
260
|
+
}).observe(this, {
|
|
261
|
+
childList: true,
|
|
262
|
+
subtree: true
|
|
263
|
+
});
|
|
264
|
+
const registerAllElements = (parent) => {
|
|
265
|
+
for (const child of Array.from(parent.children)) if (child instanceof HTMLElement) {
|
|
266
|
+
this.tryRegisterElement(child);
|
|
267
|
+
if (child.children.length > 0) registerAllElements(child);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
registerAllElements(this);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Try to register an element, auto-generating an ID if needed.
|
|
274
|
+
* Public method for external use (e.g., hierarchy selection).
|
|
275
|
+
*/
|
|
276
|
+
tryRegisterElement(element) {
|
|
277
|
+
const existingId = element.getAttribute(this.elementIdAttribute) || element.id;
|
|
278
|
+
if (existingId && this.elementRegistry.has(existingId)) return;
|
|
279
|
+
try {
|
|
280
|
+
let elementId = element.id && element.id.trim() !== "" ? element.id : element.getAttribute(this.elementIdAttribute);
|
|
281
|
+
if (!elementId) elementId = `${element.tagName.toLowerCase()}-${Array.from(element.parentElement?.children || []).indexOf(element)}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
282
|
+
if (!element.id || element.id.trim() === "") element.id = elementId;
|
|
283
|
+
if (!element.getAttribute(this.elementIdAttribute)) element.setAttribute(this.elementIdAttribute, elementId);
|
|
284
|
+
this.registerElement(element, elementId);
|
|
285
|
+
} catch (error) {}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Register an element for canvas management.
|
|
289
|
+
* @throws Error if element does not have an ID
|
|
290
|
+
*/
|
|
291
|
+
registerElement(element, id) {
|
|
292
|
+
const elementId = id || element.getAttribute(this.elementIdAttribute) || (element.id && element.id.trim() !== "" ? element.id : null);
|
|
293
|
+
if (!elementId) throw new Error(`Element must have an ID. Provide either the 'id' parameter, set the '${this.elementIdAttribute}' attribute, or set the 'id' attribute on the element.`);
|
|
294
|
+
if (this.elementRegistry.has(elementId)) {
|
|
295
|
+
if (this.elementRegistry.get(elementId) !== element) throw new Error(`Element with ID '${elementId}' is already registered. Each element must have a unique ID.`);
|
|
296
|
+
return elementId;
|
|
297
|
+
}
|
|
298
|
+
if (!element.getAttribute(this.elementIdAttribute)) element.setAttribute(this.elementIdAttribute, elementId);
|
|
299
|
+
if (!element.id || element.id.trim() === "") element.id = elementId;
|
|
300
|
+
this.elementRegistry.set(elementId, element);
|
|
301
|
+
this.setupElementHoverListeners(element, elementId);
|
|
302
|
+
if (element.parentElement === this) {
|
|
303
|
+
if (!element.style.position) element.style.position = "absolute";
|
|
304
|
+
if (!element.style.display || element.style.display === "none") element.style.display = "block";
|
|
305
|
+
}
|
|
306
|
+
this.updateElementMetadata(elementId);
|
|
307
|
+
return elementId;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Unregister an element.
|
|
311
|
+
*/
|
|
312
|
+
unregisterElement(element) {
|
|
313
|
+
const elementId = typeof element === "string" ? element : element.getAttribute(this.elementIdAttribute);
|
|
314
|
+
if (elementId) {
|
|
315
|
+
const elementToUnregister = typeof element === "string" ? this.elementRegistry.get(elementId) : element;
|
|
316
|
+
if (elementToUnregister) {
|
|
317
|
+
elementToUnregister.removeAttribute("data-selected");
|
|
318
|
+
elementToUnregister.removeAttribute("data-highlighted");
|
|
319
|
+
}
|
|
320
|
+
this.removeElementHoverListeners(elementId);
|
|
321
|
+
this.elementRegistry.delete(elementId);
|
|
322
|
+
this.elementMetadata.delete(elementId);
|
|
323
|
+
this.selectionController.selectionContext.deselect(elementId);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Setup hover event listeners for an element to update highlightedElement.
|
|
328
|
+
* The canvas is the source of truth for highlight state.
|
|
329
|
+
*/
|
|
330
|
+
setupElementHoverListeners(element, elementId) {
|
|
331
|
+
this.removeElementHoverListeners(elementId);
|
|
332
|
+
const mouseenterHandler = () => {
|
|
333
|
+
this.setHighlightedElement(element);
|
|
334
|
+
};
|
|
335
|
+
const mouseleaveHandler = () => {
|
|
336
|
+
if (this.highlightedElement === element) this.setHighlightedElement(null);
|
|
337
|
+
};
|
|
338
|
+
element.addEventListener("mouseenter", mouseenterHandler);
|
|
339
|
+
element.addEventListener("mouseleave", mouseleaveHandler);
|
|
340
|
+
this.elementHoverHandlers.set(elementId, {
|
|
341
|
+
mouseenter: mouseenterHandler,
|
|
342
|
+
mouseleave: mouseleaveHandler
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Set the highlighted element. Called by canvas hover handlers
|
|
347
|
+
* and by external panels (hierarchy, timeline) when user hovers items.
|
|
348
|
+
*/
|
|
349
|
+
setHighlightedElement(element) {
|
|
350
|
+
if (this.highlightedElement !== element) {
|
|
351
|
+
if (this.highlightedElement) this.highlightedElement.removeAttribute("data-highlighted");
|
|
352
|
+
this.highlightedElement = element;
|
|
353
|
+
if (element) element.setAttribute("data-highlighted", "true");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Remove hover event listeners for an element.
|
|
358
|
+
*/
|
|
359
|
+
removeElementHoverListeners(elementId) {
|
|
360
|
+
const handlers = this.elementHoverHandlers.get(elementId);
|
|
361
|
+
if (!handlers) return;
|
|
362
|
+
const element = this.elementRegistry.get(elementId);
|
|
363
|
+
if (element) {
|
|
364
|
+
element.removeEventListener("mouseenter", handlers.mouseenter);
|
|
365
|
+
element.removeEventListener("mouseleave", handlers.mouseleave);
|
|
366
|
+
}
|
|
367
|
+
this.elementHoverHandlers.delete(elementId);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Update element metadata from DOM.
|
|
371
|
+
*
|
|
372
|
+
* UNIFIED APPROACH - works for ALL elements regardless of rotation, scale, or nesting:
|
|
373
|
+
*
|
|
374
|
+
* 1. DIMENSIONS: Always use offsetWidth/offsetHeight
|
|
375
|
+
* - These are layout dimensions in the element's coordinate space
|
|
376
|
+
* - Unaffected by CSS transforms (rotation, scale, etc.)
|
|
377
|
+
* - Already in canvas coordinates (no scale division needed)
|
|
378
|
+
*
|
|
379
|
+
* 2. CENTER POSITION: Always use getBoundingClientRect() center
|
|
380
|
+
* - The center of the bounding box IS the element's center (transform-origin: center)
|
|
381
|
+
* - Works correctly for rotated elements (center is rotation-invariant)
|
|
382
|
+
* - Convert to canvas coordinates using screenToCanvas()
|
|
383
|
+
*
|
|
384
|
+
* 3. TOP-LEFT POSITION: Calculate from center and dimensions
|
|
385
|
+
* - x = centerX - width/2
|
|
386
|
+
* - y = centerY - height/2
|
|
387
|
+
*/
|
|
388
|
+
updateElementMetadata(elementId) {
|
|
389
|
+
const element = this.elementRegistry.get(elementId);
|
|
390
|
+
if (!element) return;
|
|
391
|
+
const canvasContent = this.shadowRoot?.querySelector(".canvas-content");
|
|
392
|
+
let actualWidth = element.offsetWidth;
|
|
393
|
+
let actualHeight = element.offsetHeight;
|
|
394
|
+
if (actualWidth === 0 || actualHeight === 0) {
|
|
395
|
+
const elementRect$1 = getElementBounds(element);
|
|
396
|
+
const scale = this.panZoomTransform?.scale || 1;
|
|
397
|
+
actualWidth = elementRect$1.width / scale;
|
|
398
|
+
actualHeight = elementRect$1.height / scale;
|
|
399
|
+
}
|
|
400
|
+
const elementRect = getElementBounds(element);
|
|
401
|
+
const screenCenterX = elementRect.left + elementRect.width / 2;
|
|
402
|
+
const screenCenterY = elementRect.top + elementRect.height / 2;
|
|
403
|
+
let canvasX;
|
|
404
|
+
let canvasY;
|
|
405
|
+
if (!canvasContent) {
|
|
406
|
+
const existingMetadata = this.elementMetadata.get(elementId);
|
|
407
|
+
canvasX = existingMetadata?.x ?? 0;
|
|
408
|
+
canvasY = existingMetadata?.y ?? 0;
|
|
409
|
+
} else {
|
|
410
|
+
const canvasCenter = screenToCanvas(screenCenterX, screenCenterY, canvasContent.getBoundingClientRect(), this.panZoomTransform);
|
|
411
|
+
canvasX = canvasCenter.x - actualWidth / 2;
|
|
412
|
+
canvasY = canvasCenter.y - actualHeight / 2;
|
|
413
|
+
}
|
|
414
|
+
let rotation = this.elementMetadata.get(elementId)?.rotation;
|
|
415
|
+
if (rotation === void 0) {
|
|
416
|
+
rotation = parseRotationFromTransform(window.getComputedStyle(element).transform);
|
|
417
|
+
if (rotation === 0) rotation = void 0;
|
|
418
|
+
}
|
|
419
|
+
this.elementMetadata.set(elementId, {
|
|
420
|
+
id: elementId,
|
|
421
|
+
element,
|
|
422
|
+
x: canvasX,
|
|
423
|
+
y: canvasY,
|
|
424
|
+
width: actualWidth,
|
|
425
|
+
height: actualHeight,
|
|
426
|
+
rotation
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Hit test - find elements intersecting with bounds.
|
|
431
|
+
* @param bounds - DOMRect in canvas coordinate space (from SelectionModel.boxSelectBounds)
|
|
432
|
+
*/
|
|
433
|
+
hitTest(bounds) {
|
|
434
|
+
const testRect = bounds;
|
|
435
|
+
const canvasRect = this.getBoundingClientRect();
|
|
436
|
+
const hitIds = [];
|
|
437
|
+
for (const [elementId, element] of this.elementRegistry.entries()) {
|
|
438
|
+
const elementBounds = getElementBounds(element);
|
|
439
|
+
const elementCanvasPos = screenToCanvas(elementBounds.left, elementBounds.top, canvasRect, this.panZoomTransform);
|
|
440
|
+
const elementCanvasWidth = elementBounds.width / (this.panZoomTransform?.scale || 1);
|
|
441
|
+
const elementCanvasHeight = elementBounds.height / (this.panZoomTransform?.scale || 1);
|
|
442
|
+
const elementRect = new DOMRect(elementCanvasPos.x, elementCanvasPos.y, elementCanvasWidth, elementCanvasHeight);
|
|
443
|
+
if (testRect.left < elementRect.right && testRect.right > elementRect.left && testRect.top < elementRect.bottom && testRect.bottom > elementRect.top) hitIds.push(elementId);
|
|
444
|
+
}
|
|
445
|
+
return hitIds;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Setup listener for selection changes to update data-selected attributes.
|
|
449
|
+
*/
|
|
450
|
+
setupSelectionListener() {
|
|
451
|
+
if (this.selectionChangeHandler) return;
|
|
452
|
+
this.selectionChangeHandler = () => {
|
|
453
|
+
this.updateSelectionAttributes();
|
|
454
|
+
};
|
|
455
|
+
this.selectionContext.addEventListener("selectionchange", this.selectionChangeHandler);
|
|
456
|
+
this.updateSelectionAttributes();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Remove selection change listener.
|
|
460
|
+
*/
|
|
461
|
+
removeSelectionListener() {
|
|
462
|
+
if (this.selectionChangeHandler) {
|
|
463
|
+
this.selectionContext.removeEventListener("selectionchange", this.selectionChangeHandler);
|
|
464
|
+
this.selectionChangeHandler = void 0;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get the root temporal element containing the current selection.
|
|
469
|
+
* Returns null if no element is selected or the selected element has no root temporal.
|
|
470
|
+
*/
|
|
471
|
+
get activeRootTemporal() {
|
|
472
|
+
return this._activeRootTemporal;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Update data-selected attributes on elements based on current selection.
|
|
476
|
+
* This enables pure CSS styling of selected elements.
|
|
477
|
+
*/
|
|
478
|
+
updateSelectionAttributes() {
|
|
479
|
+
const selectedIds = Array.from(this.selectionContext.selectedIds);
|
|
480
|
+
const allRegisteredIds = Array.from(this.elementRegistry.keys());
|
|
481
|
+
for (const id of allRegisteredIds) {
|
|
482
|
+
const element = this.elementRegistry.get(id);
|
|
483
|
+
if (element) element.removeAttribute("data-selected");
|
|
484
|
+
}
|
|
485
|
+
for (const id of selectedIds) {
|
|
486
|
+
const element = this.elementRegistry.get(id);
|
|
487
|
+
if (element) element.setAttribute("data-selected", "true");
|
|
488
|
+
}
|
|
489
|
+
const previousActiveRootTemporal = this._activeRootTemporal;
|
|
490
|
+
let newActiveRootTemporal = null;
|
|
491
|
+
if (selectedIds.length > 0) {
|
|
492
|
+
const selectedElement = document.getElementById(selectedIds[0] || "");
|
|
493
|
+
if (selectedElement) newActiveRootTemporal = findRootTemporal(selectedElement);
|
|
494
|
+
}
|
|
495
|
+
this._activeRootTemporal = newActiveRootTemporal;
|
|
496
|
+
if (previousActiveRootTemporal !== newActiveRootTemporal) this.dispatchEvent(new CustomEvent("activeroottemporalchange", {
|
|
497
|
+
detail: { activeRootTemporal: newActiveRootTemporal },
|
|
498
|
+
bubbles: true,
|
|
499
|
+
composed: true
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Update element position in canvas coordinates.
|
|
504
|
+
* Unified approach: Always calculate relative to parent (or .canvas-content for direct children).
|
|
505
|
+
* For direct children, parent position is (0, 0), so relative = absolute (no-op).
|
|
506
|
+
*
|
|
507
|
+
* For nested elements, we read parent's current position from DOM (not metadata) to ensure
|
|
508
|
+
* we're always calculating relative to the actual current position.
|
|
509
|
+
*/
|
|
510
|
+
updateElementPosition(elementId, x, y) {
|
|
511
|
+
const element = this.elementRegistry.get(elementId);
|
|
512
|
+
if (!element) {
|
|
513
|
+
console.warn("[EFCanvas] updateElementPosition: element not found", elementId);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const metadata = this.elementMetadata.get(elementId);
|
|
517
|
+
if (!metadata) return;
|
|
518
|
+
metadata.x = x;
|
|
519
|
+
metadata.y = y;
|
|
520
|
+
let parentX = 0;
|
|
521
|
+
let parentY = 0;
|
|
522
|
+
let parent = element.parentElement;
|
|
523
|
+
while (parent && parent !== this) {
|
|
524
|
+
const parentId = parent.id || parent.getAttribute(this.elementIdAttribute);
|
|
525
|
+
if (parentId && this.elementRegistry.has(parentId)) {
|
|
526
|
+
const parentWidth = parent.offsetWidth;
|
|
527
|
+
const parentHeight = parent.offsetHeight;
|
|
528
|
+
const parentRect = getElementBounds(parent);
|
|
529
|
+
const parentScreenCenterX = parentRect.left + parentRect.width / 2;
|
|
530
|
+
const parentScreenCenterY = parentRect.top + parentRect.height / 2;
|
|
531
|
+
const canvasContent = this.shadowRoot?.querySelector(".canvas-content");
|
|
532
|
+
if (canvasContent) {
|
|
533
|
+
const parentCanvasCenter = screenToCanvas(parentScreenCenterX, parentScreenCenterY, canvasContent.getBoundingClientRect(), this.panZoomTransform);
|
|
534
|
+
parentX = parentCanvasCenter.x - parentWidth / 2;
|
|
535
|
+
parentY = parentCanvasCenter.y - parentHeight / 2;
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
parent = parent.parentElement;
|
|
540
|
+
}
|
|
541
|
+
const relativeX = x - parentX;
|
|
542
|
+
const relativeY = y - parentY;
|
|
543
|
+
element.style.position = "absolute";
|
|
544
|
+
element.style.left = `${relativeX}px`;
|
|
545
|
+
element.style.top = `${relativeY}px`;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Get element metadata.
|
|
549
|
+
*/
|
|
550
|
+
getElementData(elementId) {
|
|
551
|
+
return this.elementMetadata.get(elementId) || null;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get all element data.
|
|
555
|
+
*/
|
|
556
|
+
getAllElementsData() {
|
|
557
|
+
return Array.from(this.elementMetadata.values());
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Convert screen coordinates to canvas coordinates (for API).
|
|
561
|
+
*/
|
|
562
|
+
screenToCanvasCoords(screenX, screenY) {
|
|
563
|
+
return screenToCanvas(screenX, screenY, this.getBoundingClientRect(), this.panZoomTransform);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Convert canvas coordinates to screen coordinates (for API).
|
|
567
|
+
*/
|
|
568
|
+
canvasToScreenCoords(canvasX, canvasY) {
|
|
569
|
+
return canvasToScreen(canvasX, canvasY, this.getBoundingClientRect(), this.panZoomTransform);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Setup overlay layer as sibling of panzoom (outside panzoom, same level as panzoom).
|
|
573
|
+
*/
|
|
574
|
+
setupOverlayLayer() {
|
|
575
|
+
const panZoom = this.closest("ef-pan-zoom");
|
|
576
|
+
if (!panZoom) return;
|
|
577
|
+
const panZoomParent = panZoom.parentElement;
|
|
578
|
+
if (panZoomParent) {
|
|
579
|
+
const existing = panZoomParent.querySelector("ef-overlay-layer");
|
|
580
|
+
if (existing) {
|
|
581
|
+
this.overlayLayer = existing;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (panZoomParent) {
|
|
586
|
+
const overlayLayer = document.createElement("ef-overlay-layer");
|
|
587
|
+
overlayLayer.style.position = "absolute";
|
|
588
|
+
overlayLayer.style.inset = "0";
|
|
589
|
+
overlayLayer.style.zIndex = "1";
|
|
590
|
+
overlayLayer.style.pointerEvents = "none";
|
|
591
|
+
panZoomParent.insertBefore(overlayLayer, panZoom.nextSibling);
|
|
592
|
+
this.overlayLayer = overlayLayer;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Cleanup overlay layer if we created it.
|
|
597
|
+
*/
|
|
598
|
+
cleanupOverlayLayer() {
|
|
599
|
+
if (this.overlayLayer && this.overlayLayer.parentElement) {
|
|
600
|
+
const parent = this.parentElement;
|
|
601
|
+
if (parent && parent.contains(this.overlayLayer)) {
|
|
602
|
+
if (this.overlayLayer.tagName !== "EF-CANVAS") this.overlayLayer.remove();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
this.overlayLayer = null;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Setup selection overlay as sibling of panzoom (outside panzoom, same level as panzoom).
|
|
609
|
+
* This ensures the overlay maintains 1:1 pixel ratio regardless of zoom level.
|
|
610
|
+
*/
|
|
611
|
+
setupSelectionOverlay() {
|
|
612
|
+
const panZoom = this.closest("ef-pan-zoom");
|
|
613
|
+
if (!panZoom) return;
|
|
614
|
+
const panZoomParent = panZoom.parentElement;
|
|
615
|
+
if (panZoomParent) {
|
|
616
|
+
const existing = panZoomParent.querySelector("ef-canvas-selection-overlay");
|
|
617
|
+
if (existing) {
|
|
618
|
+
this.selectionOverlay = existing;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (panZoomParent) {
|
|
623
|
+
const selectionOverlay = document.createElement("ef-canvas-selection-overlay");
|
|
624
|
+
selectionOverlay.selection = this.selectionContext;
|
|
625
|
+
selectionOverlay.canvas = this;
|
|
626
|
+
if (this.panZoomTransform) selectionOverlay.panZoomTransform = this.panZoomTransform;
|
|
627
|
+
panZoomParent.insertBefore(selectionOverlay, panZoom.nextSibling);
|
|
628
|
+
this.selectionOverlay = selectionOverlay;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Cleanup selection overlay if we created it.
|
|
633
|
+
*/
|
|
634
|
+
cleanupSelectionOverlay() {
|
|
635
|
+
if (this.selectionOverlay && this.selectionOverlay.parentElement) {
|
|
636
|
+
const panZoom = this.closest("ef-pan-zoom");
|
|
637
|
+
if (panZoom && panZoom.parentElement?.contains(this.selectionOverlay)) this.selectionOverlay.remove();
|
|
638
|
+
this.selectionOverlay = null;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Start RAF loop for overlay layer sync and transform handles updates.
|
|
643
|
+
*/
|
|
644
|
+
startOverlayRafLoop() {
|
|
645
|
+
if (this.overlayRafId !== null) return;
|
|
646
|
+
const update = () => {
|
|
647
|
+
if (this.overlayLayer && this.panZoomTransform) this.overlayLayer.panZoomTransform = this.panZoomTransform;
|
|
648
|
+
if (this.enableTransformHandles) this.updateTransformHandles();
|
|
649
|
+
this.overlayRafId = requestAnimationFrame(update);
|
|
650
|
+
};
|
|
651
|
+
this.overlayRafId = requestAnimationFrame(update);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Stop RAF loop.
|
|
655
|
+
*/
|
|
656
|
+
stopOverlayRafLoop() {
|
|
657
|
+
if (this.overlayRafId !== null) {
|
|
658
|
+
cancelAnimationFrame(this.overlayRafId);
|
|
659
|
+
this.overlayRafId = null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Update transform handles for selected elements.
|
|
664
|
+
* For multiple selections, shows a single set of handles for the bounding box.
|
|
665
|
+
*/
|
|
666
|
+
updateTransformHandles() {
|
|
667
|
+
if (!this.overlayLayer) return;
|
|
668
|
+
const selectedIds = Array.from(this.selectionController.getModel().selectedIds);
|
|
669
|
+
this.transformHandlesMap.forEach((handles$1, id) => {
|
|
670
|
+
const isMultiSelectionHandle = id === "multi-selection";
|
|
671
|
+
const isMultiSelection = selectedIds.length > 1;
|
|
672
|
+
let shouldKeep;
|
|
673
|
+
if (isMultiSelectionHandle) shouldKeep = isMultiSelection;
|
|
674
|
+
else shouldKeep = selectedIds.includes(id) && !isMultiSelection;
|
|
675
|
+
if (!shouldKeep) {
|
|
676
|
+
handles$1.remove();
|
|
677
|
+
this.transformHandlesMap.delete(id);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
if (selectedIds.length === 0) return;
|
|
681
|
+
const handleKey = selectedIds.length > 1 ? "multi-selection" : selectedIds[0] ?? "none";
|
|
682
|
+
if (handleKey === "none") return;
|
|
683
|
+
let handles = this.transformHandlesMap.get(handleKey);
|
|
684
|
+
if (!handles) {
|
|
685
|
+
handles = document.createElement("ef-transform-handles");
|
|
686
|
+
handles.setAttribute("enable-rotation", "true");
|
|
687
|
+
handles.setAttribute("enable-resize", "true");
|
|
688
|
+
handles.setAttribute("enable-drag", "false");
|
|
689
|
+
if (selectedIds.length > 1) handles.setAttribute("lock-aspect-ratio", "true");
|
|
690
|
+
handles.style.pointerEvents = "none";
|
|
691
|
+
handles.addEventListener("bounds-change", (e) => {
|
|
692
|
+
const bounds = e.detail.bounds;
|
|
693
|
+
const currentSelectedIds = Array.from(this.selectionController.getModel().selectedIds);
|
|
694
|
+
if (currentSelectedIds.length > 1) this.handleMultiSelectionTransformHandlesBoundsChange(currentSelectedIds, bounds);
|
|
695
|
+
else if (currentSelectedIds[0]) this.handleTransformHandlesBoundsChange(currentSelectedIds[0], bounds);
|
|
696
|
+
});
|
|
697
|
+
handles.addEventListener("rotation-change", (e) => {
|
|
698
|
+
const rotation = e.detail.rotation;
|
|
699
|
+
const currentSelectedIds = Array.from(this.selectionController.getModel().selectedIds);
|
|
700
|
+
if (currentSelectedIds.length > 1) this.handleMultiSelectionTransformHandlesRotationChange(currentSelectedIds, rotation);
|
|
701
|
+
else if (currentSelectedIds[0]) this.handleTransformHandlesRotationChange(currentSelectedIds[0], rotation);
|
|
702
|
+
});
|
|
703
|
+
this.overlayLayer.appendChild(handles);
|
|
704
|
+
this.transformHandlesMap.set(handleKey, handles);
|
|
705
|
+
}
|
|
706
|
+
if (selectedIds.length > 1) {
|
|
707
|
+
let minX = Infinity;
|
|
708
|
+
let minY = Infinity;
|
|
709
|
+
let maxX = -Infinity;
|
|
710
|
+
let maxY = -Infinity;
|
|
711
|
+
let hasElements = false;
|
|
712
|
+
for (const id of selectedIds) {
|
|
713
|
+
this.updateElementMetadata(id);
|
|
714
|
+
const metadata = this.elementMetadata.get(id);
|
|
715
|
+
if (metadata) {
|
|
716
|
+
const rotatedBounds = getRotatedBoundingBox(metadata.x, metadata.y, metadata.width, metadata.height, metadata.rotation ?? 0);
|
|
717
|
+
minX = Math.min(minX, rotatedBounds.minX);
|
|
718
|
+
minY = Math.min(minY, rotatedBounds.minY);
|
|
719
|
+
maxX = Math.max(maxX, rotatedBounds.maxX);
|
|
720
|
+
maxY = Math.max(maxY, rotatedBounds.maxY);
|
|
721
|
+
hasElements = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (hasElements && this.overlayLayer) {
|
|
725
|
+
const firstElementId = selectedIds[0];
|
|
726
|
+
if (firstElementId) {
|
|
727
|
+
const firstElement = this.elementRegistry.get(firstElementId);
|
|
728
|
+
if (firstElement) handles.target = firstElement;
|
|
729
|
+
}
|
|
730
|
+
const panZoomElement = this.closest("ef-pan-zoom");
|
|
731
|
+
const overlayRect = this.overlayLayer.getBoundingClientRect();
|
|
732
|
+
let screenX;
|
|
733
|
+
let screenY;
|
|
734
|
+
let screenWidth;
|
|
735
|
+
let screenHeight;
|
|
736
|
+
if (panZoomElement && typeof panZoomElement.canvasToScreen === "function" && this.panZoomTransform) {
|
|
737
|
+
const topLeft = panZoomElement.canvasToScreen(minX, minY);
|
|
738
|
+
const bottomRight = panZoomElement.canvasToScreen(maxX, maxY);
|
|
739
|
+
screenX = topLeft.x;
|
|
740
|
+
screenY = topLeft.y;
|
|
741
|
+
screenWidth = bottomRight.x - topLeft.x;
|
|
742
|
+
screenHeight = bottomRight.y - topLeft.y;
|
|
743
|
+
} else {
|
|
744
|
+
const canvasRect = this.getBoundingClientRect();
|
|
745
|
+
const topLeft = canvasToScreen(minX, minY, canvasRect, this.panZoomTransform);
|
|
746
|
+
const bottomRight = canvasToScreen(maxX, maxY, canvasRect, this.panZoomTransform);
|
|
747
|
+
screenX = topLeft.x;
|
|
748
|
+
screenY = topLeft.y;
|
|
749
|
+
screenWidth = bottomRight.x - topLeft.x;
|
|
750
|
+
screenHeight = bottomRight.y - topLeft.y;
|
|
751
|
+
}
|
|
752
|
+
if (handles.interactionMode === "rotating" || handles.interactionMode === "resizing") {
|
|
753
|
+
const newScale = this.panZoomTransform?.scale || 1;
|
|
754
|
+
if (handles.canvasScale !== newScale) handles.canvasScale = newScale;
|
|
755
|
+
} else {
|
|
756
|
+
const currentBounds = handles.bounds;
|
|
757
|
+
const newBounds = {
|
|
758
|
+
x: screenX - overlayRect.left,
|
|
759
|
+
y: screenY - overlayRect.top,
|
|
760
|
+
width: screenWidth,
|
|
761
|
+
height: screenHeight,
|
|
762
|
+
rotation: 0
|
|
763
|
+
};
|
|
764
|
+
if (!currentBounds || Math.abs(currentBounds.x - newBounds.x) > .1 || Math.abs(currentBounds.y - newBounds.y) > .1 || Math.abs(currentBounds.width - newBounds.width) > .1 || Math.abs(currentBounds.height - newBounds.height) > .1 || Math.abs((currentBounds.rotation ?? 0) - (newBounds.rotation ?? 0)) > .1) handles.bounds = newBounds;
|
|
765
|
+
const newScale = this.panZoomTransform?.scale || 1;
|
|
766
|
+
if (handles.canvasScale !== newScale) handles.canvasScale = newScale;
|
|
767
|
+
if (handles.interactionMode === "idle") {
|
|
768
|
+
this.lastMultiSelectionRotation = null;
|
|
769
|
+
this.multiSelectionRotationCenter = null;
|
|
770
|
+
this.multiSelectionResizeInitial = null;
|
|
771
|
+
handles.enableResize = true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} else if (selectedIds[0]) {
|
|
776
|
+
this.updateElementMetadata(selectedIds[0]);
|
|
777
|
+
const elementData = this.elementMetadata.get(selectedIds[0]);
|
|
778
|
+
if (elementData && elementData.element) {
|
|
779
|
+
handles.target = elementData.element;
|
|
780
|
+
this.updateTransformHandlesBounds(selectedIds[0], handles, elementData);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Update transform handles bounds for an element.
|
|
786
|
+
*/
|
|
787
|
+
updateTransformHandlesBounds(_elementId, handles, elementData) {
|
|
788
|
+
if (!this.overlayLayer) return;
|
|
789
|
+
const overlayRect = this.overlayLayer.getBoundingClientRect();
|
|
790
|
+
const canvasContent = this.shadowRoot?.querySelector(".canvas-content");
|
|
791
|
+
if (!canvasContent) return;
|
|
792
|
+
const canvasRect = canvasContent.getBoundingClientRect();
|
|
793
|
+
const scale = this.panZoomTransform?.scale || 1;
|
|
794
|
+
const centerScreen = canvasToScreen(elementData.x + elementData.width / 2, elementData.y + elementData.height / 2, canvasRect, this.panZoomTransform);
|
|
795
|
+
const screenWidth = elementData.width * scale;
|
|
796
|
+
const screenHeight = elementData.height * scale;
|
|
797
|
+
const newBounds = {
|
|
798
|
+
x: centerScreen.x - overlayRect.left - screenWidth / 2,
|
|
799
|
+
y: centerScreen.y - overlayRect.top - screenHeight / 2,
|
|
800
|
+
width: screenWidth,
|
|
801
|
+
height: screenHeight,
|
|
802
|
+
rotation: elementData.rotation || 0
|
|
803
|
+
};
|
|
804
|
+
const currentBounds = handles.bounds;
|
|
805
|
+
if (!currentBounds || Math.abs(currentBounds.x - newBounds.x) > .1 || Math.abs(currentBounds.y - newBounds.y) > .1 || Math.abs(currentBounds.width - newBounds.width) > .1 || Math.abs(currentBounds.height - newBounds.height) > .1 || (currentBounds.rotation || 0) !== (newBounds.rotation || 0)) handles.bounds = newBounds;
|
|
806
|
+
if (handles.canvasScale !== scale) handles.canvasScale = scale;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Handle transform handles bounds-change event for multi-selection.
|
|
810
|
+
* Updates all selected elements proportionally.
|
|
811
|
+
*/
|
|
812
|
+
handleMultiSelectionTransformHandlesBoundsChange(elementIds, bounds) {
|
|
813
|
+
if (!this.overlayLayer) return;
|
|
814
|
+
const newCanvasPos = {
|
|
815
|
+
x: bounds.x,
|
|
816
|
+
y: bounds.y
|
|
817
|
+
};
|
|
818
|
+
const newCanvasWidth = bounds.width;
|
|
819
|
+
const newCanvasHeight = bounds.height;
|
|
820
|
+
if (!this.multiSelectionResizeInitial) {
|
|
821
|
+
let minX$1 = Infinity;
|
|
822
|
+
let minY$1 = Infinity;
|
|
823
|
+
let maxX$1 = -Infinity;
|
|
824
|
+
let maxY$1 = -Infinity;
|
|
825
|
+
const elements = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const id of elementIds) {
|
|
827
|
+
const metadata = this.elementMetadata.get(id);
|
|
828
|
+
if (metadata) {
|
|
829
|
+
elements.set(id, {
|
|
830
|
+
x: metadata.x,
|
|
831
|
+
y: metadata.y,
|
|
832
|
+
width: metadata.width,
|
|
833
|
+
height: metadata.height,
|
|
834
|
+
rotation: metadata.rotation ?? 0
|
|
835
|
+
});
|
|
836
|
+
const rotatedBounds = getRotatedBoundingBox(metadata.x, metadata.y, metadata.width, metadata.height, metadata.rotation ?? 0);
|
|
837
|
+
minX$1 = Math.min(minX$1, rotatedBounds.minX);
|
|
838
|
+
minY$1 = Math.min(minY$1, rotatedBounds.minY);
|
|
839
|
+
maxX$1 = Math.max(maxX$1, rotatedBounds.maxX);
|
|
840
|
+
maxY$1 = Math.max(maxY$1, rotatedBounds.maxY);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
this.multiSelectionResizeInitial = {
|
|
844
|
+
elements,
|
|
845
|
+
minX: minX$1,
|
|
846
|
+
minY: minY$1,
|
|
847
|
+
maxX: maxX$1,
|
|
848
|
+
maxY: maxY$1
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const { elements: initialElements, minX, minY, maxX, maxY } = this.multiSelectionResizeInitial;
|
|
852
|
+
const oldWidth = maxX - minX;
|
|
853
|
+
const oldHeight = maxY - minY;
|
|
854
|
+
if (oldWidth === 0 || oldHeight === 0) return;
|
|
855
|
+
const scaleX = newCanvasWidth / oldWidth;
|
|
856
|
+
const scaleY = newCanvasHeight / oldHeight;
|
|
857
|
+
const safeScaleX = Math.max(.01, scaleX);
|
|
858
|
+
const safeScaleY = Math.max(.01, scaleY);
|
|
859
|
+
for (const [id, initialData] of initialElements.entries()) {
|
|
860
|
+
const relX = (initialData.x - minX) / oldWidth;
|
|
861
|
+
const relY = (initialData.y - minY) / oldHeight;
|
|
862
|
+
const newX = newCanvasPos.x + relX * newCanvasWidth;
|
|
863
|
+
const newY = newCanvasPos.y + relY * newCanvasHeight;
|
|
864
|
+
const newWidth = initialData.width * safeScaleX;
|
|
865
|
+
const newHeight = initialData.height * safeScaleY;
|
|
866
|
+
this.updateElementPosition(id, newX, newY);
|
|
867
|
+
const element = this.elementRegistry.get(id);
|
|
868
|
+
if (!element) {
|
|
869
|
+
console.warn("[EFCanvas] handleMultiSelectionTransformHandlesBoundsChange: element not found", id);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
element.style.width = `${newWidth}px`;
|
|
873
|
+
element.style.height = `${newHeight}px`;
|
|
874
|
+
const metadata = this.elementMetadata.get(id);
|
|
875
|
+
if (metadata) this.elementMetadata.set(id, {
|
|
876
|
+
...metadata,
|
|
877
|
+
x: newX,
|
|
878
|
+
y: newY,
|
|
879
|
+
width: newWidth,
|
|
880
|
+
height: newHeight
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const handles = this.transformHandlesMap.get("multi-selection");
|
|
884
|
+
if (handles && this.overlayLayer) {
|
|
885
|
+
const overlayRect = this.overlayLayer.getBoundingClientRect();
|
|
886
|
+
const canvasRect = this.getBoundingClientRect();
|
|
887
|
+
const scale = this.panZoomTransform?.scale || 1;
|
|
888
|
+
const centerScreen = canvasToScreen(newCanvasPos.x + newCanvasWidth / 2, newCanvasPos.y + newCanvasHeight / 2, canvasRect, this.panZoomTransform);
|
|
889
|
+
const screenWidth = newCanvasWidth * scale;
|
|
890
|
+
const screenHeight = newCanvasHeight * scale;
|
|
891
|
+
handles.bounds = {
|
|
892
|
+
x: centerScreen.x - overlayRect.left - screenWidth / 2,
|
|
893
|
+
y: centerScreen.y - overlayRect.top - screenHeight / 2,
|
|
894
|
+
width: screenWidth,
|
|
895
|
+
height: screenHeight,
|
|
896
|
+
rotation: 0
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Handle transform handles rotation-change event for multiple selected elements.
|
|
902
|
+
* Rotates all elements around the center of the bounding box.
|
|
903
|
+
*/
|
|
904
|
+
handleMultiSelectionTransformHandlesRotationChange(elementIds, rotation) {
|
|
905
|
+
const isFirstCall = this.lastMultiSelectionRotation === null;
|
|
906
|
+
if (isFirstCall) {
|
|
907
|
+
let minX = Infinity;
|
|
908
|
+
let minY = Infinity;
|
|
909
|
+
let maxX = -Infinity;
|
|
910
|
+
let maxY = -Infinity;
|
|
911
|
+
for (const id of elementIds) {
|
|
912
|
+
const metadata = this.elementMetadata.get(id);
|
|
913
|
+
if (metadata) {
|
|
914
|
+
minX = Math.min(minX, metadata.x);
|
|
915
|
+
minY = Math.min(minY, metadata.y);
|
|
916
|
+
maxX = Math.max(maxX, metadata.x + metadata.width);
|
|
917
|
+
maxY = Math.max(maxY, metadata.y + metadata.height);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
this.multiSelectionRotationCenter = {
|
|
921
|
+
x: (minX + maxX) / 2,
|
|
922
|
+
y: (minY + maxY) / 2
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const deltaRotation = isFirstCall ? 0 : rotation - this.lastMultiSelectionRotation;
|
|
926
|
+
this.lastMultiSelectionRotation = rotation;
|
|
927
|
+
const groupCenterX = this.multiSelectionRotationCenter.x;
|
|
928
|
+
const groupCenterY = this.multiSelectionRotationCenter.y;
|
|
929
|
+
const deltaRadians = deltaRotation * Math.PI / 180;
|
|
930
|
+
const cos = Math.cos(deltaRadians);
|
|
931
|
+
const sin = Math.sin(deltaRadians);
|
|
932
|
+
for (const id of elementIds) {
|
|
933
|
+
const metadata = this.elementMetadata.get(id);
|
|
934
|
+
const element = this.elementRegistry.get(id);
|
|
935
|
+
if (!metadata || !element) continue;
|
|
936
|
+
const data = {
|
|
937
|
+
id,
|
|
938
|
+
element,
|
|
939
|
+
x: metadata.x,
|
|
940
|
+
y: metadata.y,
|
|
941
|
+
width: metadata.width,
|
|
942
|
+
height: metadata.height,
|
|
943
|
+
rotation: metadata.rotation ?? 0
|
|
944
|
+
};
|
|
945
|
+
if (Math.abs(deltaRotation) < .001) continue;
|
|
946
|
+
const elementCenterX = data.x + data.width / 2;
|
|
947
|
+
const elementCenterY = data.y + data.height / 2;
|
|
948
|
+
const relX = elementCenterX - groupCenterX;
|
|
949
|
+
const relY = elementCenterY - groupCenterY;
|
|
950
|
+
const rotatedRelX = relX * cos - relY * sin;
|
|
951
|
+
const rotatedRelY = relX * sin + relY * cos;
|
|
952
|
+
const newX = groupCenterX + rotatedRelX - data.width / 2;
|
|
953
|
+
const newY = groupCenterY + rotatedRelY - data.height / 2;
|
|
954
|
+
this.updateElementPosition(data.id, newX, newY);
|
|
955
|
+
const newRotation = data.rotation + deltaRotation;
|
|
956
|
+
data.element.style.transform = `rotate(${newRotation}deg)`;
|
|
957
|
+
data.element.style.transformOrigin = "center";
|
|
958
|
+
this.elementMetadata.set(data.id, {
|
|
959
|
+
id: data.id,
|
|
960
|
+
element: data.element,
|
|
961
|
+
x: newX,
|
|
962
|
+
y: newY,
|
|
963
|
+
width: data.width,
|
|
964
|
+
height: data.height,
|
|
965
|
+
rotation: newRotation
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
const handles = this.transformHandlesMap.get("multi-selection");
|
|
969
|
+
if (handles) {
|
|
970
|
+
handles.enableResize = false;
|
|
971
|
+
const currentBounds = handles.bounds;
|
|
972
|
+
if (currentBounds) {
|
|
973
|
+
handles.bounds = {
|
|
974
|
+
...currentBounds,
|
|
975
|
+
rotation: this.lastMultiSelectionRotation ?? 0
|
|
976
|
+
};
|
|
977
|
+
handles.requestUpdate();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Handle transform handles bounds-change event for single element.
|
|
983
|
+
* Converts bounds from overlay-relative screen coordinates to canvas coordinates.
|
|
984
|
+
*/
|
|
985
|
+
handleTransformHandlesBoundsChange(elementId, bounds) {
|
|
986
|
+
if (!this.overlayLayer) return;
|
|
987
|
+
const canvasPos = {
|
|
988
|
+
x: bounds.x,
|
|
989
|
+
y: bounds.y
|
|
990
|
+
};
|
|
991
|
+
const canvasWidth = bounds.width;
|
|
992
|
+
const canvasHeight = bounds.height;
|
|
993
|
+
const element = this.elementRegistry.get(elementId);
|
|
994
|
+
if (element) {
|
|
995
|
+
this.updateElementPosition(elementId, canvasPos.x, canvasPos.y);
|
|
996
|
+
const metadata = this.elementMetadata.get(elementId);
|
|
997
|
+
if (metadata) {
|
|
998
|
+
element.style.width = `${canvasWidth}px`;
|
|
999
|
+
element.style.height = `${canvasHeight}px`;
|
|
1000
|
+
metadata.width = canvasWidth;
|
|
1001
|
+
metadata.height = canvasHeight;
|
|
1002
|
+
}
|
|
1003
|
+
if (bounds.rotation !== void 0 && bounds.rotation !== (metadata?.rotation || 0)) {
|
|
1004
|
+
element.style.transform = `rotate(${bounds.rotation}deg)`;
|
|
1005
|
+
if (metadata) metadata.rotation = bounds.rotation;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const handles = this.transformHandlesMap.get(elementId);
|
|
1009
|
+
if (handles && this.overlayLayer) {
|
|
1010
|
+
const overlayRect = this.overlayLayer.getBoundingClientRect();
|
|
1011
|
+
const canvasRect = this.getBoundingClientRect();
|
|
1012
|
+
const scale = this.panZoomTransform?.scale || 1;
|
|
1013
|
+
const centerScreen = canvasToScreen(canvasPos.x + canvasWidth / 2, canvasPos.y + canvasHeight / 2, canvasRect, this.panZoomTransform);
|
|
1014
|
+
const screenWidth = canvasWidth * scale;
|
|
1015
|
+
const screenHeight = canvasHeight * scale;
|
|
1016
|
+
handles.bounds = {
|
|
1017
|
+
x: centerScreen.x - overlayRect.left - screenWidth / 2,
|
|
1018
|
+
y: centerScreen.y - overlayRect.top - screenHeight / 2,
|
|
1019
|
+
width: screenWidth,
|
|
1020
|
+
height: screenHeight,
|
|
1021
|
+
rotation: bounds.rotation || 0
|
|
1022
|
+
};
|
|
1023
|
+
handles.requestUpdate();
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Handle transform handles rotation-change event for single element.
|
|
1028
|
+
*/
|
|
1029
|
+
handleTransformHandlesRotationChange(elementId, rotation) {
|
|
1030
|
+
const element = this.elementRegistry.get(elementId);
|
|
1031
|
+
if (!element) {
|
|
1032
|
+
console.warn("[EFCanvas] handleTransformHandlesRotationChange: element not found", elementId);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
element.style.transform = `rotate(${rotation}deg)`;
|
|
1036
|
+
element.style.transformOrigin = "center";
|
|
1037
|
+
const metadata = this.elementMetadata.get(elementId);
|
|
1038
|
+
if (metadata) this.elementMetadata.set(elementId, {
|
|
1039
|
+
...metadata,
|
|
1040
|
+
rotation
|
|
1041
|
+
});
|
|
1042
|
+
else {
|
|
1043
|
+
this.updateElementMetadata(elementId);
|
|
1044
|
+
const updatedMetadata = this.elementMetadata.get(elementId);
|
|
1045
|
+
if (updatedMetadata) this.elementMetadata.set(elementId, {
|
|
1046
|
+
...updatedMetadata,
|
|
1047
|
+
rotation
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Cleanup transform handles.
|
|
1053
|
+
*/
|
|
1054
|
+
cleanupTransformHandles() {
|
|
1055
|
+
this.transformHandlesMap.forEach((handles) => {
|
|
1056
|
+
handles.remove();
|
|
1057
|
+
});
|
|
1058
|
+
this.transformHandlesMap.clear();
|
|
1059
|
+
}
|
|
1060
|
+
render() {
|
|
1061
|
+
return html`
|
|
1062
|
+
<div class="canvas-content">
|
|
1063
|
+
<slot></slot>
|
|
1064
|
+
</div>
|
|
1065
|
+
`;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
__decorate([consume({
|
|
1069
|
+
context: panZoomTransformContext,
|
|
1070
|
+
subscribe: true
|
|
1071
|
+
})], EFCanvas.prototype, "panZoomTransform", void 0);
|
|
1072
|
+
__decorate([property({
|
|
1073
|
+
type: String,
|
|
1074
|
+
attribute: "data-element-id-attribute"
|
|
1075
|
+
})], EFCanvas.prototype, "elementIdAttribute", void 0);
|
|
1076
|
+
__decorate([property({
|
|
1077
|
+
type: Boolean,
|
|
1078
|
+
attribute: "enable-transform-handles"
|
|
1079
|
+
})], EFCanvas.prototype, "enableTransformHandles", void 0);
|
|
1080
|
+
__decorate([state()], EFCanvas.prototype, "elementRegistry", void 0);
|
|
1081
|
+
__decorate([state()], EFCanvas.prototype, "elementMetadata", void 0);
|
|
1082
|
+
__decorate([state()], EFCanvas.prototype, "_activeRootTemporal", void 0);
|
|
1083
|
+
__decorate([provide({ context: selectionContext }), state()], EFCanvas.prototype, "selectionContext", void 0);
|
|
1084
|
+
__decorate([state()], EFCanvas.prototype, "highlightedElement", void 0);
|
|
1085
|
+
EFCanvas = __decorate([customElement("ef-canvas")], EFCanvas);
|
|
1086
|
+
|
|
1087
|
+
//#endregion
|
|
1088
|
+
export { EFCanvas };
|
|
1089
|
+
//# sourceMappingURL=EFCanvas.js.map
|