@vvfx/artis 0.0.1-alpha.1 → 0.0.1-alpha.2

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/index.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  * Description: 智能画布
4
4
  * Author: Ant Group CO., Ltd.
5
5
  * Contributors: 赤芍,何即,不择,意绮
6
- * Version: v0.0.1-alpha.1
6
+ * Version: v0.0.1-alpha.2
7
7
  */
8
8
 
9
9
  function _define_property$1(obj, key, value) {
@@ -71092,6 +71092,29 @@ const textareaDefaultCssClass = `
71092
71092
  return this._editingItem !== undefined;
71093
71093
  }
71094
71094
  /**
71095
+ * 选中当前编辑文本的全部内容,并将焦点交给输入框。
71096
+ *
71097
+ * 输入框尚未完成首次画布投影时会先保存全文选区,待投影打开后恢复。
71098
+ * @returns 当前存在可编辑文本时返回 true
71099
+ */ selectAllText() {
71100
+ if (this._disposed) {
71101
+ return false;
71102
+ }
71103
+ const textComponent = this._editingItem?.getComponent(TextComponent);
71104
+ if (!textComponent) {
71105
+ return false;
71106
+ }
71107
+ const textLength = this.textAreaElement.style.display === 'block' ? this.textAreaElement.value.length : (textComponent.text ?? '').length;
71108
+ this._selectionStart = 0;
71109
+ this._selectionEnd = textLength;
71110
+ if (this.textAreaElement.style.display === 'block') {
71111
+ this.textAreaElement.focus();
71112
+ this.textAreaElement.select();
71113
+ this.textAreaElement.scrollTop = 0;
71114
+ }
71115
+ return true;
71116
+ }
71117
+ /**
71095
71118
  * 使用当前单选文本元素开始编辑。
71096
71119
  * @param textareaType 文本框激活方式
71097
71120
  * @returns 是否成功开始编辑
@@ -71530,11 +71553,11 @@ const textareaDefaultCssClass = `
71530
71553
  /** 角点旋转工具。 */ GizmoType["CORNER_ROTATION"] = "corner-rotation";
71531
71554
  /** 选区缩放工具。 */ GizmoType["RESIZE_SELECTION"] = "resize-selection";
71532
71555
  /** 视口覆盖层。 */ GizmoType["VIEWPORT_OVERLAY"] = "viewport-overlay";
71533
- /** 图片裁切工具。 */ GizmoType["PICTURE_CUT"] = "picture-cut";
71556
+ /** 图片裁切工具。 */ GizmoType["IMAGE_CUT"] = "image-cut";
71534
71557
  /** 文本编辑工具。 */ GizmoType["TEXT"] = "text";
71535
71558
  /** 蒙版编辑工具。 */ GizmoType["MASK"] = "mask";
71536
71559
  /** 加载状态工具。 */ GizmoType["LOADING"] = "loading";
71537
- /** 图片扩边工具。 */ GizmoType["PICTURE_EXPAND"] = "picture-expand";
71560
+ /** 图片扩边工具。 */ GizmoType["IMAGE_EXPAND"] = "image-expand";
71538
71561
  /** 精准文字编辑工具。 */ GizmoType["SPRITE_TEXT_EDIT"] = "sprite-text-edit";
71539
71562
  /** 媒体图标工具。 */ GizmoType["ICON"] = "icon";
71540
71563
  /** 元素创建工具。 */ GizmoType["ITEM_CREATE"] = "item-create";
@@ -71678,95 +71701,311 @@ class GizmoViewportUtils {
71678
71701
  defaults: freezeConfigValue(cloneConfigValue(definition.defaults))
71679
71702
  });
71680
71703
  }
71681
- /** 创建 VFXItem、挂载组件并加入合成的底层工厂。 */ class VFXItemFactory {
71682
- /**
71683
- * 创建并挂载 VFXItem。
71684
- * @param composition 目标合成
71685
- * @param parent 父元素
71686
- * @param name 元素名称
71687
- * @param types 待挂载的组件类型
71688
- * @returns 新建的 VFXItem
71689
- */ static createVFXItem(composition, parent = null, name = 'NewVFXItem', ...types) {
71690
- const vfxItem = new VFXItem(composition.engine);
71691
- vfxItem.name = name;
71692
- for (const type of types){
71693
- vfxItem.addComponent(type);
71694
- }
71695
- composition.addItem(vfxItem);
71696
- // @ts-expect-error rootComposition.items 的类型未公开受支持的写入操作。
71697
- composition.rootComposition.items.push(vfxItem);
71698
- if (parent) {
71699
- vfxItem.setParent(parent);
71700
- }
71701
- return vfxItem;
71704
+ const DEFAULT_LOADING_FRAGMENT = `
71705
+ // Shadertoy Fragment Shader
71706
+ // Diagonal background gradient + wider colorful transparent shimmer
71707
+
71708
+ precision highp float;
71709
+
71710
+ varying vec2 vUV;
71711
+
71712
+ uniform vec4 _Time;
71713
+
71714
+ // ------------------------------------------------------------
71715
+ // Background
71716
+ // ------------------------------------------------------------
71717
+ vec3 background(vec2 uv) {
71718
+ vec3 topLeft = vec3(0.88, 0.85, 1.00);
71719
+ vec3 bottomRight = vec3(0.98, 0.98, 0.98);
71720
+
71721
+ float d = (uv.x + (1.0 - uv.y)) * 0.5;
71722
+ d = smoothstep(0.0, 1.0, d);
71723
+
71724
+ vec3 col = mix(topLeft, bottomRight, d);
71725
+
71726
+ vec2 p1 = uv - vec2(1.13, 1.40);
71727
+ float r1 = length(p1 / vec2(0.85, 0.85));
71728
+ float g1 = 1.0 - smoothstep(0.0, 0.96, r1);
71729
+ col += vec3(172.0/255.0, 183.0/255.0, 1.0) * g1 * 0.28;
71730
+
71731
+ vec2 p2 = uv - vec2(0.25, 0.23);
71732
+ float r2 = length(p2 / vec2(0.74, 0.64));
71733
+ float g2 = 1.0 - smoothstep(0.0, 1.0, r2);
71734
+ col += vec3(147.0/255.0, 108.0/255.0, 1.0) * g2 * 0.10;
71735
+
71736
+ return col;
71737
+ }
71738
+
71739
+ // ------------------------------------------------------------
71740
+ // Easing: slow at start, fast at end
71741
+ // ------------------------------------------------------------
71742
+ float easeInQuad(float x) {
71743
+ return x * x;
71744
+ }
71745
+
71746
+ // ------------------------------------------------------------
71747
+ // Color palette helper
71748
+ // ------------------------------------------------------------
71749
+ vec3 palette(float t) {
71750
+ // 柔和的彩色条:蓝 -> 青 -> 紫 -> 粉 -> 蓝
71751
+ vec3 a = vec3(0.62, 0.82, 1.00);
71752
+ vec3 b = vec3(0.72, 0.95, 1.00);
71753
+ vec3 c = vec3(0.83, 0.76, 1.00);
71754
+ vec3 d = vec3(1.00, 0.82, 0.95);
71755
+
71756
+ t = fract(t);
71757
+
71758
+ if (t < 0.33) {
71759
+ float k = smoothstep(0.0, 0.33, t);
71760
+ return mix(a, b, k);
71761
+ } else if (t < 0.66) {
71762
+ float k = smoothstep(0.33, 0.66, t);
71763
+ return mix(b, c, k);
71764
+ } else {
71765
+ float k = smoothstep(0.66, 1.0, t);
71766
+ return mix(c, d, k);
71702
71767
  }
71703
71768
  }
71704
- /** 创建内置类型 VFXItem 的便捷工厂。 */ class DefaultVFXItems {
71705
- /**
71706
- * 创建空元素。
71707
- * @param composition 目标合成
71708
- * @param parent 父元素
71709
- * @param name 元素名称
71710
- * @returns 新建的空元素
71711
- */ static createEmpty(composition, parent = null, name = 'NewVFXItem') {
71712
- const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name);
71713
- vfxItem.type = index$1.ItemType.null;
71714
- return vfxItem;
71769
+
71770
+ // ------------------------------------------------------------
71771
+ // Wider colorful transparent left-to-right shimmer
71772
+ // ------------------------------------------------------------
71773
+ vec4 shimmerBand(vec2 uv, float t) {
71774
+ float cycle = fract(t * 0.70);
71775
+ cycle = easeInQuad(cycle);
71776
+
71777
+ float x = cycle * 1.55 - 0.25; // sweep from left to right
71778
+
71779
+ float d = abs(uv.x - x);
71780
+ float band = 1.0 - smoothstep(0.0, 0.30, d);
71781
+ float core = 1.0 - smoothstep(0.0, 0.10, d);
71782
+
71783
+ float x2 = x + (uv.y - 0.5) * 0.18;
71784
+ float d2 = abs(uv.x - x2);
71785
+ float band2 = 1.0 - smoothstep(0.0, 0.20, d2);
71786
+
71787
+ float s = max(band * 0.85, band2 * 0.60);
71788
+ s = max(s, core * 0.95);
71789
+ s = clamp(s, 0.0, 1.0);
71790
+
71791
+ // 颜色沿条带变化,模拟彩色渐变
71792
+ float hueT = uv.y * 0.25 + uv.x * 0.60 + t * 0.10;
71793
+ vec3 color1 = palette(hueT);
71794
+
71795
+ // 中间更亮一点,边缘更淡
71796
+ vec3 color2 = mix(color1, vec3(1.0), core * 0.35);
71797
+
71798
+ return vec4(color2, s);
71799
+ }
71800
+
71801
+ // ------------------------------------------------------------
71802
+ // Main
71803
+ // ------------------------------------------------------------
71804
+ void main() {
71805
+ vec2 uv = vUV;
71806
+
71807
+ vec3 col = background(uv);
71808
+
71809
+ vec4 sh = shimmerBand(uv, _Time.y);
71810
+
71811
+ // 可选:增加一点柔光感
71812
+ col += sh.rgb * sh.a * 0.06;
71813
+
71814
+ gl_FragColor = vec4(col, 1.0);
71815
+ }`;
71816
+ const loadingConfig = defineConfig({
71817
+ id: 'feedback.loading',
71818
+ defaults: {
71819
+ loadingFragment: DEFAULT_LOADING_FRAGMENT
71715
71820
  }
71716
- /**
71717
- * 创建图片元素。
71718
- * @param composition 目标合成
71719
- * @param parent 父元素
71720
- * @param name 元素名称
71721
- * @returns 新建的图片元素
71722
- */ static createSprite(composition, parent = null, name = 'NewSprite') {
71723
- const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name, SpriteComponent);
71724
- vfxItem.type = index$1.ItemType.sprite;
71725
- return vfxItem;
71821
+ });
71822
+ const viewportNavigationConfig = defineConfig({
71823
+ id: 'viewport.navigation',
71824
+ defaults: {
71825
+ scrollWheelZoom: false,
71826
+ invertZoom: false,
71827
+ zoomStep: 0.1
71726
71828
  }
71727
- /**
71728
- * 创建文本元素。
71729
- * @param composition 目标合成
71730
- * @param parent 父元素
71731
- * @param name 元素名称
71732
- * @returns 新建的文本元素
71733
- */ static createText(composition, parent = null, name = 'NewText') {
71734
- const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name, TextComponent);
71735
- vfxItem.type = index$1.ItemType.text;
71736
- return vfxItem;
71829
+ });
71830
+ const viewportOverlayConfig = defineConfig({
71831
+ id: 'viewport.overlay',
71832
+ defaults: {
71833
+ boxColor: 0xFF0000,
71834
+ boxWidth: 1,
71835
+ outerMaskEnabled: true,
71836
+ markColor: 0x000000,
71837
+ markAlpha: 0.17,
71838
+ safeAreaEnabled: true,
71839
+ safeAreaBoxColor: 0x00FF00,
71840
+ safeAreaBoxAlpha: 0.3
71737
71841
  }
71738
- /**
71739
- * 创建视频元素。
71740
- * @param composition 目标合成
71741
- * @param parent 父元素
71742
- * @param name 元素名称
71743
- * @returns 新建的视频元素
71744
- */ static createVideo(composition, parent = null, name = 'NewVideo') {
71745
- const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name, VideoComponent);
71746
- vfxItem.type = index$1.ItemType.video;
71747
- return vfxItem;
71842
+ });
71843
+ const selectionPreviewConfig = defineConfig({
71844
+ id: 'selection.preview',
71845
+ defaults: {
71846
+ videoPreSelectedPlay: true,
71847
+ preSelectedColor: 0x3b82f6,
71848
+ preSelectedWidth: 2,
71849
+ regionBoxColor: 0x3b82f6,
71850
+ regionBoxAlpha: 0.17,
71851
+ regionWireframeColor: 0x3b82f6,
71852
+ regionWireframeAlpha: 0.78,
71853
+ regionWireframeWidth: 1
71748
71854
  }
71749
- /**
71750
- * 加载并创建特效合成元素。
71751
- * @param composition 目标合成
71752
- * @param effects 特效地址或场景数据
71753
- * @param parent 父元素
71754
- * @param urlname 元素名称
71755
- * @returns 新建的特效元素
71756
- */ static async createEffects(composition, effects, parent = null, urlname = 'NewEffects') {
71757
- const preComposition = await AssetManager$1.loadPrecomposition(effects, {
71758
- autoplay: false
71759
- });
71760
- const vfxItem = PrecompositionManager.instantiate(preComposition, composition);
71761
- vfxItem.name = urlname;
71762
- vfxItem.type = index$1.ItemType.composition;
71763
- vfxItem.getComponent(CompositionComponent).endBehavior = vfxItem.endBehavior;
71764
- if (parent) {
71765
- vfxItem.setParent(parent);
71766
- }
71767
- return vfxItem;
71855
+ });
71856
+ const selectionSnapConfig = defineConfig({
71857
+ id: 'selection.snap',
71858
+ defaults: {
71859
+ enabled: true,
71860
+ lineWidth: 0.8,
71861
+ lineColor: 0x0BD6FF,
71862
+ distance: 6
71768
71863
  }
71769
- }
71864
+ });
71865
+ const resizeSelectionConfig = defineConfig({
71866
+ id: 'resize-selection',
71867
+ defaults: {
71868
+ pixelRatio: 1,
71869
+ contentRatio: 1,
71870
+ wireframeColor: 0x3b82f6,
71871
+ wireframeAlpha: 1,
71872
+ wireframeWidth: 1.5,
71873
+ cornerFillColor: 0xFFFFFF,
71874
+ cornerLineColor: 0x3b82f6,
71875
+ cornerLineWidth: 1.5,
71876
+ cornerLineAlpha: 1,
71877
+ scaleCircleSize: 4,
71878
+ rotationCircleSize: 7,
71879
+ infoShowEnabled: true,
71880
+ sizeTextColor: 0x666666,
71881
+ nameTextColor: 0x666666,
71882
+ frameMoveLineColor: 0x3b82f6,
71883
+ frameMoveLineWidth: 2,
71884
+ imageLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*F2wVS7x0MfIAAAAAQBAAAAgAev-aAQ/original',
71885
+ groupLogoUrl: 'https://mdn.alipayobjects.com/huamei_ppzin5/afts/img/Yo69Sr7boqYAAAAAH3AAAAgADjdkAQFr/original',
71886
+ textLogoUrl: 'https://mdn.alipayobjects.com/huamei_ppzin5/afts/img/Yo69Sr7boqYAAAAAH3AAAAgADjdkAQFr/original',
71887
+ videoLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*w1fnS4mq0VgAAAAAQCAAAAgAev-aAQ/original',
71888
+ frameLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*DRF_RpndkjUAAAAAQDAAAAgAev-aAQ/original',
71889
+ effectsLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*RMewR4ruUnYAAAAAQGAAAAgAev-aAQ/original'
71890
+ }
71891
+ });
71892
+ const imageCutConfig = defineConfig({
71893
+ id: 'tool.image-cut',
71894
+ defaults: {
71895
+ maskColor: 0xFFFFFF,
71896
+ maskAlpha: 0.5,
71897
+ cutBoxLineWidth: 2,
71898
+ cutBoxLineColor: 0x6A34FF,
71899
+ cutBoxLineAlpha: 1,
71900
+ itemBoxLineWidth: 1,
71901
+ itemBoxLineColor: 0x6A34FF,
71902
+ itemBoxLineAlpha: 1,
71903
+ cutBoxCornerRadius: 5,
71904
+ cutBoxCornerFillColor: 0xFFFFFF,
71905
+ cutBoxCornerLineWidth: 2,
71906
+ cutBoxCornerLineColor: 0x6A34FF,
71907
+ cutBoxCornerLineAlpha: 1,
71908
+ scaleInteractionDistance: 8,
71909
+ directionScaleInteractionDistance: 5,
71910
+ gridLineWidth: 1,
71911
+ gridLineColor: 0xFFFFFF,
71912
+ gridLineAlpha: 1,
71913
+ gridCount: 2
71914
+ }
71915
+ });
71916
+ const imageExpandConfig = defineConfig({
71917
+ id: 'tool.image-expand',
71918
+ defaults: {
71919
+ maskColor: 0x6A34FF,
71920
+ maskAlpha: 0.2,
71921
+ expandBoxLineWidth: 2,
71922
+ expandBoxLineColor: 0x6A34FF,
71923
+ expandBoxLineAlpha: 1,
71924
+ expandBoxCornerRadius: 5,
71925
+ expandBoxCornerLineWidth: 2,
71926
+ expandBoxCornerLineColor: 0x6A34FF,
71927
+ expandBoxCornerLineAlpha: 1,
71928
+ expandBoxCornerFillColor: 0xFFFFFF,
71929
+ scaleInteractionDistance: 8,
71930
+ directionScaleInteractionDistance: 5,
71931
+ gridLineWidth: 1,
71932
+ gridLineColor: 0xFFFFFF,
71933
+ gridLineAlpha: 1,
71934
+ gridCount: 2
71935
+ }
71936
+ });
71937
+ const maskConfig = defineConfig({
71938
+ id: 'tool.mask',
71939
+ defaults: {
71940
+ maskImage: '',
71941
+ brushSize: 20,
71942
+ brushColor: 0x6A34FF,
71943
+ brushAlpha: 0.5,
71944
+ maskColor: 0x00FF00,
71945
+ maskBackgroundColor: 0xFFFFFF,
71946
+ maskAlpha: 1,
71947
+ boxLineWidth: 1,
71948
+ boxLineColor: 0x6A34FF,
71949
+ boxLineAlpha: 1
71950
+ }
71951
+ });
71952
+ const spriteTextEditConfig = defineConfig({
71953
+ id: 'tool.sprite-text-edit',
71954
+ defaults: {
71955
+ textColor: 0xFFFFFF,
71956
+ preSelectedTextColor: 0xFFFFFF,
71957
+ boxLineWidth: 3,
71958
+ dashLineDash: 8,
71959
+ dashLineGap: 8,
71960
+ editBoxAlpha: 0.15,
71961
+ editBoxColor: 0x3B82F6,
71962
+ editBoxLineAlpha: 1,
71963
+ editBoxLineColor: 0x3B82F6,
71964
+ editBoxPreSelectedAlpha: 0.25,
71965
+ editBoxPreSelectedColor: 0x3B82F6,
71966
+ editBoxLinePreSelectedAlpha: 1,
71967
+ editBoxLinePreSelectedColor: 0x3B82F6,
71968
+ hasChangedEditBoxAlpha: 0.2,
71969
+ hasChangedEditBoxColor: 0x22C55E,
71970
+ hasChangedEditBoxLineAlpha: 1,
71971
+ hasChangedEditBoxLineColor: 0x22C55E,
71972
+ hasChangedEditBoxPreSelectedAlpha: 0.3,
71973
+ hasChangedEditBoxPreSelectedColor: 0x22C55E,
71974
+ hasChangedEditBoxLinePreSelectedAlpha: 1,
71975
+ hasChangedEditBoxLinePreSelectedColor: 0x22C55E,
71976
+ editBoxSelectedAlpha: 0.6,
71977
+ editBoxSelectedColor: 0xFFFF00
71978
+ }
71979
+ });
71980
+ const iconConfig = defineConfig({
71981
+ id: 'feedback.icon',
71982
+ defaults: {
71983
+ autoShow: true,
71984
+ videoPlayUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*ORMmSYYHIHUAAAAAJbAAAAgAev-aAQ/original',
71985
+ videoPlayShift: [
71986
+ 20,
71987
+ 20
71988
+ ],
71989
+ videoPlayWidth: 20,
71990
+ videoPlayHeight: 20,
71991
+ imageGeneratorUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*bYB-TIEWLBkAAAAAQGAAAAgAev-aAQ/original',
71992
+ videoGeneratorUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*6cTFT44CuKEAAAAAQCAAAAgAev-aAQ/original',
71993
+ generatorWidth: 200,
71994
+ generatorHeight: 200
71995
+ }
71996
+ });
71997
+ const itemCreateConfig = defineConfig({
71998
+ id: 'tool.item-create',
71999
+ defaults: {
72000
+ frameBorderColor: 0x2178FF,
72001
+ frameBorderWidth: 1,
72002
+ frameBorderAlpha: 0.8,
72003
+ frameFillColor: 0x2178FF,
72004
+ frameFillAlpha: 0.15,
72005
+ frameChildBoxAlpha: 0.35,
72006
+ frameChildBoxColor: 0x2178FF
72007
+ }
72008
+ });
71770
72009
  /**
71771
72010
  * 获取元素所在合成的相机视图投影矩阵。
71772
72011
  * @param item 播放器元素
@@ -71886,23 +72125,244 @@ class GizmoViewportUtils {
71886
72125
  return plane;
71887
72126
  }
71888
72127
  /**
71889
- * 判断播放器元素是否为画板元素。
71890
- * @param item 播放器元素
71891
- * @returns 是否为画板元素
72128
+ * 缩放视图。
72129
+ * @param engine 引擎
72130
+ * @param zoom 缩放值
72131
+ * @param center 缩放中心
72132
+ */ function zoomView(engine, zoom, center = new Vector2()) {
72133
+ const composition = engine.compositions[0];
72134
+ if (!composition) {
72135
+ return;
72136
+ }
72137
+ const { camera } = composition;
72138
+ const scale = camera.getViewportMatrix().elements[0];
72139
+ const translation = new Vector2(camera.getViewportMatrix().elements[12], camera.getViewportMatrix().elements[13]);
72140
+ const result = scale + zoom;
72141
+ // 1. 反算缩放中心对应的世界坐标。
72142
+ const worldX = (center.x - translation.x) / scale;
72143
+ const worldY = (center.y - translation.y) / scale;
72144
+ // 2. 调整平移,使缩放中心在屏幕上的位置保持不变。
72145
+ const newNDCTranslation = new Vector2(center.x - worldX * result, center.y - worldY * result);
72146
+ const viewportMatrix = new Matrix4().compose(new Vector3(newNDCTranslation.x, newNDCTranslation.y, 0), new Quaternion(), new Vector3(result, result, 1));
72147
+ composition.camera.setViewportMatrix(viewportMatrix);
72148
+ }
72149
+ /**
72150
+ * 平移视图。
72151
+ * @param engine 引擎
72152
+ * @param translation 位移值
72153
+ */ function panView(engine, translation) {
72154
+ const composition = engine.compositions[0];
72155
+ if (!composition) {
72156
+ return;
72157
+ }
72158
+ const { camera } = composition;
72159
+ const scale = camera.getViewportMatrix().elements[0];
72160
+ const resultTranslation = new Vector2(camera.getViewportMatrix().elements[12], camera.getViewportMatrix().elements[13]).add(translation);
72161
+ const viewportMatrix = new Matrix4().compose(new Vector3(resultTranslation.x, resultTranslation.y, 0), new Quaternion(), new Vector3(scale, scale, 1));
72162
+ camera.setViewportMatrix(viewportMatrix);
72163
+ }
72164
+ /** 统一处理手形工具和滚轮触发的视口平移与缩放。 */ class ViewportNavigationController extends EventEmitter {
72165
+ /** 当前视口导航配置。 */ get config() {
72166
+ return this._owner.getConfigManager().get(viewportNavigationConfig);
72167
+ }
72168
+ /** 当前 Effects 引擎。 */ get _engine() {
72169
+ return this._owner.getEngine();
72170
+ }
72171
+ /**
72172
+ * 设置允许的视口缩放范围。
72173
+ * @param minScale 最小缩放
72174
+ * @param maxScale 最大缩放
72175
+ */ setScaleRange(minScale, maxScale) {
72176
+ if (!Number.isFinite(minScale) || !Number.isFinite(maxScale) || minScale <= 0 || maxScale <= 0 || minScale > maxScale) {
72177
+ throw new RangeError('Viewport scale range must be finite, positive, and ordered.');
72178
+ }
72179
+ this._minScale = minScale;
72180
+ this._maxScale = maxScale;
72181
+ }
72182
+ /**
72183
+ * 根据滚轮来源和修饰键执行平移或缩放。
72184
+ * @param event 滚轮事件
72185
+ * @param _pointerCaptured 指针是否已被捕获
72186
+ * @returns 视口是否发生变化
72187
+ */ handleWheel(event, _pointerCaptured) {
72188
+ if (!this._hasViewport()) {
72189
+ return false;
72190
+ }
72191
+ event.accept();
72192
+ if (event.buttonMask !== MouseButtonMask.None) {
72193
+ return false;
72194
+ }
72195
+ const { delta, source } = this._normalizeWheel(event);
72196
+ const mode = this._classifyWheel(event, source);
72197
+ const center = new Vector2(event.position.x, event.position.y);
72198
+ if (mode === 'zoom') {
72199
+ if (delta.y === 0) {
72200
+ return false;
72201
+ }
72202
+ const direction = this.config.invertZoom ? -delta.y : delta.y;
72203
+ const zoomStep = this.config.zoomStep;
72204
+ const zoomShift = clamp(direction * 0.01, -zoomStep, zoomStep);
72205
+ return this._zoomByShift(zoomShift, center, event.ctrlPressed && source === 'trackpad' ? 'pinch-zoom' : 'wheel-zoom');
72206
+ }
72207
+ if (delta.x === 0 && delta.y === 0) {
72208
+ return false;
72209
+ }
72210
+ return this.panByViewDelta(delta, center, 'wheel-pan');
72211
+ }
72212
+ /**
72213
+ * 按视图像素增量平移视口。
72214
+ * @param delta 视图像素位移
72215
+ * @param center 操作中心
72216
+ * @param source 操作来源
72217
+ * @returns 视口是否发生变化
72218
+ */ panByViewDelta(delta, center, source) {
72219
+ if (!this._hasViewport() || delta.x === 0 && delta.y === 0) {
72220
+ return false;
72221
+ }
72222
+ const containerSize = this._containerSize();
72223
+ if (containerSize.x <= 0 || containerSize.y <= 0) {
72224
+ return false;
72225
+ }
72226
+ panView(this._engine, viewSizeToNDC(delta, containerSize));
72227
+ this._emitChange(center, source);
72228
+ return true;
72229
+ }
72230
+ /**
72231
+ * 按倍率缩放视口。
72232
+ * @param factor 缩放倍率
72233
+ * @param center 缩放中心
72234
+ * @param source 操作来源
72235
+ * @returns 视口是否发生变化
72236
+ */ zoomByFactor(factor, center, source) {
72237
+ if (!this._hasViewport() || !Number.isFinite(factor) || factor <= 0) {
72238
+ return false;
72239
+ }
72240
+ const currentScale = GizmoViewportUtils.getViewScale(this._engine);
72241
+ const nextScale = clamp(currentScale * factor, this._minScale, this._maxScale);
72242
+ if (nextScale === currentScale) {
72243
+ return false;
72244
+ }
72245
+ const containerSize = this._containerSize();
72246
+ if (containerSize.x <= 0 || containerSize.y <= 0) {
72247
+ return false;
72248
+ }
72249
+ zoomView(this._engine, nextScale - currentScale, viewPositionToNDC(center, containerSize));
72250
+ this._emitChange(center, source);
72251
+ return true;
72252
+ }
72253
+ /** 释放配置订阅和事件监听器。 */ dispose() {
72254
+ this._configOff?.();
72255
+ this._configOff = undefined;
72256
+ for (const listener of this.getListeners('change').slice()){
72257
+ this.off('change', listener);
72258
+ }
72259
+ }
72260
+ /**
72261
+ * 根据滚轮来源和修饰键确定导航模式。
72262
+ * @param event 滚轮事件
72263
+ * @param source 输入设备类型
72264
+ * @returns 滚轮导航模式
72265
+ */ _classifyWheel(event, source) {
72266
+ const signature = `${source}:${Number(event.ctrlPressed)}:${Number(event.metaPressed)}`;
72267
+ if (signature !== this._wheelSignature) {
72268
+ this._wheelSignature = signature;
72269
+ this._wheelMode = event.ctrlPressed || event.metaPressed || source === 'mouse' && this.config.scrollWheelZoom ? 'zoom' : 'pan';
72270
+ }
72271
+ return this._wheelMode;
72272
+ }
72273
+ /**
72274
+ * 将方向型滚轮事件转换为视图位移。
72275
+ * @param event 滚轮事件
72276
+ * @returns 视图位移和输入设备类型
72277
+ */ _normalizeWheel(event) {
72278
+ const delta = new Vector2();
72279
+ switch(event.buttonIndex){
72280
+ case MouseButton.WheelUp:
72281
+ delta.y = event.factor;
72282
+ break;
72283
+ case MouseButton.WheelDown:
72284
+ delta.y = -event.factor;
72285
+ break;
72286
+ case MouseButton.WheelLeft:
72287
+ delta.x = event.factor;
72288
+ break;
72289
+ case MouseButton.WheelRight:
72290
+ delta.x = -event.factor;
72291
+ break;
72292
+ }
72293
+ // Windows 下 Shift + 滚轮映射为水平平移。
72294
+ const isApplePlatform = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
72295
+ if (!isApplePlatform && event.shiftPressed && delta.x === 0) {
72296
+ delta.x = delta.y;
72297
+ delta.y = 0;
72298
+ }
72299
+ const source = event.ctrlPressed || event.metaPressed || !Number.isInteger(event.factor) || delta.x !== 0 ? 'trackpad' : 'mouse';
72300
+ return {
72301
+ delta,
72302
+ source
72303
+ };
72304
+ }
72305
+ /**
72306
+ * 按绝对缩放增量更新视口。
72307
+ * @param shift 缩放增量
72308
+ * @param center 缩放中心
72309
+ * @param source 操作来源
72310
+ * @returns 视口是否发生变化
72311
+ */ _zoomByShift(shift, center, source) {
72312
+ const currentScale = GizmoViewportUtils.getViewScale(this._engine);
72313
+ const nextScale = roundNumber(clamp(currentScale + shift, this._minScale, this._maxScale), 2);
72314
+ if (nextScale === currentScale) {
72315
+ return false;
72316
+ }
72317
+ return this.zoomByFactor(nextScale / currentScale, center, source);
72318
+ }
72319
+ /**
72320
+ * 发出当前视口快照。
72321
+ * @param center 操作中心
72322
+ * @param source 操作来源
72323
+ */ _emitChange(center, source) {
72324
+ const containerSize = this._containerSize();
72325
+ this.emit('change', {
72326
+ source,
72327
+ scale: GizmoViewportUtils.getViewScale(this._engine),
72328
+ translation: ndcSizeToViewSize(GizmoViewportUtils.getViewportTranslation(this._engine), containerSize),
72329
+ center: center.clone()
72330
+ });
72331
+ }
72332
+ /** @returns 当前画布容器尺寸。 */ _containerSize() {
72333
+ return GizmoViewportUtils.getContainerSize(this._engine.canvas.parentElement);
72334
+ }
72335
+ /** @returns 是否存在可操作的画布容器和相机。 */ _hasViewport() {
72336
+ return Boolean(this._engine.canvas.parentElement && this._engine.compositions[0]?.camera);
72337
+ }
72338
+ /**
72339
+ * @param owner Gizmo 宿主
72340
+ */ constructor(owner){
72341
+ super(), _define_property(this, "_owner", void 0), _define_property(this, "_minScale", 0.01), _define_property(this, "_maxScale", 20), _define_property(this, "_configOff", void 0), _define_property(this, "_wheelSignature", ''), _define_property(this, "_wheelMode", 'pan');
72342
+ this._owner = owner;
72343
+ this._configOff = owner.getConfigManager().onChange(viewportNavigationConfig, ()=>{
72344
+ this._wheelSignature = '';
72345
+ });
72346
+ }
72347
+ }
72348
+ /**
72349
+ * 判断播放器元素是否为画板元素。
72350
+ * @param item 播放器元素
72351
+ * @returns 是否为画板元素
71892
72352
  */ function isFramePlayerItem(item) {
71893
72353
  return item.type === index$1.ItemType.null && item.getComponent(FrameComponent) !== undefined && item.name === '画板';
71894
72354
  }
71895
- /**
71896
- * 判断播放器元素是否为特效元素。
71897
- * @param item 播放器元素
71898
- * @returns 是否为特效元素
72355
+ /**
72356
+ * 判断播放器元素是否为特效元素。
72357
+ * @param item 播放器元素
72358
+ * @returns 是否为特效元素
71899
72359
  */ function isEffectsPlayerItem(item) {
71900
72360
  return item.type === index$1.ItemType.null && item.getComponent(FrameComponent) !== undefined && item.name === '特效';
71901
72361
  }
71902
- /**
71903
- * 判断播放器元素是否为生成器元素。
71904
- * @param item 播放器元素
71905
- * @returns 是否为生成器元素
72362
+ /**
72363
+ * 判断播放器元素是否为生成器元素。
72364
+ * @param item 播放器元素
72365
+ * @returns 是否为生成器元素
71906
72366
  */ function isGeneratorPlayerItem(item) {
71907
72367
  if (item.type !== index$1.ItemType.sprite) {
71908
72368
  return false;
@@ -71921,18 +72381,95 @@ class GizmoViewportUtils {
71921
72381
  const content = item.definition?.content;
71922
72382
  return content?.generatorType === 'video' || content?.isVideoGenerator === true || item.name.includes('视频');
71923
72383
  }
71924
- /**
71925
- * 判断播放器元素是否为组元素。
71926
- * @param item 播放器元素
71927
- * @returns 是否为组元素
72384
+ /**
72385
+ * 判断播放器元素是否为组元素。
72386
+ * @param item 播放器元素
72387
+ * @returns 是否为组元素
71928
72388
  */ function isGroupPlayerItem(item) {
71929
72389
  return item.type === index$1.ItemType.null && !item.getComponent(FrameComponent);
71930
72390
  }
71931
- /**
71932
- * 获取播放器元素的视图包围盒。
71933
- * @param item 播放器元素
71934
- * @param containerSize 视图大小
71935
- * @returns 视图包围盒
72391
+ /**
72392
+ * 获取播放器元素的所有子元素。
72393
+ * @param item 播放器元素
72394
+ * @returns 子元素数组
72395
+ */ function getItemChildren(item) {
72396
+ const children = [];
72397
+ const isFrameItem = isFramePlayerItem(item);
72398
+ const isEffectItem = isEffectsPlayerItem(item);
72399
+ if (isEffectItem) {
72400
+ return children;
72401
+ }
72402
+ /**
72403
+ * 递归展开子元素。
72404
+ * @param items 当前层子元素
72405
+ * @returns 当前层及其全部后代
72406
+ */ function getAllChildren(items) {
72407
+ const result = [];
72408
+ for (const child of items){
72409
+ result.push(child);
72410
+ if (child.children && child.children.length > 0) {
72411
+ result.push(...getAllChildren(child.children));
72412
+ }
72413
+ }
72414
+ return result;
72415
+ }
72416
+ if (isFrameItem) {
72417
+ return getAllChildren(item.children);
72418
+ } else if (item.children && item.children.length > 0) {
72419
+ return getAllChildren(item.children);
72420
+ }
72421
+ return children;
72422
+ }
72423
+ /**
72424
+ * 在合成元素树内按实例 ID 查找元素。
72425
+ * @param composition 合成(无合成返回 undefined)
72426
+ * @param id 元素 instanceId
72427
+ * @returns 命中元素;未命中或无合成返回 undefined
72428
+ */ function getPlayerItemById(composition, id) {
72429
+ if (!composition) {
72430
+ return undefined;
72431
+ }
72432
+ /**
72433
+ * 在当前元素子树中递归查找实例。
72434
+ * @param items 当前层元素。
72435
+ * @param targetId 目标实例 ID。
72436
+ * @returns 命中的元素。
72437
+ */ const dfs = (items, targetId)=>{
72438
+ for (const item of items){
72439
+ if (item.getInstanceId() === targetId) {
72440
+ return item;
72441
+ }
72442
+ const found = dfs(item.children, targetId);
72443
+ if (found) {
72444
+ return found;
72445
+ }
72446
+ }
72447
+ return undefined;
72448
+ };
72449
+ return dfs(composition.items, id);
72450
+ }
72451
+ /**
72452
+ * 过滤掉祖先也在列表中的元素,仅保留顶层元素。
72453
+ * @param items 待过滤元素
72454
+ * @returns 保持原顺序的顶层元素
72455
+ */ function filterTopLevelPlayerItems(items) {
72456
+ const selectedItems = new Set(items);
72457
+ return items.filter((item)=>{
72458
+ let parent = item.parent;
72459
+ while(parent){
72460
+ if (selectedItems.has(parent)) {
72461
+ return false;
72462
+ }
72463
+ parent = parent.parent;
72464
+ }
72465
+ return true;
72466
+ });
72467
+ }
72468
+ /**
72469
+ * 获取播放器元素的视图包围盒。
72470
+ * @param item 播放器元素
72471
+ * @param containerSize 视图大小
72472
+ * @returns 视图包围盒
71936
72473
  */ function getItemViewBox(item, containerSize) {
71937
72474
  // 1. 跳过不可见元素并收集子元素。
71938
72475
  const box = new Box2();
@@ -71985,21 +72522,21 @@ class GizmoViewportUtils {
71985
72522
  }
71986
72523
  return box;
71987
72524
  }
71988
- /**
71989
- * 获取播放器元素在视图中的锚点坐标。
71990
- * @param item 播放器元素
71991
- * @param containerSize 视图容器大小
71992
- * @returns 视图锚点坐标
72525
+ /**
72526
+ * 获取播放器元素在视图中的锚点坐标。
72527
+ * @param item 播放器元素
72528
+ * @param containerSize 视图容器大小
72529
+ * @returns 视图锚点坐标
71993
72530
  */ function getItemViewAnchor(item, containerSize) {
71994
72531
  const anchor = item.transform.anchor ?? item.transform.position;
71995
72532
  const worldAnchor = new Vector3().copyFrom(anchor).applyMatrix(new Matrix4().copyFrom(item.transform.getWorldMatrix()));
71996
72533
  const viewAnchor = projectPoint(worldAnchor, item, containerSize).multiply(100).round().divide(100);
71997
72534
  return viewAnchor;
71998
72535
  }
71999
- /**
72000
- * 获取元素的变换矩阵和父级变换矩阵。
72001
- * @param item 播放器元素
72002
- * @returns 元素的变换矩阵和父级变换矩阵
72536
+ /**
72537
+ * 获取元素的变换矩阵和父级变换矩阵。
72538
+ * @param item 播放器元素
72539
+ * @returns 元素的变换矩阵和父级变换矩阵
72003
72540
  */ function getItemTransform(item) {
72004
72541
  const transform = {
72005
72542
  matrix: new Matrix4(),
@@ -72013,43 +72550,11 @@ class GizmoViewportUtils {
72013
72550
  }
72014
72551
  return transform;
72015
72552
  }
72016
- /**
72017
- * 获取播放器元素的所有子元素。
72018
- * @param item 播放器元素
72019
- * @returns 子元素数组
72020
- */ function getItemChildren(item) {
72021
- const children = [];
72022
- const isFrameItem = isFramePlayerItem(item);
72023
- const isEffectItem = isEffectsPlayerItem(item);
72024
- if (isEffectItem) {
72025
- return children;
72026
- }
72027
- /**
72028
- * 递归展开子元素。
72029
- * @param items 当前层子元素
72030
- * @returns 当前层及其全部后代
72031
- */ function getAllChildren(items) {
72032
- const result = [];
72033
- for (const child of items){
72034
- result.push(child);
72035
- if (child.children && child.children.length > 0) {
72036
- result.push(...getAllChildren(child.children));
72037
- }
72038
- }
72039
- return result;
72040
- }
72041
- if (isFrameItem) {
72042
- return getAllChildren(item.children);
72043
- } else if (item.children && item.children.length > 0) {
72044
- return getAllChildren(item.children);
72045
- }
72046
- return children;
72047
- }
72048
- /**
72049
- * 获取元素在所属合成坐标系中的实际世界尺寸。
72050
- *
72051
- * `transform.size` 是元素本地尺寸;这里叠加元素到合成根之间的缩放,
72052
- * 但排除合成本身的世界变换,与 Frame 尺寸写回使用的坐标系保持一致。
72553
+ /**
72554
+ * 获取元素在所属合成坐标系中的实际世界尺寸。
72555
+ *
72556
+ * `transform.size` 是元素本地尺寸;这里叠加元素到合成根之间的缩放,
72557
+ * 但排除合成本身的世界变换,与 Frame 尺寸写回使用的坐标系保持一致。
72053
72558
  * @param item 播放器元素
72054
72559
  * @returns 元素在所属合成坐标系中的尺寸
72055
72560
  */ function getItemWorldSize(item) {
@@ -72061,1256 +72566,432 @@ class GizmoViewportUtils {
72061
72566
  itemToComposition.decompose(new Vector3(), new Quaternion(), worldScale);
72062
72567
  return new Vector2(Math.abs(transform.size.x * worldScale.x), Math.abs(transform.size.y * worldScale.y));
72063
72568
  }
72064
- /**
72065
- * 在合成元素树内按实例 ID 查找元素。
72066
- * @param composition 合成(无合成返回 undefined)
72067
- * @param id 元素 instanceId
72068
- * @returns 命中元素;未命中或无合成返回 undefined
72069
- */ function getPlayerItemById(composition, id) {
72070
- if (!composition) {
72071
- return undefined;
72072
- }
72073
- /**
72074
- * 在当前元素子树中递归查找实例。
72075
- * @param items 当前层元素。
72076
- * @param targetId 目标实例 ID。
72077
- * @returns 命中的元素。
72078
- */ const dfs = (items, targetId)=>{
72079
- for (const item of items){
72080
- if (item.getInstanceId() === targetId) {
72081
- return item;
72082
- }
72083
- const found = dfs(item.children, targetId);
72084
- if (found) {
72085
- return found;
72086
- }
72087
- }
72088
- return undefined;
72089
- };
72090
- return dfs(composition.items, id);
72091
- }
72092
72569
  /**
72093
- * 过滤掉祖先也在列表中的元素,仅保留顶层元素。
72094
- * @param items 待过滤元素
72095
- * @returns 保持原顺序的顶层元素
72096
- */ function filterTopLevelPlayerItems(items) {
72097
- const selectedItems = new Set(items);
72098
- return items.filter((item)=>{
72099
- let parent = item.parent;
72100
- while(parent){
72101
- if (selectedItems.has(parent)) {
72102
- return false;
72103
- }
72104
- parent = parent.parent;
72105
- }
72106
- return true;
72107
- });
72108
- }
72109
- /**
72110
- * 播放视频元素。
72111
- * @param engine 引擎
72112
- * @param id 视频元素 id
72113
- */ function playVideoItem(engine, id) {
72114
- const playerItem = getPlayerItemById(engine.compositions[0], id);
72115
- if (playerItem?.type !== index$1.ItemType.video) {
72570
+ * 调整画板元素(Frame)的尺寸与位置,并同步其子合成 Item 的位移。
72571
+ * @param frameItem 画板元素(Frame 空节点控制器)
72572
+ * @param worldSize 目标世界尺寸(所属合成坐标系,不受 viewport zoom 影响)
72573
+ * @param translation 位移
72574
+ * @param initialSize pointer down 时冻结的本地 / 世界尺寸;传入后允许尺寸连续经过 0 并变为负值
72575
+ */ function resizeFrameItem(frameItem, worldSize, translation, initialSize) {
72576
+ if (!isFramePlayerItem(frameItem)) {
72577
+ console.warn(`Item ${frameItem.getInstanceId()} is not a frame item.`);
72116
72578
  return;
72117
72579
  }
72118
- const videoComponent = playerItem.getComponent(VideoComponent);
72119
- videoComponent?.playVideo();
72120
- }
72121
- /**
72122
- * 暂停视频元素。
72123
- * @param engine 引擎
72124
- * @param id 视频元素 id
72125
- */ function pauseVideoItem(engine, id) {
72126
- const playerItem = getPlayerItemById(engine.compositions[0], id);
72127
- if (playerItem?.type !== index$1.ItemType.video) {
72128
- return;
72580
+ // 1. 根据本地尺寸与世界尺寸的比例写回画板尺寸。
72581
+ const currentWorldSize = initialSize?.worldSize ?? getItemWorldSize(frameItem);
72582
+ const currentLocalSize = initialSize?.localSize ?? frameItem.transform.size;
72583
+ const localWidth = currentWorldSize.x === 0 ? worldSize.x : currentLocalSize.x * worldSize.x / currentWorldSize.x;
72584
+ const localHeight = currentWorldSize.y === 0 ? worldSize.y : currentLocalSize.y * worldSize.y / currentWorldSize.y;
72585
+ frameItem.transform.setSize(localWidth, localHeight);
72586
+ // 2. 将交互位移应用到画板。
72587
+ if (translation && (translation.x !== 0 || translation.y !== 0)) {
72588
+ const currentPosition = frameItem.transform.position;
72589
+ frameItem.setPosition(currentPosition.x + translation.x, currentPosition.y + translation.y, currentPosition.z);
72129
72590
  }
72130
- const videoComponent = playerItem.getComponent(VideoComponent);
72131
- videoComponent?.pauseVideo();
72132
- }
72133
- /**
72134
- * 播放特效元素。
72135
- * @param engine 引擎
72136
- * @param id 特效元素 id
72137
- */ function playEffectsItem(engine, id) {
72138
- const controlItem = getPlayerItemById(engine.compositions[0], id);
72139
- const compositionItem = controlItem?.children?.[0];
72140
- if (compositionItem?.type !== index$1.ItemType.composition) {
72141
- return;
72591
+ // 3. 反向补偿子合成元素,保持其世界位置不变。
72592
+ const subCompositionItem = frameItem?.children?.[0];
72593
+ if (subCompositionItem && translation) {
72594
+ subCompositionItem.children.forEach((item)=>{
72595
+ const parentMatrix = new Matrix4().copyFrom(item.transform.getParentMatrix() ?? new Matrix4());
72596
+ parentMatrix.setPosition(new Vector3());
72597
+ const result = translation.clone().applyMatrix(parentMatrix.invert()).negate();
72598
+ item.translate(...result.toArray());
72599
+ item.transform.updateLocalMatrix();
72600
+ });
72142
72601
  }
72143
- compositionItem.getComponent(CompositionComponent).play();
72144
72602
  }
72145
- /**
72146
- * 暂停特效元素。
72147
- * @param engine 引擎
72148
- * @param id 特效元素 id
72149
- */ function pauseEffectsItem(engine, id) {
72150
- const controlItem = getPlayerItemById(engine.compositions[0], id);
72151
- const compositionItem = controlItem?.children?.[0];
72152
- if (compositionItem?.type !== index$1.ItemType.composition) {
72153
- return;
72603
+ /** 创建 VFXItem、挂载组件并加入合成的底层工厂。 */ class VFXItemFactory {
72604
+ /**
72605
+ * 创建并挂载 VFXItem。
72606
+ * @param composition 目标合成
72607
+ * @param parent 父元素
72608
+ * @param name 元素名称
72609
+ * @param types 待挂载的组件类型
72610
+ * @returns 新建的 VFXItem
72611
+ */ static createVFXItem(composition, parent = null, name = 'NewVFXItem', ...types) {
72612
+ const vfxItem = new VFXItem(composition.engine);
72613
+ vfxItem.name = name;
72614
+ for (const type of types){
72615
+ vfxItem.addComponent(type);
72616
+ }
72617
+ composition.addItem(vfxItem);
72618
+ // @ts-expect-error rootComposition.items 的类型未公开受支持的写入操作。
72619
+ composition.rootComposition.items.push(vfxItem);
72620
+ if (parent) {
72621
+ vfxItem.setParent(parent);
72622
+ }
72623
+ return vfxItem;
72154
72624
  }
72155
- compositionItem.getComponent(CompositionComponent).pause();
72156
72625
  }
72157
- /** 信息标签默认字号。 */ const INFO_TEXT_FONT_SIZE = 14;
72158
- /** 信息标签默认字体。 */ const INFO_TEXT_FONT_FAMILY = 'sans-serif';
72159
- /** effects Control 字符 atlas 的固定单边留白。 */ const CONTROL_TEXT_GLYPH_PADDING = 4;
72160
- /** 用于测量字体高度的代表字符。 */ const METRICS_STRING = '|ÉqÅ';
72161
- /** 用于测量字体基线的字符。 */ const BASELINE_SYMBOL = 'M';
72162
- /**
72163
- * 颜色缓存:`hex_alpha` → effects Color。键到颜色映射不可变,全局缓存无需清理。
72164
- */ const colorCache = new Map();
72165
- /**
72166
- * 将十六进制色值 + alpha 转为 effects Color(带全局缓存)。
72167
- * @param hex 0xRRGGBB 颜色
72168
- * @param alpha 透明度 0..1
72169
- * @returns effects Color
72170
- */ function toColor(hex, alpha = 1) {
72171
- const key = `${hex}_${alpha}`;
72172
- const cached = colorCache.get(key);
72173
- if (cached) {
72174
- return cached;
72175
- }
72176
- const color = new Color((hex >> 16 & 0xff) / 255, (hex >> 8 & 0xff) / 255, (hex & 0xff) / 255, alpha);
72177
- colorCache.set(key, color);
72178
- return color;
72179
- }
72180
- /**
72181
- * 离屏文本测量上下文,懒加载并复用。
72182
- */ let _measureCtx = null;
72183
- /**
72184
- * 测量文本的宽度与可见字形边界。
72185
- * @param text 文本内容
72186
- * @param fontSize 字号
72187
- * @param fontFamily 字体
72188
- * @param fontWeight 字重
72189
- * @param fontStyle 字体样式
72190
- * @returns 文本测量结果
72191
- */ function measureTextMetrics(text, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY, fontWeight, fontStyle) {
72192
- _measureCtx ?? (_measureCtx = document.createElement('canvas').getContext('2d'));
72193
- if (!_measureCtx) {
72194
- return {
72195
- width: 0,
72196
- actualBoundingBoxAscent: 0,
72197
- actualBoundingBoxDescent: 0
72198
- };
72199
- }
72200
- _measureCtx.font = `${''}${fontWeight ? `${fontWeight} ` : ''}${fontSize}px ${fontFamily}`;
72201
- const metrics = _measureCtx.measureText(text);
72202
- return {
72203
- width: metrics.width,
72204
- actualBoundingBoxAscent: metrics.actualBoundingBoxAscent ?? 0,
72205
- actualBoundingBoxDescent: metrics.actualBoundingBoxDescent ?? 0
72206
- };
72207
- }
72208
- /**
72209
- * 计算 effects Control.drawText 实际使用的 atlas cell 高度。
72210
- * @param fontSize 字号
72211
- * @param fontFamily 字体
72212
- * @param fontWeight 字重
72213
- * @param fontStyle 字体样式
72214
- * @param resolution 渲染分辨率
72215
- * @returns 字符图集单元格高度
72216
- */ function measureControlTextCellHeight(fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY, fontWeight, fontStyle, resolution = 1) {
72217
- const safeResolution = Number.isFinite(resolution) && resolution > 0 ? resolution : 1;
72218
- const scaledFontSize = fontSize * safeResolution;
72219
- const metrics = measureTextMetrics(METRICS_STRING + BASELINE_SYMBOL, scaledFontSize, fontFamily, fontWeight);
72220
- const ascent = metrics.actualBoundingBoxAscent || scaledFontSize * 0.8;
72221
- const descent = metrics.actualBoundingBoxDescent || scaledFontSize * 0.2;
72222
- return Math.ceil(ascent + descent + CONTROL_TEXT_GLYPH_PADDING * 2 * safeResolution) / safeResolution;
72223
- }
72224
- /**
72225
- * 测量文本宽度(逻辑像素),用于信息标签的溢出判断与右对齐定位。
72226
- * @param text 文本内容
72227
- * @param fontSize 字号
72228
- * @param fontFamily 字体
72229
- * @param fontWeight 字重
72230
- * @returns 文本宽度
72231
- */ function measureTextWidth(text, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY, fontWeight) {
72232
- return measureTextMetrics(text, fontSize, fontFamily, fontWeight).width;
72233
- }
72234
- /**
72235
- * 单行文本截断:逐字符累加,超出可用宽度时把最后两个字符替换为省略号。
72236
- * @param text 文本内容
72237
- * @param maxWidth 可用宽度
72238
- * @param fontSize 字号
72239
- * @param fontFamily 字体
72240
- * @returns 截断后的文本
72241
- */ function truncateText(text, maxWidth, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY) {
72242
- if (measureTextWidth(text, fontSize, fontFamily) <= maxWidth) {
72243
- return text;
72244
- }
72245
- let current = '';
72246
- for (const char of text){
72247
- if (measureTextWidth(current + char, fontSize, fontFamily) > maxWidth) {
72248
- return current.length > 2 ? current.slice(0, -2) + '...' : '...';
72249
- }
72250
- current += char;
72251
- }
72252
- return current;
72253
- }
72254
- /**
72255
- * 多行逐字符折行:行宽超出 maxWidth 时换行;行数达到 maxLines 时末行用省略号收尾。
72256
- * @param text 文本内容
72257
- * @param maxWidth 单行可用宽度
72258
- * @param maxLines 最大行数
72259
- * @param fontSize 字号
72260
- * @param fontFamily 字体
72261
- * @returns 折行后的文本行数组
72262
- */ function wrapText(text, maxWidth, maxLines, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY) {
72263
- const lines = [];
72264
- let currentLine = '';
72265
- const chars = text.split('');
72266
- for(let i = 0; i < chars.length; i++){
72267
- const char = chars[i];
72268
- const testLine = currentLine + char;
72269
- if (measureTextWidth(testLine, fontSize, fontFamily) > maxWidth) {
72270
- lines.push(currentLine);
72271
- if (lines.length >= maxLines) {
72272
- const lastLine = lines[maxLines - 1];
72273
- lines[maxLines - 1] = lastLine.length > 2 ? lastLine.slice(0, -2) + '...' : '...';
72274
- break;
72275
- }
72276
- currentLine = char;
72277
- } else {
72278
- currentLine = testLine;
72279
- }
72280
- if (i === chars.length - 1 && currentLine) {
72281
- lines.push(currentLine);
72282
- }
72283
- }
72284
- return lines.slice(0, maxLines);
72285
- }
72286
- /**
72287
- * 绘制由 4 个角点构成的(可能非正交的)包围盒边框。
72288
- * @param control 绘制控制器
72289
- * @param corners 4 个角点(视图坐标,Y 向下)
72290
- * @param color 边框颜色
72291
- * @param width 线宽
72292
- */ function drawCorners(control, corners, color, width) {
72293
- if (corners.length < 4) {
72294
- return;
72295
- }
72296
- for(let i = 0; i < 4; i++){
72297
- const start = corners[i];
72298
- const end = corners[(i + 1) % 4];
72299
- control.drawLine(start.x, start.y, end.x, end.y, color, width);
72300
- }
72301
- }
72302
- /**
72303
- * 绘制虚线包围盒,并在相邻边之间保持连续虚线相位。
72304
- * @param control 绘制控制器
72305
- * @param corners 四个视图角点
72306
- * @param color 虚线颜色
72307
- * @param width 线宽
72308
- * @param dashLength 实线段长度
72309
- * @param gapLength 间隔长度
72310
- */ function drawDashedCorners(control, corners, color, width, dashLength, gapLength) {
72311
- // 1. 校验角点和虚线参数。
72312
- if (corners.length < 4 || dashLength <= 0 || gapLength < 0) {
72313
- return;
72314
- }
72315
- const patternLength = dashLength + gapLength;
72316
- let perimeterOffset = 0;
72317
- // 2. 逐边计算虚线相位,并继承上一条边的周长偏移。
72318
- for(let i = 0; i < 4; i++){
72319
- const start = corners[i];
72320
- const end = corners[(i + 1) % 4];
72321
- const dx = end.x - start.x;
72322
- const dy = end.y - start.y;
72323
- const edgeLength = Math.hypot(dx, dy);
72324
- if (edgeLength <= Number.EPSILON) {
72325
- continue;
72326
- }
72327
- let edgeOffset = 0;
72328
- while(edgeOffset < edgeLength){
72329
- const patternOffset = perimeterOffset % patternLength;
72330
- const drawingDash = gapLength === 0 || patternOffset < dashLength;
72331
- const phaseRemaining = drawingDash ? dashLength - patternOffset : patternLength - patternOffset;
72332
- const segmentLength = Math.min(phaseRemaining, edgeLength - edgeOffset);
72333
- // 3. 仅绘制当前相位中的实线段。
72334
- if (drawingDash && segmentLength > Number.EPSILON) {
72335
- const startRatio = edgeOffset / edgeLength;
72336
- const endRatio = (edgeOffset + segmentLength) / edgeLength;
72337
- control.drawLine(start.x + dx * startRatio, start.y + dy * startRatio, start.x + dx * endRatio, start.y + dy * endRatio, color, width);
72338
- }
72339
- edgeOffset += segmentLength;
72340
- perimeterOffset += segmentLength;
72341
- }
72342
- }
72343
- }
72344
- /**
72345
- * 绘制包围盒边框(沿 4 个角点连边)。
72346
- * @param control 绘制控制器
72347
- * @param box 包围盒(视图坐标,Y 向下)
72348
- * @param color 边框颜色
72349
- * @param width 线宽
72350
- */ function drawBox(control, box, color, width) {
72351
- drawCorners(control, box.corners, color, width);
72352
- }
72353
- /**
72354
- * 填充轴对齐包围盒(实心矩形)。
72355
- * @param control 绘制控制器
72356
- * @param box 包围盒(视图坐标,Y 向下)
72357
- * @param color 填充颜色
72358
- */ function fillBox(control, box, color) {
72359
- const size = box.getSize();
72360
- control.fillRect(box.min.x, box.min.y, size.x, size.y, color);
72361
- }
72362
- /**
72363
- * 构造沿局部轴展开的旋转矩形角点。
72364
- * @param center 矩形中心(视图坐标)
72365
- * @param xAxis 局部 X 轴单位向量(视图坐标,宽方向)
72366
- * @param yAxis 局部 Y 轴单位向量(视图坐标,高方向)
72367
- * @param halfWidth 半宽(沿 xAxis,视图像素)
72368
- * @param halfHeight 半高(沿 yAxis,视图像素)
72369
- * @returns 4 个角点(视图坐标,环绕顺序)
72370
- */ function rotatedRectCorners(center, xAxis, yAxis, halfWidth, halfHeight) {
72371
- const halfW = xAxis.clone().multiply(halfWidth);
72372
- const halfH = yAxis.clone().multiply(halfHeight);
72373
- return [
72374
- center.clone().subtract(halfW).subtract(halfH),
72375
- center.clone().add(halfW).subtract(halfH),
72376
- center.clone().add(halfW).add(halfH),
72377
- center.clone().subtract(halfW).add(halfH)
72378
- ];
72379
- }
72380
- /**
72381
- * 填充沿局部轴展开的旋转矩形。
72382
- * @param control 绘制控制器
72383
- * @param center 矩形中心(视图坐标)
72384
- * @param xAxis 局部 X 轴单位向量(视图坐标,宽方向)
72385
- * @param yAxis 局部 Y 轴单位向量(视图坐标,高方向)
72386
- * @param width 宽(沿 xAxis,视图像素)
72387
- * @param height 高(沿 yAxis,视图像素)
72388
- * @param color 填充颜色
72389
- */ function fillRotatedRect(control, center, xAxis, yAxis, width, height, color) {
72390
- const ex = xAxis.clone();
72391
- const ey = yAxis.clone();
72392
- const anchor = center.clone();
72393
- drawRotatedQuad(control, anchor, ex, ey, ()=>{
72394
- control.fillRect(-width / 2, -height / 2, width, height, color);
72395
- });
72396
- }
72397
- /**
72398
- * 在由锚点和两个基向量定义的局部坐标系内执行绘制。
72399
- * @param control 绘制控制器
72400
- * @param anchor Control 绘制空间锚点(局部原点)
72401
- * @param ex 局部 X 轴基向量(单位向量)
72402
- * @param ey 局部 Y 轴基向量(单位向量)
72403
- * @param drawFn 在局部坐标系内执行的绘制
72404
- */ function drawRotatedQuad(control, anchor, ex, ey, drawFn) {
72405
- const graphics = control.engine.graphics;
72406
- // 列优先:第一列 = ex,第二列 = ey,第三列 = anchor(平移)
72407
- const matrix = Matrix3.fromColumnVectors(new Vector3(ex.x, ex.y, 0), new Vector3(ey.x, ey.y, 0), new Vector3(anchor.x, anchor.y, 1));
72408
- graphics.pushTransform(matrix);
72409
- drawFn();
72410
- graphics.popTransform();
72411
- }
72412
- const DEFAULT_LOADING_FRAGMENT = `
72413
- // Shadertoy Fragment Shader
72414
- // Diagonal background gradient + wider colorful transparent shimmer
72415
-
72416
- precision highp float;
72417
-
72418
- varying vec2 vUV;
72419
-
72420
- uniform vec4 _Time;
72421
-
72422
- // ------------------------------------------------------------
72423
- // Background
72424
- // ------------------------------------------------------------
72425
- vec3 background(vec2 uv) {
72426
- vec3 topLeft = vec3(0.88, 0.85, 1.00);
72427
- vec3 bottomRight = vec3(0.98, 0.98, 0.98);
72428
-
72429
- float d = (uv.x + (1.0 - uv.y)) * 0.5;
72430
- d = smoothstep(0.0, 1.0, d);
72431
-
72432
- vec3 col = mix(topLeft, bottomRight, d);
72433
-
72434
- vec2 p1 = uv - vec2(1.13, 1.40);
72435
- float r1 = length(p1 / vec2(0.85, 0.85));
72436
- float g1 = 1.0 - smoothstep(0.0, 0.96, r1);
72437
- col += vec3(172.0/255.0, 183.0/255.0, 1.0) * g1 * 0.28;
72438
-
72439
- vec2 p2 = uv - vec2(0.25, 0.23);
72440
- float r2 = length(p2 / vec2(0.74, 0.64));
72441
- float g2 = 1.0 - smoothstep(0.0, 1.0, r2);
72442
- col += vec3(147.0/255.0, 108.0/255.0, 1.0) * g2 * 0.10;
72443
-
72444
- return col;
72445
- }
72446
-
72447
- // ------------------------------------------------------------
72448
- // Easing: slow at start, fast at end
72449
- // ------------------------------------------------------------
72450
- float easeInQuad(float x) {
72451
- return x * x;
72452
- }
72453
-
72454
- // ------------------------------------------------------------
72455
- // Color palette helper
72456
- // ------------------------------------------------------------
72457
- vec3 palette(float t) {
72458
- // 柔和的彩色条:蓝 -> 青 -> 紫 -> 粉 -> 蓝
72459
- vec3 a = vec3(0.62, 0.82, 1.00);
72460
- vec3 b = vec3(0.72, 0.95, 1.00);
72461
- vec3 c = vec3(0.83, 0.76, 1.00);
72462
- vec3 d = vec3(1.00, 0.82, 0.95);
72463
-
72464
- t = fract(t);
72465
-
72466
- if (t < 0.33) {
72467
- float k = smoothstep(0.0, 0.33, t);
72468
- return mix(a, b, k);
72469
- } else if (t < 0.66) {
72470
- float k = smoothstep(0.33, 0.66, t);
72471
- return mix(b, c, k);
72472
- } else {
72473
- float k = smoothstep(0.66, 1.0, t);
72474
- return mix(c, d, k);
72475
- }
72476
- }
72477
-
72478
- // ------------------------------------------------------------
72479
- // Wider colorful transparent left-to-right shimmer
72480
- // ------------------------------------------------------------
72481
- vec4 shimmerBand(vec2 uv, float t) {
72482
- float cycle = fract(t * 0.70);
72483
- cycle = easeInQuad(cycle);
72484
-
72485
- float x = cycle * 1.55 - 0.25; // sweep from left to right
72486
-
72487
- float d = abs(uv.x - x);
72488
- float band = 1.0 - smoothstep(0.0, 0.30, d);
72489
- float core = 1.0 - smoothstep(0.0, 0.10, d);
72490
-
72491
- float x2 = x + (uv.y - 0.5) * 0.18;
72492
- float d2 = abs(uv.x - x2);
72493
- float band2 = 1.0 - smoothstep(0.0, 0.20, d2);
72494
-
72495
- float s = max(band * 0.85, band2 * 0.60);
72496
- s = max(s, core * 0.95);
72497
- s = clamp(s, 0.0, 1.0);
72498
-
72499
- // 颜色沿条带变化,模拟彩色渐变
72500
- float hueT = uv.y * 0.25 + uv.x * 0.60 + t * 0.10;
72501
- vec3 color1 = palette(hueT);
72502
-
72503
- // 中间更亮一点,边缘更淡
72504
- vec3 color2 = mix(color1, vec3(1.0), core * 0.35);
72505
-
72506
- return vec4(color2, s);
72507
- }
72508
-
72509
- // ------------------------------------------------------------
72510
- // Main
72511
- // ------------------------------------------------------------
72512
- void main() {
72513
- vec2 uv = vUV;
72514
-
72515
- vec3 col = background(uv);
72516
-
72517
- vec4 sh = shimmerBand(uv, _Time.y);
72518
-
72519
- // 可选:增加一点柔光感
72520
- col += sh.rgb * sh.a * 0.06;
72521
-
72522
- gl_FragColor = vec4(col, 1.0);
72523
- }`;
72524
- const loadingConfig = defineConfig({
72525
- id: 'feedback.loading',
72526
- defaults: {
72527
- loadingFragment: DEFAULT_LOADING_FRAGMENT
72528
- }
72529
- });
72530
- const LOADING_TIP_FONT_SIZE = 16;
72531
- const LOADING_TIP_FONT_FAMILY = 'sans-serif';
72532
- const LOADING_TIP_COLOR = 0x000000;
72533
- /** 为目标元素叠加加载动画与提示文案。 */ class LoadingGizmo extends Gizmo {
72534
- /** 当前加载覆盖层配置。 */ get config() {
72535
- return this._owner.getConfigManager().get(loadingConfig);
72536
- }
72537
- /** 当前 Gizmo 实例持有的可销毁渲染投影。 */ get idMap() {
72538
- return this._idMap;
72539
- }
72540
- /** 当前 loading 元素 id 列表(由稳定 manager 派生)。 */ get loadingIds() {
72541
- return this._manager.ids;
72542
- }
72543
- /**
72544
- * 在片元着色器变化时重建加载动画对象。
72545
- * @param change 加载配置变更。
72546
- */ _onConfigChange(change) {
72547
- if (change.previous.loadingFragment === change.current.loadingFragment) {
72548
- return;
72549
- }
72550
- for (const [id, loadingItem] of this._idMap){
72551
- this._disposeLoadingVFXItem(loadingItem.loadingVFXItem);
72552
- loadingItem.loadingVFXItem = this.createLoadingVFXItem();
72553
- this.updateLoadingVFXItemTransform(id, loadingItem);
72554
- }
72555
- }
72556
- /**
72557
- * 订阅 loading 集合变化:add/delete 后回调当前全量 id 列表。
72558
- * @param cb 收到当前全量 loading id 列表的回调
72559
- * @returns 退订函数,调用后取消对应监听
72560
- */ onLoadingChange(cb) {
72561
- return this._manager.on('change', (change)=>{
72562
- if (change.addedIds.length > 0 || change.removedIds.length > 0) {
72563
- cb(change.ids);
72564
- }
72565
- });
72566
- }
72567
- /**
72568
- * 为指定元素添加 loading 覆盖层;同一元素不可重复添加。
72569
- * @param id 目标元素 ID
72570
- * @param options loading 配置(文案、位置、自定义区域、是否清空选中)
72571
- */ add(id, options) {
72572
- this._manager.add(id, options);
72626
+ /** 创建内置类型 VFXItem 的便捷工厂。 */ class DefaultVFXItems {
72627
+ /**
72628
+ * 创建空元素。
72629
+ * @param composition 目标合成
72630
+ * @param parent 父元素
72631
+ * @param name 元素名称
72632
+ * @returns 新建的空元素
72633
+ */ static createEmpty(composition, parent = null, name = 'NewVFXItem') {
72634
+ const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name);
72635
+ vfxItem.type = index$1.ItemType.null;
72636
+ return vfxItem;
72573
72637
  }
72574
72638
  /**
72575
- * 移除指定元素的 loading 覆盖层并释放对应 VFXItem。
72576
- * @param id 目标元素 ID
72577
- */ delete(id) {
72578
- this._manager.delete(id);
72579
- }
72580
- /**
72581
- * 更新指定 loading 元素的提示文案。
72582
- * @param id 目标元素 ID
72583
- * @param options 待合并更新的 LoadingTip 属性
72584
- */ updateItem(id, options) {
72585
- this._manager.update(id, options);
72586
- }
72587
- /** 根据目标元素的实时包围盒刷新加载动画。 */ onPreRender() {
72588
- if (this._idMap.size === 0) {
72589
- return;
72590
- }
72591
- for (const [id, loadingItem] of this._idMap){
72592
- this.updateLoadingVFXItemTransform(id, loadingItem);
72593
- }
72594
- }
72595
- /**
72596
- * 绘制所有加载提示文案。
72597
- * @param control 绘制控制器。
72598
- */ draw(control) {
72599
- if (this._idMap.size === 0) {
72600
- return;
72601
- }
72602
- for (const [id, loadingItem] of this._idMap){
72603
- if (!loadingItem.tip?.text) {
72604
- continue;
72605
- }
72606
- const item = getPlayerItemById(this._owner.getEngine().compositions[0], id);
72607
- const itemBox = item ? getItemViewBox(item, GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement)) : new Box2();
72608
- const loadingBox = getBoxByNormalizeBox(itemBox, loadingItem.loadingBox);
72609
- this.drawLoadingTip(control, loadingBox, itemBox, loadingItem.tip);
72610
- }
72611
- }
72612
- /**
72613
- * 绘制单条加载提示,并将文本限制在加载区域内。
72614
- * @param control 绘制控制器。
72615
- * @param loadingBox 加载区域。
72616
- * @param itemBox 目标元素包围盒。
72617
- * @param tip 加载提示配置。
72618
- */ drawLoadingTip(control, loadingBox, itemBox, tip) {
72619
- // 步骤 1:计算提示位置与可用宽度。
72620
- const { x: width, y: height } = loadingBox.getSize(this._loadingBoxSize);
72621
- if (!tip.text || !Number.isFinite(width) || width <= 0) {
72622
- return;
72623
- }
72624
- const positionedLeft = tip.position ? itemBox.min.x + (Number.isFinite(tip.position.x) ? tip.position.x : 0) : loadingBox.min.x;
72625
- const left = tip.position ? Math.min(Math.max(positionedLeft, loadingBox.min.x), loadingBox.max.x) : loadingBox.min.x;
72626
- const availableWidth = tip.position ? loadingBox.max.x - left : width;
72627
- if (availableWidth <= 0) {
72628
- return;
72629
- }
72630
- /**
72631
- * 测量加载提示的粗体字形。
72632
- * @param text 提示文本。
72633
- * @param fontSize 字号。
72634
- * @returns 文本字形尺寸。
72635
- */ const measureTipText = (text, fontSize)=>{
72636
- const metrics = measureTextMetrics(text, fontSize, LOADING_TIP_FONT_FAMILY, 'bold');
72637
- return {
72638
- width: metrics.width || Array.from(text).length * fontSize,
72639
- actualBoundingBoxAscent: metrics.actualBoundingBoxAscent || fontSize * 0.8,
72640
- actualBoundingBoxDescent: metrics.actualBoundingBoxDescent || fontSize * 0.2
72641
- };
72642
- };
72643
- // 步骤 2:测量文本并缩小字号以适应可用宽度。
72644
- const measuredAtBaseSize = measureTipText(tip.text, LOADING_TIP_FONT_SIZE);
72645
- let fontSize = measuredAtBaseSize.width > availableWidth ? LOADING_TIP_FONT_SIZE * availableWidth / measuredAtBaseSize.width : LOADING_TIP_FONT_SIZE;
72646
- let textMetrics = measureTipText(tip.text, fontSize);
72647
- if (textMetrics.width > availableWidth) {
72648
- fontSize *= availableWidth / textMetrics.width;
72649
- textMetrics = measureTipText(tip.text, fontSize);
72650
- }
72651
- // 步骤 3:按字形基线计算最终绘制位置。
72652
- const textLeft = tip.position ? left : loadingBox.min.x + (width - textMetrics.width) / 2;
72653
- const inkHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
72654
- const top = tip.position ? itemBox.min.y + (Number.isFinite(tip.position.y) ? tip.position.y : 0) : loadingBox.min.y + (height - inkHeight) / 2;
72655
- const probeMetrics = measureTipText(METRICS_STRING + BASELINE_SYMBOL, fontSize);
72656
- const inkTopFromCellTop = CONTROL_TEXT_GLYPH_PADDING + probeMetrics.actualBoundingBoxAscent - textMetrics.actualBoundingBoxAscent;
72657
- control.drawText(textLeft, top - inkTopFromCellTop, tip.text, fontSize, toColor(LOADING_TIP_COLOR, 1), LOADING_TIP_FONT_FAMILY, 'bold');
72658
- }
72659
- /** 释放当前实例的渲染投影;稳定 LoadingManager 不随 Gizmo 重建清空。 */ dispose() {
72660
- this._configOff?.();
72661
- this._configOff = undefined;
72662
- this._managerOff?.();
72663
- this._managerOff = undefined;
72664
- this._idMap.forEach((loadingItem)=>{
72665
- this._disposeLoadingVFXItem(loadingItem.loadingVFXItem);
72666
- });
72667
- this._idMap.clear();
72668
- super.dispose();
72669
- }
72670
- /** 将可销毁的渲染投影同步到当前加载状态。 */ _syncManager() {
72671
- const activeIds = new Set(this._manager.ids);
72672
- for (const [id, loadingItem] of this._idMap){
72673
- if (!activeIds.has(id)) {
72674
- this._disposeLoadingVFXItem(loadingItem.loadingVFXItem);
72675
- this._idMap.delete(id);
72676
- }
72677
- }
72678
- for (const id of activeIds){
72679
- const state = this._manager.get(id);
72680
- if (!state) {
72681
- continue;
72682
- }
72683
- const existing = this._idMap.get(id);
72684
- if (existing) {
72685
- existing.tip = {
72686
- ...state.tip,
72687
- position: state.tip.position ? state.tip.position.clone() : undefined
72688
- };
72689
- } else {
72690
- this._createProjection(id, state);
72691
- }
72692
- }
72693
- }
72694
- /**
72695
- * 为加载状态创建渲染投影。
72696
- * @param id 目标元素 ID。
72697
- * @param state 加载状态。
72698
- */ _createProjection(id, state) {
72699
- const item = getPlayerItemById(this._owner.getEngine().compositions[0], id);
72700
- const itemViewBox = item ? getItemViewBox(item, GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement)) : new Box2();
72701
- const currentBox = state.loadingBox ? this.getViewBoxByBox(state.loadingBox) : itemViewBox;
72702
- this._idMap.set(id, {
72703
- loadingBox: getNormalizeBoxByBoxes(itemViewBox, currentBox),
72704
- loadingVFXItem: this.createLoadingVFXItem(),
72705
- tip: {
72706
- ...state.tip,
72707
- position: state.tip.position ? state.tip.position.clone() : undefined
72708
- }
72709
- });
72710
- this.updateLoadingVFXItemTransform(id, this._idMap.get(id));
72711
- }
72712
- /** @returns 当前相机信息。 */ getCameraInfo() {
72713
- return GizmoViewportUtils.getCameraInfo(this._owner.getEngine());
72714
- }
72715
- /** @returns 当前视口的缩放、平移与尺寸。 */ getViewportParams() {
72716
- const composition = this._owner.getEngine().compositions[0];
72717
- const camera = composition?.camera;
72718
- if (camera) {
72719
- const viewportMatrix = camera.getViewportMatrix();
72720
- const scale = viewportMatrix.elements[0];
72721
- const translation = new Vector2(viewportMatrix.elements[12], viewportMatrix.elements[13]);
72722
- const width = GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).x;
72723
- const height = GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).y;
72724
- return {
72725
- scale,
72726
- translation,
72727
- width,
72728
- height
72729
- };
72730
- }
72731
- return {
72732
- scale: 1,
72733
- translation: new Vector2(),
72734
- width: GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).x,
72735
- height: GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).y
72736
- };
72737
- }
72738
- /**
72739
- * 将视图坐标下的包围盒按当前视口缩放与平移变换为实际渲染包围盒。
72740
- * @param box 视图坐标下的包围盒
72741
- * @returns 经视口缩放与平移后的实际渲染包围盒
72742
- */ getViewBoxByBox(box) {
72743
- const { scale, translation, width, height } = this.getViewportParams();
72744
- const center = new Vector2(width / 2, height / 2);
72745
- const result = scaleBox(box.clone(), scale, center).translate(translation);
72746
- return result;
72639
+ * 创建图片元素。
72640
+ * @param composition 目标合成
72641
+ * @param parent 父元素
72642
+ * @param name 元素名称
72643
+ * @returns 新建的图片元素
72644
+ */ static createSprite(composition, parent = null, name = 'NewSprite') {
72645
+ const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name, SpriteComponent);
72646
+ vfxItem.type = index$1.ItemType.sprite;
72647
+ return vfxItem;
72747
72648
  }
72748
72649
  /**
72749
- * 创建承载加载动画的 VFXItem。
72750
- * @returns 已绑定几何与材质的加载动画对象。
72751
- */ createLoadingVFXItem() {
72752
- // 步骤 1:创建加载动画对象与平面几何。
72753
- const composition = this._owner.getEngine().compositions[0];
72754
- const loadingVFXItem = VFXItemFactory.createVFXItem(composition, null, 'LoadingVFXItem', EffectComponent);
72755
- this._owner.getSelection().addIgnoreIds([
72756
- loadingVFXItem.getInstanceId()
72757
- ]);
72758
- loadingVFXItem.type = index$1.ItemType.effect;
72759
- const effects = loadingVFXItem.getComponent(EffectComponent);
72760
- const engine = this._owner.getEngine();
72761
- const geometry = Geometry.create(engine, {
72762
- attributes: {
72763
- aPos: {
72764
- size: 3,
72765
- data: new Float32Array([
72766
- -0.5,
72767
- -0.5,
72768
- 0,
72769
- 0.5,
72770
- -0.5,
72771
- 0,
72772
- 0.5,
72773
- 0.5,
72774
- 0,
72775
- -0.5,
72776
- 0.5,
72777
- 0
72778
- ])
72779
- },
72780
- aUV: {
72781
- size: 2,
72782
- data: new Float32Array([
72783
- 0,
72784
- 0,
72785
- 1,
72786
- 0,
72787
- 1,
72788
- 1,
72789
- 0,
72790
- 1
72791
- ])
72792
- }
72793
- },
72794
- indices: {
72795
- data: new Uint16Array([
72796
- 0,
72797
- 1,
72798
- 2,
72799
- 0,
72800
- 2,
72801
- 3
72802
- ])
72803
- },
72804
- mode: glContext.TRIANGLES,
72805
- drawCount: 6
72806
- });
72807
- // 步骤 2:使用当前配置创建动画材质。
72808
- const material = Material.create(engine, {
72809
- shader: {
72810
- vertex: `
72811
- precision highp float;
72812
- attribute vec3 aPos;
72813
- attribute vec2 aUV;
72814
- uniform mat4 effects_MatrixVP;
72815
- uniform mat4 effects_ObjectToWorld;
72816
- varying vec2 vUV;
72817
- void main() {
72818
- vUV = aUV;
72819
- gl_Position = effects_MatrixVP * effects_ObjectToWorld * vec4(aPos, 1.0);
72820
- }`,
72821
- fragment: this.config.loadingFragment
72822
- }
72823
- });
72824
- // 步骤 3:绑定运行时几何与材质。
72825
- // @ts-expect-error Effects 类型未暴露可写 geometry。
72826
- effects.geometry = geometry;
72827
- effects.material = material;
72828
- return loadingVFXItem;
72829
- }
72830
- /** 释放 loading 渲染对象,并同步恢复 Selection 的命中过滤。 */ _disposeLoadingVFXItem(item) {
72831
- this._owner.getSelection().deleteIgnoreIds([
72832
- item.getInstanceId()
72833
- ]);
72834
- item.dispose();
72650
+ * 创建文本元素。
72651
+ * @param composition 目标合成
72652
+ * @param parent 父元素
72653
+ * @param name 元素名称
72654
+ * @returns 新建的文本元素
72655
+ */ static createText(composition, parent = null, name = 'NewText') {
72656
+ const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name, TextComponent);
72657
+ vfxItem.type = index$1.ItemType.text;
72658
+ return vfxItem;
72835
72659
  }
72836
72660
  /**
72837
- * 按目标元素的实时包围盒更新加载动画变换。
72838
- * @param id 目标元素 ID。
72839
- * @param loadingItem 对应的加载渲染投影。
72840
- */ updateLoadingVFXItemTransform(id, loadingItem) {
72841
- const item = getPlayerItemById(this._owner.getEngine().compositions[0], id);
72842
- const containerSize = GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement);
72843
- const itemViewBox = item ? getItemViewBox(item, containerSize) : new Box2();
72844
- const currentBox = getBoxByNormalizeBox(itemViewBox, loadingItem.loadingBox);
72845
- const viewPosition = new Vector2((currentBox.max.x + currentBox.min.x) / 2, (currentBox.max.y + currentBox.min.y) / 2);
72846
- const cameraInfo = this.getCameraInfo();
72847
- const interactionPlane = new Plane(0, new Vector3(0, 0, 1));
72848
- const worldPosition = viewPositionToWorld(viewPosition, cameraInfo, interactionPlane, containerSize);
72849
- const viewSize = new Vector2(currentBox.max.x - currentBox.min.x, currentBox.max.y - currentBox.min.y);
72850
- const worldSize = viewSizeToWorld(viewSize, containerSize, loadingItem.loadingVFXItem, cameraInfo, interactionPlane);
72851
- if (worldPosition) {
72852
- loadingItem.loadingVFXItem.transform.setPosition(worldPosition.x, worldPosition.y, 0);
72853
- }
72854
- loadingItem.loadingVFXItem.transform.setScale(worldSize.x, worldSize.y, 1);
72661
+ * 创建视频元素。
72662
+ * @param composition 目标合成
72663
+ * @param parent 父元素
72664
+ * @param name 元素名称
72665
+ * @returns 新建的视频元素
72666
+ */ static createVideo(composition, parent = null, name = 'NewVideo') {
72667
+ const vfxItem = VFXItemFactory.createVFXItem(composition, parent, name, VideoComponent);
72668
+ vfxItem.type = index$1.ItemType.video;
72669
+ return vfxItem;
72855
72670
  }
72856
72671
  /**
72857
- * 创建加载覆盖层。
72858
- * @param owner Gizmo 宿主。
72859
- * @param manager 加载状态管理器。
72860
- */ constructor(owner, manager){
72861
- super(owner), _define_property(this, "type", 'loading'), _define_property(this, "_configOff", void 0), _define_property(this, "_managerOff", void 0), _define_property(this, "_manager", void 0), _define_property(this, "_loadingBoxSize", new Vector2()), _define_property(this, "_idMap", new Map());
72862
- this._manager = manager;
72863
- this._configOff = owner.getConfigManager().onChange(loadingConfig, (change)=>{
72864
- this._onConfigChange(change);
72865
- });
72866
- this._managerOff = this._manager.on('change', ()=>{
72867
- this._syncManager();
72868
- });
72869
- this._syncManager();
72870
- }
72871
- }
72872
- const viewportNavigationConfig = defineConfig({
72873
- id: 'viewport.navigation',
72874
- defaults: {
72875
- scrollWheelZoom: false,
72876
- invertZoom: false,
72877
- zoomStep: 0.1
72878
- }
72879
- });
72880
- const viewportOverlayConfig = defineConfig({
72881
- id: 'viewport.overlay',
72882
- defaults: {
72883
- boxColor: 0xFF0000,
72884
- boxWidth: 1,
72885
- outerMaskEnabled: true,
72886
- markColor: 0x000000,
72887
- markAlpha: 0.17,
72888
- safeAreaEnabled: true,
72889
- safeAreaBoxColor: 0x00FF00,
72890
- safeAreaBoxAlpha: 0.3
72891
- }
72892
- });
72893
- const selectionPreviewConfig = defineConfig({
72894
- id: 'selection.preview',
72895
- defaults: {
72896
- videoPreSelectedPlay: true,
72897
- preSelectedColor: 0x3b82f6,
72898
- preSelectedWidth: 2,
72899
- regionBoxColor: 0x3b82f6,
72900
- regionBoxAlpha: 0.17,
72901
- regionWireframeColor: 0x3b82f6,
72902
- regionWireframeAlpha: 0.78,
72903
- regionWireframeWidth: 1
72904
- }
72905
- });
72906
- const selectionSnapConfig = defineConfig({
72907
- id: 'selection.snap',
72908
- defaults: {
72909
- enabled: true,
72910
- lineWidth: 0.8,
72911
- lineColor: 0x0BD6FF,
72912
- distance: 6
72913
- }
72914
- });
72915
- const resizeSelectionConfig = defineConfig({
72916
- id: 'resize-selection',
72917
- defaults: {
72918
- pixelRatio: 1,
72919
- contentRatio: 1,
72920
- wireframeColor: 0x3b82f6,
72921
- wireframeAlpha: 1,
72922
- wireframeWidth: 1.5,
72923
- cornerFillColor: 0xFFFFFF,
72924
- cornerLineColor: 0x3b82f6,
72925
- cornerLineWidth: 1.5,
72926
- cornerLineAlpha: 1,
72927
- scaleCircleSize: 4,
72928
- rotationCircleSize: 7,
72929
- infoShowEnabled: true,
72930
- sizeTextColor: 0x666666,
72931
- nameTextColor: 0x666666,
72932
- frameMoveLineColor: 0x3b82f6,
72933
- frameMoveLineWidth: 2,
72934
- pictureLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*F2wVS7x0MfIAAAAAQBAAAAgAev-aAQ/original',
72935
- groupLogoUrl: 'https://mdn.alipayobjects.com/huamei_ppzin5/afts/img/Yo69Sr7boqYAAAAAH3AAAAgADjdkAQFr/original',
72936
- textLogoUrl: 'https://mdn.alipayobjects.com/huamei_ppzin5/afts/img/Yo69Sr7boqYAAAAAH3AAAAgADjdkAQFr/original',
72937
- videoLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*w1fnS4mq0VgAAAAAQCAAAAgAev-aAQ/original',
72938
- frameLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*DRF_RpndkjUAAAAAQDAAAAgAev-aAQ/original',
72939
- effectsLogoUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*RMewR4ruUnYAAAAAQGAAAAgAev-aAQ/original'
72940
- }
72941
- });
72942
- const pictureCutConfig = defineConfig({
72943
- id: 'tool.picture-cut',
72944
- defaults: {
72945
- maskColor: 0xFFFFFF,
72946
- maskAlpha: 0.5,
72947
- cutBoxLineWidth: 2,
72948
- cutBoxLineColor: 0x6A34FF,
72949
- cutBoxLineAlpha: 1,
72950
- itemBoxLineWidth: 1,
72951
- itemBoxLineColor: 0x6A34FF,
72952
- itemBoxLineAlpha: 1,
72953
- cutBoxCornerRadius: 5,
72954
- cutBoxCornerFillColor: 0xFFFFFF,
72955
- cutBoxCornerLineWidth: 2,
72956
- cutBoxCornerLineColor: 0x6A34FF,
72957
- cutBoxCornerLineAlpha: 1,
72958
- scaleInteractionDistance: 8,
72959
- directionScaleInteractionDistance: 5,
72960
- gridLineWidth: 1,
72961
- gridLineColor: 0xFFFFFF,
72962
- gridLineAlpha: 1,
72963
- gridCount: 2
72964
- }
72965
- });
72966
- const pictureExpandConfig = defineConfig({
72967
- id: 'tool.picture-expand',
72968
- defaults: {
72969
- maskColor: 0x6A34FF,
72970
- maskAlpha: 0.2,
72971
- expandBoxLineWidth: 2,
72972
- expandBoxLineColor: 0x6A34FF,
72973
- expandBoxLineAlpha: 1,
72974
- expandBoxCornerRadius: 5,
72975
- expandBoxCornerLineWidth: 2,
72976
- expandBoxCornerLineColor: 0x6A34FF,
72977
- expandBoxCornerLineAlpha: 1,
72978
- expandBoxCornerFillColor: 0xFFFFFF,
72979
- scaleInteractionDistance: 8,
72980
- directionScaleInteractionDistance: 5,
72981
- gridLineWidth: 1,
72982
- gridLineColor: 0xFFFFFF,
72983
- gridLineAlpha: 1,
72984
- gridCount: 2
72985
- }
72986
- });
72987
- const maskConfig = defineConfig({
72988
- id: 'tool.mask',
72989
- defaults: {
72990
- maskImage: '',
72991
- brushSize: 20,
72992
- brushColor: 0x6A34FF,
72993
- brushAlpha: 0.5,
72994
- maskColor: 0x00FF00,
72995
- maskBackgroundColor: 0xFFFFFF,
72996
- maskAlpha: 1,
72997
- boxLineWidth: 1,
72998
- boxLineColor: 0x6A34FF,
72999
- boxLineAlpha: 1
73000
- }
73001
- });
73002
- const spriteTextEditConfig = defineConfig({
73003
- id: 'tool.sprite-text-edit',
73004
- defaults: {
73005
- textColor: 0xFFFFFF,
73006
- preSelectedTextColor: 0xFFFFFF,
73007
- boxLineWidth: 3,
73008
- dashLineDash: 8,
73009
- dashLineGap: 8,
73010
- editBoxAlpha: 0.15,
73011
- editBoxColor: 0x3B82F6,
73012
- editBoxLineAlpha: 1,
73013
- editBoxLineColor: 0x3B82F6,
73014
- editBoxPreSelectedAlpha: 0.25,
73015
- editBoxPreSelectedColor: 0x3B82F6,
73016
- editBoxLinePreSelectedAlpha: 1,
73017
- editBoxLinePreSelectedColor: 0x3B82F6,
73018
- hasChangedEditBoxAlpha: 0.2,
73019
- hasChangedEditBoxColor: 0x22C55E,
73020
- hasChangedEditBoxLineAlpha: 1,
73021
- hasChangedEditBoxLineColor: 0x22C55E,
73022
- hasChangedEditBoxPreSelectedAlpha: 0.3,
73023
- hasChangedEditBoxPreSelectedColor: 0x22C55E,
73024
- hasChangedEditBoxLinePreSelectedAlpha: 1,
73025
- hasChangedEditBoxLinePreSelectedColor: 0x22C55E,
73026
- editBoxSelectedAlpha: 0.6,
73027
- editBoxSelectedColor: 0xFFFF00
73028
- }
73029
- });
73030
- const iconConfig = defineConfig({
73031
- id: 'feedback.icon',
73032
- defaults: {
73033
- autoShow: true,
73034
- videoPlayUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*ORMmSYYHIHUAAAAAJbAAAAgAev-aAQ/original',
73035
- videoPlayShift: [
73036
- 20,
73037
- 20
73038
- ],
73039
- videoPlayWidth: 20,
73040
- videoPlayHeight: 20,
73041
- imageGeneratorUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*bYB-TIEWLBkAAAAAQGAAAAgAev-aAQ/original',
73042
- videoGeneratorUrl: 'https://mdn.alipayobjects.com/huamei_ixsp8m/afts/img/A*6cTFT44CuKEAAAAAQCAAAAgAev-aAQ/original',
73043
- generatorWidth: 200,
73044
- generatorHeight: 200
73045
- }
73046
- });
73047
- const itemCreateConfig = defineConfig({
73048
- id: 'tool.item-create',
73049
- defaults: {
73050
- frameBorderColor: 0x2178FF,
73051
- frameBorderWidth: 1,
73052
- frameBorderAlpha: 0.8,
73053
- frameFillColor: 0x2178FF,
73054
- frameFillAlpha: 0.15,
73055
- frameChildBoxAlpha: 0.35,
73056
- frameChildBoxColor: 0x2178FF
72672
+ * 加载并创建特效合成元素。
72673
+ * @param composition 目标合成
72674
+ * @param effects 特效地址或场景数据
72675
+ * @param parent 父元素
72676
+ * @param urlname 元素名称
72677
+ * @returns 新建的特效元素
72678
+ */ static async createEffects(composition, effects, parent = null, urlname = 'NewEffects') {
72679
+ const preComposition = await AssetManager$1.loadPrecomposition(effects, {
72680
+ autoplay: false
72681
+ });
72682
+ const vfxItem = PrecompositionManager.instantiate(preComposition, composition);
72683
+ vfxItem.name = urlname;
72684
+ vfxItem.type = index$1.ItemType.composition;
72685
+ vfxItem.getComponent(CompositionComponent).endBehavior = vfxItem.endBehavior;
72686
+ if (parent) {
72687
+ vfxItem.setParent(parent);
72688
+ }
72689
+ return vfxItem;
73057
72690
  }
73058
- });
72691
+ }
73059
72692
  /**
73060
- * 缩放视图。
72693
+ * 播放视频元素。
73061
72694
  * @param engine 引擎
73062
- * @param zoom 缩放值
73063
- * @param center 缩放中心
73064
- */ function zoomView(engine, zoom, center = new Vector2()) {
73065
- const composition = engine.compositions[0];
73066
- if (!composition) {
72695
+ * @param id 视频元素 id
72696
+ */ function playVideoItem(engine, id) {
72697
+ const playerItem = getPlayerItemById(engine.compositions[0], id);
72698
+ if (playerItem?.type !== index$1.ItemType.video) {
73067
72699
  return;
73068
72700
  }
73069
- const { camera } = composition;
73070
- const scale = camera.getViewportMatrix().elements[0];
73071
- const translation = new Vector2(camera.getViewportMatrix().elements[12], camera.getViewportMatrix().elements[13]);
73072
- const result = scale + zoom;
73073
- // 1. 反算缩放中心对应的世界坐标。
73074
- const worldX = (center.x - translation.x) / scale;
73075
- const worldY = (center.y - translation.y) / scale;
73076
- // 2. 调整平移,使缩放中心在屏幕上的位置保持不变。
73077
- const newNDCTranslation = new Vector2(center.x - worldX * result, center.y - worldY * result);
73078
- const viewportMatrix = new Matrix4().compose(new Vector3(newNDCTranslation.x, newNDCTranslation.y, 0), new Quaternion(), new Vector3(result, result, 1));
73079
- composition.camera.setViewportMatrix(viewportMatrix);
72701
+ const videoComponent = playerItem.getComponent(VideoComponent);
72702
+ videoComponent?.playVideo();
73080
72703
  }
73081
72704
  /**
73082
- * 平移视图。
72705
+ * 暂停视频元素。
73083
72706
  * @param engine 引擎
73084
- * @param translation 位移值
73085
- */ function panView(engine, translation) {
73086
- const composition = engine.compositions[0];
73087
- if (!composition) {
72707
+ * @param id 视频元素 id
72708
+ */ function pauseVideoItem(engine, id) {
72709
+ const playerItem = getPlayerItemById(engine.compositions[0], id);
72710
+ if (playerItem?.type !== index$1.ItemType.video) {
73088
72711
  return;
73089
72712
  }
73090
- const { camera } = composition;
73091
- const scale = camera.getViewportMatrix().elements[0];
73092
- const resultTranslation = new Vector2(camera.getViewportMatrix().elements[12], camera.getViewportMatrix().elements[13]).add(translation);
73093
- const viewportMatrix = new Matrix4().compose(new Vector3(resultTranslation.x, resultTranslation.y, 0), new Quaternion(), new Vector3(scale, scale, 1));
73094
- camera.setViewportMatrix(viewportMatrix);
72713
+ const videoComponent = playerItem.getComponent(VideoComponent);
72714
+ videoComponent?.pauseVideo();
73095
72715
  }
73096
72716
  /**
73097
- * 调整画板元素(Frame)的尺寸与位置,并同步其子合成 Item 的位移。
73098
- * @param frameItem 画板元素(Frame 空节点控制器)
73099
- * @param worldSize 目标世界尺寸(所属合成坐标系,不受 viewport zoom 影响)
73100
- * @param translation 位移
73101
- * @param initialSize pointer down 时冻结的本地 / 世界尺寸;传入后允许尺寸连续经过 0 并变为负值
73102
- */ function resizeFrameItem(frameItem, worldSize, translation, initialSize) {
73103
- if (!isFramePlayerItem(frameItem)) {
73104
- console.warn(`Item ${frameItem.getInstanceId()} is not a frame item.`);
72717
+ * 播放特效元素。
72718
+ * @param engine 引擎
72719
+ * @param id 特效元素 id
72720
+ */ function playEffectsItem(engine, id) {
72721
+ const controlItem = getPlayerItemById(engine.compositions[0], id);
72722
+ const compositionItem = controlItem?.children?.[0];
72723
+ if (compositionItem?.type !== index$1.ItemType.composition) {
73105
72724
  return;
73106
72725
  }
73107
- // 1. 根据本地尺寸与世界尺寸的比例写回画板尺寸。
73108
- const currentWorldSize = initialSize?.worldSize ?? getItemWorldSize(frameItem);
73109
- const currentLocalSize = initialSize?.localSize ?? frameItem.transform.size;
73110
- const localWidth = currentWorldSize.x === 0 ? worldSize.x : currentLocalSize.x * worldSize.x / currentWorldSize.x;
73111
- const localHeight = currentWorldSize.y === 0 ? worldSize.y : currentLocalSize.y * worldSize.y / currentWorldSize.y;
73112
- frameItem.transform.setSize(localWidth, localHeight);
73113
- // 2. 将交互位移应用到画板。
73114
- if (translation && (translation.x !== 0 || translation.y !== 0)) {
73115
- const currentPosition = frameItem.transform.position;
73116
- frameItem.setPosition(currentPosition.x + translation.x, currentPosition.y + translation.y, currentPosition.z);
72726
+ compositionItem.getComponent(CompositionComponent).play();
72727
+ }
72728
+ /**
72729
+ * 暂停特效元素。
72730
+ * @param engine 引擎
72731
+ * @param id 特效元素 id
72732
+ */ function pauseEffectsItem(engine, id) {
72733
+ const controlItem = getPlayerItemById(engine.compositions[0], id);
72734
+ const compositionItem = controlItem?.children?.[0];
72735
+ if (compositionItem?.type !== index$1.ItemType.composition) {
72736
+ return;
73117
72737
  }
73118
- // 3. 反向补偿子合成元素,保持其世界位置不变。
73119
- const subCompositionItem = frameItem?.children?.[0];
73120
- if (subCompositionItem && translation) {
73121
- subCompositionItem.children.forEach((item)=>{
73122
- const parentMatrix = new Matrix4().copyFrom(item.transform.getParentMatrix() ?? new Matrix4());
73123
- parentMatrix.setPosition(new Vector3());
73124
- const result = translation.clone().applyMatrix(parentMatrix.invert()).negate();
73125
- item.translate(...result.toArray());
73126
- item.transform.updateLocalMatrix();
73127
- });
72738
+ compositionItem.getComponent(CompositionComponent).pause();
72739
+ }
72740
+ /** 信息标签默认字号。 */ const INFO_TEXT_FONT_SIZE = 14;
72741
+ /** 信息标签默认字体。 */ const INFO_TEXT_FONT_FAMILY = 'sans-serif';
72742
+ /** effects Control 字符 atlas 的固定单边留白。 */ const CONTROL_TEXT_GLYPH_PADDING = 4;
72743
+ /** 用于测量字体高度的代表字符。 */ const METRICS_STRING = '|ÉqÅ';
72744
+ /** 用于测量字体基线的字符。 */ const BASELINE_SYMBOL = 'M';
72745
+ /**
72746
+ * 颜色缓存:`hex_alpha` → effects Color。键到颜色映射不可变,全局缓存无需清理。
72747
+ */ const colorCache = new Map();
72748
+ /**
72749
+ * 将十六进制色值 + alpha 转为 effects Color(带全局缓存)。
72750
+ * @param hex 0xRRGGBB 颜色
72751
+ * @param alpha 透明度 0..1
72752
+ * @returns effects Color
72753
+ */ function toColor(hex, alpha = 1) {
72754
+ const key = `${hex}_${alpha}`;
72755
+ const cached = colorCache.get(key);
72756
+ if (cached) {
72757
+ return cached;
73128
72758
  }
72759
+ const color = new Color((hex >> 16 & 0xff) / 255, (hex >> 8 & 0xff) / 255, (hex & 0xff) / 255, alpha);
72760
+ colorCache.set(key, color);
72761
+ return color;
73129
72762
  }
73130
- /** 统一处理手形工具和滚轮触发的视口平移与缩放。 */ class ViewportNavigationController extends EventEmitter {
73131
- /** 当前视口导航配置。 */ get config() {
73132
- return this._owner.getConfigManager().get(viewportNavigationConfig);
72763
+ /**
72764
+ * 离屏文本测量上下文,懒加载并复用。
72765
+ */ let _measureCtx = null;
72766
+ /**
72767
+ * 测量文本的宽度与可见字形边界。
72768
+ * @param text 文本内容
72769
+ * @param fontSize 字号
72770
+ * @param fontFamily 字体
72771
+ * @param fontWeight 字重
72772
+ * @param fontStyle 字体样式
72773
+ * @returns 文本测量结果
72774
+ */ function measureTextMetrics(text, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY, fontWeight, fontStyle) {
72775
+ _measureCtx ?? (_measureCtx = document.createElement('canvas').getContext('2d'));
72776
+ if (!_measureCtx) {
72777
+ return {
72778
+ width: 0,
72779
+ actualBoundingBoxAscent: 0,
72780
+ actualBoundingBoxDescent: 0
72781
+ };
73133
72782
  }
73134
- /** 当前 Effects 引擎。 */ get _engine() {
73135
- return this._owner.getEngine();
72783
+ _measureCtx.font = `${''}${fontWeight ? `${fontWeight} ` : ''}${fontSize}px ${fontFamily}`;
72784
+ const metrics = _measureCtx.measureText(text);
72785
+ return {
72786
+ width: metrics.width,
72787
+ actualBoundingBoxAscent: metrics.actualBoundingBoxAscent ?? 0,
72788
+ actualBoundingBoxDescent: metrics.actualBoundingBoxDescent ?? 0
72789
+ };
72790
+ }
72791
+ /**
72792
+ * 计算 effects Control.drawText 实际使用的 atlas cell 高度。
72793
+ * @param fontSize 字号
72794
+ * @param fontFamily 字体
72795
+ * @param fontWeight 字重
72796
+ * @param fontStyle 字体样式
72797
+ * @param resolution 渲染分辨率
72798
+ * @returns 字符图集单元格高度
72799
+ */ function measureControlTextCellHeight(fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY, fontWeight, fontStyle, resolution = 1) {
72800
+ const safeResolution = Number.isFinite(resolution) && resolution > 0 ? resolution : 1;
72801
+ const scaledFontSize = fontSize * safeResolution;
72802
+ const metrics = measureTextMetrics(METRICS_STRING + BASELINE_SYMBOL, scaledFontSize, fontFamily, fontWeight);
72803
+ const ascent = metrics.actualBoundingBoxAscent || scaledFontSize * 0.8;
72804
+ const descent = metrics.actualBoundingBoxDescent || scaledFontSize * 0.2;
72805
+ return Math.ceil(ascent + descent + CONTROL_TEXT_GLYPH_PADDING * 2 * safeResolution) / safeResolution;
72806
+ }
72807
+ /**
72808
+ * 测量文本宽度(逻辑像素),用于信息标签的溢出判断与右对齐定位。
72809
+ * @param text 文本内容
72810
+ * @param fontSize 字号
72811
+ * @param fontFamily 字体
72812
+ * @param fontWeight 字重
72813
+ * @returns 文本宽度
72814
+ */ function measureTextWidth(text, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY, fontWeight) {
72815
+ return measureTextMetrics(text, fontSize, fontFamily, fontWeight).width;
72816
+ }
72817
+ /**
72818
+ * 单行文本截断:逐字符累加,超出可用宽度时把最后两个字符替换为省略号。
72819
+ * @param text 文本内容
72820
+ * @param maxWidth 可用宽度
72821
+ * @param fontSize 字号
72822
+ * @param fontFamily 字体
72823
+ * @returns 截断后的文本
72824
+ */ function truncateText(text, maxWidth, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY) {
72825
+ if (measureTextWidth(text, fontSize, fontFamily) <= maxWidth) {
72826
+ return text;
73136
72827
  }
73137
- /**
73138
- * 设置允许的视口缩放范围。
73139
- * @param minScale 最小缩放
73140
- * @param maxScale 最大缩放
73141
- */ setScaleRange(minScale, maxScale) {
73142
- if (!Number.isFinite(minScale) || !Number.isFinite(maxScale) || minScale <= 0 || maxScale <= 0 || minScale > maxScale) {
73143
- throw new RangeError('Viewport scale range must be finite, positive, and ordered.');
72828
+ let current = '';
72829
+ for (const char of text){
72830
+ if (measureTextWidth(current + char, fontSize, fontFamily) > maxWidth) {
72831
+ return current.length > 2 ? current.slice(0, -2) + '...' : '...';
73144
72832
  }
73145
- this._minScale = minScale;
73146
- this._maxScale = maxScale;
72833
+ current += char;
73147
72834
  }
73148
- /**
73149
- * 根据滚轮来源和修饰键执行平移或缩放。
73150
- * @param event 滚轮事件
73151
- * @param _pointerCaptured 指针是否已被捕获
73152
- * @returns 视口是否发生变化
73153
- */ handleWheel(event, _pointerCaptured) {
73154
- if (!this._hasViewport()) {
73155
- return false;
73156
- }
73157
- event.accept();
73158
- if (event.buttonMask !== MouseButtonMask.None) {
73159
- return false;
73160
- }
73161
- const { delta, source } = this._normalizeWheel(event);
73162
- const mode = this._classifyWheel(event, source);
73163
- const center = new Vector2(event.position.x, event.position.y);
73164
- if (mode === 'zoom') {
73165
- if (delta.y === 0) {
73166
- return false;
72835
+ return current;
72836
+ }
72837
+ /**
72838
+ * 多行逐字符折行:行宽超出 maxWidth 时换行;行数达到 maxLines 时末行用省略号收尾。
72839
+ * @param text 文本内容
72840
+ * @param maxWidth 单行可用宽度
72841
+ * @param maxLines 最大行数
72842
+ * @param fontSize 字号
72843
+ * @param fontFamily 字体
72844
+ * @returns 折行后的文本行数组
72845
+ */ function wrapText(text, maxWidth, maxLines, fontSize = INFO_TEXT_FONT_SIZE, fontFamily = INFO_TEXT_FONT_FAMILY) {
72846
+ const lines = [];
72847
+ let currentLine = '';
72848
+ const chars = text.split('');
72849
+ for(let i = 0; i < chars.length; i++){
72850
+ const char = chars[i];
72851
+ const testLine = currentLine + char;
72852
+ if (measureTextWidth(testLine, fontSize, fontFamily) > maxWidth) {
72853
+ lines.push(currentLine);
72854
+ if (lines.length >= maxLines) {
72855
+ const lastLine = lines[maxLines - 1];
72856
+ lines[maxLines - 1] = lastLine.length > 2 ? lastLine.slice(0, -2) + '...' : '...';
72857
+ break;
73167
72858
  }
73168
- const direction = this.config.invertZoom ? -delta.y : delta.y;
73169
- const zoomStep = this.config.zoomStep;
73170
- const zoomShift = clamp(direction * 0.01, -zoomStep, zoomStep);
73171
- return this._zoomByShift(zoomShift, center, event.ctrlPressed && source === 'trackpad' ? 'pinch-zoom' : 'wheel-zoom');
73172
- }
73173
- if (delta.x === 0 && delta.y === 0) {
73174
- return false;
73175
- }
73176
- return this.panByViewDelta(delta, center, 'wheel-pan');
73177
- }
73178
- /**
73179
- * 按视图像素增量平移视口。
73180
- * @param delta 视图像素位移
73181
- * @param center 操作中心
73182
- * @param source 操作来源
73183
- * @returns 视口是否发生变化
73184
- */ panByViewDelta(delta, center, source) {
73185
- if (!this._hasViewport() || delta.x === 0 && delta.y === 0) {
73186
- return false;
72859
+ currentLine = char;
72860
+ } else {
72861
+ currentLine = testLine;
73187
72862
  }
73188
- const containerSize = this._containerSize();
73189
- if (containerSize.x <= 0 || containerSize.y <= 0) {
73190
- return false;
72863
+ if (i === chars.length - 1 && currentLine) {
72864
+ lines.push(currentLine);
73191
72865
  }
73192
- panView(this._engine, viewSizeToNDC(delta, containerSize));
73193
- this._emitChange(center, source);
73194
- return true;
73195
72866
  }
73196
- /**
73197
- * 按倍率缩放视口。
73198
- * @param factor 缩放倍率
73199
- * @param center 缩放中心
73200
- * @param source 操作来源
73201
- * @returns 视口是否发生变化
73202
- */ zoomByFactor(factor, center, source) {
73203
- if (!this._hasViewport() || !Number.isFinite(factor) || factor <= 0) {
73204
- return false;
73205
- }
73206
- const currentScale = GizmoViewportUtils.getViewScale(this._engine);
73207
- const nextScale = clamp(currentScale * factor, this._minScale, this._maxScale);
73208
- if (nextScale === currentScale) {
73209
- return false;
73210
- }
73211
- const containerSize = this._containerSize();
73212
- if (containerSize.x <= 0 || containerSize.y <= 0) {
73213
- return false;
73214
- }
73215
- zoomView(this._engine, nextScale - currentScale, viewPositionToNDC(center, containerSize));
73216
- this._emitChange(center, source);
73217
- return true;
72867
+ return lines.slice(0, maxLines);
72868
+ }
72869
+ /**
72870
+ * 绘制由 4 个角点构成的(可能非正交的)包围盒边框。
72871
+ * @param control 绘制控制器
72872
+ * @param corners 4 个角点(视图坐标,Y 向下)
72873
+ * @param color 边框颜色
72874
+ * @param width 线宽
72875
+ */ function drawCorners(control, corners, color, width) {
72876
+ if (corners.length < 4) {
72877
+ return;
73218
72878
  }
73219
- /** 释放配置订阅和事件监听器。 */ dispose() {
73220
- this._configOff?.();
73221
- this._configOff = undefined;
73222
- for (const listener of this.getListeners('change').slice()){
73223
- this.off('change', listener);
73224
- }
72879
+ for(let i = 0; i < 4; i++){
72880
+ const start = corners[i];
72881
+ const end = corners[(i + 1) % 4];
72882
+ control.drawLine(start.x, start.y, end.x, end.y, color, width);
73225
72883
  }
73226
- /**
73227
- * 根据滚轮来源和修饰键确定导航模式。
73228
- * @param event 滚轮事件
73229
- * @param source 输入设备类型
73230
- * @returns 滚轮导航模式
73231
- */ _classifyWheel(event, source) {
73232
- const signature = `${source}:${Number(event.ctrlPressed)}:${Number(event.metaPressed)}`;
73233
- if (signature !== this._wheelSignature) {
73234
- this._wheelSignature = signature;
73235
- this._wheelMode = event.ctrlPressed || event.metaPressed || source === 'mouse' && this.config.scrollWheelZoom ? 'zoom' : 'pan';
73236
- }
73237
- return this._wheelMode;
72884
+ }
72885
+ /**
72886
+ * 绘制虚线包围盒,并在相邻边之间保持连续虚线相位。
72887
+ * @param control 绘制控制器
72888
+ * @param corners 四个视图角点
72889
+ * @param color 虚线颜色
72890
+ * @param width 线宽
72891
+ * @param dashLength 实线段长度
72892
+ * @param gapLength 间隔长度
72893
+ */ function drawDashedCorners(control, corners, color, width, dashLength, gapLength) {
72894
+ // 1. 校验角点和虚线参数。
72895
+ if (corners.length < 4 || dashLength <= 0 || gapLength < 0) {
72896
+ return;
73238
72897
  }
73239
- /**
73240
- * 将方向型滚轮事件转换为视图位移。
73241
- * @param event 滚轮事件
73242
- * @returns 视图位移和输入设备类型
73243
- */ _normalizeWheel(event) {
73244
- const delta = new Vector2();
73245
- switch(event.buttonIndex){
73246
- case MouseButton.WheelUp:
73247
- delta.y = event.factor;
73248
- break;
73249
- case MouseButton.WheelDown:
73250
- delta.y = -event.factor;
73251
- break;
73252
- case MouseButton.WheelLeft:
73253
- delta.x = event.factor;
73254
- break;
73255
- case MouseButton.WheelRight:
73256
- delta.x = -event.factor;
73257
- break;
73258
- }
73259
- // Windows 下 Shift + 滚轮映射为水平平移。
73260
- const isApplePlatform = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
73261
- if (!isApplePlatform && event.shiftPressed && delta.x === 0) {
73262
- delta.x = delta.y;
73263
- delta.y = 0;
72898
+ const patternLength = dashLength + gapLength;
72899
+ let perimeterOffset = 0;
72900
+ // 2. 逐边计算虚线相位,并继承上一条边的周长偏移。
72901
+ for(let i = 0; i < 4; i++){
72902
+ const start = corners[i];
72903
+ const end = corners[(i + 1) % 4];
72904
+ const dx = end.x - start.x;
72905
+ const dy = end.y - start.y;
72906
+ const edgeLength = Math.hypot(dx, dy);
72907
+ if (edgeLength <= Number.EPSILON) {
72908
+ continue;
73264
72909
  }
73265
- const source = event.ctrlPressed || event.metaPressed || !Number.isInteger(event.factor) || delta.x !== 0 ? 'trackpad' : 'mouse';
73266
- return {
73267
- delta,
73268
- source
73269
- };
73270
- }
73271
- /**
73272
- * 按绝对缩放增量更新视口。
73273
- * @param shift 缩放增量
73274
- * @param center 缩放中心
73275
- * @param source 操作来源
73276
- * @returns 视口是否发生变化
73277
- */ _zoomByShift(shift, center, source) {
73278
- const currentScale = GizmoViewportUtils.getViewScale(this._engine);
73279
- const nextScale = roundNumber(clamp(currentScale + shift, this._minScale, this._maxScale), 2);
73280
- if (nextScale === currentScale) {
73281
- return false;
72910
+ let edgeOffset = 0;
72911
+ while(edgeOffset < edgeLength){
72912
+ const patternOffset = perimeterOffset % patternLength;
72913
+ const drawingDash = gapLength === 0 || patternOffset < dashLength;
72914
+ const phaseRemaining = drawingDash ? dashLength - patternOffset : patternLength - patternOffset;
72915
+ const segmentLength = Math.min(phaseRemaining, edgeLength - edgeOffset);
72916
+ // 3. 仅绘制当前相位中的实线段。
72917
+ if (drawingDash && segmentLength > Number.EPSILON) {
72918
+ const startRatio = edgeOffset / edgeLength;
72919
+ const endRatio = (edgeOffset + segmentLength) / edgeLength;
72920
+ control.drawLine(start.x + dx * startRatio, start.y + dy * startRatio, start.x + dx * endRatio, start.y + dy * endRatio, color, width);
72921
+ }
72922
+ edgeOffset += segmentLength;
72923
+ perimeterOffset += segmentLength;
73282
72924
  }
73283
- return this.zoomByFactor(nextScale / currentScale, center, source);
73284
- }
73285
- /**
73286
- * 发出当前视口快照。
73287
- * @param center 操作中心
73288
- * @param source 操作来源
73289
- */ _emitChange(center, source) {
73290
- const containerSize = this._containerSize();
73291
- this.emit('change', {
73292
- source,
73293
- scale: GizmoViewportUtils.getViewScale(this._engine),
73294
- translation: ndcSizeToViewSize(GizmoViewportUtils.getViewportTranslation(this._engine), containerSize),
73295
- center: center.clone()
73296
- });
73297
- }
73298
- /** @returns 当前画布容器尺寸。 */ _containerSize() {
73299
- return GizmoViewportUtils.getContainerSize(this._engine.canvas.parentElement);
73300
- }
73301
- /** @returns 是否存在可操作的画布容器和相机。 */ _hasViewport() {
73302
- return Boolean(this._engine.canvas.parentElement && this._engine.compositions[0]?.camera);
73303
- }
73304
- /**
73305
- * @param owner Gizmo 宿主
73306
- */ constructor(owner){
73307
- super(), _define_property(this, "_owner", void 0), _define_property(this, "_minScale", 0.01), _define_property(this, "_maxScale", 20), _define_property(this, "_configOff", void 0), _define_property(this, "_wheelSignature", ''), _define_property(this, "_wheelMode", 'pan');
73308
- this._owner = owner;
73309
- this._configOff = owner.getConfigManager().onChange(viewportNavigationConfig, ()=>{
73310
- this._wheelSignature = '';
73311
- });
73312
72925
  }
73313
72926
  }
72927
+ /**
72928
+ * 绘制包围盒边框(沿 4 个角点连边)。
72929
+ * @param control 绘制控制器
72930
+ * @param box 包围盒(视图坐标,Y 向下)
72931
+ * @param color 边框颜色
72932
+ * @param width 线宽
72933
+ */ function drawBox(control, box, color, width) {
72934
+ drawCorners(control, box.corners, color, width);
72935
+ }
72936
+ /**
72937
+ * 填充轴对齐包围盒(实心矩形)。
72938
+ * @param control 绘制控制器
72939
+ * @param box 包围盒(视图坐标,Y 向下)
72940
+ * @param color 填充颜色
72941
+ */ function fillBox(control, box, color) {
72942
+ const size = box.getSize();
72943
+ control.fillRect(box.min.x, box.min.y, size.x, size.y, color);
72944
+ }
72945
+ /**
72946
+ * 构造沿局部轴展开的旋转矩形角点。
72947
+ * @param center 矩形中心(视图坐标)
72948
+ * @param xAxis 局部 X 轴单位向量(视图坐标,宽方向)
72949
+ * @param yAxis 局部 Y 轴单位向量(视图坐标,高方向)
72950
+ * @param halfWidth 半宽(沿 xAxis,视图像素)
72951
+ * @param halfHeight 半高(沿 yAxis,视图像素)
72952
+ * @returns 4 个角点(视图坐标,环绕顺序)
72953
+ */ function rotatedRectCorners(center, xAxis, yAxis, halfWidth, halfHeight) {
72954
+ const halfW = xAxis.clone().multiply(halfWidth);
72955
+ const halfH = yAxis.clone().multiply(halfHeight);
72956
+ return [
72957
+ center.clone().subtract(halfW).subtract(halfH),
72958
+ center.clone().add(halfW).subtract(halfH),
72959
+ center.clone().add(halfW).add(halfH),
72960
+ center.clone().subtract(halfW).add(halfH)
72961
+ ];
72962
+ }
72963
+ /**
72964
+ * 填充沿局部轴展开的旋转矩形。
72965
+ * @param control 绘制控制器
72966
+ * @param center 矩形中心(视图坐标)
72967
+ * @param xAxis 局部 X 轴单位向量(视图坐标,宽方向)
72968
+ * @param yAxis 局部 Y 轴单位向量(视图坐标,高方向)
72969
+ * @param width 宽(沿 xAxis,视图像素)
72970
+ * @param height 高(沿 yAxis,视图像素)
72971
+ * @param color 填充颜色
72972
+ */ function fillRotatedRect(control, center, xAxis, yAxis, width, height, color) {
72973
+ const ex = xAxis.clone();
72974
+ const ey = yAxis.clone();
72975
+ const anchor = center.clone();
72976
+ drawRotatedQuad(control, anchor, ex, ey, ()=>{
72977
+ control.fillRect(-width / 2, -height / 2, width, height, color);
72978
+ });
72979
+ }
72980
+ /**
72981
+ * 在由锚点和两个基向量定义的局部坐标系内执行绘制。
72982
+ * @param control 绘制控制器
72983
+ * @param anchor Control 绘制空间锚点(局部原点)
72984
+ * @param ex 局部 X 轴基向量(单位向量)
72985
+ * @param ey 局部 Y 轴基向量(单位向量)
72986
+ * @param drawFn 在局部坐标系内执行的绘制
72987
+ */ function drawRotatedQuad(control, anchor, ex, ey, drawFn) {
72988
+ const graphics = control.engine.graphics;
72989
+ // 列优先:第一列 = ex,第二列 = ey,第三列 = anchor(平移)
72990
+ const matrix = Matrix3.fromColumnVectors(new Vector3(ex.x, ex.y, 0), new Vector3(ey.x, ey.y, 0), new Vector3(anchor.x, anchor.y, 1));
72991
+ graphics.pushTransform(matrix);
72992
+ drawFn();
72993
+ graphics.popTransform();
72994
+ }
73314
72995
  /** 缩放角点的桌面端命中区域边长;视觉手柄仍由 scaleCircleSize 控制。 */ const SCALE_CORNER_HIT_SIZE = 14;
73315
72996
  /** 单选信息标签中的类型图标尺寸。 */ const INFO_ICON_SIZE = 15;
73316
72997
  /** 图标相对选框顶边的底部偏移。 */ const INFO_ICON_BOTTOM = 5.5;
@@ -74018,7 +73699,7 @@ const itemCreateConfig = defineConfig({
74018
73699
  */ _getIconUrl(type) {
74019
73700
  const urlMap = {
74020
73701
  'null': '',
74021
- 'image': this.config.pictureLogoUrl,
73702
+ 'image': this.config.imageLogoUrl,
74022
73703
  'group': this.config.groupLogoUrl,
74023
73704
  'text': this.config.textLogoUrl,
74024
73705
  'video': this.config.videoLogoUrl,
@@ -74036,7 +73717,7 @@ const itemCreateConfig = defineConfig({
74036
73717
  const logoKeys = [
74037
73718
  {
74038
73719
  type: "image",
74039
- key: 'pictureLogoUrl'
73720
+ key: 'imageLogoUrl'
74040
73721
  },
74041
73722
  {
74042
73723
  type: "group",
@@ -76202,6 +75883,12 @@ const itemCreateConfig = defineConfig({
76202
75883
  this._session.syncTextArea(this.result, this.viewScale);
76203
75884
  }
76204
75885
  /**
75886
+ * 选中当前编辑文本的全部内容,并将焦点交给输入框。
75887
+ * @returns 当前存在可编辑文本时返回 true
75888
+ */ selectAllText() {
75889
+ return this._session.selectAllText();
75890
+ }
75891
+ /**
76205
75892
  * 绘制文本编辑包围盒。
76206
75893
  * @param control 绘制控制器
76207
75894
  */ _drawEditBox(control) {
@@ -76485,16 +76172,16 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76485
76172
  this._session = new TextEditSession(_owner);
76486
76173
  }
76487
76174
  }
76488
- /** 图片裁切和扩边的交互类型。 */ var PictureInteractionType = /*#__PURE__*/ function(PictureInteractionType) {
76489
- /** 无交互。 */ PictureInteractionType["NONE"] = "none";
76490
- /** 角点双轴缩放。 */ PictureInteractionType["SCALE"] = "scale";
76491
- /** 边中点单轴缩放。 */ PictureInteractionType["DIRECTION_SCALE"] = "direction-scale";
76492
- /** 移动交互框。 */ PictureInteractionType["MOVE"] = "move";
76493
- return PictureInteractionType;
76175
+ /** 图片裁切和扩边的交互类型。 */ var ImageInteractionType = /*#__PURE__*/ function(ImageInteractionType) {
76176
+ /** 无交互。 */ ImageInteractionType["NONE"] = "none";
76177
+ /** 角点双轴缩放。 */ ImageInteractionType["SCALE"] = "scale";
76178
+ /** 边中点单轴缩放。 */ ImageInteractionType["DIRECTION_SCALE"] = "direction-scale";
76179
+ /** 移动交互框。 */ ImageInteractionType["MOVE"] = "move";
76180
+ return ImageInteractionType;
76494
76181
  }({});
76495
- /** 在图片元素范围内移动或缩放归一化裁剪框。 */ class PictureCutGizmo extends Gizmo {
76182
+ /** 在图片元素范围内移动或缩放归一化裁剪框。 */ class ImageCutGizmo extends Gizmo {
76496
76183
  /** 当前图片裁剪配置。 */ get config() {
76497
- return this._owner.getConfigManager().get(pictureCutConfig);
76184
+ return this._owner.getConfigManager().get(imageCutConfig);
76498
76185
  }
76499
76186
  /** 当前选中的元素(直读 selection.getSelectedPlayerItems:id→VFXItem 已解析)。 */ get selectedItems() {
76500
76187
  return this._owner.getSelection().getSelectedPlayerItems();
@@ -76530,7 +76217,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76530
76217
  const inputPosition = new Vector2(event.position.x, event.position.y);
76531
76218
  this.refreshInteractionType(hover, inputPosition);
76532
76219
  this._owner.setCursor(this.cursorResult);
76533
- if (event.buttonMask === MouseButtonMask.None && (this.interactionParam.type === PictureInteractionType.SCALE || this.interactionParam.type === PictureInteractionType.DIRECTION_SCALE)) {
76220
+ if (event.buttonMask === MouseButtonMask.None && (this.interactionParam.type === ImageInteractionType.SCALE || this.interactionParam.type === ImageInteractionType.DIRECTION_SCALE)) {
76534
76221
  event.accept();
76535
76222
  }
76536
76223
  }
@@ -76559,9 +76246,9 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76559
76246
  return;
76560
76247
  }
76561
76248
  this.interactionParam = {
76562
- type: PictureInteractionType.NONE
76249
+ type: ImageInteractionType.NONE
76563
76250
  };
76564
- this.refreshCursorResult(PictureInteractionType.NONE);
76251
+ this.refreshCursorResult(ImageInteractionType.NONE);
76565
76252
  this._owner.setCursor(this.cursorResult);
76566
76253
  }
76567
76254
  /** @returns 是否仅选中了一个可裁剪元素。 */ _isApplicableSelection() {
@@ -76576,7 +76263,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76576
76263
  if (!this._mouseGrabbed) {
76577
76264
  return false;
76578
76265
  }
76579
- if (this.interactionParam.type === PictureInteractionType.NONE) {
76266
+ if (this.interactionParam.type === ImageInteractionType.NONE) {
76580
76267
  return true;
76581
76268
  }
76582
76269
  // 步骤 2:获取实时投影并计算指针位移。
@@ -76591,7 +76278,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76591
76278
  const { min: itemBoxMin, max: itemBoxMax } = itemBox;
76592
76279
  // 步骤 3:按移动、角点缩放或边缩放更新归一化裁剪框。
76593
76280
  switch(this.interactionParam.type){
76594
- case PictureInteractionType.MOVE:
76281
+ case ImageInteractionType.MOVE:
76595
76282
  {
76596
76283
  const { min: originMin, max: originMax } = box;
76597
76284
  const xMinShift = itemBoxMin.x - originMin.x;
@@ -76604,7 +76291,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76604
76291
  this.result.normalizeCutBox = getNormalizeBoxByBoxes(itemBox, resultBox);
76605
76292
  break;
76606
76293
  }
76607
- case PictureInteractionType.SCALE:
76294
+ case ImageInteractionType.SCALE:
76608
76295
  {
76609
76296
  const { startCorner, anchor } = this.interactionParam;
76610
76297
  const resultCorner = new Vector2();
@@ -76669,7 +76356,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76669
76356
  }
76670
76357
  break;
76671
76358
  }
76672
- case PictureInteractionType.DIRECTION_SCALE:
76359
+ case ImageInteractionType.DIRECTION_SCALE:
76673
76360
  {
76674
76361
  var _this_interactionParam1;
76675
76362
  const { index } = this.interactionParam;
@@ -76744,7 +76431,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76744
76431
  const inputPosition = new Vector2(event.position.x, event.position.y);
76745
76432
  this.refreshInteractionType(mouse, inputPosition);
76746
76433
  this._owner.setCursor(this.cursorResult);
76747
- if (this.interactionParam.type === PictureInteractionType.NONE) {
76434
+ if (this.interactionParam.type === ImageInteractionType.NONE) {
76748
76435
  return;
76749
76436
  }
76750
76437
  this._mouseGrabbed = true;
@@ -76841,7 +76528,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76841
76528
  * @param angle 指针方向
76842
76529
  */ refreshCursorResult(activeType, angle = 0) {
76843
76530
  switch(activeType){
76844
- case PictureInteractionType.SCALE:
76531
+ case ImageInteractionType.SCALE:
76845
76532
  {
76846
76533
  this.cursorResult = {
76847
76534
  type: GestureCursorType.SCALE,
@@ -76849,7 +76536,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76849
76536
  };
76850
76537
  break;
76851
76538
  }
76852
- case PictureInteractionType.DIRECTION_SCALE:
76539
+ case ImageInteractionType.DIRECTION_SCALE:
76853
76540
  {
76854
76541
  this.cursorResult = {
76855
76542
  type: GestureCursorType.SCALE,
@@ -76857,8 +76544,8 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76857
76544
  };
76858
76545
  break;
76859
76546
  }
76860
- case PictureInteractionType.MOVE:
76861
- case PictureInteractionType.NONE:
76547
+ case ImageInteractionType.MOVE:
76548
+ case ImageInteractionType.NONE:
76862
76549
  {
76863
76550
  this.cursorResult = {
76864
76551
  type: GestureCursorType.NORMAL,
@@ -76908,7 +76595,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76908
76595
  */ refreshInteractionType(point, inputPosition) {
76909
76596
  // 步骤 1:重置交互状态并获取实时裁剪投影。
76910
76597
  this.interactionParam = {
76911
- type: PictureInteractionType.NONE
76598
+ type: ImageInteractionType.NONE
76912
76599
  };
76913
76600
  const projection = this._projectSelection();
76914
76601
  if (!projection) {
@@ -76925,7 +76612,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76925
76612
  if (corner.distance(point) < this.config.scaleInteractionDistance) {
76926
76613
  hasInteractionType = true;
76927
76614
  this.interactionParam = {
76928
- type: PictureInteractionType.SCALE,
76615
+ type: ImageInteractionType.SCALE,
76929
76616
  box: cutBox.clone(),
76930
76617
  startMouse: inputPosition,
76931
76618
  startCorner: corner.clone(),
@@ -76950,7 +76637,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76950
76637
  if (state) {
76951
76638
  hasInteractionType = true;
76952
76639
  this.interactionParam = {
76953
- type: PictureInteractionType.DIRECTION_SCALE,
76640
+ type: ImageInteractionType.DIRECTION_SCALE,
76954
76641
  index,
76955
76642
  box: cutBox.clone(),
76956
76643
  startMouse: inputPosition
@@ -76961,7 +76648,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76961
76648
  // 步骤 4:最后检测裁剪区域整体移动并刷新光标。
76962
76649
  if (cutBox.containsPoint(point) && !hasInteractionType) {
76963
76650
  this.interactionParam = {
76964
- type: PictureInteractionType.MOVE,
76651
+ type: ImageInteractionType.MOVE,
76965
76652
  box: cutBox.clone(),
76966
76653
  startMouse: inputPosition
76967
76654
  };
@@ -76973,8 +76660,8 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76973
76660
  super(...args), _define_property(this, "result", {
76974
76661
  normalizeCutBox: new Box2(new Vector2(), new Vector2(1, 1))
76975
76662
  }), /** 仅供当前渲染帧使用的投影快照;输入不读取它。 */ _define_property(this, "_renderProjection", void 0), _define_property(this, "interactionParam", {
76976
- type: PictureInteractionType.NONE
76977
- }), _define_property(this, "type", GizmoType.PICTURE_CUT), /** 光标结果。 */ _define_property(this, "cursorResult", {
76663
+ type: ImageInteractionType.NONE
76664
+ }), _define_property(this, "type", GizmoType.IMAGE_CUT), /** 光标结果。 */ _define_property(this, "cursorResult", {
76978
76665
  type: GestureCursorType.NORMAL,
76979
76666
  angle: 0
76980
76667
  }), _define_property(this, "_isLockScale", false), _define_property(this, "_mouseGrabbed", false);
@@ -76983,13 +76670,13 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
76983
76670
  /** 图片裁切工具。 */ class ImageCutGizmoTool extends GizmoTool {
76984
76671
  /** @returns 图片裁切 Gizmo 图。 */ createGizmos() {
76985
76672
  return [
76986
- new PictureCutGizmo(this.owner)
76673
+ new ImageCutGizmo(this.owner)
76987
76674
  ];
76988
76675
  }
76989
76676
  }
76990
- /** 在图片范围外移动或缩放归一化扩边框。 */ class PictureExpandGizmo extends Gizmo {
76677
+ /** 在图片范围外移动或缩放归一化扩边框。 */ class ImageExpandGizmo extends Gizmo {
76991
76678
  /** 当前图片扩边配置。 */ get config() {
76992
- return this._owner.getConfigManager().get(pictureExpandConfig);
76679
+ return this._owner.getConfigManager().get(imageExpandConfig);
76993
76680
  }
76994
76681
  /** 当前选中的元素(直读 selection.getSelectedPlayerItems:id→VFXItem 已解析)。 */ get selectedItems() {
76995
76682
  return this._owner.getSelection().getSelectedPlayerItems();
@@ -77025,7 +76712,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77025
76712
  const inputPosition = new Vector2(event.position.x, event.position.y);
77026
76713
  this.refreshInteractionType(hover, inputPosition);
77027
76714
  this._owner.setCursor(this.cursorResult);
77028
- if (event.buttonMask === MouseButtonMask.None && (this.interactionParam.type === PictureInteractionType.SCALE || this.interactionParam.type === PictureInteractionType.DIRECTION_SCALE)) {
76715
+ if (event.buttonMask === MouseButtonMask.None && (this.interactionParam.type === ImageInteractionType.SCALE || this.interactionParam.type === ImageInteractionType.DIRECTION_SCALE)) {
77029
76716
  event.accept();
77030
76717
  }
77031
76718
  }
@@ -77054,9 +76741,9 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77054
76741
  return;
77055
76742
  }
77056
76743
  this.interactionParam = {
77057
- type: PictureInteractionType.NONE
76744
+ type: ImageInteractionType.NONE
77058
76745
  };
77059
- this.refreshCursorResult(PictureInteractionType.NONE);
76746
+ this.refreshCursorResult(ImageInteractionType.NONE);
77060
76747
  this._owner.setCursor(this.cursorResult);
77061
76748
  }
77062
76749
  /** @returns 是否仅选中了一个可扩边元素。 */ _isApplicableSelection() {
@@ -77071,7 +76758,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77071
76758
  if (!this._mouseGrabbed) {
77072
76759
  return false;
77073
76760
  }
77074
- if (this.interactionParam.type === PictureInteractionType.NONE) {
76761
+ if (this.interactionParam.type === ImageInteractionType.NONE) {
77075
76762
  return true;
77076
76763
  }
77077
76764
  // 步骤 2:获取实时投影并计算指针位移。
@@ -77086,7 +76773,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77086
76773
  const { min: itemBoxMin, max: itemBoxMax } = itemBox;
77087
76774
  // 步骤 3:按移动、角点缩放或边缩放更新归一化扩边框。
77088
76775
  switch(this.interactionParam.type){
77089
- case PictureInteractionType.MOVE:
76776
+ case ImageInteractionType.MOVE:
77090
76777
  {
77091
76778
  const { min: originMin, max: originMax } = box;
77092
76779
  const xMinShift = itemBoxMax.x - originMax.x;
@@ -77099,7 +76786,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77099
76786
  this.result.normalizeExpandBox = getNormalizeBoxByBoxes(itemBox, resultBox);
77100
76787
  break;
77101
76788
  }
77102
- case PictureInteractionType.SCALE:
76789
+ case ImageInteractionType.SCALE:
77103
76790
  {
77104
76791
  const { index } = this.interactionParam;
77105
76792
  const { min, max } = box;
@@ -77158,7 +76845,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77158
76845
  ]));
77159
76846
  break;
77160
76847
  }
77161
- case PictureInteractionType.DIRECTION_SCALE:
76848
+ case ImageInteractionType.DIRECTION_SCALE:
77162
76849
  {
77163
76850
  var _this_interactionParam1;
77164
76851
  const { index } = this.interactionParam;
@@ -77276,7 +76963,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77276
76963
  const inputPosition = new Vector2(event.position.x, event.position.y);
77277
76964
  this.refreshInteractionType(mouse, inputPosition);
77278
76965
  this._owner.setCursor(this.cursorResult);
77279
- if (this.interactionParam.type === PictureInteractionType.NONE) {
76966
+ if (this.interactionParam.type === ImageInteractionType.NONE) {
77280
76967
  return;
77281
76968
  }
77282
76969
  this._mouseGrabbed = true;
@@ -77363,7 +77050,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77363
77050
  * @param angle 指针方向角度。
77364
77051
  */ refreshCursorResult(activeType, angle = 0) {
77365
77052
  switch(activeType){
77366
- case PictureInteractionType.SCALE:
77053
+ case ImageInteractionType.SCALE:
77367
77054
  {
77368
77055
  this.cursorResult = {
77369
77056
  type: GestureCursorType.SCALE,
@@ -77371,7 +77058,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77371
77058
  };
77372
77059
  break;
77373
77060
  }
77374
- case PictureInteractionType.DIRECTION_SCALE:
77061
+ case ImageInteractionType.DIRECTION_SCALE:
77375
77062
  {
77376
77063
  this.cursorResult = {
77377
77064
  type: GestureCursorType.SCALE,
@@ -77379,8 +77066,8 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77379
77066
  };
77380
77067
  break;
77381
77068
  }
77382
- case PictureInteractionType.MOVE:
77383
- case PictureInteractionType.NONE:
77069
+ case ImageInteractionType.MOVE:
77070
+ case ImageInteractionType.NONE:
77384
77071
  {
77385
77072
  this.cursorResult = {
77386
77073
  type: GestureCursorType.NORMAL,
@@ -77429,7 +77116,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77429
77116
  * @param inputPosition Control 本地坐标鼠标位置
77430
77117
  */ refreshInteractionType(point, inputPosition) {
77431
77118
  // 步骤 1:重置交互状态并获取实时扩边投影。
77432
- this.interactionParam.type = PictureInteractionType.NONE;
77119
+ this.interactionParam.type = ImageInteractionType.NONE;
77433
77120
  const projection = this._projectSelection();
77434
77121
  if (!projection) {
77435
77122
  return;
@@ -77445,7 +77132,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77445
77132
  if (corner.distance(point) < 20) {
77446
77133
  hasInteractionType = true;
77447
77134
  this.interactionParam = {
77448
- type: PictureInteractionType.SCALE,
77135
+ type: ImageInteractionType.SCALE,
77449
77136
  index,
77450
77137
  box: expandBox.clone(),
77451
77138
  startMouse: inputPosition
@@ -77469,7 +77156,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77469
77156
  if (state) {
77470
77157
  hasInteractionType = true;
77471
77158
  this.interactionParam = {
77472
- type: PictureInteractionType.DIRECTION_SCALE,
77159
+ type: ImageInteractionType.DIRECTION_SCALE,
77473
77160
  index,
77474
77161
  box: expandBox.clone(),
77475
77162
  startMouse: inputPosition
@@ -77480,7 +77167,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77480
77167
  // 步骤 4:最后检测扩边区域整体移动并刷新光标。
77481
77168
  if (expandBox.containsPoint(point) && !hasInteractionType) {
77482
77169
  this.interactionParam = {
77483
- type: PictureInteractionType.MOVE,
77170
+ type: ImageInteractionType.MOVE,
77484
77171
  box: expandBox.clone(),
77485
77172
  startMouse: inputPosition
77486
77173
  };
@@ -77492,8 +77179,8 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77492
77179
  super(...args), _define_property(this, "result", {
77493
77180
  normalizeExpandBox: new Box2(new Vector2(), new Vector2(1, 1))
77494
77181
  }), _define_property(this, "_renderProjection", void 0), _define_property(this, "interactionParam", {
77495
- type: PictureInteractionType.NONE
77496
- }), _define_property(this, "type", GizmoType.PICTURE_EXPAND), /** 光标结果。 */ _define_property(this, "cursorResult", {
77182
+ type: ImageInteractionType.NONE
77183
+ }), _define_property(this, "type", GizmoType.IMAGE_EXPAND), /** 光标结果。 */ _define_property(this, "cursorResult", {
77497
77184
  type: GestureCursorType.NORMAL,
77498
77185
  angle: 0
77499
77186
  }), _define_property(this, "_isLockScale", false), _define_property(this, "_mouseGrabbed", false);
@@ -77502,7 +77189,7 @@ _define_property(SelectTextGizmo, "HOVER_GAP_LENGTH", 4);
77502
77189
  /** 图片扩边工具。 */ class ImageExpandGizmoTool extends GizmoTool {
77503
77190
  /** @returns 图片扩边 Gizmo 图。 */ createGizmos() {
77504
77191
  return [
77505
- new PictureExpandGizmo(this.owner)
77192
+ new ImageExpandGizmo(this.owner)
77506
77193
  ];
77507
77194
  }
77508
77195
  }
@@ -77746,7 +77433,7 @@ const EDIT_TEXT_FONT_FAMILY = 'sans-serif';
77746
77433
  }), /** onMouseMove 命中缓存(坐标未变则跳过 _computePreSelected,避免每帧全 result 命中扫描)。 */ _define_property(this, "_cachedCoords", null);
77747
77434
  }
77748
77435
  }
77749
- /** 图片精准文字编辑工具。 */ class ImageTextEditGizmoTool extends GizmoTool {
77436
+ /** Sprite 精准文字编辑工具。 */ class SpriteTextEditGizmoTool extends GizmoTool {
77750
77437
  /** @returns 精准文字编辑 Gizmo 图。 */ createGizmos() {
77751
77438
  return [
77752
77439
  new SpriteTextEditGizmo(this.owner)
@@ -78317,8 +78004,8 @@ const EDIT_TEXT_FONT_FAMILY = 'sans-serif';
78317
78004
  void this._refreshMaskImage(this.config.maskImage);
78318
78005
  }
78319
78006
  }
78320
- /** 图片蒙版编辑工具。 */ class ImageMaskGizmoTool extends GizmoTool {
78321
- /** @returns 图片蒙版 Gizmo 图。 */ createGizmos() {
78007
+ /** 蒙版编辑工具。 */ class MaskGizmoTool extends GizmoTool {
78008
+ /** @returns 蒙版 Gizmo 图。 */ createGizmos() {
78322
78009
  return [
78323
78010
  new MaskGizmo(this.owner)
78324
78011
  ];
@@ -80464,6 +80151,348 @@ const cursorMap = {
80464
80151
  console.error(e);
80465
80152
  });
80466
80153
  }
80154
+ const LOADING_TIP_FONT_SIZE = 16;
80155
+ const LOADING_TIP_FONT_FAMILY = 'sans-serif';
80156
+ const LOADING_TIP_COLOR = 0x000000;
80157
+ /** 为目标元素叠加加载动画与提示文案。 */ class LoadingGizmo extends Gizmo {
80158
+ /** 当前加载覆盖层配置。 */ get config() {
80159
+ return this._owner.getConfigManager().get(loadingConfig);
80160
+ }
80161
+ /** 当前 Gizmo 实例持有的可销毁渲染投影。 */ get idMap() {
80162
+ return this._idMap;
80163
+ }
80164
+ /** 当前 loading 元素 id 列表(由稳定 manager 派生)。 */ get loadingIds() {
80165
+ return this._manager.ids;
80166
+ }
80167
+ /**
80168
+ * 在片元着色器变化时重建加载动画对象。
80169
+ * @param change 加载配置变更。
80170
+ */ _onConfigChange(change) {
80171
+ if (change.previous.loadingFragment === change.current.loadingFragment) {
80172
+ return;
80173
+ }
80174
+ for (const [id, loadingItem] of this._idMap){
80175
+ this._disposeLoadingVFXItem(loadingItem.loadingVFXItem);
80176
+ loadingItem.loadingVFXItem = this.createLoadingVFXItem();
80177
+ this.updateLoadingVFXItemTransform(id, loadingItem);
80178
+ }
80179
+ }
80180
+ /**
80181
+ * 订阅 loading 集合变化:add/delete 后回调当前全量 id 列表。
80182
+ * @param cb 收到当前全量 loading id 列表的回调
80183
+ * @returns 退订函数,调用后取消对应监听
80184
+ */ onLoadingChange(cb) {
80185
+ return this._manager.on('change', (change)=>{
80186
+ if (change.addedIds.length > 0 || change.removedIds.length > 0) {
80187
+ cb(change.ids);
80188
+ }
80189
+ });
80190
+ }
80191
+ /**
80192
+ * 为指定元素添加 loading 覆盖层;同一元素不可重复添加。
80193
+ * @param id 目标元素 ID
80194
+ * @param options loading 配置(文案、位置、自定义区域、是否清空选中)
80195
+ */ add(id, options) {
80196
+ this._manager.add(id, options);
80197
+ }
80198
+ /**
80199
+ * 移除指定元素的 loading 覆盖层并释放对应 VFXItem。
80200
+ * @param id 目标元素 ID
80201
+ */ delete(id) {
80202
+ this._manager.delete(id);
80203
+ }
80204
+ /**
80205
+ * 更新指定 loading 元素的提示文案。
80206
+ * @param id 目标元素 ID
80207
+ * @param options 待合并更新的 LoadingTip 属性
80208
+ */ updateItem(id, options) {
80209
+ this._manager.update(id, options);
80210
+ }
80211
+ /** 根据目标元素的实时包围盒刷新加载动画。 */ onPreRender() {
80212
+ if (this._idMap.size === 0) {
80213
+ return;
80214
+ }
80215
+ for (const [id, loadingItem] of this._idMap){
80216
+ this.updateLoadingVFXItemTransform(id, loadingItem);
80217
+ }
80218
+ }
80219
+ /**
80220
+ * 绘制所有加载提示文案。
80221
+ * @param control 绘制控制器。
80222
+ */ draw(control) {
80223
+ if (this._idMap.size === 0) {
80224
+ return;
80225
+ }
80226
+ for (const [id, loadingItem] of this._idMap){
80227
+ if (!loadingItem.tip?.text) {
80228
+ continue;
80229
+ }
80230
+ const item = getPlayerItemById(this._owner.getEngine().compositions[0], id);
80231
+ const itemBox = item ? getItemViewBox(item, GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement)) : new Box2();
80232
+ const loadingBox = getBoxByNormalizeBox(itemBox, loadingItem.loadingBox);
80233
+ this.drawLoadingTip(control, loadingBox, itemBox, loadingItem.tip);
80234
+ }
80235
+ }
80236
+ /**
80237
+ * 绘制单条加载提示,并将文本限制在加载区域内。
80238
+ * @param control 绘制控制器。
80239
+ * @param loadingBox 加载区域。
80240
+ * @param itemBox 目标元素包围盒。
80241
+ * @param tip 加载提示配置。
80242
+ */ drawLoadingTip(control, loadingBox, itemBox, tip) {
80243
+ // 步骤 1:计算提示位置与可用宽度。
80244
+ const { x: width, y: height } = loadingBox.getSize(this._loadingBoxSize);
80245
+ if (!tip.text || !Number.isFinite(width) || width <= 0) {
80246
+ return;
80247
+ }
80248
+ const positionedLeft = tip.position ? itemBox.min.x + (Number.isFinite(tip.position.x) ? tip.position.x : 0) : loadingBox.min.x;
80249
+ const left = tip.position ? Math.min(Math.max(positionedLeft, loadingBox.min.x), loadingBox.max.x) : loadingBox.min.x;
80250
+ const availableWidth = tip.position ? loadingBox.max.x - left : width;
80251
+ if (availableWidth <= 0) {
80252
+ return;
80253
+ }
80254
+ /**
80255
+ * 测量加载提示的粗体字形。
80256
+ * @param text 提示文本。
80257
+ * @param fontSize 字号。
80258
+ * @returns 文本字形尺寸。
80259
+ */ const measureTipText = (text, fontSize)=>{
80260
+ const metrics = measureTextMetrics(text, fontSize, LOADING_TIP_FONT_FAMILY, 'bold');
80261
+ return {
80262
+ width: metrics.width || Array.from(text).length * fontSize,
80263
+ actualBoundingBoxAscent: metrics.actualBoundingBoxAscent || fontSize * 0.8,
80264
+ actualBoundingBoxDescent: metrics.actualBoundingBoxDescent || fontSize * 0.2
80265
+ };
80266
+ };
80267
+ // 步骤 2:测量文本并缩小字号以适应可用宽度。
80268
+ const measuredAtBaseSize = measureTipText(tip.text, LOADING_TIP_FONT_SIZE);
80269
+ let fontSize = measuredAtBaseSize.width > availableWidth ? LOADING_TIP_FONT_SIZE * availableWidth / measuredAtBaseSize.width : LOADING_TIP_FONT_SIZE;
80270
+ let textMetrics = measureTipText(tip.text, fontSize);
80271
+ if (textMetrics.width > availableWidth) {
80272
+ fontSize *= availableWidth / textMetrics.width;
80273
+ textMetrics = measureTipText(tip.text, fontSize);
80274
+ }
80275
+ // 步骤 3:按字形基线计算最终绘制位置。
80276
+ const textLeft = tip.position ? left : loadingBox.min.x + (width - textMetrics.width) / 2;
80277
+ const inkHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
80278
+ const top = tip.position ? itemBox.min.y + (Number.isFinite(tip.position.y) ? tip.position.y : 0) : loadingBox.min.y + (height - inkHeight) / 2;
80279
+ const probeMetrics = measureTipText(METRICS_STRING + BASELINE_SYMBOL, fontSize);
80280
+ const inkTopFromCellTop = CONTROL_TEXT_GLYPH_PADDING + probeMetrics.actualBoundingBoxAscent - textMetrics.actualBoundingBoxAscent;
80281
+ control.drawText(textLeft, top - inkTopFromCellTop, tip.text, fontSize, toColor(LOADING_TIP_COLOR, 1), LOADING_TIP_FONT_FAMILY, 'bold');
80282
+ }
80283
+ /** 释放当前实例的渲染投影;稳定 LoadingManager 不随 Gizmo 重建清空。 */ dispose() {
80284
+ this._configOff?.();
80285
+ this._configOff = undefined;
80286
+ this._managerOff?.();
80287
+ this._managerOff = undefined;
80288
+ this._idMap.forEach((loadingItem)=>{
80289
+ this._disposeLoadingVFXItem(loadingItem.loadingVFXItem);
80290
+ });
80291
+ this._idMap.clear();
80292
+ super.dispose();
80293
+ }
80294
+ /** 将可销毁的渲染投影同步到当前加载状态。 */ _syncManager() {
80295
+ const activeIds = new Set(this._manager.ids);
80296
+ for (const [id, loadingItem] of this._idMap){
80297
+ if (!activeIds.has(id)) {
80298
+ this._disposeLoadingVFXItem(loadingItem.loadingVFXItem);
80299
+ this._idMap.delete(id);
80300
+ }
80301
+ }
80302
+ for (const id of activeIds){
80303
+ const state = this._manager.get(id);
80304
+ if (!state) {
80305
+ continue;
80306
+ }
80307
+ const existing = this._idMap.get(id);
80308
+ if (existing) {
80309
+ existing.tip = {
80310
+ ...state.tip,
80311
+ position: state.tip.position ? state.tip.position.clone() : undefined
80312
+ };
80313
+ } else {
80314
+ this._createProjection(id, state);
80315
+ }
80316
+ }
80317
+ }
80318
+ /**
80319
+ * 为加载状态创建渲染投影。
80320
+ * @param id 目标元素 ID。
80321
+ * @param state 加载状态。
80322
+ */ _createProjection(id, state) {
80323
+ const item = getPlayerItemById(this._owner.getEngine().compositions[0], id);
80324
+ const itemViewBox = item ? getItemViewBox(item, GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement)) : new Box2();
80325
+ const currentBox = state.loadingBox ? this.getViewBoxByBox(state.loadingBox) : itemViewBox;
80326
+ this._idMap.set(id, {
80327
+ loadingBox: getNormalizeBoxByBoxes(itemViewBox, currentBox),
80328
+ loadingVFXItem: this.createLoadingVFXItem(),
80329
+ tip: {
80330
+ ...state.tip,
80331
+ position: state.tip.position ? state.tip.position.clone() : undefined
80332
+ }
80333
+ });
80334
+ this.updateLoadingVFXItemTransform(id, this._idMap.get(id));
80335
+ }
80336
+ /** @returns 当前相机信息。 */ getCameraInfo() {
80337
+ return GizmoViewportUtils.getCameraInfo(this._owner.getEngine());
80338
+ }
80339
+ /** @returns 当前视口的缩放、平移与尺寸。 */ getViewportParams() {
80340
+ const composition = this._owner.getEngine().compositions[0];
80341
+ const camera = composition?.camera;
80342
+ if (camera) {
80343
+ const viewportMatrix = camera.getViewportMatrix();
80344
+ const scale = viewportMatrix.elements[0];
80345
+ const translation = new Vector2(viewportMatrix.elements[12], viewportMatrix.elements[13]);
80346
+ const width = GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).x;
80347
+ const height = GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).y;
80348
+ return {
80349
+ scale,
80350
+ translation,
80351
+ width,
80352
+ height
80353
+ };
80354
+ }
80355
+ return {
80356
+ scale: 1,
80357
+ translation: new Vector2(),
80358
+ width: GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).x,
80359
+ height: GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement).y
80360
+ };
80361
+ }
80362
+ /**
80363
+ * 将视图坐标下的包围盒按当前视口缩放与平移变换为实际渲染包围盒。
80364
+ * @param box 视图坐标下的包围盒
80365
+ * @returns 经视口缩放与平移后的实际渲染包围盒
80366
+ */ getViewBoxByBox(box) {
80367
+ const { scale, translation, width, height } = this.getViewportParams();
80368
+ const center = new Vector2(width / 2, height / 2);
80369
+ const result = scaleBox(box.clone(), scale, center).translate(translation);
80370
+ return result;
80371
+ }
80372
+ /**
80373
+ * 创建承载加载动画的 VFXItem。
80374
+ * @returns 已绑定几何与材质的加载动画对象。
80375
+ */ createLoadingVFXItem() {
80376
+ // 步骤 1:创建加载动画对象与平面几何。
80377
+ const composition = this._owner.getEngine().compositions[0];
80378
+ const loadingVFXItem = VFXItemFactory.createVFXItem(composition, null, 'LoadingVFXItem', EffectComponent);
80379
+ this._owner.getSelection().addIgnoreIds([
80380
+ loadingVFXItem.getInstanceId()
80381
+ ]);
80382
+ loadingVFXItem.type = index$1.ItemType.effect;
80383
+ const effects = loadingVFXItem.getComponent(EffectComponent);
80384
+ const engine = this._owner.getEngine();
80385
+ const geometry = Geometry.create(engine, {
80386
+ attributes: {
80387
+ aPos: {
80388
+ size: 3,
80389
+ data: new Float32Array([
80390
+ -0.5,
80391
+ -0.5,
80392
+ 0,
80393
+ 0.5,
80394
+ -0.5,
80395
+ 0,
80396
+ 0.5,
80397
+ 0.5,
80398
+ 0,
80399
+ -0.5,
80400
+ 0.5,
80401
+ 0
80402
+ ])
80403
+ },
80404
+ aUV: {
80405
+ size: 2,
80406
+ data: new Float32Array([
80407
+ 0,
80408
+ 0,
80409
+ 1,
80410
+ 0,
80411
+ 1,
80412
+ 1,
80413
+ 0,
80414
+ 1
80415
+ ])
80416
+ }
80417
+ },
80418
+ indices: {
80419
+ data: new Uint16Array([
80420
+ 0,
80421
+ 1,
80422
+ 2,
80423
+ 0,
80424
+ 2,
80425
+ 3
80426
+ ])
80427
+ },
80428
+ mode: glContext.TRIANGLES,
80429
+ drawCount: 6
80430
+ });
80431
+ // 步骤 2:使用当前配置创建动画材质。
80432
+ const material = Material.create(engine, {
80433
+ shader: {
80434
+ vertex: `
80435
+ precision highp float;
80436
+ attribute vec3 aPos;
80437
+ attribute vec2 aUV;
80438
+ uniform mat4 effects_MatrixVP;
80439
+ uniform mat4 effects_ObjectToWorld;
80440
+ varying vec2 vUV;
80441
+ void main() {
80442
+ vUV = aUV;
80443
+ gl_Position = effects_MatrixVP * effects_ObjectToWorld * vec4(aPos, 1.0);
80444
+ }`,
80445
+ fragment: this.config.loadingFragment
80446
+ }
80447
+ });
80448
+ // 步骤 3:绑定运行时几何与材质。
80449
+ // @ts-expect-error Effects 类型未暴露可写 geometry。
80450
+ effects.geometry = geometry;
80451
+ effects.material = material;
80452
+ return loadingVFXItem;
80453
+ }
80454
+ /** 释放 loading 渲染对象,并同步恢复 Selection 的命中过滤。 */ _disposeLoadingVFXItem(item) {
80455
+ this._owner.getSelection().deleteIgnoreIds([
80456
+ item.getInstanceId()
80457
+ ]);
80458
+ item.dispose();
80459
+ }
80460
+ /**
80461
+ * 按目标元素的实时包围盒更新加载动画变换。
80462
+ * @param id 目标元素 ID。
80463
+ * @param loadingItem 对应的加载渲染投影。
80464
+ */ updateLoadingVFXItemTransform(id, loadingItem) {
80465
+ const item = getPlayerItemById(this._owner.getEngine().compositions[0], id);
80466
+ const containerSize = GizmoViewportUtils.getContainerSize(this._owner.getEngine().canvas.parentElement);
80467
+ const itemViewBox = item ? getItemViewBox(item, containerSize) : new Box2();
80468
+ const currentBox = getBoxByNormalizeBox(itemViewBox, loadingItem.loadingBox);
80469
+ const viewPosition = new Vector2((currentBox.max.x + currentBox.min.x) / 2, (currentBox.max.y + currentBox.min.y) / 2);
80470
+ const cameraInfo = this.getCameraInfo();
80471
+ const interactionPlane = new Plane(0, new Vector3(0, 0, 1));
80472
+ const worldPosition = viewPositionToWorld(viewPosition, cameraInfo, interactionPlane, containerSize);
80473
+ const viewSize = new Vector2(currentBox.max.x - currentBox.min.x, currentBox.max.y - currentBox.min.y);
80474
+ const worldSize = viewSizeToWorld(viewSize, containerSize, loadingItem.loadingVFXItem, cameraInfo, interactionPlane);
80475
+ if (worldPosition) {
80476
+ loadingItem.loadingVFXItem.transform.setPosition(worldPosition.x, worldPosition.y, 0);
80477
+ }
80478
+ loadingItem.loadingVFXItem.transform.setScale(worldSize.x, worldSize.y, 1);
80479
+ }
80480
+ /**
80481
+ * 创建加载覆盖层。
80482
+ * @param owner Gizmo 宿主。
80483
+ * @param manager 加载状态管理器。
80484
+ */ constructor(owner, manager){
80485
+ super(owner), _define_property(this, "type", 'loading'), _define_property(this, "_configOff", void 0), _define_property(this, "_managerOff", void 0), _define_property(this, "_manager", void 0), _define_property(this, "_loadingBoxSize", new Vector2()), _define_property(this, "_idMap", new Map());
80486
+ this._manager = manager;
80487
+ this._configOff = owner.getConfigManager().onChange(loadingConfig, (change)=>{
80488
+ this._onConfigChange(change);
80489
+ });
80490
+ this._managerOff = this._manager.on('change', ()=>{
80491
+ this._syncManager();
80492
+ });
80493
+ this._syncManager();
80494
+ }
80495
+ }
80467
80496
  /** 通过中键或抓手模式拖拽平移视口。 */ class HandGizmo extends Gizmo {
80468
80497
  /** @returns 是否正在平移或等待空格拖拽。 */ isPanning() {
80469
80498
  return this._dragType === "pan" || this._panKeyPressed;
@@ -83964,7 +83993,6 @@ const identityFrameItemIdAdapter = {
83964
83993
  },
83965
83994
  sceneBindings: []
83966
83995
  };
83967
- const [width, height] = size;
83968
83996
  const composition = {
83969
83997
  id: resolvedCompositionId,
83970
83998
  name: name + '_inner',
@@ -83988,9 +84016,7 @@ const identityFrameItemIdAdapter = {
83988
84016
  0,
83989
84017
  0,
83990
84018
  0
83991
- ],
83992
- // @ts-expect-error: aspect 属性在 CameraOptions 上有定义但类型声明不完善
83993
- aspect: width / height
84019
+ ]
83994
84020
  },
83995
84021
  components: [
83996
84022
  {
@@ -86944,8 +86970,9 @@ const identityFrameItemIdAdapter = {
86944
86970
  if (width === undefined || height === undefined) {
86945
86971
  return box;
86946
86972
  }
86947
- const sx = scale?.[0] ?? 1;
86948
- const sy = scale?.[1] ?? 1;
86973
+ // 文本元素的scale不作为计算包围盒尺寸运算
86974
+ const sx = schema.type === ArtisItemType.TEXT ? 1 : scale?.[0] ?? 1;
86975
+ const sy = schema.type === ArtisItemType.TEXT ? 1 : scale?.[1] ?? 1;
86949
86976
  const rot = Array.isArray(rotation) ? rotation[2] : rotation ?? 0;
86950
86977
  rotateBox(box.setFromCenterAndSize(new Vector2(...position ?? [
86951
86978
  0,
@@ -88434,6 +88461,7 @@ const identityFrameItemIdAdapter = {
88434
88461
  ...this._newTransform,
88435
88462
  ...other._newTransform
88436
88463
  };
88464
+ this.eventPayload.propertyKeys = Object.keys(this._newTransform);
88437
88465
  return true;
88438
88466
  }
88439
88467
  /**
@@ -88442,6 +88470,7 @@ const identityFrameItemIdAdapter = {
88442
88470
  */ constructor(args){
88443
88471
  _define_property$1(this, "id", void 0);
88444
88472
  _define_property$1(this, "commandId", 'item.transform');
88473
+ _define_property$1(this, "eventPayload", void 0);
88445
88474
  _define_property$1(this, "_itemId", void 0);
88446
88475
  _define_property$1(this, "_newTransform", void 0);
88447
88476
  _define_property$1(this, "_oldTransform", void 0);
@@ -88452,6 +88481,10 @@ const identityFrameItemIdAdapter = {
88452
88481
  this._newTransform = args.newTransform;
88453
88482
  this._itemService = args.itemService;
88454
88483
  this._store = args.store;
88484
+ this.eventPayload = {
88485
+ itemId: args.itemId,
88486
+ propertyKeys: Object.keys(args.newTransform)
88487
+ };
88455
88488
  }
88456
88489
  }
88457
88490
 
@@ -89346,6 +89379,7 @@ const DEFAULT_GROUP_POSITION = [
89346
89379
  */ constructor(args){
89347
89380
  _define_property$1(this, "id", void 0);
89348
89381
  _define_property$1(this, "commandId", 'item.updatePropertyBySchema');
89382
+ _define_property$1(this, "eventPayload", void 0);
89349
89383
  _define_property$1(this, "completion", Promise.resolve());
89350
89384
  _define_property$1(this, "_schemas", void 0);
89351
89385
  _define_property$1(this, "_itemService", void 0);
@@ -89355,6 +89389,14 @@ const DEFAULT_GROUP_POSITION = [
89355
89389
  args.schema
89356
89390
  ];
89357
89391
  this._itemService = args.itemService;
89392
+ this.eventPayload = {
89393
+ propertyChanges: this._schemas.flatMap((schema)=>schema.id ? [
89394
+ {
89395
+ itemId: schema.id,
89396
+ propertyKeys: Object.keys(schema.property)
89397
+ }
89398
+ ] : [])
89399
+ };
89358
89400
  }
89359
89401
  }
89360
89402
 
@@ -89401,6 +89443,7 @@ const DEFAULT_GROUP_POSITION = [
89401
89443
  */ constructor(args){
89402
89444
  _define_property$1(this, "id", void 0);
89403
89445
  _define_property$1(this, "commandId", 'item.setFontFamily');
89446
+ _define_property$1(this, "eventPayload", void 0);
89404
89447
  _define_property$1(this, "completion", Promise.resolve());
89405
89448
  _define_property$1(this, "_itemId", void 0);
89406
89449
  _define_property$1(this, "_fontFamily", void 0);
@@ -89414,6 +89457,13 @@ const DEFAULT_GROUP_POSITION = [
89414
89457
  this._fontFamily = args.fontFamily;
89415
89458
  this._fontUrl = args.fontUrl;
89416
89459
  this._itemService = args.itemService;
89460
+ this.eventPayload = {
89461
+ itemId: args.itemId,
89462
+ propertyKeys: [
89463
+ 'fontFamily',
89464
+ 'fontUrl'
89465
+ ]
89466
+ };
89417
89467
  }
89418
89468
  }
89419
89469
 
@@ -90767,7 +90817,7 @@ const DEFAULT_GROUP_POSITION = [
90767
90817
  }
90768
90818
  }
90769
90819
 
90770
- /** 提交图片裁切区域变化的可撤销命令。 */ class PictureCutCommitCommand {
90820
+ /** 提交图片裁切区域变化的可撤销命令。 */ class ImageCutCommitCommand {
90771
90821
  /** 应用新的裁切包围盒 */ execute() {
90772
90822
  this._applyBox(this._newBox);
90773
90823
  }
@@ -90782,18 +90832,18 @@ const DEFAULT_GROUP_POSITION = [
90782
90832
  * @param args 命令参数(含新旧裁切包围盒与 applyBox 回调)
90783
90833
  */ constructor(args){
90784
90834
  _define_property$1(this, "id", void 0);
90785
- _define_property$1(this, "commandId", 'pictureCut.commit');
90835
+ _define_property$1(this, "commandId", 'imageCut.commit');
90786
90836
  _define_property$1(this, "_newBox", void 0);
90787
90837
  _define_property$1(this, "_oldBox", void 0);
90788
90838
  _define_property$1(this, "_applyBox", void 0);
90789
- this.id = `pictureCut.commit_${Date.now()}`;
90839
+ this.id = `imageCut.commit_${Date.now()}`;
90790
90840
  this._newBox = args.newBox;
90791
90841
  this._oldBox = args.oldBox;
90792
90842
  this._applyBox = args.applyBox;
90793
90843
  }
90794
90844
  }
90795
90845
 
90796
- /** 提交图片扩边区域变化的可撤销命令。 */ class PictureExpandCommitCommand {
90846
+ /** 提交图片扩边区域变化的可撤销命令。 */ class ImageExpandCommitCommand {
90797
90847
  /** 应用新的扩图包围盒 */ execute() {
90798
90848
  this._applyBox(this._newBox);
90799
90849
  }
@@ -90808,11 +90858,11 @@ const DEFAULT_GROUP_POSITION = [
90808
90858
  * @param args 命令参数(含新旧扩图包围盒与 applyBox 回调)
90809
90859
  */ constructor(args){
90810
90860
  _define_property$1(this, "id", void 0);
90811
- _define_property$1(this, "commandId", 'pictureExpand.commit');
90861
+ _define_property$1(this, "commandId", 'imageExpand.commit');
90812
90862
  _define_property$1(this, "_newBox", void 0);
90813
90863
  _define_property$1(this, "_oldBox", void 0);
90814
90864
  _define_property$1(this, "_applyBox", void 0);
90815
- this.id = `pictureExpand.commit_${Date.now()}`;
90865
+ this.id = `imageExpand.commit_${Date.now()}`;
90816
90866
  this._newBox = args.newBox;
90817
90867
  this._oldBox = args.oldBox;
90818
90868
  this._applyBox = args.applyBox;
@@ -91040,13 +91090,29 @@ const DEFAULT_GROUP_POSITION = [
91040
91090
 
91041
91091
  /** 按元素类型静默替换在线资源且不写入历史的命令。 */ class ItemReplaceSourceCommand {
91042
91092
  /** 替换为新资源(async,execute 契约为 void,故 fire-and-forget) */ execute() {
91043
- void this._itemService.replaceSourceSilent(this._itemId, this._newUrl);
91093
+ this.completion = this._replaceSource(this._newUrl);
91044
91094
  }
91045
91095
  /** 回退为旧资源 */ undo() {
91046
- void this._itemService.replaceSourceSilent(this._itemId, this._oldUrl);
91096
+ this.completion = this._replaceSource(this._oldUrl);
91047
91097
  }
91048
91098
  /** 重新应用新资源 */ redo() {
91049
- void this._itemService.replaceSourceSilent(this._itemId, this._newUrl);
91099
+ this.completion = this._replaceSource(this._newUrl);
91100
+ }
91101
+ /** 仅在资源实际替换成功时保留属性变化载荷。 */ async _replaceSource(url) {
91102
+ if (this._propertyKey) {
91103
+ this.eventPayload = {
91104
+ itemId: this._itemId,
91105
+ propertyKeys: [
91106
+ this._propertyKey
91107
+ ]
91108
+ };
91109
+ }
91110
+ const changed = await this._itemService.replaceSourceSilent(this._itemId, url);
91111
+ if (!changed) {
91112
+ this.eventPayload = {
91113
+ propertyChanges: []
91114
+ };
91115
+ }
91050
91116
  }
91051
91117
  /** 将在线换源标记为不可重放,避免复现流程依赖外部资源时序。 */ serializeReplay() {
91052
91118
  return {
@@ -91065,9 +91131,12 @@ const DEFAULT_GROUP_POSITION = [
91065
91131
  _define_property$1(this, "commandId", 'item.replaceSource');
91066
91132
  _define_property$1(this, "isHistorical", false);
91067
91133
  _define_property$1(this, "params", void 0);
91134
+ _define_property$1(this, "eventPayload", void 0);
91135
+ _define_property$1(this, "completion", Promise.resolve());
91068
91136
  _define_property$1(this, "_itemId", void 0);
91069
91137
  _define_property$1(this, "_newUrl", void 0);
91070
91138
  _define_property$1(this, "_oldUrl", void 0);
91139
+ _define_property$1(this, "_propertyKey", void 0);
91071
91140
  _define_property$1(this, "_itemService", void 0);
91072
91141
  _define_property$1(this, "_store", void 0);
91073
91142
  this.id = `item.replaceSource_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
@@ -91082,12 +91151,15 @@ const DEFAULT_GROUP_POSITION = [
91082
91151
  switch(item.type){
91083
91152
  case ArtisItemType.SPRITE:
91084
91153
  this._oldUrl = typeof prop?.image === 'string' ? prop.image : '';
91154
+ this._propertyKey = 'image';
91085
91155
  break;
91086
91156
  case ArtisItemType.VIDEO:
91087
91157
  this._oldUrl = typeof prop?.video === 'string' ? prop.video : '';
91158
+ this._propertyKey = 'video';
91088
91159
  break;
91089
91160
  case ArtisItemType.EFFECTS:
91090
91161
  this._oldUrl = typeof prop?.effects === 'string' ? prop.effects : '';
91162
+ this._propertyKey = 'effects';
91091
91163
  break;
91092
91164
  default:
91093
91165
  this._oldUrl = '';
@@ -91095,6 +91167,14 @@ const DEFAULT_GROUP_POSITION = [
91095
91167
  } else {
91096
91168
  this._oldUrl = '';
91097
91169
  }
91170
+ if (this._propertyKey) {
91171
+ this.eventPayload = {
91172
+ itemId: this._itemId,
91173
+ propertyKeys: [
91174
+ this._propertyKey
91175
+ ]
91176
+ };
91177
+ }
91098
91178
  this.params = {
91099
91179
  itemId: this._itemId,
91100
91180
  newUrl: this._newUrl,
@@ -91989,7 +92069,7 @@ function hasTransformItemIds(event) {
91989
92069
  * @param context 提供当前 Selection、ID 翻译与像素框查询
91990
92070
  * @param normalizeBox Gizmo 维护的归一化编辑框
91991
92071
  * @returns 图片像素坐标系下的包围盒;输入无效时返回空盒
91992
- */ const getSelectedPicturePixelBox = (context, normalizeBox)=>{
92072
+ */ const getSelectedImagePixelBox = (context, normalizeBox)=>{
91993
92073
  const selectedId = context.selection.getSelectedIds()[0];
91994
92074
  if (!normalizeBox || selectedId === undefined) {
91995
92075
  return new Box2();
@@ -92001,14 +92081,14 @@ function hasTransformItemIds(event) {
92001
92081
  * 创建图片裁剪事件绑定,以每个 Gizmo 实例的旧框快照生成可撤销命令。
92002
92082
  * @param context Command 绑定所需的 Artis 服务上下文
92003
92083
  * @returns 图片裁剪生命周期事件处理器
92004
- */ const createPictureCutCommandBinding = (context)=>{
92084
+ */ const createImageCutCommandBinding = (context)=>{
92005
92085
  const oldBoxBySource = new WeakMap();
92006
92086
  /**
92007
92087
  * 读取当前裁剪框并转换为可序列化快照。
92008
- * @param pictureCutGizmo 要读取的裁剪 Gizmo
92088
+ * @param imageCutGizmo 要读取的裁剪 Gizmo
92009
92089
  * @returns 归一化裁剪框;Gizmo 尚无框时返回 undefined
92010
- */ const snapshot = (pictureCutGizmo)=>{
92011
- const box = pictureCutGizmo.getCutBox();
92090
+ */ const snapshot = (imageCutGizmo)=>{
92091
+ const box = imageCutGizmo.getCutBox();
92012
92092
  if (!box) {
92013
92093
  return undefined;
92014
92094
  }
@@ -92021,10 +92101,10 @@ function hasTransformItemIds(event) {
92021
92101
  };
92022
92102
  /**
92023
92103
  * 将归一化快照恢复为 Gizmo 使用的 Box2。
92024
- * @param pictureCutGizmo 要恢复的裁剪 Gizmo
92104
+ * @param imageCutGizmo 要恢复的裁剪 Gizmo
92025
92105
  * @param box 命令保存的归一化裁剪框
92026
- */ const apply = (pictureCutGizmo, box)=>{
92027
- pictureCutGizmo.setCutBox(new Box2(new Vector2(box.x, box.y), new Vector2(box.x + box.width, box.y + box.height)));
92106
+ */ const apply = (imageCutGizmo, box)=>{
92107
+ imageCutGizmo.setCutBox(new Box2(new Vector2(box.x, box.y), new Vector2(box.x + box.width, box.y + box.height)));
92028
92108
  };
92029
92109
  return {
92030
92110
  /**
@@ -92042,8 +92122,8 @@ function hasTransformItemIds(event) {
92042
92122
  * @param event 图片裁剪 actionupdate 事件
92043
92123
  */ actionupdate: (event)=>{
92044
92124
  const normalizeBox = event.source.getCutBox();
92045
- context.emit('picture-cut.change', {
92046
- box: getSelectedPicturePixelBox(context, normalizeBox)
92125
+ context.emit('image-cut.change', {
92126
+ box: getSelectedImagePixelBox(context, normalizeBox)
92047
92127
  });
92048
92128
  },
92049
92129
  /**
@@ -92057,7 +92137,7 @@ function hasTransformItemIds(event) {
92057
92137
  if (!oldBox || !newBox) {
92058
92138
  return;
92059
92139
  }
92060
- context.commandService.record(new PictureCutCommitCommand({
92140
+ context.commandService.record(new ImageCutCommitCommand({
92061
92141
  newBox,
92062
92142
  oldBox,
92063
92143
  /**
@@ -92074,14 +92154,14 @@ function hasTransformItemIds(event) {
92074
92154
  * 创建图片扩边事件绑定,以每个 Gizmo 实例的旧框快照生成可撤销命令。
92075
92155
  * @param context Command 绑定所需的 Artis 服务上下文
92076
92156
  * @returns 图片扩边生命周期事件处理器
92077
- */ const createPictureExpandCommandBinding = (context)=>{
92157
+ */ const createImageExpandCommandBinding = (context)=>{
92078
92158
  const oldBoxBySource = new WeakMap();
92079
92159
  /**
92080
92160
  * 读取当前扩边框并转换为可序列化快照。
92081
- * @param pictureExpandGizmo 要读取的扩边 Gizmo
92161
+ * @param imageExpandGizmo 要读取的扩边 Gizmo
92082
92162
  * @returns 归一化扩边框;Gizmo 尚无框时返回 undefined
92083
- */ const snapshot = (pictureExpandGizmo)=>{
92084
- const box = pictureExpandGizmo.getExpandBox();
92163
+ */ const snapshot = (imageExpandGizmo)=>{
92164
+ const box = imageExpandGizmo.getExpandBox();
92085
92165
  if (!box) {
92086
92166
  return undefined;
92087
92167
  }
@@ -92115,8 +92195,8 @@ function hasTransformItemIds(event) {
92115
92195
  * @param event 图片扩边 actionupdate 事件
92116
92196
  */ actionupdate: (event)=>{
92117
92197
  const normalizeBox = event.source.getExpandBox();
92118
- context.emit('picture-expand.change', {
92119
- box: getSelectedPicturePixelBox(context, normalizeBox)
92198
+ context.emit('image-expand.change', {
92199
+ box: getSelectedImagePixelBox(context, normalizeBox)
92120
92200
  });
92121
92201
  },
92122
92202
  /**
@@ -92130,7 +92210,7 @@ function hasTransformItemIds(event) {
92130
92210
  if (!oldBox || !newBox) {
92131
92211
  return;
92132
92212
  }
92133
- context.commandService.record(new PictureExpandCommitCommand({
92213
+ context.commandService.record(new ImageExpandCommitCommand({
92134
92214
  newBox,
92135
92215
  oldBox,
92136
92216
  /**
@@ -92191,8 +92271,8 @@ const builtinCommandBindingFactories = {
92191
92271
  [GizmoType.BOX_SELECTION]: registerSelectionGizmoCommandBinding,
92192
92272
  'item-create': createItemCreateCommandBinding,
92193
92273
  mask: createMaskCommandBinding,
92194
- 'picture-cut': createPictureCutCommandBinding,
92195
- 'picture-expand': createPictureExpandCommandBinding
92274
+ 'image-cut': createImageCutCommandBinding,
92275
+ 'image-expand': createImageExpandCommandBinding
92196
92276
  };
92197
92277
  /**
92198
92278
  * 将全部内置 Gizmo 类型与对应的 Command 绑定工厂注册到 Binder。
@@ -92401,8 +92481,8 @@ var BuiltinGizmoId = /*#__PURE__*/ function(BuiltinGizmoId) {
92401
92481
  BuiltinGizmoId["LOADING"] = "loading";
92402
92482
  BuiltinGizmoId["VIEWPORT_OVERLAY"] = "viewport-overlay";
92403
92483
  BuiltinGizmoId["READONLY_SELECTION"] = "readonly-selection";
92404
- BuiltinGizmoId["PICTURE_CUT"] = "picture-cut";
92405
- BuiltinGizmoId["PICTURE_EXPAND"] = "picture-expand";
92484
+ BuiltinGizmoId["IMAGE_CUT"] = "image-cut";
92485
+ BuiltinGizmoId["IMAGE_EXPAND"] = "image-expand";
92406
92486
  BuiltinGizmoId["SPRITE_TEXT_EDIT"] = "sprite-text-edit";
92407
92487
  BuiltinGizmoId["MASK"] = "mask";
92408
92488
  BuiltinGizmoId["ITEM_CREATE"] = "item-create";
@@ -101936,13 +102016,13 @@ var OfficialStateId = /*#__PURE__*/ function(OfficialStateId) {
101936
102016
  }
101937
102017
  /** 表示编辑图片文字内容的状态。 */ class ImageTextEditState extends BaseState {
101938
102018
  /** 创建隔离其他交互的图片文字编辑状态。 */ constructor(){
101939
- super(OfficialStateId.IMAGE_TEXT_EDIT, '精准改字', ImageTextEditGizmoTool);
102019
+ super(OfficialStateId.IMAGE_TEXT_EDIT, '精准改字', SpriteTextEditGizmoTool);
101940
102020
  this.isolated = true;
101941
102021
  }
101942
102022
  }
101943
102023
  /** 表示编辑图片蒙版的状态。 */ class ImageMaskState extends BaseState {
101944
102024
  /** 创建隔离其他交互的图片蒙版状态。 */ constructor(){
101945
- super(OfficialStateId.IMAGE_MASK, '图片蒙版', ImageMaskGizmoTool);
102025
+ super(OfficialStateId.IMAGE_MASK, '图片蒙版', MaskGizmoTool);
101946
102026
  this.isolated = true;
101947
102027
  }
101948
102028
  }
@@ -123704,6 +123784,12 @@ const CARD_HTML_EVENT_MESSAGE_SOURCE = 'vvfx-card-html-event';
123704
123784
  this.artis.leaveTextEditing();
123705
123785
  }
123706
123786
  /**
123787
+ * 选中当前 TextGizmo 正在编辑的全部文字,并聚焦文本输入框。
123788
+ * @returns TextGizmo 已就绪且存在正在编辑的文本时返回 true
123789
+ */ selectAllText() {
123790
+ return this.artis.gizmoManager.get('text')?.selectAllText() ?? false;
123791
+ }
123792
+ /**
123707
123793
  * 应用笔刷大小后开启蒙版 Gizmo;非法尺寸回退为 1。
123708
123794
  * @param brushSize 初始笔刷像素大小
123709
123795
  */ openMaskGizmo(brushSize) {
@@ -123751,69 +123837,69 @@ const CARD_HTML_EVENT_MESSAGE_SOURCE = 'vvfx-card-html-event';
123751
123837
  */ closeLoadingGizmo(id) {
123752
123838
  this.artis.gestureHandler.getLoadingManager().delete(this.artis.item.toVfxItemId(id));
123753
123839
  }
123754
- /** 开启图片裁切 Gizmo。 */ openPictureCutGizmo() {
123840
+ /** 开启图片裁切 Gizmo。 */ openImageCutGizmo() {
123755
123841
  this.artis.states.activate('imageCut');
123756
123842
  }
123757
123843
  /**
123758
123844
  * 设置图片裁切 Gizmo 是否锁定裁切框比例。
123759
123845
  * @param lock 是否锁定比例
123760
- */ setPictureCutGizmoLockScale(lock) {
123761
- const pictureCutGizmo = this.artis.gizmoManager.get('picture-cut');
123762
- if (!pictureCutGizmo) {
123763
- console.warn('[Artis.operations] setPictureCutGizmoLockScalepicture-cut Gizmo 未注册或未启用。');
123846
+ */ setImageCutGizmoLockScale(lock) {
123847
+ const imageCutGizmo = this.artis.gizmoManager.get('image-cut');
123848
+ if (!imageCutGizmo) {
123849
+ console.warn('[Artis.operations] setImageCutGizmoLockScaleimage-cut Gizmo 未注册或未启用。');
123764
123850
  return;
123765
123851
  }
123766
- pictureCutGizmo.isLockScale = lock;
123852
+ imageCutGizmo.isLockScale = lock;
123767
123853
  }
123768
123854
  /**
123769
- * 获取当前裁切信息(归一化裁切框 + 元素视图框),委托 PictureCutGizmo.getCutInfo。
123855
+ * 获取当前裁切信息(归一化裁切框 + 元素视图框),委托 ImageCutGizmo.getCutInfo。
123770
123856
  * @returns 裁切信息;Gizmo 未就绪 / 未选中单个元素 / 结果无效时返回 undefined
123771
123857
  */ getCutInfo() {
123772
- return this.artis.gizmoManager.get('picture-cut')?.getCutInfo();
123858
+ return this.artis.gizmoManager.get('image-cut')?.getCutInfo();
123773
123859
  }
123774
123860
  /**
123775
- * 获取归一化裁切包围盒(min/max ∈ [0,1],相对元素包围盒),委托 PictureCutGizmo.getCutBox。
123861
+ * 获取归一化裁切包围盒(min/max ∈ [0,1],相对元素包围盒),委托 ImageCutGizmo.getCutBox。
123776
123862
  * @returns 归一化裁切框;Gizmo 未就绪时返回 undefined
123777
123863
  */ getCutBox() {
123778
- return this.artis.gizmoManager.get('picture-cut')?.getCutBox();
123864
+ return this.artis.gizmoManager.get('image-cut')?.getCutBox();
123779
123865
  }
123780
123866
  /**
123781
- * 设置归一化裁切包围盒(入参为归一化坐标),委托 PictureCutGizmo.setCutBox。
123867
+ * 设置归一化裁切包围盒(入参为归一化坐标),委托 ImageCutGizmo.setCutBox。
123782
123868
  * @param normalizeBox 归一化裁切框
123783
123869
  */ setCutBox(normalizeBox) {
123784
- this.artis.gizmoManager.get('picture-cut')?.setCutBox(normalizeBox);
123870
+ this.artis.gizmoManager.get('image-cut')?.setCutBox(normalizeBox);
123785
123871
  }
123786
- /** 关闭图片裁切 Gizmo。 */ closePictureCutGizmo() {
123872
+ /** 关闭图片裁切 Gizmo。 */ closeImageCutGizmo() {
123787
123873
  this.artis.states.activate('move');
123788
123874
  }
123789
123875
  /**
123790
123876
  * 设置图片扩边 Gizmo 是否锁定扩边框比例。
123791
123877
  * @param lock 是否锁定比例
123792
- */ setPictureExpandGizmoLockScale(lock) {
123793
- const pictureExpandGizmo = this.artis.gizmoManager.get('picture-expand');
123794
- if (!pictureExpandGizmo) {
123795
- console.warn('[Artis.operations] setPictureExpandGizmoLockScalepicture-expand Gizmo 未注册或未启用。');
123878
+ */ setImageExpandGizmoLockScale(lock) {
123879
+ const imageExpandGizmo = this.artis.gizmoManager.get('image-expand');
123880
+ if (!imageExpandGizmo) {
123881
+ console.warn('[Artis.operations] setImageExpandGizmoLockScaleimage-expand Gizmo 未注册或未启用。');
123796
123882
  return;
123797
123883
  }
123798
- pictureExpandGizmo.isLockScale = lock;
123884
+ imageExpandGizmo.isLockScale = lock;
123799
123885
  }
123800
123886
  /**
123801
- * 获取当前扩边信息(归一化扩边框 + 元素视图框),委托 PictureExpandGizmo.getExpandInfo。
123887
+ * 获取当前扩边信息(归一化扩边框 + 元素视图框),委托 ImageExpandGizmo.getExpandInfo。
123802
123888
  * @returns 扩边信息;Gizmo 未就绪 / 未选中单个元素 / 结果无效时返回 undefined
123803
123889
  */ getExpandInfo() {
123804
- return this.artis.gizmoManager.get('picture-expand')?.getExpandInfo();
123890
+ return this.artis.gizmoManager.get('image-expand')?.getExpandInfo();
123805
123891
  }
123806
123892
  /**
123807
- * 获取归一化扩边包围盒(相对元素包围盒,可超出 [0,1]),委托 PictureExpandGizmo.getExpandBox。
123893
+ * 获取归一化扩边包围盒(相对元素包围盒,可超出 [0,1]),委托 ImageExpandGizmo.getExpandBox。
123808
123894
  * @returns 归一化扩边框;Gizmo 未就绪时返回 undefined
123809
123895
  */ getExpandBox() {
123810
- return this.artis.gizmoManager.get('picture-expand')?.getExpandBox();
123896
+ return this.artis.gizmoManager.get('image-expand')?.getExpandBox();
123811
123897
  }
123812
123898
  /**
123813
- * 设置归一化扩边包围盒(入参为归一化坐标),委托 PictureExpandGizmo.setExpandBox。
123899
+ * 设置归一化扩边包围盒(入参为归一化坐标),委托 ImageExpandGizmo.setExpandBox。
123814
123900
  * @param normalizeBox 归一化扩边框
123815
123901
  */ setExpandBox(normalizeBox) {
123816
- this.artis.gizmoManager.get('picture-expand')?.setExpandBox(normalizeBox);
123902
+ this.artis.gizmoManager.get('image-expand')?.setExpandBox(normalizeBox);
123817
123903
  }
123818
123904
  /**
123819
123905
  * 绑定操作门面所属的 Artis 实例。
@@ -123898,8 +123984,8 @@ const CARD_HTML_EVENT_MESSAGE_SOURCE = 'vvfx-card-html-event';
123898
123984
  createSelectionInteractionGizmo(owner)
123899
123985
  ];
123900
123986
  });
123901
- this.registry.register(BuiltinGizmoId.PICTURE_CUT, ({ owner })=>new PictureCutGizmo(owner));
123902
- this.registry.register(BuiltinGizmoId.PICTURE_EXPAND, ({ owner })=>new PictureExpandGizmo(owner));
123987
+ this.registry.register(BuiltinGizmoId.IMAGE_CUT, ({ owner })=>new ImageCutGizmo(owner));
123988
+ this.registry.register(BuiltinGizmoId.IMAGE_EXPAND, ({ owner })=>new ImageExpandGizmo(owner));
123903
123989
  this.registry.register(BuiltinGizmoId.SPRITE_TEXT_EDIT, ({ owner })=>new SpriteTextEditGizmo(owner));
123904
123990
  this.registry.register(BuiltinGizmoId.MASK, ({ owner })=>new MaskGizmo(owner));
123905
123991
  this.registry.register(BuiltinGizmoId.ITEM_CREATE, ({ owner })=>{
@@ -124427,6 +124513,26 @@ const CARD_HTML_EVENT_MESSAGE_SOURCE = 'vvfx-card-html-event';
124427
124513
  });
124428
124514
  }
124429
124515
 
124516
+ /** 从 Command 的标准载荷中提取单个或批量属性变化。 */ function getItemPropertyChanges(payload) {
124517
+ const changes = Array.isArray(payload?.propertyChanges) ? payload.propertyChanges : [
124518
+ payload
124519
+ ];
124520
+ return changes.flatMap((change)=>{
124521
+ if (!change || typeof change !== 'object') {
124522
+ return [];
124523
+ }
124524
+ const { itemId, propertyKeys, propertyKey } = change;
124525
+ const keys = Array.isArray(propertyKeys) ? propertyKeys.filter((key)=>typeof key === 'string') : typeof propertyKey === 'string' ? [
124526
+ propertyKey
124527
+ ] : [];
124528
+ return typeof itemId === 'string' && keys.length > 0 ? [
124529
+ {
124530
+ itemId,
124531
+ propertyKeys: keys
124532
+ }
124533
+ ] : [];
124534
+ });
124535
+ }
124430
124536
  /**
124431
124537
  * 注册内置 Command,并配置命令执行后、历史变化后的同步与事件转发。
124432
124538
  * @param ctx 已完成核心服务装配的 wiring 上下文
@@ -124462,22 +124568,20 @@ const CARD_HTML_EVENT_MESSAGE_SOURCE = 'vvfx-card-html-event';
124462
124568
  }
124463
124569
  ctx.emit('history.change', operation);
124464
124570
  });
124465
- // 属性命令在执行、撤销与重做时共用同一事件出口。
124571
+ // 任何提供标准属性载荷的命令,在实际投影完成后共用同一事件出口。
124466
124572
  ctx.commandService.onCommandLifecycle(({ command, payload })=>{
124467
- if (command.commandId !== 'item.updateProperty') {
124468
- return;
124469
- }
124470
- const id = payload?.itemId;
124471
- const propertyKeys = Array.isArray(payload?.propertyKeys) ? payload.propertyKeys.filter((key)=>typeof key === 'string') : typeof payload?.propertyKey === 'string' ? [
124472
- payload.propertyKey
124473
- ] : [];
124474
- if (typeof id !== 'string' || propertyKeys.length === 0) {
124573
+ if (getItemPropertyChanges(payload).length === 0) {
124475
124574
  return;
124476
124575
  }
124477
- ctx.emit('item.propertychange', {
124478
- id,
124479
- propertyKeys
124480
- });
124576
+ void (command.completion ?? Promise.resolve()).then(()=>{
124577
+ const changes = getItemPropertyChanges(command.eventPayload ?? payload);
124578
+ for (const { itemId, propertyKeys } of changes){
124579
+ ctx.emit('item.propertychange', {
124580
+ id: itemId,
124581
+ propertyKeys
124582
+ });
124583
+ }
124584
+ }).catch(()=>undefined);
124481
124585
  });
124482
124586
  }
124483
124587
 
@@ -125695,8 +125799,8 @@ const BUILTIN_DEFS = [
125695
125799
  this._emitBase('item.dragend', type);
125696
125800
  break;
125697
125801
  }
125698
- case 'picture-cut.change':
125699
- case 'picture-expand.change':
125802
+ case 'image-cut.change':
125803
+ case 'image-expand.change':
125700
125804
  this._handleVariantGizmoEvent(event, args);
125701
125805
  break;
125702
125806
  default:
@@ -125983,9 +126087,9 @@ const BUILTIN_DEFS = [
125983
126087
  * @param args 事件携带的参数列表
125984
126088
  * @internal
125985
126089
  */ _handleVariantGizmoEvent(event, args) {
125986
- if (event === 'picture-cut.change') {
126090
+ if (event === 'image-cut.change') {
125987
126091
  this.emit(event, ...args);
125988
- } else if (event === 'picture-expand.change') {
126092
+ } else if (event === 'image-expand.change') {
125989
126093
  this.emit(event, ...args);
125990
126094
  }
125991
126095
  }
@@ -125995,11 +126099,11 @@ const BUILTIN_DEFS = [
125995
126099
  * @internal
125996
126100
  */ _handleStateChange(event) {
125997
126101
  const { previousState, currentState, source } = event;
125998
- const isPictureCut = currentState.id === OfficialStateId.IMAGE_CUT;
125999
- const isPictureExpand = currentState.id === OfficialStateId.IMAGE_EXPAND;
126102
+ const isImageCut = currentState.id === OfficialStateId.IMAGE_CUT;
126103
+ const isImageExpand = currentState.id === OfficialStateId.IMAGE_EXPAND;
126000
126104
  const isItemCreate = currentState.id === OfficialStateId.ITEM_CREATE;
126001
126105
  let eventSource = source;
126002
- if (isPictureCut || isPictureExpand) {
126106
+ if (isImageCut || isImageExpand) {
126003
126107
  const selectedId = this.selection.getSelectedIds()[0];
126004
126108
  eventSource = {
126005
126109
  ...source,
@@ -126821,5 +126925,5 @@ const BUILTIN_DEFS = [
126821
126925
  'sprite.image'
126822
126926
  ]));
126823
126927
 
126824
- export { Artis, ArtisItemType, AssetLoadState, AssetManager, AssetType, BackgroundController, BackgroundManager, BaseArtis, BaseItem, BaseState, Box2, BuiltinGizmoId, CardItem, Circle, CommandService, CompositeCommand, DEG2RAD, DuplicateComboError, DuplicateElementIdError, EffectsItem, ElementFacade, ElementRegistry, ElementRegistryErrorCode, ErrorMonitor, ErrorMonitorFacade, Euler, EulerOrder, FrameItem, FrameLayoutCommand, FrameLayoutMode, FrameLayoutModeCommand, FrameMoveInCommand, FrameMoveOutCommand, GeneratorItem, GeneratorType, GizmoBootstrap, GizmoCommandBinder, GizmoManager, GizmoRegistry, GlobalLayoutMode, GroupChildrenDetachCommand, GroupItem, HTMLCardService, HandState, HistoryManager, IOPlugin, IdentityStrategy, IgnoredInteractionError, ImageCutState, ImageExpandState, ImageMaskState, ImageTextEditState, InteractionService, ItemAlignCommand, ItemCreateCommand, ItemCreateState, ItemCreateType, ItemDeleteCommand, ItemDistributeCommand, ItemGroupCommand, ItemOrderAction, ItemReorderCommand, ItemReplaceSourceCommand, ItemService, ItemSetFontFamilyCommand, ItemSetGeneratorResourceCommand, ItemTransformCommand, ItemUngroupCommand, ItemUpdatePropertyBySchemaCommand, ItemUpdatePropertyCommand, JsonSceneSyncService, KeyBindingController, Line2, Line3, LocalStorageKeyBindingStore, MaskCommitCommand, Matrix4, MissingKeyBindingError, MoveState, OfficialStateId, Operations, PageAutoLayoutCommand, PerfMonitor, PerfMonitorFacade, PerfPhase, PictureCutCommitCommand, PictureExpandCommitCommand, Plane, PlayerAdapter, PluginSystem, Quaternion, RAD2DEG, Ray, RayCaster, RegistryIdentityStrategy, Renderer, ReorderType, SafeConstraint, SceneArtis, SceneDataAssembler, SceneOperations, SceneStore, SceneSwitchCommand, ScreenShot, ScreenshotService, SelectionSetCommand, SpriteImageCommand, SpriteItem, SpriteTextEditCommitCommand, StabilityMonitor, StabilityMonitorFacade, StabilitySignal, StateHandCommand, StateImageCutCommand, StateImageExpandCommand, StateImageMaskCommand, StateImageTextEditCommand, StateItemCreateCommand, StateManager, StateMoveCommand, StateSwitchCommandBase, Store, TextItem, TextMoveCommand, TextRotateCommand, TextScaleCommand, TextUpdateContentCommand, TransformMoveCommand, TransformMoveWithFrameChangeCommand, TransformMoveWithLayoutCommand, TransformRotateCommand, TransformScaleCommand, UserPreferencesManager, Vector2, Vector3, VideoItem, VideoPlayCommand, VideoSourceCommand, ViewService, ViewportFitToBoxCommand, ViewportFitToItemsCommand, ViewportMoveCommand, ViewportZoomCommand, addCompositionItemByComposition, addItemInfoToScene, adjustOtherElementsLayoutInfo, applyGizmoConfigs, arrAdd, assertExist, asserts, autoLayout, boxCenter, boxSize, buildFrameLayoutInput, buildItemSchemaByCreateType, calculateDisplacement, calculateLayoutDisplacements, clamp, clearItemParentInfo, comboToString, computeAlignedPositions, computeDistributedPositions, createCameraProvider, createCompositionItemJSON, createDefaultStates, createExportableScene, createFrameCompositionScene, createItemTransformProvider, createKeyComboFromEvent, createMaskScene, createNullItemJSON, createSpriteItemJSON, createStatesByIds, createTextItemJSON, createVideoGeneratorItemJSON, createVideoItemJSON, createViewportProvider, deepClone, deepEquals, deleteCompositionFromScene, deleteCompositionItemFromScene, deleteItemInfoFromScene, downloadBlob, extractSubCompositionToScene, findPlayerItemByInstanceId, fixStandardJSON, generateGUID, getAbsMaxIndex, getAngleByVectors, getBasicScene, getBoxByNormalizeBox, getColorByNormalizeColor, getCompositionDataByRefCompositionItemId, getCompositionIdByRefCompositionItemId, getContainerSize, getItemTransformUnderParent, getNormalizeBoxByBoxes, getNormalizeColorByColor, getPixelBoxBySchema, getPixelBoxBySchemas, getTextureUrlById, getUniqueName, getVector2Angle, gizmoConfig, globalAutoLayout, isBaseItem, isCardItem, isEffectsItem, isEqual, isEqual$1 as isEqualNumber, isFrameItem, isGeneratorItem, isGroupItem, isObj, isSpriteItem, isTextItem, isVideoItem, loadScriptAsync, localToWorldBox, logger, maskConfig, matchCombo, moveItemBetweenCompositions, moveItemOutOfFrame, moveItemToFrame, moveItemToRoot, parseCombo, pictureCutConfig, pictureExpandConfig, registerCommands, removeItemInfoFromScene, resetSubCompositionItemId, resizeSelectionConfig, roundNumber, schemaToBoxMap, selectionPreviewConfig, serializeArtisItemToJSON, serializeReplayArgs, setItemJSONParentId, setJSONItemName, setupCommands, setupCoreEventBindings, setupKeyBindings, setupPlugins, setupSelectionBindings, index$1 as spec, transformBox, unionBoxes, updateItemLayoutInfo, viewportNavigationConfig, viewportOverlayConfig, worldToLocalBox };
126928
+ export { Artis, ArtisItemType, AssetLoadState, AssetManager, AssetType, BackgroundController, BackgroundManager, BaseArtis, BaseItem, BaseState, Box2, BuiltinGizmoId, CardItem, Circle, CommandService, CompositeCommand, DEG2RAD, DuplicateComboError, DuplicateElementIdError, EffectsItem, ElementFacade, ElementRegistry, ElementRegistryErrorCode, ErrorMonitor, ErrorMonitorFacade, Euler, EulerOrder, FrameItem, FrameLayoutCommand, FrameLayoutMode, FrameLayoutModeCommand, FrameMoveInCommand, FrameMoveOutCommand, GeneratorItem, GeneratorType, GizmoBootstrap, GizmoCommandBinder, GizmoManager, GizmoRegistry, GlobalLayoutMode, GroupChildrenDetachCommand, GroupItem, HTMLCardService, HandState, HistoryManager, IOPlugin, IdentityStrategy, IgnoredInteractionError, ImageCutCommitCommand, ImageCutState, ImageExpandCommitCommand, ImageExpandState, ImageMaskState, ImageTextEditState, InteractionService, ItemAlignCommand, ItemCreateCommand, ItemCreateState, ItemCreateType, ItemDeleteCommand, ItemDistributeCommand, ItemGroupCommand, ItemOrderAction, ItemReorderCommand, ItemReplaceSourceCommand, ItemService, ItemSetFontFamilyCommand, ItemSetGeneratorResourceCommand, ItemTransformCommand, ItemUngroupCommand, ItemUpdatePropertyBySchemaCommand, ItemUpdatePropertyCommand, JsonSceneSyncService, KeyBindingController, Line2, Line3, LocalStorageKeyBindingStore, MaskCommitCommand, Matrix4, MissingKeyBindingError, MoveState, OfficialStateId, Operations, PageAutoLayoutCommand, PerfMonitor, PerfMonitorFacade, PerfPhase, Plane, PlayerAdapter, PluginSystem, Quaternion, RAD2DEG, Ray, RayCaster, RegistryIdentityStrategy, Renderer, ReorderType, SafeConstraint, SceneArtis, SceneDataAssembler, SceneOperations, SceneStore, SceneSwitchCommand, ScreenShot, ScreenshotService, SelectionSetCommand, SpriteImageCommand, SpriteItem, SpriteTextEditCommitCommand, StabilityMonitor, StabilityMonitorFacade, StabilitySignal, StateHandCommand, StateImageCutCommand, StateImageExpandCommand, StateImageMaskCommand, StateImageTextEditCommand, StateItemCreateCommand, StateManager, StateMoveCommand, StateSwitchCommandBase, Store, TextItem, TextMoveCommand, TextRotateCommand, TextScaleCommand, TextUpdateContentCommand, TransformMoveCommand, TransformMoveWithFrameChangeCommand, TransformMoveWithLayoutCommand, TransformRotateCommand, TransformScaleCommand, UserPreferencesManager, Vector2, Vector3, VideoItem, VideoPlayCommand, VideoSourceCommand, ViewService, ViewportFitToBoxCommand, ViewportFitToItemsCommand, ViewportMoveCommand, ViewportZoomCommand, addCompositionItemByComposition, addItemInfoToScene, adjustOtherElementsLayoutInfo, applyGizmoConfigs, arrAdd, assertExist, asserts, autoLayout, boxCenter, boxSize, buildFrameLayoutInput, buildItemSchemaByCreateType, calculateDisplacement, calculateLayoutDisplacements, clamp, clearItemParentInfo, comboToString, computeAlignedPositions, computeDistributedPositions, createCameraProvider, createCompositionItemJSON, createDefaultStates, createExportableScene, createFrameCompositionScene, createItemTransformProvider, createKeyComboFromEvent, createMaskScene, createNullItemJSON, createSpriteItemJSON, createStatesByIds, createTextItemJSON, createVideoGeneratorItemJSON, createVideoItemJSON, createViewportProvider, deepClone, deepEquals, deleteCompositionFromScene, deleteCompositionItemFromScene, deleteItemInfoFromScene, downloadBlob, extractSubCompositionToScene, findPlayerItemByInstanceId, fixStandardJSON, generateGUID, getAbsMaxIndex, getAngleByVectors, getBasicScene, getBoxByNormalizeBox, getColorByNormalizeColor, getCompositionDataByRefCompositionItemId, getCompositionIdByRefCompositionItemId, getContainerSize, getItemTransformUnderParent, getNormalizeBoxByBoxes, getNormalizeColorByColor, getPixelBoxBySchema, getPixelBoxBySchemas, getTextureUrlById, getUniqueName, getVector2Angle, gizmoConfig, globalAutoLayout, imageCutConfig, imageExpandConfig, isBaseItem, isCardItem, isEffectsItem, isEqual, isEqual$1 as isEqualNumber, isFrameItem, isGeneratorItem, isGroupItem, isObj, isSpriteItem, isTextItem, isVideoItem, loadScriptAsync, localToWorldBox, logger, maskConfig, matchCombo, moveItemBetweenCompositions, moveItemOutOfFrame, moveItemToFrame, moveItemToRoot, parseCombo, registerCommands, removeItemInfoFromScene, resetSubCompositionItemId, resizeSelectionConfig, roundNumber, schemaToBoxMap, selectionPreviewConfig, serializeArtisItemToJSON, serializeReplayArgs, setItemJSONParentId, setJSONItemName, setupCommands, setupCoreEventBindings, setupKeyBindings, setupPlugins, setupSelectionBindings, index$1 as spec, transformBox, unionBoxes, updateItemLayoutInfo, viewportNavigationConfig, viewportOverlayConfig, worldToLocalBox };
126825
126929
  //# sourceMappingURL=index.mjs.map