@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.js CHANGED
@@ -938,7 +938,35 @@ function buildLayerOutline(doc, lockedPaths = []) {
938
938
  }
939
939
  return roots;
940
940
  }
941
- function createDefaultLayer(type) {
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
- return {
989
+ case "TextLayer": {
990
+ const layer = {
963
991
  ...base,
964
992
  type,
965
- shape: { ...base.shape, width: 200, height: 60 },
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
- /** 打开/替换文档(转发 store;解码在宿主侧完成,内核只消费领域结构) */
2028
- openDocument(canvas) {
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,