@fulate/tools 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/README.md CHANGED
@@ -5,8 +5,10 @@ gesture、Content session、Line/LineTree branch draft 与 semantic History 边
5
5
  surface,`LineTool` 只保留 command/preview surface。Transform 的每个已派发 pointer 输入都立即从
6
6
  frozen baseline 解析并提交一个 trusted batch;Point gesture 继续只在每个显示帧消费最后一个 intent。
7
7
  每个 owner 都把 world delta 交给同一个 Core reference-transform kernel;普通 transform 与显式
8
- reparent intent由该 intent决定写 pose、resize-owned geometry或preserve-owned geometry。Group手势只写
9
- Group,DirectChild手势只写该childderived content bounds不形成command、History或补偿写入。
8
+ reparent intent由该 intent决定写 pose、resize-owned geometry或preserve-owned geometry。移动/旋转
9
+ Group时只写Group poseGroup或多选的尺寸调整、镜像会递归把同一个world仿射结果写回可编辑子元素
10
+ 的 authored geometry(anchor、cubic control、LineTree point),不会把尺寸变化留在Group scale上。
11
+ DirectChild手势只写该child;derived content bounds不形成command、History或补偿写入。
10
12
  Line/LineTree固定world paint padding只属于自身visual/render coverage,不进入Group selection、resize或
11
13
  snap bounds。所有工作复用现有`RootFrameRuntime`,Tools 不创建 RAF、timer 或第二 scheduler。
12
14
  `Select`、`Snap`、`Rule` 与 `LineTool` 都使用 Core 生成的普通 Node key。工具间协作由应用组合时交付
@@ -26,7 +28,7 @@ DirectChild开始axis Snap时,手势排除集按候选使用的同一selection
26
28
  ancestor Group不会成为自身拖拽候选,Group外部元素仍正常参与对齐。
27
29
 
28
30
  LineTree Point改变一个anchor时,即使当前pose包含resize或反向scale,也在同一owner change内保持
29
- 既有world matrix,未编辑points不发生位移;整树resize继续走O(1) pose路径。Group默认旋转中心与九点
31
+ 既有world matrix,未编辑points不发生位移;整树resize把拓扑点按完整仿射写回。Group默认旋转中心与九点
30
32
  pivot吸附按当前local visual/content selection bounds解析;稳定layout reference只拥有Group pose。
31
33
 
32
34
  Selection controls 是一份 immutable `SelectionOverlayProjection`。paint、control/body hit 与 cursor
@@ -143,8 +145,10 @@ select.resize({
143
145
  始终读取 authored/layout box,即使 `overflow="visible"` 且未换行的 glyph 超出该 box;`anchor` 省略等于
144
146
  `"top-left"`,支持九个 frame 固定点:`top-left`/`top`/`top-right`/`left`/`center`/`right`/
145
147
  `bottom-left`/`bottom`/`bottom-right`。这些值是 world logical 单位,不包含 Viewport 缩放;
146
- controller 会自动处理父级和多层 Group 变换,并通过同一个 trusted batch 和 History 记录。选中
147
- Group 时只改 Group pose/scale,不改写直接子元素的 authored width/height。
148
+ controller 会自动处理父级和多层 Group 变换,并通过同一个 trusted batch 和 History 记录。Group resize
149
+ 会递归改写可编辑子元素的 authored geometry;Group 自身不再承载这次尺寸变化的 scale。移动/旋转
150
+ Group 仍只改 Group pose。对 Rectangle/Text 等 frame-authored 子元素,尺寸变化写入它们自己的
151
+ frame width/height;对 VectorPath/Polygon/Line/LineTree,则写入点、控制柄或拓扑。
148
152
 
149
153
  `Select.flip()` 是同一 Transform 命令链上的镜像操作:
150
154
 
@@ -155,8 +159,8 @@ select.flip("vertical");
155
159
 
156
160
  镜像轴经过当前 selection frame 中心。单选旋转元素沿其 frame 自身水平/垂直轴镜像;多选的
157
161
  aggregate frame 为 world 轴对齐,因此沿 world 水平/垂直轴镜像。命令遵守 selection 的 resize 轴
158
- 策略,使用同一批 selected owner baseline、trusted batch 与 semantic History;选中 Group 时只写
159
- Group pose/scale,不 bake children authored geometry。空选择或被策略禁止的方向是普通 noop。
162
+ 策略,使用同一批 selected owner baseline、trusted batch 与 semantic HistoryGroup 不承载这次镜像的
163
+ scale,镜像会递归 bake children authored geometry。空选择或被策略禁止的方向是普通 noop。
160
164
 
161
165
  ## Keyboard、Clipboard 与应用命令 owner
162
166
 
@@ -214,10 +218,12 @@ const stopSelection = select.addEventListener("select:end", syncPanel);
214
218
  const stopGeometry = select.addEventListener("select:update", syncPanel);
215
219
  ```
216
220
 
217
- 场景持久化只监听 configured 语义完成事件:
218
-
219
- ```ts
220
- const stopPersistence = select.addEventListener(
221
+ 场景持久化只监听 configured 语义完成事件:
222
+
223
+ ```ts
224
+ import { serializeScene } from "@fulate/import";
225
+
226
+ const stopPersistence = select.addEventListener(
221
227
  "select:configured-change",
222
228
  () => saveToIndexedDB(serializeScene(root))
223
229
  );
@@ -0,0 +1,5 @@
1
+ import { type ConnectionPortRef, type Root } from "@fulate/core";
2
+ import { type EndpointRef } from "@fulate/ui";
3
+ import type { HistoryManager } from "../../history";
4
+ import type { ToolsConnectionIntent } from "../../select";
5
+ export declare function createToolsConnectionIntent(root: Root, history: HistoryManager, source: EndpointRef, target: ConnectionPortRef): ToolsConnectionIntent;
@@ -0,0 +1,166 @@
1
+ import { Element, type ConnectionPortRef, type OwnerPropertyChange, type Root, type StructureChange } from "@fulate/core";
2
+ import { LineTree, type LineTreeBranchTailSource, type LineDecoration } from "@fulate/ui";
3
+ import { type PointView, type Rect } from "@fulate/share";
4
+ import type { HistoryManager, HistoryScope } from "../../history";
5
+ import type { LineTool } from "../../line";
6
+ import type { Select, SelectedPropertyPatch } from "../../select";
7
+ import { type SelectionOverlayProjection, type SelectionOverlayTarget } from "../../select/projection";
8
+ import { type FlipDirection, type ResizeOptions } from "../../select/transform";
9
+ import type { Snap } from "../../select/snap";
10
+ import { type PointerIntent } from "./helpers";
11
+ import { PointGesture, SelectionPressGesture, TransformGesture } from "./gestures";
12
+ import type { ApplyCommandOptions, EditorInteractionState, LineDrawingSession, PointerGesture, SelectionIdentity } from "./types";
13
+ export type { EditorInteractionState, SelectControllerState, SelectionIdentity } from "./types";
14
+ export declare class EditorInteractionController {
15
+ readonly select: Select;
16
+ readonly root: Root;
17
+ readonly history: HistoryManager;
18
+ readonly frameFlush: any;
19
+ private readonly cleanups;
20
+ private selectionValue;
21
+ private owners;
22
+ private policy;
23
+ private rotationPivotPreview;
24
+ private selectionFrame;
25
+ private preserveSelectionFrame;
26
+ private projectionState;
27
+ private projectionOwners;
28
+ private projectionMembership;
29
+ private projectionTopology;
30
+ private overlayDirty;
31
+ private pendingSelectionUpdate;
32
+ private pendingSelectionRepair;
33
+ private rotationPivotDisplayOwner;
34
+ private hoveredTarget;
35
+ private hoveredContent;
36
+ private controllerState;
37
+ private pasteTask;
38
+ private pointerWorld;
39
+ marquee: Readonly<Rect> | null;
40
+ constructor(select: Select, root: Root, history: HistoryManager);
41
+ get selection(): readonly Element[];
42
+ get projection(): SelectionOverlayProjection;
43
+ publishConfiguredChange(changed: boolean | undefined): void;
44
+ /** 把纯外部通知排到当前同步命令、回放或 frame completion 返回之后。 */
45
+ private notifySelect;
46
+ get contentHover(): Element<import("@fulate/core").ElementProperties, import("@fulate/core").BaseElementOption<any>, Partial<Omit<import("@fulate/core").BaseElementOption<any>, "key" | "children">>>;
47
+ get overlayTarget(): SelectionOverlayTarget;
48
+ get editorElements(): readonly Element[];
49
+ get snapTool(): Snap | undefined;
50
+ get state(): EditorInteractionState;
51
+ /** @internal Captures the canonical selection and interaction identities. */
52
+ captureSelectionIdentity(): SelectionIdentity;
53
+ /** @internal Makes one concrete clipboard Promise the latest paste intent. */
54
+ beginPaste(task: Promise<void>): void;
55
+ /** @internal Tests whether one paste still owns the captured selection intent. */
56
+ ownsPasteSelection(identity: SelectionIdentity, task: Promise<void>): boolean;
57
+ /** @internal Releases the latest paste intent after its Promise settles. */
58
+ finishPaste(task: Promise<void>): void;
59
+ private get selectState();
60
+ private installSelectState;
61
+ private get activeGesture();
62
+ private set activeGesture(value);
63
+ private get activePointGesture();
64
+ private get pointState();
65
+ private set pointState(value);
66
+ private get activePointTarget();
67
+ private set activePointTarget(value);
68
+ private get contentSession();
69
+ private set contentSession(value);
70
+ private get contentHistory();
71
+ private set contentHistory(value);
72
+ private get lineSession();
73
+ private set lineSession(value);
74
+ private get lineTreeSession();
75
+ private set lineTreeSession(value);
76
+ private get rotationPivotState();
77
+ private installInput;
78
+ private readonly handleSceneChange;
79
+ private sceneChangeAffectsSelection;
80
+ private selectionGeometryIsDirty;
81
+ private invalidatePointTopology;
82
+ private resolvePolicy;
83
+ setSelection(elements: readonly Element[]): void;
84
+ private readonly pointProjectionOwnerIsCurrent;
85
+ private restoreSelectionForHistory;
86
+ private replaceSelection;
87
+ private requestSelectionEnd;
88
+ private pointContextIsCurrent;
89
+ private resolveCurrentPointContext;
90
+ private resolvePointGestureContext;
91
+ pointGestureOwnsCurrentContext(gesture: PointGesture): boolean;
92
+ private setPointState;
93
+ private exitPointState;
94
+ private setActivePointTarget;
95
+ private invalidateOverlay;
96
+ private shouldShowRotationPivot;
97
+ private showRotationPivotForCurrentOwner;
98
+ hasPendingOverlayWork(): boolean;
99
+ flushOverlayProjection(): void;
100
+ hasPendingCompletionWork(): boolean;
101
+ flushCompletionWork(): void;
102
+ resolveCursor(point: PointView | undefined, contentCursor: string): string;
103
+ private updateHover;
104
+ private applyAuthoredBatch;
105
+ private onPointerDown;
106
+ private onPointerMove;
107
+ private onPointerFinish;
108
+ private onDoubleClick;
109
+ private onKeyDown;
110
+ private captureTransformRecords;
111
+ private startTransform;
112
+ promoteSelectionPress(gesture: SelectionPressGesture): TransformGesture | null;
113
+ finishSelectionPress(gesture: SelectionPressGesture): void;
114
+ finishSelectionGesture(gesture: PointerGesture, selection?: readonly Element[]): void;
115
+ consumeTransform(gesture: TransformGesture, intent: PointerIntent): void;
116
+ private startPointGesture;
117
+ consumePoint(gesture: PointGesture, intent: PointerIntent, first: boolean): void;
118
+ deletePointTarget(): boolean;
119
+ nudge(dx: number, dy: number): void;
120
+ setProperties(patch: SelectedPropertyPatch): void;
121
+ private prepareTransformCommandFrame;
122
+ private applyTransformCommand;
123
+ resize(options?: Readonly<ResizeOptions>): void;
124
+ flip(direction: FlipDirection): void;
125
+ previewRotationPivot(owner: Element, point: PointView): void;
126
+ clearRotationPivotPreview(owner: Element): void;
127
+ commitRotationPivot(owner: Element, point: PointView): boolean;
128
+ private resolveRotationPivot;
129
+ setMarquee(rect: Readonly<Rect> | null): void;
130
+ finishPointerGesture(gesture: PointerGesture): void;
131
+ private pointConnectionCandidateIsCurrent;
132
+ finishPointConnectionGesture(gesture: PointGesture): void;
133
+ private beginContentEditing;
134
+ finishContentEditing(discard: boolean): void;
135
+ startLineDrawing(tool: LineTool): void;
136
+ private startLineTreeBranchDrawing;
137
+ private updateLineTreeSourceHover;
138
+ private confirmLineTreeSource;
139
+ private beginLineTreeBranchDraft;
140
+ private updateLineTreePreview;
141
+ private probeLineTreeTail;
142
+ private confirmLineTreePoint;
143
+ private confirmLineTreeBranchDrawing;
144
+ private updateLinePreview;
145
+ private probeLineAnchor;
146
+ private confirmLinePoint;
147
+ stopLineDrawing(): void;
148
+ cancelLineDrawing(): void;
149
+ private confirmLineDrawing;
150
+ getLineDrawingView(tool: LineTool): LineDrawingSession | {
151
+ readonly kind: "branch-draw";
152
+ readonly tool: LineTool;
153
+ readonly owner: LineTree;
154
+ readonly source: LineTreeBranchTailSource;
155
+ readonly tailDecoration: LineDecoration;
156
+ readonly confirmed: PointView[];
157
+ tailTarget: ConnectionPortRef | undefined;
158
+ preview: PointView | null;
159
+ snap: import("../../select/snap").PortSnapResult | null;
160
+ };
161
+ hasLineToolSession(tool: LineTool): boolean;
162
+ applyCommand(ownerEffects: readonly OwnerPropertyChange[], structureEffects: readonly StructureChange[], scope: HistoryScope | undefined, selectionAfter: readonly Element[], options?: ApplyCommandOptions): void;
163
+ finishCurrentInteraction(): void;
164
+ destroy(): void;
165
+ }
166
+ export declare function getEditorInteractionController(root: Root): EditorInteractionController;
@@ -0,0 +1,93 @@
1
+ import type { ConnectionPortRef, Element, Node } from "@fulate/core";
2
+ import type { EndpointConnectionRole, EndpointRef, EditorAuthoredGesture, PointEditTarget } from "@fulate/ui";
3
+ import type { PointView } from "@fulate/share";
4
+ import type { HistoryScope } from "../../history";
5
+ import type { ResizeHandle, SelectionOverlayProjection } from "../../select/projection";
6
+ import { type PointerIntent } from "./helpers";
7
+ import type { EditorInteractionController } from "./controller";
8
+ import type { PointerGesture, TransformOwnerRecord } from "./types";
9
+ export declare class TransformGesture implements PointerGesture {
10
+ readonly controller: EditorInteractionController;
11
+ readonly kind: "move" | "resize" | "rotate";
12
+ readonly start: PointView;
13
+ readonly frame: NonNullable<SelectionOverlayProjection["frame"]>;
14
+ readonly records: readonly TransformOwnerRecord[];
15
+ readonly snapPoints: readonly PointView[];
16
+ readonly movableLineEndpoints: readonly PointView[];
17
+ readonly snapExcludes: readonly Element[];
18
+ readonly rotationPivotWorld: PointView;
19
+ readonly history: HistoryScope | undefined;
20
+ readonly resizeHandle?: ResizeHandle;
21
+ private lastIntent;
22
+ private previousRotationAngle;
23
+ private unwrappedRotationDelta;
24
+ constructor(controller: EditorInteractionController, kind: "move" | "resize" | "rotate", start: PointView, initialIntent: PointerIntent, frame: NonNullable<SelectionOverlayProjection["frame"]>, records: readonly TransformOwnerRecord[], snapPoints: readonly PointView[], movableLineEndpoints: readonly PointView[], snapExcludes: readonly Element[], rotationPivotWorld: PointView, history: HistoryScope | undefined, resizeHandle?: ResizeHandle);
25
+ consumeRotationDelta(point: PointView): number;
26
+ probeForbiddenPort(point: PointView): boolean;
27
+ update(intent: PointerIntent): void;
28
+ ownsCurrentContext(): boolean;
29
+ finishInvalidated(): void;
30
+ finish(intent?: PointerIntent): void;
31
+ }
32
+ export declare class PointGesture implements PointerGesture, EditorAuthoredGesture {
33
+ readonly controller: EditorInteractionController;
34
+ readonly ownerKey: string;
35
+ readonly startParent: Node | undefined;
36
+ readonly startTarget: PointEditTarget;
37
+ readonly insertT: number | undefined;
38
+ readonly role: "endpoint" | "vertex" | "segment" | "handle";
39
+ readonly connectionRole: EndpointConnectionRole | null;
40
+ readonly snapExcludes: readonly Element[];
41
+ readonly history: HistoryScope | undefined;
42
+ readonly startPointerWorld: PointView;
43
+ readonly startTargetWorld: PointView;
44
+ private latestIntent;
45
+ private consumedIntent;
46
+ connectionTarget: ConnectionPortRef | undefined;
47
+ connectionTargetOwner: Element | undefined;
48
+ connectionTargetParent: Node | undefined;
49
+ constructor(controller: EditorInteractionController, ownerKey: string, startParent: Node | undefined, startTarget: PointEditTarget, insertT: number | undefined, role: "endpoint" | "vertex" | "segment" | "handle", connectionRole: EndpointConnectionRole | null, snapExcludes: readonly Element[], history: HistoryScope | undefined, startPointerWorld: PointView, startTargetWorld: PointView);
50
+ currentTarget: PointEditTarget;
51
+ ownsCurrentContext(): boolean;
52
+ get connectionSource(): EndpointRef | undefined;
53
+ resolveWorldPoint(pointer: PointView): {
54
+ x: number;
55
+ y: number;
56
+ };
57
+ update(intent: PointerIntent): void;
58
+ consumeLatestIntent(): void;
59
+ finish(intent?: PointerIntent, cancelled?: boolean): void;
60
+ finishInvalidated(): void;
61
+ }
62
+ export declare class PivotGesture implements PointerGesture {
63
+ readonly controller: EditorInteractionController;
64
+ readonly owner: Element;
65
+ readonly startPointer: PointView;
66
+ readonly startPivot: PointView;
67
+ private finalPoint;
68
+ constructor(controller: EditorInteractionController, owner: Element, startPointer: PointView, startPivot: PointView);
69
+ update(intent: PointerIntent): void;
70
+ finish(intent?: PointerIntent): void;
71
+ }
72
+ export declare class SelectionPressGesture implements PointerGesture {
73
+ readonly controller: EditorInteractionController;
74
+ readonly start: PointerIntent;
75
+ readonly candidate: Element | undefined;
76
+ readonly contentTarget: Element | null;
77
+ readonly directDragOwner?: Element;
78
+ constructor(controller: EditorInteractionController, start: PointerIntent, candidate: Element | undefined, contentTarget: Element | null, directDragOwner?: Element);
79
+ update(intent: PointerIntent): void;
80
+ finish(intent?: PointerIntent, cancelled?: boolean): void;
81
+ }
82
+ export declare class MarqueeGesture implements PointerGesture {
83
+ readonly controller: EditorInteractionController;
84
+ readonly start: PointView;
85
+ readonly candidate: Element | undefined;
86
+ readonly additive: boolean;
87
+ readonly initialSelection: readonly Element[];
88
+ private current;
89
+ private moved;
90
+ constructor(controller: EditorInteractionController, start: PointView, candidate: Element | undefined, additive: boolean, initialSelection: readonly Element[]);
91
+ update(intent: PointerIntent): void;
92
+ finish(intent?: PointerIntent, cancelled?: boolean): void;
93
+ }
@@ -0,0 +1,16 @@
1
+ import { Element, type FulateEvent, type OriginX, type OriginY } from "@fulate/core";
2
+ import { type PointView, type Rect } from "@fulate/share";
3
+ import type { PointEditingScope } from "@fulate/ui";
4
+ import type { PortSnapProbeResult } from "../../select/snap";
5
+ import type { SelectionOverlayTarget } from "../../select/projection";
6
+ export interface PointerIntent {
7
+ readonly point: PointView;
8
+ readonly shiftKey: boolean;
9
+ }
10
+ export declare function isBlockedPortProbe(probe: PortSnapProbeResult | null | undefined): boolean;
11
+ export declare function resolvePortProbeCursor(probe: PortSnapProbeResult | undefined): "not-allowed" | "crosshair";
12
+ export declare function pointIntent(event: FulateEvent): PointerIntent;
13
+ export declare function matchesMarquee(element: Element, rect: Readonly<Rect>, requireContainment: boolean): boolean;
14
+ export declare function sameTarget(left: SelectionOverlayTarget | null, right: SelectionOverlayTarget | null): boolean;
15
+ export declare function samePointScope(left: PointEditingScope, right: PointEditingScope): boolean;
16
+ export declare function resolveOriginPoint(bounds: Readonly<Rect>, x: OriginX, y: OriginY): PointView;
@@ -0,0 +1,2 @@
1
+ export { EditorInteractionController, getEditorInteractionController } from "./controller";
2
+ export type { EditorInteractionState, SelectControllerState, SelectionIdentity } from "./types";
@@ -0,0 +1,104 @@
1
+ import type { ConnectionPortRef, Element, Node } from "@fulate/core";
2
+ import type { ContentEditingSession, EndpointRef, LineDecoration, LineTree, LineTreeBranchTailSource, PointEditTarget, PointEditingScope, SelectionBehavior, TransformInteractionCapability } from "@fulate/ui";
3
+ import type { PointView } from "@fulate/share";
4
+ import type { HistoryScope } from "../../history";
5
+ import type { LineTool } from "../../line";
6
+ import type { ProjectionOwner, SelectionRotationPivot } from "../../select/projection";
7
+ import type { PortSnapResult } from "../../select/snap";
8
+ import type { PointerIntent } from "./helpers";
9
+ import type { MarqueeGesture, PointGesture, PivotGesture, SelectionPressGesture, TransformGesture } from "./gestures";
10
+ export interface SelectedOwner extends ProjectionOwner {
11
+ readonly behavior: SelectionBehavior;
12
+ }
13
+ export interface SelectionIdentity {
14
+ readonly owners: readonly SelectedOwner[];
15
+ readonly interaction: EditorInteractionState;
16
+ }
17
+ export interface TransformOwnerRecord {
18
+ readonly owner: Element;
19
+ readonly parent: Node | undefined;
20
+ readonly capability: TransformInteractionCapability;
21
+ readonly baseline: ReturnType<TransformInteractionCapability["captureTransformBaseline"]>;
22
+ }
23
+ export interface PointerGesture {
24
+ update(intent: PointerIntent): void;
25
+ finish(intent?: PointerIntent, cancelled?: boolean): void;
26
+ }
27
+ export interface ApplyCommandOptions {
28
+ readonly beforeDrain?: () => void;
29
+ readonly afterHistory?: () => void;
30
+ readonly installSelection?: () => boolean;
31
+ }
32
+ export type SelectNotification = "select:end" | "select:configured-change" | "select:undo" | "select:redo";
33
+ export interface SelectNotificationData {
34
+ readonly elements: readonly Element[];
35
+ }
36
+ export interface LineDrawingSession {
37
+ readonly tool: LineTool;
38
+ readonly resultType: "line" | "lineTree";
39
+ readonly tailDecoration: LineDecoration;
40
+ readonly history: HistoryScope | undefined;
41
+ readonly confirmed: PointView[];
42
+ headTarget: ConnectionPortRef | undefined;
43
+ tailTarget: ConnectionPortRef | undefined;
44
+ preview: PointView | null;
45
+ snap: PortSnapResult | null;
46
+ }
47
+ export interface PointState {
48
+ readonly ownerKey: string;
49
+ readonly scope: PointEditingScope;
50
+ }
51
+ export interface ActivePointTarget {
52
+ readonly ownerKey: string;
53
+ readonly target: PointEditTarget;
54
+ }
55
+ export type SelectControllerState = {
56
+ readonly kind: "idle";
57
+ readonly gesture: MarqueeGesture | null;
58
+ } | {
59
+ readonly kind: "transform";
60
+ readonly gesture: TransformGesture | PivotGesture | SelectionPressGesture | null;
61
+ } | {
62
+ readonly kind: "point";
63
+ readonly ownerKey: string;
64
+ readonly scope: PointEditingScope;
65
+ readonly gesture: PointGesture | SelectionPressGesture | TransformGesture | null;
66
+ readonly activeTarget: ActivePointTarget | null;
67
+ } | {
68
+ readonly kind: "port";
69
+ readonly ownerKey: string;
70
+ readonly scope: PointEditingScope;
71
+ readonly gesture: PointGesture;
72
+ readonly source: EndpointRef;
73
+ readonly activeTarget: ActivePointTarget | null;
74
+ } | {
75
+ readonly kind: "content";
76
+ readonly session: ContentEditingSession;
77
+ readonly history: HistoryScope | undefined;
78
+ };
79
+ export type LineTreeDrawingSession = {
80
+ readonly kind: "source-pick";
81
+ readonly tool: LineTool;
82
+ readonly owner: LineTree;
83
+ readonly tailDecoration: LineDecoration;
84
+ hover: LineTreeBranchTailSource | null;
85
+ } | {
86
+ readonly kind: "branch-draw";
87
+ readonly tool: LineTool;
88
+ readonly owner: LineTree;
89
+ readonly source: LineTreeBranchTailSource;
90
+ readonly tailDecoration: LineDecoration;
91
+ readonly confirmed: PointView[];
92
+ tailTarget: ConnectionPortRef | undefined;
93
+ preview: PointView | null;
94
+ snap: PortSnapResult | null;
95
+ };
96
+ export type EditorInteractionState = {
97
+ readonly kind: "select";
98
+ readonly interaction: SelectControllerState;
99
+ readonly selection: readonly Element[];
100
+ readonly pivot: SelectionRotationPivot;
101
+ } | {
102
+ readonly kind: "line-draw";
103
+ readonly session: LineDrawingSession | LineTreeDrawingSession;
104
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { checkElement } from "./select/checkElement";
2
1
  export { Select } from "./select/index";
3
2
  export type { ConnectionGesture, CopyContext, ClipboardDataProducer, DeleteContext, FlipDirection, PasteContext, PasteOptions, PasteParentResolver, ResizeAnchor, ResizeOptions, SelectedPropertyPatch, SelectKeyboardCommands, SelectOption, ToolsConnectionIntent } from "./select/index";
4
3
  export { Snap } from "./select/snap";
@@ -13,5 +12,3 @@ export { alignElements } from "./select/align";
13
12
  export type { AlignType } from "./select/align";
14
13
  export { ArrangeType } from "./select/arrange";
15
14
  export type { ClipboardPlugin } from "./select/clipboard";
16
- export { serializeScene, serializeSceneToJSON, deserializeElement, restoreScene, parseFileData, exportToFile, importFromFile } from "@fulate/import";
17
- export type { ElementFilter, FileData, DeserializeFactories, DeserializeFactory } from "@fulate/import";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Element, ElementProperties, ElementTransform, Layer, isFormControlTarget, projectConnectionPortPoint, resolveOrigin, resolveReferenceTransform } from "@fulate/core";
2
- import { BaseLine, ContentEditingCapabilityToken, DEFAULT_LINE_TREE_BRANCH_STYLE, Group, Line, LineTree, PointEditingCapabilityToken, SelectionBehaviorCapabilityToken, SelectionBehaviorMode, SelectionDiveMode, SelectionSnapMode, SnapSourceCapabilityToken, TransformInteractionCapabilityToken, addLineTreeBranchAtTail, applyLineTreeTopologyDelta, connectionPortAccepts, createLineTreeTopology, createLineTreeTopologyDelta, getEditorCapability, getEditorFrameFlush, getEndpointRelationIndex, hasConnectionPortCapacity, isEndpointRelationSource, moveLineTreePoint, remapLineTreeRelations, resolveConnectEndpointChange, resolveConnectionPortChanges, resolveDisconnectEndpointChanges, workspaceFocusInsetsKey } from "@fulate/ui";
3
- import { Bound, Intersection, makeBoundingBoxFromRects, qrDecompose, transformPoint } from "@fulate/share";
4
- import { deserializeElement, deserializeElement as deserializeElement$1, exportToFile, importFromFile, parseFileData, restoreScene, serializeScene, serializeSceneToJSON } from "@fulate/import";
2
+ import { BaseLine, ContentEditingCapabilityToken, DEFAULT_LINE_TREE_BRANCH_STYLE, Group, Line, LineTree, PointEditingCapabilityToken, SelectionBehaviorCapabilityToken, SelectionBehaviorMode, SelectionDiveMode, SelectionSnapMode, SnapSourceCapabilityToken, TransformInteractionCapabilityToken, addLineTreeBranchAtTail, applyLineTreeTopologyDelta, connectionPortAccepts, createLineTreeTopology, createLineTreeTopologyDelta, getEditorCapability, getEditorFrameFlush, getEndpointRelationIndex, hasConnectionPortCapacity, isEndpointRelationSource, moveLineTreePoint, remapLineTreeRelations, resolveConnectEndpointChange, resolveConnectionPortChanges, resolveDisconnectEndpointChanges, resolveSelectionFrameBounds, workspaceFocusInsetsKey } from "@fulate/ui";
3
+ import { Bound, Intersection, isIdentityMatrix, qrDecompose, transformPoint } from "@fulate/share";
4
+ import { deserializeElement } from "@fulate/import";
5
5
  import { isNull, isString, isUndefined } from "lodash-es";
6
6
  import RBush from "rbush";
7
7
  //#region packages/tools/src/select/checkElement.ts
@@ -33,6 +33,12 @@ function checkElement(element, excludes = [], scopeRootKey) {
33
33
  return getEditorCapability(resolved, SelectionBehaviorCapabilityToken) ? resolved : void 0;
34
34
  }
35
35
  //#endregion
36
+ //#region packages/tools/src/select/identity.ts
37
+ /** Compare stable point-edit targets without reading live geometry. */
38
+ function samePointEditTarget(left, right) {
39
+ return left.kind === right.kind && (left.kind === "anchor" ? right.kind === "anchor" && left.anchorId === right.anchorId : left.kind === "segment" ? right.kind === "segment" && left.segmentId === right.segmentId : right.kind === "handle" && left.segmentId === right.segmentId && left.role === right.role);
40
+ }
41
+ //#endregion
36
42
  //#region packages/tools/src/select/projection.ts
37
43
  var ROTATE_CURSOR = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"></path><path d="M21 13a9 9 0 1 1-3-7.7L21 8"></path></svg>') 9 9, crosshair`;
38
44
  var RESIZE_CURSORS = {
@@ -81,8 +87,37 @@ var RESIZE_TARGETS = {
81
87
  };
82
88
  var ROTATE_TARGET = { kind: "rotate" };
83
89
  var PIVOT_TARGET = { kind: "pivot" };
84
- function samePointEditTarget(left, right) {
85
- return left.kind === right.kind && (left.kind === "anchor" ? right.kind === "anchor" && left.anchorId === right.anchorId : left.kind === "segment" ? right.kind === "segment" && left.segmentId === right.segmentId : right.kind === "handle" && left.segmentId === right.segmentId && left.role === right.role);
90
+ function rectCorners$1(rect) {
91
+ const right = rect.left + rect.width;
92
+ const bottom = rect.top + rect.height;
93
+ return [
94
+ {
95
+ x: rect.left,
96
+ y: rect.top
97
+ },
98
+ {
99
+ x: right,
100
+ y: rect.top
101
+ },
102
+ {
103
+ x: right,
104
+ y: bottom
105
+ },
106
+ {
107
+ x: rect.left,
108
+ y: bottom
109
+ }
110
+ ];
111
+ }
112
+ /**
113
+ * Resolve the points that define an owner's world selection frame. A frame
114
+ * capability is authoritative (for example Text/KeyValue authored bounds);
115
+ * owners without one use the shared local visual/layout fallback.
116
+ */
117
+ function worldSelectionFramePoints(owner) {
118
+ const localFrame = resolveSelectionFrameBounds(owner.element, owner.transform);
119
+ const matrix = owner.element.transform.getWorldMatrixView();
120
+ return rectCorners$1(localFrame).map((point) => transformPoint(matrix, point));
86
121
  }
87
122
  var ROTATED_RESIZE_CURSORS = [
88
123
  "ns-resize",
@@ -135,20 +170,23 @@ function createSelectionFrameProjection({ left, top, width, height, angle }) {
135
170
  };
136
171
  }
137
172
  /** @internal Build a command frame from the current owner bounds. */
138
- function createWorldVisualBoundsSelectionFrame(owners, fallback) {
173
+ function createWorldSelectionFrame(owners, fallback) {
139
174
  if (owners.length === 0) return fallback ?? null;
140
175
  if (owners.length === 1) {
141
- if (owners[0].transform?.getSelectionFrameBounds) {
176
+ const ownerFrame = owners[0].transform?.getSelectionFrameBounds?.();
177
+ if (ownerFrame) {
142
178
  if (fallback) return fallback;
143
- return frameFromSingleOwner(owners[0]);
179
+ return frameFromSingleOwner(owners[0], ownerFrame);
144
180
  }
145
- const bounds = owners[0].element.transform.getWorldVisualBoundsView();
146
- if ((bounds.width === 0 || bounds.height === 0) && fallback) return fallback;
181
+ const bounds = resolveSelectionFrameBounds(owners[0].element, owners[0].transform);
182
+ const matrix = owners[0].element.transform.getWorldMatrixView();
183
+ const worldBounds = Bound.fromPoints(rectCorners$1(bounds).map((point) => transformPoint(matrix, point)));
184
+ if ((worldBounds.width === 0 || worldBounds.height === 0) && fallback) return fallback;
147
185
  return createSelectionFrameProjection({
148
- left: bounds.left,
149
- top: bounds.top,
150
- width: bounds.width,
151
- height: bounds.height,
186
+ left: worldBounds.minX,
187
+ top: worldBounds.minY,
188
+ width: worldBounds.width,
189
+ height: worldBounds.height,
152
190
  angle: 0
153
191
  });
154
192
  }
@@ -156,13 +194,13 @@ function createWorldVisualBoundsSelectionFrame(owners, fallback) {
156
194
  let top = Infinity;
157
195
  let right = -Infinity;
158
196
  let bottom = -Infinity;
159
- for (const { element } of owners) {
160
- const bounds = element.transform.getWorldVisualBoundsView();
161
- if (bounds.left < left) left = bounds.left;
162
- if (bounds.top < top) top = bounds.top;
163
- if (bounds.left + bounds.width > right) right = bounds.left + bounds.width;
164
- if (bounds.top + bounds.height > bottom) bottom = bounds.top + bounds.height;
197
+ for (const owner of owners) for (const point of worldSelectionFramePoints(owner)) {
198
+ if (point.x < left) left = point.x;
199
+ if (point.y < top) top = point.y;
200
+ if (point.x > right) right = point.x;
201
+ if (point.y > bottom) bottom = point.y;
165
202
  }
203
+ if (!Number.isFinite(left) || !Number.isFinite(top) || !Number.isFinite(right) || !Number.isFinite(bottom)) return fallback ?? null;
166
204
  const width = right - left;
167
205
  const height = bottom - top;
168
206
  if ((width === 0 || height === 0) && fallback) return fallback;
@@ -212,10 +250,8 @@ function createSelectionRotationPivotSnapMarkers(frame) {
212
250
  vertical
213
251
  })));
214
252
  }
215
- function frameFromSingleOwner(owner) {
216
- const geometry = owner.element.transform;
217
- const bounds = owner.transform?.getSelectionFrameBounds?.() ?? geometry.getLocalVisualBoundsView() ?? geometry.getLayoutReferenceBoundsView();
218
- const matrix = geometry.getWorldMatrixView();
253
+ function frameFromSingleOwner(owner, bounds = resolveSelectionFrameBounds(owner.element, owner.transform)) {
254
+ const matrix = owner.element.transform.getWorldMatrixView();
219
255
  const decomposition = qrDecompose(matrix);
220
256
  const angleRadians = decomposition.angle * Math.PI / 180;
221
257
  const skewScaleX = matrix.c * Math.cos(angleRadians) + matrix.d * Math.sin(angleRadians);
@@ -237,7 +273,7 @@ function frameFromSingleOwner(owner) {
237
273
  function frameFromOwners(owners) {
238
274
  if (owners.length === 0) return null;
239
275
  if (owners.length === 1) return frameFromSingleOwner(owners[0]);
240
- return createWorldVisualBoundsSelectionFrame(owners);
276
+ return createWorldSelectionFrame(owners);
241
277
  }
242
278
  function resizeControls(frame, policy) {
243
279
  const x = policy.resize === "x" || policy.resize === "both";
@@ -306,8 +342,7 @@ function rotationControls(frame) {
306
342
  target: ROTATE_TARGET
307
343
  }));
308
344
  }
309
- function pointControls(owner, topology, activePointTarget) {
310
- const matrix = owner.element.transform.getWorldMatrixView();
345
+ function pointControls(topology, activePointTarget, matrix) {
311
346
  return topology.pointControls.map((control) => {
312
347
  return {
313
348
  world: transformPoint(matrix, control.local),
@@ -386,8 +421,12 @@ function createProjectionOwner(element, frameVisible = true, pointScope = null,
386
421
  function buildSelectionOverlayProjection({ owners, membership, topology, frame: frameOverride, policy, pivot, showPivot, pivotSnapMarkers = [], activePointTarget = null, marquee }) {
387
422
  const frame = frameOverride ?? frameFromOwners(owners);
388
423
  const controls = [];
389
- if (owners.length === 1) controls.push(...pointControls(owners[0], topology, activePointTarget));
390
- const rotationPivotWorld = frame && owners.length === 1 && pivot.kind === "custom-owner-local" && pivot.ownerKey === owners[0].element.key ? transformPoint(owners[0].element.transform.getWorldMatrixView(), pivot.local) : frame?.worldCenter ?? {
424
+ const singleOwner = owners.length === 1 ? owners[0] : void 0;
425
+ const singleOwnerMatrix = singleOwner !== void 0 && (topology.pointControls.length > 0 || frame !== null && pivot.kind === "custom-owner-local" && pivot.ownerKey === singleOwner.element.key) ? singleOwner.element.transform.getWorldMatrixView() : void 0;
426
+ if (owners.length === 1) {
427
+ if (topology.pointControls.length > 0) controls.push(...pointControls(topology, activePointTarget, singleOwnerMatrix));
428
+ }
429
+ const rotationPivotWorld = frame && owners.length === 1 && pivot.kind === "custom-owner-local" && pivot.ownerKey === owners[0].element.key ? transformPoint(singleOwnerMatrix, pivot.local) : frame?.worldCenter ?? {
391
430
  x: 0,
392
431
  y: 0
393
432
  };
@@ -520,21 +559,13 @@ function resolveRotationTransform({ frameAngle, pivot, unwrappedDelta, snapAngle
520
559
  };
521
560
  }
522
561
  //#endregion
523
- //#region packages/tools/src/editor/interaction-controller.ts
524
- var editorControllerKey = Symbol();
525
- var NO_PORT_SNAP$1 = {
526
- snap: null,
527
- state: null
528
- };
529
- var NO_ELEMENTS = [];
530
- var NO_POINTS = [];
562
+ //#region packages/tools/src/editor/interaction-controller/helpers.ts
531
563
  function isBlockedPortProbe(probe) {
532
564
  return probe?.state === "incompatible" || probe?.state === "forbidden";
533
565
  }
534
566
  function resolvePortProbeCursor(probe) {
535
567
  return isBlockedPortProbe(probe) ? "not-allowed" : "crosshair";
536
568
  }
537
- var NO_PROJECTION_OWNERS = [];
538
569
  function pointIntent(event) {
539
570
  return {
540
571
  point: {
@@ -573,6 +604,8 @@ function resolveOriginPoint(bounds, x, y) {
573
604
  y: bounds.top + (resolveOrigin(y) + .5) * bounds.height
574
605
  };
575
606
  }
607
+ //#endregion
608
+ //#region packages/tools/src/editor/interaction-controller/gestures.ts
576
609
  var TransformGesture = class {
577
610
  lastIntent;
578
611
  previousRotationAngle;
@@ -704,23 +737,6 @@ var PointGesture = class {
704
737
  }
705
738
  }
706
739
  };
707
- function createToolsConnectionIntent(root, history, source, target) {
708
- return {
709
- source,
710
- target,
711
- connect() {
712
- const change = resolveConnectEndpointChange(root, source, target);
713
- if (!change) return;
714
- const owner = root.getByKey(source.elementKey);
715
- const selection = owner instanceof Element ? [owner] : [];
716
- const scope = history.openScope(selection);
717
- root.applyChangeBatch([change], [], scope);
718
- root.frameRuntime.drain();
719
- const current = root.getByKey(source.elementKey);
720
- history.publishConfiguredChange(scope?.finish(current instanceof Element ? [current] : []));
721
- }
722
- };
723
- }
724
740
  var PivotGesture = class {
725
741
  finalPoint;
726
742
  constructor(controller, owner, startPointer, startPivot) {
@@ -826,6 +842,38 @@ var MarqueeGesture = class {
826
842
  this.controller.finishSelectionGesture(this, selection);
827
843
  }
828
844
  };
845
+ //#endregion
846
+ //#region packages/tools/src/editor/interaction-controller/connection-intent.ts
847
+ function createToolsConnectionIntent(root, history, source, target) {
848
+ return {
849
+ source,
850
+ target,
851
+ connect() {
852
+ const change = resolveConnectEndpointChange(root, source, target);
853
+ if (!change) return;
854
+ const owner = root.getByKey(source.elementKey);
855
+ const selection = owner instanceof Element ? [owner] : [];
856
+ const scope = history.openScope(selection);
857
+ root.applyChangeBatch([change], [], scope);
858
+ root.frameRuntime.drain();
859
+ const current = root.getByKey(source.elementKey);
860
+ history.publishConfiguredChange(scope?.finish(current instanceof Element ? [current] : []));
861
+ }
862
+ };
863
+ }
864
+ //#endregion
865
+ //#region packages/tools/src/editor/interaction-controller/controller.ts
866
+ var editorControllerKey = Symbol();
867
+ var NO_PORT_SNAP$1 = {
868
+ snap: null,
869
+ state: null
870
+ };
871
+ var NO_ELEMENTS = [];
872
+ var NO_POINTS = [];
873
+ var NO_PROJECTION_OWNERS = [];
874
+ function getOwnerSelectionFrameBounds(owner) {
875
+ return resolveSelectionFrameBounds(owner, getEditorCapability(owner, TransformInteractionCapabilityToken));
876
+ }
829
877
  var EditorInteractionController = class {
830
878
  frameFlush;
831
879
  cleanups = [];
@@ -1129,7 +1177,7 @@ var EditorInteractionController = class {
1129
1177
  const owner = this.owners[0].element;
1130
1178
  const preview = this.rotationPivotPreview?.owner === owner ? this.rotationPivotPreview.local : null;
1131
1179
  if (!preview && owner.rotationPivotX === "center" && owner.rotationPivotY === "center") return { kind: "selection-center" };
1132
- const bounds = owner.transform.getLocalVisualBoundsView() ?? owner.transform.getLayoutReferenceBoundsView();
1180
+ const bounds = getOwnerSelectionFrameBounds(owner);
1133
1181
  return {
1134
1182
  kind: "custom-owner-local",
1135
1183
  ownerKey: owner.key,
@@ -1714,7 +1762,7 @@ var EditorInteractionController = class {
1714
1762
  if (command && key === "a") {
1715
1763
  event.preventDefault();
1716
1764
  const elements = /* @__PURE__ */ new Set();
1717
- for (const element of this.root.queryArea(this.root.viewport.getWorldRect(), {
1765
+ for (const element of this.root.queryArea(this.root.viewport.getWorldRectView(), {
1718
1766
  bounds: "visual",
1719
1767
  order: "none"
1720
1768
  })) {
@@ -1852,9 +1900,10 @@ var EditorInteractionController = class {
1852
1900
  }
1853
1901
  const transformIntent = {
1854
1902
  worldDelta,
1903
+ ...gesture.kind === "resize" ? { bakeGeometry: true } : {},
1855
1904
  ...unwrappedRotationDelta === void 0 ? {} : { unwrappedRotationDelta }
1856
1905
  };
1857
- const changes = gesture.records.map((record) => record.capability.resolveTransformChange(record.baseline, transformIntent));
1906
+ const changes = gesture.records.flatMap((record) => record.capability.resolveTransformChange(record.baseline, transformIntent));
1858
1907
  this.selectionFrame = resolveSelectionFrameTransform(gesture.frame, worldDelta, gesture.kind);
1859
1908
  this.invalidatePointTopology();
1860
1909
  this.applyAuthoredBatch(changes, [], gesture.history);
@@ -2004,7 +2053,7 @@ var EditorInteractionController = class {
2004
2053
  const scope = this.history.openScope(this.selection);
2005
2054
  const worldDelta = new DOMMatrix().translate(dx, dy);
2006
2055
  const intent = { worldDelta };
2007
- const changes = records.map((record) => record.capability.resolveTransformChange(record.baseline, intent));
2056
+ const changes = records.flatMap((record) => record.capability.resolveTransformChange(record.baseline, intent));
2008
2057
  if (changes.length > 0) {
2009
2058
  if (frame) this.selectionFrame = resolveSelectionFrameTransform(frame, worldDelta, "move");
2010
2059
  this.invalidatePointTopology();
@@ -2046,11 +2095,14 @@ var EditorInteractionController = class {
2046
2095
  }
2047
2096
  return this.projectionState.frame;
2048
2097
  }
2049
- applyTransformCommand(worldDelta) {
2098
+ applyTransformCommand(worldDelta, bakeGeometry = false) {
2050
2099
  const records = this.captureTransformRecords();
2051
2100
  if (records.length !== this.owners.length) return;
2052
- const intent = { worldDelta };
2053
- const changes = records.map((record) => record.capability.resolveTransformChange(record.baseline, intent));
2101
+ const intent = {
2102
+ worldDelta,
2103
+ bakeGeometry
2104
+ };
2105
+ const changes = records.flatMap((record) => record.capability.resolveTransformChange(record.baseline, intent));
2054
2106
  this.invalidatePointTopology();
2055
2107
  const scope = this.history.openScope(this.selection);
2056
2108
  this.applyCommand(changes, [], scope, this.selection);
@@ -2058,7 +2110,7 @@ var EditorInteractionController = class {
2058
2110
  resize(options = {}) {
2059
2111
  const projectionFrame = this.prepareTransformCommandFrame();
2060
2112
  if (!projectionFrame) return;
2061
- const frame = this.owners.length === 1 ? createWorldVisualBoundsSelectionFrame(this.owners, projectionFrame) : projectionFrame;
2113
+ const frame = this.owners.length === 1 ? createWorldSelectionFrame(this.owners, projectionFrame) : projectionFrame;
2062
2114
  if (!frame) return;
2063
2115
  const resizeX = this.policy.resize === "x" || this.policy.resize === "both";
2064
2116
  const resizeY = this.policy.resize === "y" || this.policy.resize === "both";
@@ -2066,14 +2118,14 @@ var EditorInteractionController = class {
2066
2118
  const height = resizeY && options.height !== void 0 ? options.height : frame.height;
2067
2119
  if (width === frame.width && height === frame.height) return;
2068
2120
  const worldDelta = resolveResizeWorldDeltaForSize(frame, width, height, options.anchor);
2069
- if (worldDelta.a === 1 && worldDelta.b === 0 && worldDelta.c === 0 && worldDelta.d === 1 && worldDelta.e === 0 && worldDelta.f === 0) return;
2070
- this.applyTransformCommand(worldDelta);
2121
+ if (isIdentityMatrix(worldDelta)) return;
2122
+ this.applyTransformCommand(worldDelta, true);
2071
2123
  }
2072
2124
  flip(direction) {
2073
2125
  const frame = this.prepareTransformCommandFrame();
2074
2126
  if (!frame) return;
2075
2127
  if (!(direction === "horizontal" ? this.policy.resize === "x" || this.policy.resize === "both" : this.policy.resize === "y" || this.policy.resize === "both")) return;
2076
- this.applyTransformCommand(resolveFlipWorldDelta(frame, direction));
2128
+ this.applyTransformCommand(resolveFlipWorldDelta(frame, direction), true);
2077
2129
  }
2078
2130
  previewRotationPivot(owner, point) {
2079
2131
  const preview = this.resolveRotationPivot(owner, point, true);
@@ -2117,7 +2169,7 @@ var EditorInteractionController = class {
2117
2169
  if (!local) return;
2118
2170
  const frame = this.projectionState.frame;
2119
2171
  if (!frame) return;
2120
- const bounds = owner.transform.getLocalVisualBoundsView() ?? owner.transform.getLayoutReferenceBoundsView();
2172
+ const bounds = getOwnerSelectionFrameBounds(owner);
2121
2173
  const markers = createSelectionRotationPivotSnapMarkers(frame);
2122
2174
  if (snap) {
2123
2175
  const threshold = 8 / this.root.viewport.scale;
@@ -2930,7 +2982,7 @@ function alignElements(select, type) {
2930
2982
  const capability = getEditorCapability(rect.element, TransformInteractionCapabilityToken);
2931
2983
  if (!capability) continue;
2932
2984
  const baseline = capability.captureTransformBaseline();
2933
- changes.push(capability.resolveTransformChange(baseline, { worldDelta: new DOMMatrix().translate(rect.dx, rect.dy) }));
2985
+ changes.push(...capability.resolveTransformChange(baseline, { worldDelta: new DOMMatrix().translate(rect.dx, rect.dy) }));
2934
2986
  }
2935
2987
  if (changes.length === 0) return;
2936
2988
  const scope = select.history.openScope(elements);
@@ -3030,7 +3082,7 @@ function buildLayerMoves(select, selection, type) {
3030
3082
  else children.push(...elements);
3031
3083
  }
3032
3084
  return {
3033
- changes: moves.map(({ change }) => change),
3085
+ changes: moves.flatMap(({ change }) => change),
3034
3086
  structure: [...finalChildren].map(([owner, children]) => ({
3035
3087
  owner,
3036
3088
  children
@@ -3235,7 +3287,7 @@ function deserializeClones(entries, deserializers) {
3235
3287
  const json = structuredClone(entry);
3236
3288
  removeKeys(json);
3237
3289
  offsetTopLevel(json);
3238
- const element = deserializeElement$1(json, deserializers);
3290
+ const element = deserializeElement(json, deserializers);
3239
3291
  mapClonedKeys(entry, element, keyMap);
3240
3292
  return element;
3241
3293
  });
@@ -3404,6 +3456,47 @@ function hasSelectedAncestor(element, selected) {
3404
3456
  function parentWorld(parent) {
3405
3457
  return parent instanceof Element ? parent.transform.getChildWorldInverseMatrixView().inverse() : new DOMMatrix();
3406
3458
  }
3459
+ function rectCorners(rect) {
3460
+ const right = rect.left + rect.width;
3461
+ const bottom = rect.top + rect.height;
3462
+ return [
3463
+ {
3464
+ x: rect.left,
3465
+ y: rect.top
3466
+ },
3467
+ {
3468
+ x: right,
3469
+ y: rect.top
3470
+ },
3471
+ {
3472
+ x: right,
3473
+ y: bottom
3474
+ },
3475
+ {
3476
+ x: rect.left,
3477
+ y: bottom
3478
+ }
3479
+ ];
3480
+ }
3481
+ /**
3482
+ * Group creation uses the same owner frame that selection uses. Owners that
3483
+ * do not expose a frame retain their logical content-bound contract;
3484
+ * Text-like frame owners therefore cannot enlarge the new Group with visible
3485
+ * overflow.
3486
+ */
3487
+ function groupBounds(children) {
3488
+ let bounds = null;
3489
+ for (const child of children) {
3490
+ const frame = getEditorCapability(child, TransformInteractionCapabilityToken)?.getSelectionFrameBounds?.();
3491
+ const childBounds = frame ? Bound.fromPoints(rectCorners(frame).map((point) => transformPoint(child.transform.getWorldMatrixView(), point))) : child.transform.getContentBounds();
3492
+ if (!bounds) {
3493
+ bounds = childBounds;
3494
+ continue;
3495
+ }
3496
+ bounds = bounds.merge(childBounds);
3497
+ }
3498
+ return bounds ?? new Bound();
3499
+ }
3407
3500
  function captureChangesForParent(elements, targetParentWorldMatrix) {
3408
3501
  const changes = [];
3409
3502
  const intent = {
@@ -3413,7 +3506,7 @@ function captureChangesForParent(elements, targetParentWorldMatrix) {
3413
3506
  for (const element of elements) {
3414
3507
  const capability = getEditorCapability(element, TransformInteractionCapabilityToken);
3415
3508
  const baseline = capability.captureTransformBaseline();
3416
- changes.push(capability.resolveTransformChange(baseline, intent));
3509
+ changes.push(...capability.resolveTransformChange(baseline, intent));
3417
3510
  }
3418
3511
  return changes;
3419
3512
  }
@@ -3430,7 +3523,7 @@ function doGroup(select) {
3430
3523
  if (children.length <= 1) return null;
3431
3524
  const parent = children[0].parent;
3432
3525
  if (!parent) return null;
3433
- const bounds = makeBoundingBoxFromRects(children.map((element) => element.transform.getContentBounds()));
3526
+ const bounds = groupBounds(children);
3434
3527
  const targetWorld = new DOMMatrix().translate(bounds.left, bounds.top);
3435
3528
  const resolution = resolveReferenceTransform({
3436
3529
  writeMode: "pose-authored",
@@ -4791,7 +4884,7 @@ var Rule = class extends Element {
4791
4884
  ctx.clearRect(rulerSize, 0, w - rulerSize, rulerSize);
4792
4885
  ctx.fillStyle = this.backgroundColor;
4793
4886
  ctx.fillRect(rulerSize, 0, Math.max(0, w - rulerSize), rulerSize);
4794
- const vpRect = viewport.getWorldRect();
4887
+ const vpRect = viewport.getWorldRectView();
4795
4888
  const stepX = niceStep(pxPerTick, viewport.scale);
4796
4889
  const startWorldX = vpRect.left;
4797
4890
  const endWorldX = vpRect.left + vpRect.width;
@@ -4973,4 +5066,4 @@ var EditorLayer = class extends Layer {
4973
5066
  }
4974
5067
  };
4975
5068
  //#endregion
4976
- export { ArrangeType, EditorLayer, HistoryManager, LineTool, Rule, Select, Snap, alignElements, checkElement, deserializeElement, exportToFile, importFromFile, parseFileData, restoreScene, serializeScene, serializeSceneToJSON };
5069
+ export { ArrangeType, EditorLayer, HistoryManager, LineTool, Rule, Select, Snap, alignElements };
@@ -0,0 +1,3 @@
1
+ import type { PointEditTarget } from "@fulate/ui";
2
+ /** Compare stable point-edit targets without reading live geometry. */
3
+ export declare function samePointEditTarget(left: PointEditTarget, right: PointEditTarget): boolean;
@@ -109,10 +109,9 @@ export interface SelectionProjectionTopology {
109
109
  readonly pointScopeOutline: readonly PointView[] | null;
110
110
  readonly pointScopeValid: boolean;
111
111
  }
112
- export declare function samePointEditTarget(left: PointEditTarget, right: PointEditTarget): boolean;
113
112
  export declare function createSelectionFrameProjection({ left, top, width, height, angle }: Pick<SelectionFrameProjection, "left" | "top" | "width" | "height" | "angle">): SelectionFrameProjection;
114
113
  /** @internal Build a command frame from the current owner bounds. */
115
- export declare function createWorldVisualBoundsSelectionFrame(owners: readonly ProjectionOwner[], fallback?: SelectionFrameProjection | null): SelectionFrameProjection;
114
+ export declare function createWorldSelectionFrame(owners: readonly ProjectionOwner[], fallback?: SelectionFrameProjection | null): SelectionFrameProjection;
116
115
  export declare function createSelectionRotationPivotSnapMarkers(frame: SelectionFrameProjection): readonly SelectionRotationPivotSnapMarker[];
117
116
  export declare function createSelectionProjectionMembership(owners: readonly ProjectionOwner[], policy: ProjectionPolicy): SelectionProjectionMembership;
118
117
  export declare function createSelectionProjectionTopology(owners: readonly ProjectionOwner[], isCurrent?: (owner: ProjectionOwner) => boolean): SelectionProjectionTopology;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fulate/tools",
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",
@@ -1,339 +0,0 @@
1
- import { Element, Node, type ConnectionPortRef, type OwnerPropertyChange, type Root, type StructureChange } from "@fulate/core";
2
- import { LineTree, type EndpointConnectionRole, type EndpointRef, type ContentEditingSession, type EditorAuthoredGesture, type PointEditTarget, type PointEditingScope, type LineTreeBranchTailSource, type LineDecoration, type SelectionBehavior, type TransformInteractionCapability } from "@fulate/ui";
3
- import { type PointView, type Rect } from "@fulate/share";
4
- import type { HistoryManager, HistoryScope } from "../history";
5
- import type { LineTool } from "../line";
6
- import type { Select, SelectedPropertyPatch } from "../select";
7
- import { type ProjectionOwner, type ResizeHandle, type SelectionOverlayProjection, type SelectionOverlayTarget, type SelectionRotationPivot } from "../select/projection";
8
- import { type FlipDirection, type ResizeOptions } from "../select/transform";
9
- import type { PortSnapResult, Snap } from "../select/snap";
10
- interface SelectedOwner extends ProjectionOwner {
11
- readonly behavior: SelectionBehavior;
12
- }
13
- export interface SelectionIdentity {
14
- readonly owners: readonly SelectedOwner[];
15
- readonly interaction: EditorInteractionState;
16
- }
17
- interface TransformOwnerRecord {
18
- readonly owner: Element;
19
- readonly parent: Node | undefined;
20
- readonly capability: TransformInteractionCapability;
21
- readonly baseline: ReturnType<TransformInteractionCapability["captureTransformBaseline"]>;
22
- }
23
- interface PointerIntent {
24
- readonly point: PointView;
25
- readonly shiftKey: boolean;
26
- }
27
- interface PointerGesture {
28
- update(intent: PointerIntent): void;
29
- finish(intent?: PointerIntent, cancelled?: boolean): void;
30
- }
31
- interface ApplyCommandOptions {
32
- readonly beforeDrain?: () => void;
33
- readonly afterHistory?: () => void;
34
- readonly installSelection?: () => boolean;
35
- }
36
- interface LineDrawingSession {
37
- readonly tool: LineTool;
38
- readonly resultType: "line" | "lineTree";
39
- readonly tailDecoration: LineDecoration;
40
- readonly history: HistoryScope | undefined;
41
- readonly confirmed: PointView[];
42
- headTarget: ConnectionPortRef | undefined;
43
- tailTarget: ConnectionPortRef | undefined;
44
- preview: PointView | null;
45
- snap: PortSnapResult | null;
46
- }
47
- interface ActivePointTarget {
48
- readonly ownerKey: string;
49
- readonly target: PointEditTarget;
50
- }
51
- export type SelectControllerState = {
52
- readonly kind: "idle";
53
- readonly gesture: MarqueeGesture | null;
54
- } | {
55
- readonly kind: "transform";
56
- readonly gesture: TransformGesture | PivotGesture | SelectionPressGesture | null;
57
- } | {
58
- readonly kind: "point";
59
- readonly ownerKey: string;
60
- readonly scope: PointEditingScope;
61
- readonly gesture: PointGesture | SelectionPressGesture | TransformGesture | null;
62
- readonly activeTarget: ActivePointTarget | null;
63
- } | {
64
- readonly kind: "port";
65
- readonly ownerKey: string;
66
- readonly scope: PointEditingScope;
67
- readonly gesture: PointGesture;
68
- readonly source: EndpointRef;
69
- readonly activeTarget: ActivePointTarget | null;
70
- } | {
71
- readonly kind: "content";
72
- readonly session: ContentEditingSession;
73
- readonly history: HistoryScope | undefined;
74
- };
75
- type LineTreeDrawingSession = {
76
- readonly kind: "source-pick";
77
- readonly tool: LineTool;
78
- readonly owner: LineTree;
79
- readonly tailDecoration: LineDecoration;
80
- hover: LineTreeBranchTailSource | null;
81
- } | {
82
- readonly kind: "branch-draw";
83
- readonly tool: LineTool;
84
- readonly owner: LineTree;
85
- readonly source: LineTreeBranchTailSource;
86
- readonly tailDecoration: LineDecoration;
87
- readonly confirmed: PointView[];
88
- tailTarget: ConnectionPortRef | undefined;
89
- preview: PointView | null;
90
- snap: PortSnapResult | null;
91
- };
92
- export type EditorInteractionState = {
93
- readonly kind: "select";
94
- readonly interaction: SelectControllerState;
95
- readonly selection: readonly Element[];
96
- readonly pivot: SelectionRotationPivot;
97
- } | {
98
- readonly kind: "line-draw";
99
- readonly session: LineDrawingSession | LineTreeDrawingSession;
100
- };
101
- declare class TransformGesture implements PointerGesture {
102
- readonly controller: EditorInteractionController;
103
- readonly kind: "move" | "resize" | "rotate";
104
- readonly start: PointView;
105
- readonly frame: NonNullable<SelectionOverlayProjection["frame"]>;
106
- readonly records: readonly TransformOwnerRecord[];
107
- readonly snapPoints: readonly PointView[];
108
- readonly movableLineEndpoints: readonly PointView[];
109
- readonly snapExcludes: readonly Element[];
110
- readonly rotationPivotWorld: PointView;
111
- readonly history: HistoryScope | undefined;
112
- readonly resizeHandle?: ResizeHandle;
113
- private lastIntent;
114
- private previousRotationAngle;
115
- private unwrappedRotationDelta;
116
- constructor(controller: EditorInteractionController, kind: "move" | "resize" | "rotate", start: PointView, initialIntent: PointerIntent, frame: NonNullable<SelectionOverlayProjection["frame"]>, records: readonly TransformOwnerRecord[], snapPoints: readonly PointView[], movableLineEndpoints: readonly PointView[], snapExcludes: readonly Element[], rotationPivotWorld: PointView, history: HistoryScope | undefined, resizeHandle?: ResizeHandle);
117
- consumeRotationDelta(point: PointView): number;
118
- probeForbiddenPort(point: PointView): boolean;
119
- update(intent: PointerIntent): void;
120
- ownsCurrentContext(): boolean;
121
- finishInvalidated(): void;
122
- finish(intent?: PointerIntent): void;
123
- }
124
- declare class PointGesture implements PointerGesture, EditorAuthoredGesture {
125
- readonly controller: EditorInteractionController;
126
- readonly ownerKey: string;
127
- readonly startParent: Node | undefined;
128
- readonly startTarget: PointEditTarget;
129
- readonly insertT: number | undefined;
130
- readonly role: "endpoint" | "vertex" | "segment" | "handle";
131
- readonly connectionRole: EndpointConnectionRole | null;
132
- readonly snapExcludes: readonly Element[];
133
- readonly history: HistoryScope | undefined;
134
- readonly startPointerWorld: PointView;
135
- readonly startTargetWorld: PointView;
136
- private latestIntent;
137
- private consumedIntent;
138
- connectionTarget: ConnectionPortRef | undefined;
139
- connectionTargetOwner: Element | undefined;
140
- connectionTargetParent: Node | undefined;
141
- constructor(controller: EditorInteractionController, ownerKey: string, startParent: Node | undefined, startTarget: PointEditTarget, insertT: number | undefined, role: "endpoint" | "vertex" | "segment" | "handle", connectionRole: EndpointConnectionRole | null, snapExcludes: readonly Element[], history: HistoryScope | undefined, startPointerWorld: PointView, startTargetWorld: PointView);
142
- currentTarget: PointEditTarget;
143
- ownsCurrentContext(): boolean;
144
- get connectionSource(): EndpointRef | undefined;
145
- resolveWorldPoint(pointer: PointView): {
146
- x: number;
147
- y: number;
148
- };
149
- update(intent: PointerIntent): void;
150
- consumeLatestIntent(): void;
151
- finish(intent?: PointerIntent, cancelled?: boolean): void;
152
- finishInvalidated(): void;
153
- }
154
- declare class PivotGesture implements PointerGesture {
155
- readonly controller: EditorInteractionController;
156
- readonly owner: Element;
157
- readonly startPointer: PointView;
158
- readonly startPivot: PointView;
159
- private finalPoint;
160
- constructor(controller: EditorInteractionController, owner: Element, startPointer: PointView, startPivot: PointView);
161
- update(intent: PointerIntent): void;
162
- finish(intent?: PointerIntent): void;
163
- }
164
- declare class SelectionPressGesture implements PointerGesture {
165
- readonly controller: EditorInteractionController;
166
- readonly start: PointerIntent;
167
- readonly candidate: Element | undefined;
168
- readonly contentTarget: Element | null;
169
- readonly directDragOwner?: Element;
170
- constructor(controller: EditorInteractionController, start: PointerIntent, candidate: Element | undefined, contentTarget: Element | null, directDragOwner?: Element);
171
- update(intent: PointerIntent): void;
172
- finish(intent?: PointerIntent, cancelled?: boolean): void;
173
- }
174
- declare class MarqueeGesture implements PointerGesture {
175
- readonly controller: EditorInteractionController;
176
- readonly start: PointView;
177
- readonly candidate: Element | undefined;
178
- readonly additive: boolean;
179
- readonly initialSelection: readonly Element[];
180
- private current;
181
- private moved;
182
- constructor(controller: EditorInteractionController, start: PointView, candidate: Element | undefined, additive: boolean, initialSelection: readonly Element[]);
183
- update(intent: PointerIntent): void;
184
- finish(intent?: PointerIntent, cancelled?: boolean): void;
185
- }
186
- export declare class EditorInteractionController {
187
- readonly select: Select;
188
- readonly root: Root;
189
- readonly history: HistoryManager;
190
- readonly frameFlush: any;
191
- private readonly cleanups;
192
- private selectionValue;
193
- private owners;
194
- private policy;
195
- private rotationPivotPreview;
196
- private selectionFrame;
197
- private preserveSelectionFrame;
198
- private projectionState;
199
- private projectionOwners;
200
- private projectionMembership;
201
- private projectionTopology;
202
- private overlayDirty;
203
- private pendingSelectionUpdate;
204
- private pendingSelectionRepair;
205
- private rotationPivotDisplayOwner;
206
- private hoveredTarget;
207
- private hoveredContent;
208
- private controllerState;
209
- private pasteTask;
210
- private pointerWorld;
211
- marquee: Readonly<Rect> | null;
212
- constructor(select: Select, root: Root, history: HistoryManager);
213
- get selection(): readonly Element[];
214
- get projection(): SelectionOverlayProjection;
215
- publishConfiguredChange(changed: boolean | undefined): void;
216
- /** 把纯外部通知排到当前同步命令、回放或 frame completion 返回之后。 */
217
- private notifySelect;
218
- get contentHover(): Element<import("@fulate/core").ElementProperties, import("@fulate/core").BaseElementOption<any>, Partial<Omit<import("@fulate/core").BaseElementOption<any>, "key" | "children">>>;
219
- get overlayTarget(): SelectionOverlayTarget;
220
- get editorElements(): readonly Element[];
221
- get snapTool(): Snap | undefined;
222
- get state(): EditorInteractionState;
223
- /** @internal Captures the canonical selection and interaction identities. */
224
- captureSelectionIdentity(): SelectionIdentity;
225
- /** @internal Makes one concrete clipboard Promise the latest paste intent. */
226
- beginPaste(task: Promise<void>): void;
227
- /** @internal Tests whether one paste still owns the captured selection intent. */
228
- ownsPasteSelection(identity: SelectionIdentity, task: Promise<void>): boolean;
229
- /** @internal Releases the latest paste intent after its Promise settles. */
230
- finishPaste(task: Promise<void>): void;
231
- private get selectState();
232
- private installSelectState;
233
- private get activeGesture();
234
- private set activeGesture(value);
235
- private get activePointGesture();
236
- private get pointState();
237
- private set pointState(value);
238
- private get activePointTarget();
239
- private set activePointTarget(value);
240
- private get contentSession();
241
- private set contentSession(value);
242
- private get contentHistory();
243
- private set contentHistory(value);
244
- private get lineSession();
245
- private set lineSession(value);
246
- private get lineTreeSession();
247
- private set lineTreeSession(value);
248
- private get rotationPivotState();
249
- private installInput;
250
- private readonly handleSceneChange;
251
- private sceneChangeAffectsSelection;
252
- private selectionGeometryIsDirty;
253
- private invalidatePointTopology;
254
- private resolvePolicy;
255
- setSelection(elements: readonly Element[]): void;
256
- private readonly pointProjectionOwnerIsCurrent;
257
- private restoreSelectionForHistory;
258
- private replaceSelection;
259
- private requestSelectionEnd;
260
- private pointContextIsCurrent;
261
- private resolveCurrentPointContext;
262
- private resolvePointGestureContext;
263
- pointGestureOwnsCurrentContext(gesture: PointGesture): boolean;
264
- private setPointState;
265
- private exitPointState;
266
- private setActivePointTarget;
267
- private invalidateOverlay;
268
- private shouldShowRotationPivot;
269
- private showRotationPivotForCurrentOwner;
270
- hasPendingOverlayWork(): boolean;
271
- flushOverlayProjection(): void;
272
- hasPendingCompletionWork(): boolean;
273
- flushCompletionWork(): void;
274
- resolveCursor(point: PointView | undefined, contentCursor: string): string;
275
- private updateHover;
276
- private applyAuthoredBatch;
277
- private onPointerDown;
278
- private onPointerMove;
279
- private onPointerFinish;
280
- private onDoubleClick;
281
- private onKeyDown;
282
- private captureTransformRecords;
283
- private startTransform;
284
- promoteSelectionPress(gesture: SelectionPressGesture): TransformGesture | null;
285
- finishSelectionPress(gesture: SelectionPressGesture): void;
286
- finishSelectionGesture(gesture: PointerGesture, selection?: readonly Element[]): void;
287
- consumeTransform(gesture: TransformGesture, intent: PointerIntent): void;
288
- private startPointGesture;
289
- consumePoint(gesture: PointGesture, intent: PointerIntent, first: boolean): void;
290
- deletePointTarget(): boolean;
291
- nudge(dx: number, dy: number): void;
292
- setProperties(patch: SelectedPropertyPatch): void;
293
- private prepareTransformCommandFrame;
294
- private applyTransformCommand;
295
- resize(options?: Readonly<ResizeOptions>): void;
296
- flip(direction: FlipDirection): void;
297
- previewRotationPivot(owner: Element, point: PointView): void;
298
- clearRotationPivotPreview(owner: Element): void;
299
- commitRotationPivot(owner: Element, point: PointView): boolean;
300
- private resolveRotationPivot;
301
- setMarquee(rect: Readonly<Rect> | null): void;
302
- finishPointerGesture(gesture: PointerGesture): void;
303
- private pointConnectionCandidateIsCurrent;
304
- finishPointConnectionGesture(gesture: PointGesture): void;
305
- private beginContentEditing;
306
- finishContentEditing(discard: boolean): void;
307
- startLineDrawing(tool: LineTool): void;
308
- private startLineTreeBranchDrawing;
309
- private updateLineTreeSourceHover;
310
- private confirmLineTreeSource;
311
- private beginLineTreeBranchDraft;
312
- private updateLineTreePreview;
313
- private probeLineTreeTail;
314
- private confirmLineTreePoint;
315
- private confirmLineTreeBranchDrawing;
316
- private updateLinePreview;
317
- private probeLineAnchor;
318
- private confirmLinePoint;
319
- stopLineDrawing(): void;
320
- cancelLineDrawing(): void;
321
- private confirmLineDrawing;
322
- getLineDrawingView(tool: LineTool): LineDrawingSession | {
323
- readonly kind: "branch-draw";
324
- readonly tool: LineTool;
325
- readonly owner: LineTree;
326
- readonly source: LineTreeBranchTailSource;
327
- readonly tailDecoration: LineDecoration;
328
- readonly confirmed: PointView[];
329
- tailTarget: ConnectionPortRef | undefined;
330
- preview: PointView | null;
331
- snap: PortSnapResult | null;
332
- };
333
- hasLineToolSession(tool: LineTool): boolean;
334
- applyCommand(ownerEffects: readonly OwnerPropertyChange[], structureEffects: readonly StructureChange[], scope: HistoryScope | undefined, selectionAfter: readonly Element[], options?: ApplyCommandOptions): void;
335
- finishCurrentInteraction(): void;
336
- destroy(): void;
337
- }
338
- export declare function getEditorInteractionController(root: Root): EditorInteractionController;
339
- export {};