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