@fulate/core 1.0.6 → 1.0.8

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.d.ts CHANGED
@@ -3,12 +3,12 @@ export type { EventName, EventCallback, FulateEvent, FulateEventDetail, CustomEv
3
3
  export { Node, NodeLifecycleEvent } from "./node/node";
4
4
  export type { NodeCreationOptions } from "./node/node";
5
5
  export { ElementTransform } from "./node/element-transform";
6
- export type { ElementTransformClass, ElementTransformConstructor, GeometryBoundsView } from "./node/element-transform";
6
+ export type { ElementTransformClass, GeometryBoundsView } from "./node/element-transform";
7
7
  export { ChangeImpact, NodeProperties, resolveOrigin, TransformProperties } from "./node/properties";
8
8
  export type { OriginX, OriginY, PropertyChanges, PropertyUpdates, TransformableOptions } from "./node/properties";
9
9
  export { Element, ElementProperties } from "./node/element";
10
10
  export type { AnyElement, BaseElementOption, ElementJSON, ElementOptionPatch, AnimateOptions, AnimateTarget } from "./node/element";
11
- export { isReparentTransformIntent, resolveReferenceTransform, resolveReferenceTransformTarget } from "./node/reference-transform";
11
+ export { isReparentTransformIntent, resolveAuthoredGeometryFramePatch, resolveReferenceTransform, resolveReferenceTransformTarget } from "./node/reference-transform";
12
12
  export type { ElementTransformBaseline, ReferenceTransformBaseline, ReferenceTransformFrame, ReferenceTransformResolution, ReferenceTransformTarget, ReparentTransformIntent, TransformIntent } from "./node/reference-transform";
13
13
  export { DEFAULT_CONNECTION_PORT_SCHEMA, getElementConnectionPort, getElementConnectionPortPoint, getElementConnectionPortPoints, projectConnectionPortPoint } from "./utils/connection-port";
14
14
  export type { ConnectionPortId, ConnectionPortConfiguration, ConnectionPortDefinition, ConnectionPortLabelStyle, ConnectionPortDirection, ConnectionPortLimit, ConnectionPortLimits, ConnectionPortRef, DerivedConnectionPort, EdgeConnectionPortPosition, ResolvedConnectionPort, ResolvedConnectionPortSlot } from "./utils/connection-port";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { clamp, isEqual, isFunction, isNull, isNumber, isObjectLike, isString, isUndefined } from "lodash-es";
2
- import { Bound, Intersection, clipPolygonToRect, createCanvasGradient, createPointView, formatPremultipliedColor, isGradient, makeBoundingBoxFromRects, parsePremultipliedColor, qrDecompose, rectEdgePosition, transformPoint } from "@fulate/share";
2
+ import { Bound, Intersection, clipPolygonToRect, createCanvasGradient, createPointView, formatPremultipliedColor, isGradient, isIdentityMatrix, makeBoundingBoxFromRects, parsePremultipliedColor, qrDecompose, rectEdgePosition, transformPoint } from "@fulate/share";
3
3
  import RBush from "rbush";
4
4
  import { Easing, Group, Tween } from "@tweenjs/tween.js";
5
5
  //#region packages/core/src/event.ts
@@ -1790,6 +1790,36 @@ function createGeometryQuery(snapshot) {
1790
1790
  return snapshot instanceof RectangleGeometrySnapshot ? new RectangleSnapshotGeometryQuery(snapshot) : new SnapshotGeometryQuery(snapshot);
1791
1791
  }
1792
1792
  //#endregion
1793
+ //#region packages/core/src/node/element-transform-geometry.ts
1794
+ /** Project a local rectangle into a matrix space and return its AABB. */
1795
+ function projectBounds(matrix, bounds) {
1796
+ if (!bounds) return new Bound();
1797
+ const left = bounds.left;
1798
+ const top = bounds.top;
1799
+ const right = left + bounds.width;
1800
+ const bottom = top + bounds.height;
1801
+ if (matrix.b === 0 && matrix.c === 0) {
1802
+ const firstX = matrix.a * left + matrix.e;
1803
+ const secondX = matrix.a * right + matrix.e;
1804
+ const firstY = matrix.d * top + matrix.f;
1805
+ const secondY = matrix.d * bottom + matrix.f;
1806
+ return new Bound(Math.min(firstX, secondX), Math.min(firstY, secondY), Math.max(firstX, secondX), Math.max(firstY, secondY));
1807
+ }
1808
+ const x0 = matrix.a * left + matrix.c * top + matrix.e;
1809
+ const x1 = matrix.a * right + matrix.c * top + matrix.e;
1810
+ const x2 = matrix.a * right + matrix.c * bottom + matrix.e;
1811
+ const x3 = matrix.a * left + matrix.c * bottom + matrix.e;
1812
+ const y0 = matrix.b * left + matrix.d * top + matrix.f;
1813
+ const y1 = matrix.b * right + matrix.d * top + matrix.f;
1814
+ const y2 = matrix.b * right + matrix.d * bottom + matrix.f;
1815
+ const y3 = matrix.b * left + matrix.d * bottom + matrix.f;
1816
+ return new Bound(Math.min(x0, x1, x2, x3), Math.min(y0, y1, y2, y3), Math.max(x0, x1, x2, x3), Math.max(y0, y1, y2, y3));
1817
+ }
1818
+ /** Compare a committed matrix with a previous scalar matrix sample. */
1819
+ function matrixChanged(matrix, previousA, previousB, previousC, previousD, previousE, previousF) {
1820
+ return matrix.a !== previousA || matrix.b !== previousB || matrix.c !== previousC || matrix.d !== previousD || matrix.e !== previousE || matrix.f !== previousF;
1821
+ }
1822
+ //#endregion
1793
1823
  //#region packages/core/src/node/element-transform.ts
1794
1824
  var VISUAL_GEOMETRY_IMPACT$1 = ChangeImpact.LocalGeometry | ChangeImpact.WorldTransform | ChangeImpact.VisualBounds;
1795
1825
  var GEOMETRY_CACHE_IMPACT = ChangeImpact.LocalGeometry | ChangeImpact.WorldTransform | ChangeImpact.VisualBounds | ChangeImpact.HitBounds;
@@ -2051,7 +2081,7 @@ var ElementTransform = class {
2051
2081
  /** 清除 world 投影;world-dependent local geometry 只在矩阵实际变化时失效。 */
2052
2082
  finishWorldMatrixCommit(previousA, previousB, previousC, previousD, previousE, previousF) {
2053
2083
  const matrix = this.worldMatrix;
2054
- const changed = matrix.a !== previousA || matrix.b !== previousB || matrix.c !== previousC || matrix.d !== previousD || matrix.e !== previousE || matrix.f !== previousF;
2084
+ const changed = matrixChanged(matrix, previousA, previousB, previousC, previousD, previousE, previousF);
2055
2085
  if (this.geometrySnapshot && this.worldMatrixAffectsLocalGeometry() && changed) this.invalidateCache(ChangeImpact.LocalGeometry);
2056
2086
  else this.clearWorldProjection();
2057
2087
  return changed;
@@ -2224,27 +2254,7 @@ var ElementTransform = class {
2224
2254
  }
2225
2255
  /** 将 local AABB 四角投影到指定 reference matrix。 */
2226
2256
  toBounds(matrix, bounds) {
2227
- if (!bounds) return new Bound();
2228
- const left = bounds.left;
2229
- const top = bounds.top;
2230
- const right = left + bounds.width;
2231
- const bottom = top + bounds.height;
2232
- if (matrix.b === 0 && matrix.c === 0) {
2233
- const firstX = matrix.a * left + matrix.e;
2234
- const secondX = matrix.a * right + matrix.e;
2235
- const firstY = matrix.d * top + matrix.f;
2236
- const secondY = matrix.d * bottom + matrix.f;
2237
- return new Bound(Math.min(firstX, secondX), Math.min(firstY, secondY), Math.max(firstX, secondX), Math.max(firstY, secondY));
2238
- }
2239
- const x0 = matrix.a * left + matrix.c * top + matrix.e;
2240
- const x1 = matrix.a * right + matrix.c * top + matrix.e;
2241
- const x2 = matrix.a * right + matrix.c * bottom + matrix.e;
2242
- const x3 = matrix.a * left + matrix.c * bottom + matrix.e;
2243
- const y0 = matrix.b * left + matrix.d * top + matrix.f;
2244
- const y1 = matrix.b * right + matrix.d * top + matrix.f;
2245
- const y2 = matrix.b * right + matrix.d * bottom + matrix.f;
2246
- const y3 = matrix.b * left + matrix.d * bottom + matrix.f;
2247
- return new Bound(Math.min(x0, x1, x2, x3), Math.min(y0, y1, y2, y3), Math.max(x0, x1, x2, x3), Math.max(y0, y1, y2, y3));
2257
+ return projectBounds(matrix, bounds);
2248
2258
  }
2249
2259
  /** 惰性构建当前快照对应的 local 绘制轮廓。 */
2250
2260
  getVisualOutline() {
@@ -2256,7 +2266,7 @@ var ElementTransform = class {
2256
2266
  }
2257
2267
  /** 在清理 visual 缓存前保存最后一次提交的 world 绘制范围。 */
2258
2268
  captureCurrentVisualBounds() {
2259
- if (this.hasGeometry()) this.captureVisualBounds(this.getWorldVisualBoundsView());
2269
+ if (this.worldVisualBounds && this.hasGeometry()) this.captureVisualBounds(this.worldVisualBounds);
2260
2270
  }
2261
2271
  /** 将旧 visual bounds交给当前 transform 和所属 Layer 的 damage 跟踪。 */
2262
2272
  captureVisualBounds(bounds) {
@@ -2284,6 +2294,7 @@ var ElementTransform = class {
2284
2294
  invalidate(impact, previousBounds) {
2285
2295
  if (impact & CHILD_LAYOUT_IMPACT) this.childLayoutDirty = true;
2286
2296
  const visualGeometryChanged = (impact & VISUAL_GEOMETRY_IMPACT$1) !== 0;
2297
+ const invalidateAncestors = !this.needsCommit;
2287
2298
  if (!this.needsCommit) {
2288
2299
  if (visualGeometryChanged) {
2289
2300
  if (isUndefined(previousBounds)) this.captureCurrentVisualBounds();
@@ -2292,14 +2303,20 @@ var ElementTransform = class {
2292
2303
  this.hitOnlyInvalidation = !visualGeometryChanged && (impact & ChangeImpact.HitBounds) !== 0;
2293
2304
  this.needsCommit = true;
2294
2305
  this.currentRenderCoverageBounds = void 0;
2295
- this.markDirtyAncestors();
2296
2306
  } else if (this.hitOnlyInvalidation && visualGeometryChanged) {
2297
2307
  this.captureCurrentVisualBounds();
2298
2308
  this.hitOnlyInvalidation = false;
2299
2309
  }
2300
2310
  this.invalidateCache(impact);
2311
+ if (invalidateAncestors) this.markDirtyAncestors();
2301
2312
  if (this.element.isActive && !this.resolvingFitSize) this.element.layer.invalidateLayout(this.element);
2302
2313
  }
2314
+ /** A derived layout can change visual bounds during an already pending commit. @internal */
2315
+ invalidateLayoutBounds() {
2316
+ const pending = this.needsCommit;
2317
+ this.invalidate(ChangeImpact.VisualBounds);
2318
+ if (pending) this.markDirtyAncestors();
2319
+ }
2303
2320
  /** 标记 local 几何需要重新构建。 */
2304
2321
  markGeometryNeedsLayout(notifyScene = true) {
2305
2322
  const publish = !this.needsCommit;
@@ -2484,7 +2501,7 @@ var ElementTransform = class {
2484
2501
  const element = this.element;
2485
2502
  const cacheable = !this.needsCommit && !this.hasDirtyDescendant && element.activeRoot?.frameRuntime.isStabilizingGeometry !== true;
2486
2503
  if (cacheable && !isUndefined(this.currentRenderCoverageBounds)) return this.currentRenderCoverageBounds;
2487
- const ownBounds = this.hasGeometry() ? this.getWorldVisualBounds() : null;
2504
+ const ownBounds = this.hasGeometry() ? this.getWorldVisualBoundsView() : null;
2488
2505
  let descendantBounds = null;
2489
2506
  for (const child of element.children) {
2490
2507
  if (!child.visible) continue;
@@ -2506,7 +2523,7 @@ var ElementTransform = class {
2506
2523
  descendantBounds.maxY = Math.min(descendantBounds.maxY, clipBounds.maxY);
2507
2524
  if (descendantBounds.minX >= descendantBounds.maxX || descendantBounds.minY >= descendantBounds.maxY) descendantBounds = null;
2508
2525
  }
2509
- const bounds = ownBounds && descendantBounds ? ownBounds.merge(descendantBounds) : ownBounds ?? descendantBounds;
2526
+ const bounds = ownBounds && descendantBounds ? new Bound(ownBounds.minX, ownBounds.minY, ownBounds.maxX, ownBounds.maxY).merge(descendantBounds) : ownBounds ?? descendantBounds;
2510
2527
  if (cacheable) this.currentRenderCoverageBounds = bounds;
2511
2528
  return bounds;
2512
2529
  }
@@ -3046,7 +3063,7 @@ var ElementProperties = class extends TransformProperties {
3046
3063
  }
3047
3064
  };
3048
3065
  /** 可变换、可绘制、可命中并支持事件和动画的场景节点。 */
3049
- var Element = class extends Node {
3066
+ var Element = class Element extends Node {
3050
3067
  type = "element";
3051
3068
  /** 当前 Element 唯一的变换与几何运行时。 */
3052
3069
  transform;
@@ -3158,6 +3175,7 @@ var Element = class extends Node {
3158
3175
  * @param value `false`、`true` 或 custom definitions。
3159
3176
  */
3160
3177
  replacePorts(value) {
3178
+ if (this.ports === value && Element.getConfiguredPropertyValue(this, "ports") === value) return this;
3161
3179
  this.commitConfiguredOptions({ ports: value });
3162
3180
  return this;
3163
3181
  }
@@ -3509,11 +3527,27 @@ var Element = class extends Node {
3509
3527
  }
3510
3528
  /** @internal 一次产出 custom Port position、法线与 label edge 长度。 */
3511
3529
  getConnectionPortGeometry(edge, ratio) {
3512
- const resolved = rectEdgePosition(this.width ?? 0, this.height ?? 0, edge, ratio);
3530
+ const position = this.getConnectionPortPosition(edge, ratio);
3531
+ let nx = 0;
3532
+ let ny = -1;
3533
+ switch (edge) {
3534
+ case "right":
3535
+ nx = 1;
3536
+ ny = 0;
3537
+ break;
3538
+ case "bottom":
3539
+ nx = 0;
3540
+ ny = 1;
3541
+ break;
3542
+ case "left":
3543
+ nx = -1;
3544
+ ny = 0;
3545
+ break;
3546
+ }
3513
3547
  return {
3514
- position: this.getConnectionPortPosition(edge, ratio),
3515
- nx: resolved.nx,
3516
- ny: resolved.ny,
3548
+ position,
3549
+ nx,
3550
+ ny,
3517
3551
  edgeLength: edge === "top" || edge === "bottom" ? Math.abs(this.width ?? 0) : Math.abs(this.height ?? 0)
3518
3552
  };
3519
3553
  }
@@ -3665,7 +3699,7 @@ var IDENTITY_REFERENCE_TRANSFORM = new DOMMatrix();
3665
3699
  */
3666
3700
  function resolveSparseReferenceTranslation(baseline, intent, targetParentWorldMatrix) {
3667
3701
  const delta = intent.worldDelta;
3668
- if (delta.a !== 1 || delta.b !== 0 || delta.c !== 0 || delta.d !== 1 || !sameLinearMatrix(baseline.startParentWorldMatrix, targetParentWorldMatrix)) return null;
3702
+ if (!sameLinearMatrix(delta, IDENTITY_REFERENCE_TRANSFORM) || !sameLinearMatrix(baseline.startParentWorldMatrix, targetParentWorldMatrix)) return null;
3669
3703
  const worldX = delta.e + baseline.startParentWorldMatrix.e - targetParentWorldMatrix.e;
3670
3704
  const worldY = delta.f + baseline.startParentWorldMatrix.f - targetParentWorldMatrix.f;
3671
3705
  const parentInverse = baseline.startParentWorldInverseMatrix;
@@ -3683,6 +3717,107 @@ function sameMatrix(left, right) {
3683
3717
  function sameLinearMatrix(left, right) {
3684
3718
  return left.a === right.a && left.b === right.b && left.c === right.c && left.d === right.d;
3685
3719
  }
3720
+ function createFrameLinearMatrix(frame) {
3721
+ return new DOMMatrix().rotate(0, 0, frame.angle).skewX(frame.skewX).skewY(frame.skewY).scale(frame.scaleX, frame.scaleY);
3722
+ }
3723
+ function transformReferenceBounds(bounds, matrix) {
3724
+ const right = bounds.left + bounds.width;
3725
+ const bottom = bounds.top + bounds.height;
3726
+ const corners = [
3727
+ transformPoint(matrix, {
3728
+ x: bounds.left,
3729
+ y: bounds.top
3730
+ }),
3731
+ transformPoint(matrix, {
3732
+ x: right,
3733
+ y: bounds.top
3734
+ }),
3735
+ transformPoint(matrix, {
3736
+ x: right,
3737
+ y: bottom
3738
+ }),
3739
+ transformPoint(matrix, {
3740
+ x: bounds.left,
3741
+ y: bottom
3742
+ })
3743
+ ];
3744
+ let minX = corners[0].x;
3745
+ let minY = corners[0].y;
3746
+ let maxX = corners[0].x;
3747
+ let maxY = corners[0].y;
3748
+ for (let index = 1; index < corners.length; index++) {
3749
+ const point = corners[index];
3750
+ minX = Math.min(minX, point.x);
3751
+ minY = Math.min(minY, point.y);
3752
+ maxX = Math.max(maxX, point.x);
3753
+ maxY = Math.max(maxY, point.y);
3754
+ }
3755
+ return {
3756
+ left: minX,
3757
+ top: minY,
3758
+ width: maxX - minX,
3759
+ height: maxY - minY
3760
+ };
3761
+ }
3762
+ /**
3763
+ * Factor a target local affine transform through the existing authored frame.
3764
+ * The frame keeps its current pose; the complete linear delta is written into
3765
+ * the vector's authored points. Translation remains a frame left/top patch so
3766
+ * no synthetic point offset is introduced into the canonical geometry.
3767
+ */
3768
+ function resolveBakedAuthoredGeometryTransform(baseline, target) {
3769
+ const frameLinear = createFrameLinearMatrix(baseline.frame);
3770
+ const determinant = frameLinear.a * frameLinear.d - frameLinear.b * frameLinear.c;
3771
+ if (!Number.isFinite(determinant) || determinant === 0) return null;
3772
+ const targetLocal = target.parentInverseMatrix.multiply(target.worldMatrix);
3773
+ const targetLinear = new DOMMatrix([
3774
+ targetLocal.a,
3775
+ targetLocal.b,
3776
+ targetLocal.c,
3777
+ targetLocal.d,
3778
+ 0,
3779
+ 0
3780
+ ]);
3781
+ const authoredLocalDelta = frameLinear.inverse().multiply(targetLinear);
3782
+ const nextBounds = transformReferenceBounds(baseline.referenceBounds, authoredLocalDelta);
3783
+ const offsetX = nextBounds.left + (resolveOrigin(baseline.frame.originX) + .5) * nextBounds.width;
3784
+ const offsetY = nextBounds.top + (resolveOrigin(baseline.frame.originY) + .5) * nextBounds.height;
3785
+ return {
3786
+ framePatch: {
3787
+ left: targetLocal.e - offsetX + frameLinear.a * offsetX + frameLinear.c * offsetY,
3788
+ top: targetLocal.f - offsetY + frameLinear.b * offsetX + frameLinear.d * offsetY,
3789
+ angle: baseline.frame.angle,
3790
+ scaleX: baseline.frame.scaleX,
3791
+ scaleY: baseline.frame.scaleY,
3792
+ skewX: baseline.frame.skewX,
3793
+ skewY: baseline.frame.skewY
3794
+ },
3795
+ authoredLocalDelta
3796
+ };
3797
+ }
3798
+ /**
3799
+ * 用 owner 已算出的精确 authored bounds 修正 bake frame 的位置。
3800
+ * AABB 经过旋转/错切后不能由原 bounds 四角精确推出;owner 在生成
3801
+ * 新几何后把真实 bounds 交回这里,保持 frame world matrix 与目标一致。
3802
+ */
3803
+ function resolveAuthoredGeometryFramePatch(baseline, resolution, authoredBounds) {
3804
+ if (isIdentityMatrix(resolution.authoredLocalDelta)) return resolution.framePatch;
3805
+ const approximateBounds = transformReferenceBounds(baseline.referenceBounds, resolution.authoredLocalDelta);
3806
+ const originX = resolveOrigin(baseline.frame.originX) + .5;
3807
+ const originY = resolveOrigin(baseline.frame.originY) + .5;
3808
+ const approximateOffsetX = approximateBounds.left + originX * approximateBounds.width;
3809
+ const approximateOffsetY = approximateBounds.top + originY * approximateBounds.height;
3810
+ const authoredOffsetX = authoredBounds.left + originX * authoredBounds.width;
3811
+ const authoredOffsetY = authoredBounds.top + originY * authoredBounds.height;
3812
+ const deltaX = authoredOffsetX - approximateOffsetX;
3813
+ const deltaY = authoredOffsetY - approximateOffsetY;
3814
+ const frameLinear = createFrameLinearMatrix(baseline.frame);
3815
+ return {
3816
+ ...resolution.framePatch,
3817
+ left: (resolution.framePatch.left ?? baseline.frame.left) + -deltaX + frameLinear.a * deltaX + frameLinear.c * deltaY,
3818
+ top: (resolution.framePatch.top ?? baseline.frame.top) + -deltaY + frameLinear.b * deltaX + frameLinear.d * deltaY
3819
+ };
3820
+ }
3686
3821
  /** 判断变换意图是否包含跨父级重挂载所需的目标父级矩阵。 */
3687
3822
  function isReparentTransformIntent(intent) {
3688
3823
  return "targetParentWorldMatrix" in intent;
@@ -3694,10 +3829,14 @@ function resolveTargetParentWorldMatrix(baseline, intent) {
3694
3829
  function targetsFrozenReference(baseline, intent) {
3695
3830
  const delta = intent.worldDelta;
3696
3831
  const targetParentWorldMatrix = resolveTargetParentWorldMatrix(baseline, intent);
3697
- return delta.a === 1 && delta.b === 0 && delta.c === 0 && delta.d === 1 && delta.e === 0 && delta.f === 0 && sameMatrix(baseline.startParentWorldMatrix, targetParentWorldMatrix);
3832
+ return isIdentityMatrix(delta) && sameMatrix(baseline.startParentWorldMatrix, targetParentWorldMatrix);
3698
3833
  }
3699
3834
  /** 从已持有的最终 world pose 纯计算 frame/authored delta。 */
3700
- function resolveReferenceTransformTargetWithMode(baseline, target, preserveOwnedGeometry, unwrappedRotationDelta) {
3835
+ function resolveReferenceTransformTargetWithMode(baseline, target, preserveOwnedGeometry, unwrappedRotationDelta, bakeGeometry = false) {
3836
+ if (bakeGeometry && baseline.writeMode === "authored-geometry" && !preserveOwnedGeometry) {
3837
+ const baked = resolveBakedAuthoredGeometryTransform(baseline, target);
3838
+ if (baked) return baked;
3839
+ }
3701
3840
  const targetDecomposition = qrDecompose(target.parentInverseMatrix.multiply(target.worldMatrix));
3702
3841
  const targetAngle = unwrappedRotationDelta === void 0 ? targetDecomposition.angle : targetDecomposition.angle + 360 * Math.round((baseline.frame.angle + unwrappedRotationDelta - targetDecomposition.angle) / 360);
3703
3842
  const authoredScaleX = preserveOwnedGeometry ? 1 : baseline.frame.scaleX === 0 ? 1 : Math.abs(targetDecomposition.scaleX / baseline.frame.scaleX);
@@ -3792,7 +3931,7 @@ function resolveReferenceTransform(baseline, intent) {
3792
3931
  worldMatrix: intent.worldDelta.multiply(baseline.startWorldMatrix),
3793
3932
  worldCenter: transformPoint(intent.worldDelta, startWorldCenter),
3794
3933
  parentInverseMatrix: sameMatrix(baseline.startParentWorldMatrix, targetParentWorldMatrix) ? baseline.startParentWorldInverseMatrix : targetParentWorldMatrix.inverse()
3795
- }, preserveOwnedGeometry, intent.unwrappedRotationDelta);
3934
+ }, preserveOwnedGeometry, intent.unwrappedRotationDelta, intent.bakeGeometry);
3796
3935
  }
3797
3936
  //#endregion
3798
3937
  //#region packages/core/src/node/shape.ts
@@ -4143,20 +4282,24 @@ var Shape = class extends Element {
4143
4282
  //#region packages/core/src/root/hit-test.ts
4144
4283
  /** Fixed screen-space broad-phase aperture for scene point affordances. */
4145
4284
  var POINTER_QUERY_RADIUS = 8;
4146
- /** 判断 world 点是否通过元素所有 ancestor child clip */
4147
- function isPointWithinAncestorClips(element, point) {
4285
+ /** 合并 pointer ancestor 状态与 child clip 的单次祖先遍历。 */
4286
+ function isPointerTargetWithinAncestorClips(root, element, point) {
4287
+ if (element.pickable === false || !isActiveVisibleNode(root, element) || element.silent) return false;
4148
4288
  let ancestor = element.parent instanceof Element ? element.parent : void 0;
4149
4289
  while (ancestor) {
4290
+ if (!isActiveVisibleNode(root, ancestor) || ancestor.silent) return false;
4150
4291
  if (ancestor.getChildClipRect()) {
4151
4292
  const localPoint = ancestor.transform.toLocal(point);
4152
4293
  if (!localPoint || !ancestor.containsChildClipPoint(localPoint)) return false;
4153
4294
  }
4295
+ if (ancestor.isLayerNode()) break;
4154
4296
  ancestor = ancestor.parent instanceof Element ? ancestor.parent : void 0;
4155
4297
  }
4156
4298
  return true;
4157
4299
  }
4158
4300
  /** 判断查询区域与元素 bounds 的交集是否仍通过 ancestor clips。 */
4159
- function hasAreaIntersection(element, area, boundsKind) {
4301
+ function hasAreaIntersection(root, element, area, boundsKind) {
4302
+ if (!isActiveVisibleNode(root, element)) return false;
4160
4303
  const bounds = boundsKind === "visual" ? element.transform.getWorldVisualBoundsView() : element.transform.getHitTestBounds();
4161
4304
  if (!Intersection.isAABBIntersect(area, bounds)) return false;
4162
4305
  const left = Math.max(area.left, bounds.minX);
@@ -4183,6 +4326,7 @@ function hasAreaIntersection(element, area, boundsKind) {
4183
4326
  ];
4184
4327
  let ancestor = element.parent instanceof Element ? element.parent : void 0;
4185
4328
  while (ancestor) {
4329
+ if (!isActiveVisibleNode(root, ancestor)) return false;
4186
4330
  const clip = ancestor.getChildClipRect();
4187
4331
  if (clip) {
4188
4332
  const localIntersection = [];
@@ -4195,6 +4339,7 @@ function hasAreaIntersection(element, area, boundsKind) {
4195
4339
  if (clipped.length === 0) return false;
4196
4340
  intersection = clipped.map((point) => ancestor.transform.toWorld(point));
4197
4341
  }
4342
+ if (ancestor.isLayerNode()) break;
4198
4343
  ancestor = ancestor.parent instanceof Element ? ancestor.parent : void 0;
4199
4344
  }
4200
4345
  return true;
@@ -4202,16 +4347,6 @@ function hasAreaIntersection(element, area, boundsKind) {
4202
4347
  function isActiveVisibleNode(root, element) {
4203
4348
  return element.isActive && !element.isUnmounted && element.activeRoot === root && element.visible;
4204
4349
  }
4205
- /** 判断元素及其祖先是否参与当前 Root 的区域选择。 */
4206
- function isAreaTarget(root, element) {
4207
- let node = element;
4208
- while (node) {
4209
- if (!isActiveVisibleNode(root, node)) return false;
4210
- if (node.isLayerNode()) break;
4211
- node = node.parent instanceof Element ? node.parent : void 0;
4212
- }
4213
- return true;
4214
- }
4215
4350
  /** 判断元素是否符合当前 Root 的 pointer target 语义。@internal */
4216
4351
  function isPointerTarget(root, element) {
4217
4352
  if (element.pickable === false) return false;
@@ -4248,8 +4383,7 @@ function* iteratePointElements(root, point, options = {}) {
4248
4383
  if (!element.isActive || element.isUnmounted || element.activeRoot !== root || element.layer !== layer || root.getPaintOrder(element) === void 0) continue;
4249
4384
  if (options.exclude?.has(element.key)) continue;
4250
4385
  if (scope && !isStrictDescendant(element, scope)) continue;
4251
- if (!isPointerTarget(root, element)) continue;
4252
- if (!isPointWithinAncestorClips(element, point)) continue;
4386
+ if (!isPointerTargetWithinAncestorClips(root, element, point)) continue;
4253
4387
  if (element.transform.hasPointHint(point)) yield element;
4254
4388
  }
4255
4389
  }
@@ -4338,7 +4472,7 @@ function queryAreaElements(root, area, options = {}) {
4338
4472
  for (let index = root.layers.length - 1; index >= 0; index--) {
4339
4473
  const hitElements = root.layers[index].queryCommittedElements(area, order);
4340
4474
  for (const element of hitElements) {
4341
- if (!isAreaTarget(root, element) || !isSelectable(element) || !hasAreaIntersection(element, area, bounds)) continue;
4475
+ if (!isSelectable(element) || !hasAreaIntersection(root, element, area, bounds)) continue;
4342
4476
  result.push(element);
4343
4477
  }
4344
4478
  }
@@ -6805,4 +6939,4 @@ var Layer = class Layer extends Element {
6805
6939
  }
6806
6940
  };
6807
6941
  //#endregion
6808
- export { ChangeImpact, CustomEvent, DEFAULT_CONNECTION_PORT_SCHEMA, EVENT_NAMES, Element, ElementProperties, ElementTransform, EventEmitter, FrameStabilizationError, Layer, LayerProperties, LayerTransform, Node, NodeLifecycleEvent, NodeProperties, Root, RootFrameRuntime, RootInputController, SKIN_DEFAULTS, SceneRegistry, Shape, ShapeProperties, ShapeTransform, Skin, TransformProperties, Viewport, createAuthoredLineSubpath, createGeometryQuery, createGeometrySnapshot, createLineSubpath, createPathLocalId, evaluatePathSegmentPoint, getElementConnectionPort, getElementConnectionPortPoint, getElementConnectionPortPoints, isFormControlTarget, isReparentTransformIntent, projectConnectionPortPoint, resolveAuthoredSubpath, resolveOrigin, resolveReferenceTransform, resolveReferenceTransformTarget, splitPathSegment };
6942
+ export { ChangeImpact, CustomEvent, DEFAULT_CONNECTION_PORT_SCHEMA, EVENT_NAMES, Element, ElementProperties, ElementTransform, EventEmitter, FrameStabilizationError, Layer, LayerProperties, LayerTransform, Node, NodeLifecycleEvent, NodeProperties, Root, RootFrameRuntime, RootInputController, SKIN_DEFAULTS, SceneRegistry, Shape, ShapeProperties, ShapeTransform, Skin, TransformProperties, Viewport, createAuthoredLineSubpath, createGeometryQuery, createGeometrySnapshot, createLineSubpath, createPathLocalId, evaluatePathSegmentPoint, getElementConnectionPort, getElementConnectionPortPoint, getElementConnectionPortPoints, isFormControlTarget, isReparentTransformIntent, projectConnectionPortPoint, resolveAuthoredGeometryFramePatch, resolveAuthoredSubpath, resolveOrigin, resolveReferenceTransform, resolveReferenceTransformTarget, splitPathSegment };
@@ -0,0 +1,5 @@
1
+ import { Bound, type Rect } from "@fulate/share";
2
+ /** Project a local rectangle into a matrix space and return its AABB. */
3
+ export declare function projectBounds(matrix: DOMMatrixReadOnly, bounds: Readonly<Rect> | null): Bound;
4
+ /** Compare a committed matrix with a previous scalar matrix sample. */
5
+ export declare function matrixChanged(matrix: DOMMatrixReadOnly, previousA: number, previousB: number, previousC: number, previousD: number, previousE: number, previousF: number): boolean;
@@ -7,10 +7,11 @@ import { ChangeImpact } from "./properties";
7
7
  export type GeometryBoundsView = Readonly<Rect & BoundingBox>;
8
8
  /** 一次 Layer damage 收集共享的子树 visual bounds memo。 @internal */
9
9
  export type DirtyBoundsMemo = Map<ElementTransform, Bound | null>;
10
- /** Element 构造链传递的 companion Transform constructor。 */
11
- export type ElementTransformConstructor<E extends Element = Element> = new (element: E) => ElementTransform<E>;
12
- /** Element 继承构造链内部使用的 existential companion constructor */
13
- export type ElementTransformClass = new (element: never) => ElementTransform;
10
+ /**
11
+ * Element 继承构造链内部使用的 companion constructor。默认参数保留
12
+ * existential 入口;具体 Element 子类可以同时声明 owner Transform
13
+ */
14
+ export type ElementTransformClass<E extends Element = Element, T extends ElementTransform<any> = ElementTransform<any>> = new (element: E) => T;
14
15
  /**
15
16
  * 一个 Element 唯一的变换、几何、damage 与子树派生状态 owner。
16
17
  *
@@ -191,6 +192,8 @@ export declare class ElementTransform<E extends Element = Element> {
191
192
  clearPreviousVisualBounds(): void;
192
193
  /** @internal 按影响范围失效几何并安排 Layer 提交。 */
193
194
  invalidate(impact: ChangeImpact, previousBounds?: Readonly<BoundingBox> | null): void;
195
+ /** A derived layout can change visual bounds during an already pending commit. @internal */
196
+ invalidateLayoutBounds(): void;
194
197
  /** 标记 local 几何需要重新构建。 */
195
198
  markGeometryNeedsLayout(notifyScene?: boolean): void;
196
199
  /** 安排当前元素所属 Layer 重绘。 @internal */
@@ -34,6 +34,11 @@ export interface TransformIntent {
34
34
  readonly worldDelta: DOMMatrixReadOnly;
35
35
  /** 连续旋转手势从 frozen baseline 累积的未折叠 world 角度。 */
36
36
  readonly unwrappedRotationDelta?: number;
37
+ /**
38
+ * Resize/flip 这类矢量编辑操作把线性变换写回 authored geometry,
39
+ * 而不是把它留在容器的 scale 上。普通 move/rotate 保持原有 pose owner。
40
+ */
41
+ readonly bakeGeometry?: boolean;
37
42
  }
38
43
  /** 只有显式 reparent 才携带的新 parent world reference。 */
39
44
  export interface ReparentTransformIntent extends TransformIntent {
@@ -50,6 +55,12 @@ export interface ReferenceTransformResolution {
50
55
  readonly framePatch: Readonly<ElementOptionPatch>;
51
56
  readonly authoredLocalDelta: DOMMatrixReadOnly;
52
57
  }
58
+ /**
59
+ * 用 owner 已算出的精确 authored bounds 修正 bake frame 的位置。
60
+ * AABB 经过旋转/错切后不能由原 bounds 四角精确推出;owner 在生成
61
+ * 新几何后把真实 bounds 交回这里,保持 frame world matrix 与目标一致。
62
+ */
63
+ export declare function resolveAuthoredGeometryFramePatch(baseline: Readonly<Pick<ReferenceTransformBaseline, "referenceBounds" | "frame">>, resolution: Readonly<ReferenceTransformResolution>, authoredBounds: Readonly<Rect>): Readonly<ElementOptionPatch>;
53
64
  /** 判断变换意图是否包含跨父级重挂载所需的目标父级矩阵。 */
54
65
  export declare function isReparentTransformIntent(intent: Readonly<TransformIntent | ReparentTransformIntent>): intent is Readonly<ReparentTransformIntent>;
55
66
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fulate/core",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",