@hankchen/canvas-editor 0.1.1 → 0.3.0
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 +283 -151
- package/dist/index.js +181 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -938,7 +938,35 @@ function buildLayerOutline(doc, lockedPaths = []) {
|
|
|
938
938
|
}
|
|
939
939
|
return roots;
|
|
940
940
|
}
|
|
941
|
-
|
|
941
|
+
var TEXT_PRESETS = [
|
|
942
|
+
{ id: "heading", fontSize: 40 },
|
|
943
|
+
{ id: "subheading", fontSize: 28 },
|
|
944
|
+
{ id: "body", fontSize: 16 }
|
|
945
|
+
];
|
|
946
|
+
function defaultNextTextStyle() {
|
|
947
|
+
const base = createDefaultLayer("TextLayer");
|
|
948
|
+
return {
|
|
949
|
+
font: base.font,
|
|
950
|
+
fontSize: base.fontSize,
|
|
951
|
+
fontColor: base.fontColor,
|
|
952
|
+
autowrap: base.autowrap,
|
|
953
|
+
lineHeight: base.shape.lineHeight
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function pickNextTextStylePatch(raw) {
|
|
957
|
+
const out = {};
|
|
958
|
+
if (raw === null || typeof raw !== "object") return out;
|
|
959
|
+
const source = raw;
|
|
960
|
+
if (typeof source.font === "string") out.font = source.font;
|
|
961
|
+
if (typeof source.fontSize === "number" && Number.isFinite(source.fontSize)) out.fontSize = source.fontSize;
|
|
962
|
+
if (typeof source.fontColor === "string") out.fontColor = source.fontColor;
|
|
963
|
+
if (typeof source.autowrap === "boolean") out.autowrap = source.autowrap;
|
|
964
|
+
if (typeof source.lineHeight === "number" && Number.isFinite(source.lineHeight) && source.lineHeight > 0) {
|
|
965
|
+
out.lineHeight = source.lineHeight;
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
969
|
+
function createDefaultLayer(type, textDefaults) {
|
|
942
970
|
const base = {
|
|
943
971
|
// name/visible 缺省态(layer-panel-ux 工单 01):未命名 + 可见,wire 键省略
|
|
944
972
|
name: "",
|
|
@@ -958,19 +986,26 @@ function createDefaultLayer(type) {
|
|
|
958
986
|
position: { anchor: "top-left", x: 0, y: 0 }
|
|
959
987
|
};
|
|
960
988
|
switch (type) {
|
|
961
|
-
case "TextLayer":
|
|
962
|
-
|
|
989
|
+
case "TextLayer": {
|
|
990
|
+
const layer = {
|
|
963
991
|
...base,
|
|
964
992
|
type,
|
|
965
|
-
shape: {
|
|
993
|
+
shape: {
|
|
994
|
+
...base.shape,
|
|
995
|
+
width: 200,
|
|
996
|
+
height: 60,
|
|
997
|
+
lineHeight: textDefaults?.lineHeight ?? base.shape.lineHeight
|
|
998
|
+
},
|
|
966
999
|
text: "\u6587\u672C",
|
|
967
1000
|
expression: null,
|
|
968
|
-
font: "",
|
|
969
|
-
fontSize: 24,
|
|
970
|
-
fontColor: "#111827",
|
|
1001
|
+
font: textDefaults?.font ?? "",
|
|
1002
|
+
fontSize: textDefaults?.fontSize ?? 24,
|
|
1003
|
+
fontColor: textDefaults?.fontColor ?? "#111827",
|
|
971
1004
|
angle: 0,
|
|
972
|
-
autowrap: false
|
|
1005
|
+
autowrap: textDefaults?.autowrap ?? false
|
|
973
1006
|
};
|
|
1007
|
+
return layer;
|
|
1008
|
+
}
|
|
974
1009
|
case "ImageLayer":
|
|
975
1010
|
return { ...base, type, shape: { ...base.shape, width: 200, height: 150 }, src: null, expression: null };
|
|
976
1011
|
case "QrCodeLayer":
|
|
@@ -1051,8 +1086,8 @@ function moveTableRowInDraft(draft, tablePath, fromRow, toRow) {
|
|
|
1051
1086
|
rows.splice(reduced, 0, moved);
|
|
1052
1087
|
return { from: fromRow, to: reduced };
|
|
1053
1088
|
}
|
|
1054
|
-
function addRootLayerInDraft(draft, type) {
|
|
1055
|
-
return insertRootLayerInDraft(draft, createDefaultLayer(type));
|
|
1089
|
+
function addRootLayerInDraft(draft, type, textDefaults) {
|
|
1090
|
+
return insertRootLayerInDraft(draft, createDefaultLayer(type, textDefaults));
|
|
1056
1091
|
}
|
|
1057
1092
|
function insertRootLayerInDraft(draft, layer) {
|
|
1058
1093
|
let min = Number.POSITIVE_INFINITY;
|
|
@@ -1407,6 +1442,9 @@ function resourceStatusesEqual(a, b) {
|
|
|
1407
1442
|
}
|
|
1408
1443
|
return true;
|
|
1409
1444
|
}
|
|
1445
|
+
function nextTextStyleEqual(a, b) {
|
|
1446
|
+
return a.font === b.font && a.fontSize === b.fontSize && a.fontColor === b.fontColor && a.autowrap === b.autowrap && a.lineHeight === b.lineHeight;
|
|
1447
|
+
}
|
|
1410
1448
|
var EditorStore = class {
|
|
1411
1449
|
docValue = null;
|
|
1412
1450
|
uiValue = {
|
|
@@ -1420,6 +1458,7 @@ var EditorStore = class {
|
|
|
1420
1458
|
editing: null,
|
|
1421
1459
|
renaming: null,
|
|
1422
1460
|
anchorExpanded: false,
|
|
1461
|
+
nextTextStyle: defaultNextTextStyle(),
|
|
1423
1462
|
dataSourceSchema: null,
|
|
1424
1463
|
guides: [],
|
|
1425
1464
|
rulersVisible: true,
|
|
@@ -1451,7 +1490,7 @@ var EditorStore = class {
|
|
|
1451
1490
|
}
|
|
1452
1491
|
/** 打开/替换文档:ui 选择/编辑/重命名会话与双向历史一并重置(新文档不继承旧路径/旧事务);
|
|
1453
1492
|
* 参考线/锁定集合/查找会话/建层武装与画拉会话随当次会话清空(对位轴/文档内容防护/
|
|
1454
|
-
*
|
|
1493
|
+
* 查询词/待命层型不跨文档),标尺显隐/锚点折叠/新层样式作为偏好保留 */
|
|
1455
1494
|
openDocument(canvas) {
|
|
1456
1495
|
this.docValue = canvas;
|
|
1457
1496
|
this.undoSteps = [];
|
|
@@ -1585,6 +1624,16 @@ var EditorStore = class {
|
|
|
1585
1624
|
this.uiValue = { ...this.uiValue, anchorExpanded: open };
|
|
1586
1625
|
this.notify({ scope: "ui", branch: "anchorExpanded" });
|
|
1587
1626
|
}
|
|
1627
|
+
/**
|
|
1628
|
+
* 新层样式槽逐字段合并(text-defaults 工单 01:捕获/预设/prefs 恢复的统一
|
|
1629
|
+
* 写入口):patch 并入现值,五字段值等短路(重复写同值零通知),不进历史。
|
|
1630
|
+
*/
|
|
1631
|
+
setNextTextStyle(patch) {
|
|
1632
|
+
const next = { ...this.uiValue.nextTextStyle, ...patch };
|
|
1633
|
+
if (nextTextStyleEqual(this.uiValue.nextTextStyle, next)) return;
|
|
1634
|
+
this.uiValue = { ...this.uiValue, nextTextStyle: next };
|
|
1635
|
+
this.notify({ scope: "ui", branch: "nextTextStyle" });
|
|
1636
|
+
}
|
|
1588
1637
|
/**
|
|
1589
1638
|
* 数据源 schema 声明替换(归一产物或降级 null,校验收口在注入缝不在这里):
|
|
1590
1639
|
* 同引用短路(重复注入同一归一产物不惊动订阅方);仅 ui 通知,不参与重绘
|
|
@@ -1797,6 +1846,27 @@ function classifyEditorShortcut(input, bindings = DEFAULT_EDITOR_SHORTCUTS) {
|
|
|
1797
1846
|
return null;
|
|
1798
1847
|
}
|
|
1799
1848
|
|
|
1849
|
+
// src/session/editorState.ts
|
|
1850
|
+
var EDITOR_STATE_SCHEMA_VERSION = 1;
|
|
1851
|
+
function parseCanvasEditorState(state) {
|
|
1852
|
+
const raw = state;
|
|
1853
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
1854
|
+
const source = raw;
|
|
1855
|
+
if (!Array.isArray(source.guides) || !Array.isArray(source.lockedPaths)) return null;
|
|
1856
|
+
const guides = [];
|
|
1857
|
+
for (const entry of source.guides) {
|
|
1858
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
1859
|
+
const { orientation, position } = entry;
|
|
1860
|
+
if (orientation !== "horizontal" && orientation !== "vertical") return null;
|
|
1861
|
+
if (typeof position !== "number" || !Number.isFinite(position)) return null;
|
|
1862
|
+
guides.push({ orientation, position });
|
|
1863
|
+
}
|
|
1864
|
+
for (const path of source.lockedPaths) {
|
|
1865
|
+
if (!Array.isArray(path)) return null;
|
|
1866
|
+
}
|
|
1867
|
+
return { guides, lockedPaths: source.lockedPaths };
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1800
1870
|
// src/editing/fontCatalog.ts
|
|
1801
1871
|
var FontCatalog = class {
|
|
1802
1872
|
builtin;
|
|
@@ -1931,6 +2001,12 @@ function clampFindCursor(cursor, count) {
|
|
|
1931
2001
|
if (count <= 0) return 0;
|
|
1932
2002
|
return Math.min(Math.max(cursor, 0), count - 1);
|
|
1933
2003
|
}
|
|
2004
|
+
function captureFieldOfKey(key) {
|
|
2005
|
+
const single = key.length === 1 ? key[0] : null;
|
|
2006
|
+
if (single === "font" || single === "fontSize" || single === "fontColor" || single === "autowrap") return single;
|
|
2007
|
+
if (key.length === 2 && key[0] === "shape" && key[1] === "lineHeight") return "lineHeight";
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
1934
2010
|
function hasValidExpressionFragment(template) {
|
|
1935
2011
|
return scanExpressionFragments(template).parts.some(
|
|
1936
2012
|
(part) => part.kind === "fragment" && part.error === null
|
|
@@ -2024,9 +2100,16 @@ var EditorSession = class {
|
|
|
2024
2100
|
return this.surfaceSize;
|
|
2025
2101
|
}
|
|
2026
2102
|
// ---- 相机动作(视口 {x, y, zoom} 走 ui 分支,不进历史) ----
|
|
2027
|
-
/**
|
|
2028
|
-
|
|
2103
|
+
/**
|
|
2104
|
+
* 打开/替换文档(转发 store;解码在宿主侧完成,内核只消费领域结构)。
|
|
2105
|
+
* state 传切片即按 restoreEditorState 同一管线原子应用(reset → apply 切片 →
|
|
2106
|
+
* prune)——「开了就恢复」,宿主切帧处不可能忘记重放;切片形状不合法整体拒
|
|
2107
|
+
* (画布照常以默认态打开),现网单参调用零破坏。
|
|
2108
|
+
*/
|
|
2109
|
+
openDocument(canvas, state) {
|
|
2029
2110
|
this.store.openDocument(canvas);
|
|
2111
|
+
if (state === void 0) return;
|
|
2112
|
+
this.restoreEditorState(state);
|
|
2030
2113
|
}
|
|
2031
2114
|
/**
|
|
2032
2115
|
* 预览视图(只读衍生):模板态表格在视图中获得一行预览行(文本格 = 表达式
|
|
@@ -2508,8 +2591,17 @@ var EditorSession = class {
|
|
|
2508
2591
|
* 武装建层(面板新增项/层型快捷键的统一入口,CONTEXT「武装」词条):置 ui
|
|
2509
2592
|
* 分支待命层型,下一次画布按下即开画拉;建层提交或 Esc 即解除——一次性
|
|
2510
2593
|
* 待命态,无粘性工具(spec 决策 2)。重复武装同型值等短路零通知。
|
|
2594
|
+
*
|
|
2595
|
+
* preset(text-defaults 工单 01,CONTEXT「文本预设」词条):文本预设 = 写
|
|
2596
|
+
* 新层样式槽的快捷方式——命中档位先把档位值并入槽(v1 只 fontSize),再
|
|
2597
|
+
* 武装;仅 TextLayer 生效;未知 id = 跳过写槽、武装照常(评审口径统一:
|
|
2598
|
+
* 「空转」只指写槽,武装层型本身仍合法)。
|
|
2511
2599
|
*/
|
|
2512
|
-
armLayerCreate(type) {
|
|
2600
|
+
armLayerCreate(type, preset) {
|
|
2601
|
+
if (preset !== void 0 && type === "TextLayer") {
|
|
2602
|
+
const entry = TEXT_PRESETS.find((candidate) => candidate.id === preset);
|
|
2603
|
+
if (entry) this.store.setNextTextStyle({ fontSize: entry.fontSize });
|
|
2604
|
+
}
|
|
2513
2605
|
this.store.setArmedCreate(type);
|
|
2514
2606
|
}
|
|
2515
2607
|
/** 解除建层武装(取消入口):仅清待命态,零副作用。 */
|
|
@@ -2587,7 +2679,7 @@ var EditorSession = class {
|
|
|
2587
2679
|
const clicked = Math.hypot(dx, dy) <= deadZoneScene;
|
|
2588
2680
|
const index = { v: -1 };
|
|
2589
2681
|
this.store.transact((draft) => {
|
|
2590
|
-
const layer = createDefaultLayer(gesture.type);
|
|
2682
|
+
const layer = createDefaultLayer(gesture.type, this.store.ui.nextTextStyle);
|
|
2591
2683
|
if (clicked) {
|
|
2592
2684
|
layer.position = { ...layer.position, x: gesture.startScene.x, y: gesture.startScene.y };
|
|
2593
2685
|
} else {
|
|
@@ -2738,6 +2830,58 @@ var EditorSession = class {
|
|
|
2738
2830
|
toggleRulers() {
|
|
2739
2831
|
this.store.setRulersVisible(!this.store.ui.rulersVisible);
|
|
2740
2832
|
}
|
|
2833
|
+
// ---- 编辑器状态导出与恢复(project-data 工单 01,spec §5):与 addGuide/toggleLayerLock 同门——ui 分支写、不进历史、不写 graph ----
|
|
2834
|
+
/**
|
|
2835
|
+
* 导出当前打开画布的编辑器状态切片:guides 原样带出(id 是会话内自增值,
|
|
2836
|
+
* 宿主不持久化其语义——restore 端重排重取号)、lockedPaths 原样。无文档也可
|
|
2837
|
+
* 导出(ui 分支自成事实,addGuide 无 doc 守卫同门)。内核帧盲:宿主把各帧
|
|
2838
|
+
* 切片按帧名组装成 frames 容器,加上 schemaVersion(EDITOR_STATE_SCHEMA_VERSION)
|
|
2839
|
+
* 与 getEditorPrefs() 才是完整工程数据快照,经宿主层存储——容器不进内核。
|
|
2840
|
+
*/
|
|
2841
|
+
exportEditorState() {
|
|
2842
|
+
return { guides: this.store.ui.guides, lockedPaths: this.store.ui.lockedPaths };
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* 恢复当前画布的状态切片(prefs 不在此——内核跨 openDocument 自持):轻结构
|
|
2846
|
+
* 校验(形状不合法整体拒,返 false 宿主记因)→ 整体替换 lockedPaths →
|
|
2847
|
+
* pruneDanglingPaths(悬空锁路径剔除——合法但内容悬空不算拒,prune 兜底)→
|
|
2848
|
+
* guide id 重排(清空后逐条 addGuide 重取号:快照 id 无持久语义,原样入栈必
|
|
2849
|
+
* 与新会话 guideIdSeq 撞号致 removeGuide 误删)。不进历史、不写 graph(红线
|
|
2850
|
+
* 同门)。
|
|
2851
|
+
*/
|
|
2852
|
+
restoreEditorState(state) {
|
|
2853
|
+
const slice = parseCanvasEditorState(state);
|
|
2854
|
+
if (slice === null) return false;
|
|
2855
|
+
this.store.setLockedPaths(slice.lockedPaths);
|
|
2856
|
+
this.pruneDanglingPaths();
|
|
2857
|
+
if (this.store.ui.guides.length > 0) this.store.setGuides([]);
|
|
2858
|
+
for (const guide of slice.guides) this.addGuide(guide);
|
|
2859
|
+
return true;
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* 只读偏好(保存 prefs 的读取口):与 store.ui 同源,给宿主一个不用下钻
|
|
2863
|
+
* store 的口。读 = getEditorPrefs,写 = applyEditorPrefs,对称成对;帧切片
|
|
2864
|
+
* 走 restoreEditorState,两不相混。
|
|
2865
|
+
*/
|
|
2866
|
+
getEditorPrefs() {
|
|
2867
|
+
const ui = this.store.ui;
|
|
2868
|
+
return {
|
|
2869
|
+
rulersVisible: ui.rulersVisible,
|
|
2870
|
+
anchorExpanded: ui.anchorExpanded,
|
|
2871
|
+
nextTextStyle: ui.nextTextStyle
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
/**
|
|
2875
|
+
* 应用偏好(会话建立时一次性,跨会话恢复 prefs 的写入口):整体替换
|
|
2876
|
+
* rulersVisible/anchorExpanded,不进历史;值等写入经 store 短路零通知。
|
|
2877
|
+
* 新层样式(text-defaults 工单 01)走宽容读逐字段合并(pickNextTextStylePatch
|
|
2878
|
+
* 共享谓词——与捕获同门,缺字/类型不符保留现值)。
|
|
2879
|
+
*/
|
|
2880
|
+
applyEditorPrefs(prefs) {
|
|
2881
|
+
this.store.setRulersVisible(prefs.rulersVisible);
|
|
2882
|
+
this.store.setAnchorExpanded(prefs.anchorExpanded);
|
|
2883
|
+
this.store.setNextTextStyle(pickNextTextStylePatch(prefs.nextTextStyle));
|
|
2884
|
+
}
|
|
2741
2885
|
// ---- 对齐画布(layer-align-snap 工单 01):图层盒整体对齐画布几何 ----
|
|
2742
2886
|
/**
|
|
2743
2887
|
* 对齐画布 action 族(对齐画布/贴边/贴角,ADR 0011):把 path 处图层盒整体
|
|
@@ -2964,6 +3108,23 @@ var EditorSession = class {
|
|
|
2964
3108
|
renormalizeAutoHeightInDraft(layer);
|
|
2965
3109
|
canonicalizeTableSyncInDraft(draft, path, key, this.textPolicies);
|
|
2966
3110
|
}, options);
|
|
3111
|
+
this.captureNextTextStyle(path, key, value);
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* 新层样式捕获(text-defaults 工单 01,CONTEXT「新层样式」词条):属性面板
|
|
3115
|
+
* 对 TextLayer 文本族五字段的权威写顺带更新偏好槽——「用户改过一次,下一个
|
|
3116
|
+
* 新层沿用」。捕获口唯一(updateSpec 漏斗):内容写(updateData)、样式粘贴
|
|
3117
|
+
* (搬运不是偏好)、撤销重做都不经过此口。字段合法性走 editing 域共享谓词
|
|
3118
|
+
* pickNextTextStylePatch(与 prefs 宽容读同门,评审合流不再双写):目标层非
|
|
3119
|
+
* TextLayer、键不命中或值类型不符即空转,符合字段经值等短路入槽。
|
|
3120
|
+
*/
|
|
3121
|
+
captureNextTextStyle(path, key, value) {
|
|
3122
|
+
const layer = this.store.doc ? resolveLayer(this.store.doc, path) : null;
|
|
3123
|
+
if (!layer || layer.type !== "TextLayer") return;
|
|
3124
|
+
const field = captureFieldOfKey(key);
|
|
3125
|
+
if (field === null) return;
|
|
3126
|
+
const patch = pickNextTextStylePatch({ [field]: value });
|
|
3127
|
+
if (patch[field] !== void 0) this.store.setNextTextStyle(patch);
|
|
2967
3128
|
}
|
|
2968
3129
|
/**
|
|
2969
3130
|
* 更新图层数据字段(wire data.value 的领域展开,按 type 分派):
|
|
@@ -3150,7 +3311,7 @@ var EditorSession = class {
|
|
|
3150
3311
|
if (!this.store.doc) return;
|
|
3151
3312
|
const index = { v: -1 };
|
|
3152
3313
|
this.store.transact((draft) => {
|
|
3153
|
-
index.v = addRootLayerInDraft(draft, type);
|
|
3314
|
+
index.v = addRootLayerInDraft(draft, type, this.store.ui.nextTextStyle);
|
|
3154
3315
|
});
|
|
3155
3316
|
if (index.v >= 0) this.store.setSelection(["layers", index.v]);
|
|
3156
3317
|
}
|
|
@@ -4161,6 +4322,7 @@ export {
|
|
|
4161
4322
|
CREATE_DEAD_ZONE_SCREEN_PX,
|
|
4162
4323
|
DEFAULT_EDITOR_SHORTCUTS,
|
|
4163
4324
|
DEFAULT_ZOOM_BOUNDS,
|
|
4325
|
+
EDITOR_STATE_SCHEMA_VERSION,
|
|
4164
4326
|
EditorSession,
|
|
4165
4327
|
EditorStore,
|
|
4166
4328
|
FontCatalog,
|
|
@@ -4176,6 +4338,7 @@ export {
|
|
|
4176
4338
|
RESIZE_MIN_SIZE_PX,
|
|
4177
4339
|
SNAP_THRESHOLD_SCREEN_PX,
|
|
4178
4340
|
STYLE_FIELD_KEYS_BY_TYPE,
|
|
4341
|
+
TEXT_PRESETS,
|
|
4179
4342
|
UploadHandlerMissingError,
|
|
4180
4343
|
VERTICAL_ALIGNS,
|
|
4181
4344
|
WHEEL_ZOOM_STEP,
|
|
@@ -4199,6 +4362,7 @@ export {
|
|
|
4199
4362
|
createDefaultLayer,
|
|
4200
4363
|
createRubberBandRect,
|
|
4201
4364
|
createTemplateTable,
|
|
4365
|
+
defaultNextTextStyle,
|
|
4202
4366
|
deleteLayerInDraft,
|
|
4203
4367
|
enumerateExpressionCandidates,
|
|
4204
4368
|
enumerateRowsPathCandidates,
|
|
@@ -4234,6 +4398,7 @@ export {
|
|
|
4234
4398
|
pastePosition,
|
|
4235
4399
|
pathStartsWith,
|
|
4236
4400
|
pathsEqual,
|
|
4401
|
+
pickNextTextStylePatch,
|
|
4237
4402
|
prepareRootPaste,
|
|
4238
4403
|
readSpecField,
|
|
4239
4404
|
remapPathAfterSplice,
|