@hankchen/canvas-editor 0.5.0 → 0.7.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
@@ -382,11 +382,22 @@ function resolveSnap(box, axes, threshold) {
382
382
 
383
383
  // src/spatial/create.ts
384
384
  var CREATE_DEAD_ZONE_SCREEN_PX = 4;
385
- function createRubberBandRect(type, start, current, axes, threshold) {
385
+ function createRubberBandRect(type, start, current, axes, threshold, ratio) {
386
386
  const x = Math.min(start.x, current.x);
387
387
  const y = Math.min(start.y, current.y);
388
- const width = Math.abs(current.x - start.x);
389
- const height = type === "QrCodeLayer" ? width : Math.abs(current.y - start.y);
388
+ const dx = Math.abs(current.x - start.x);
389
+ const dy = Math.abs(current.y - start.y);
390
+ let width = dx;
391
+ let height = dy;
392
+ if (type === "QrCodeLayer") {
393
+ height = width;
394
+ } else if (ratio) {
395
+ if (dx >= dy) {
396
+ height = Math.round(dx * ratio.h / ratio.w);
397
+ } else {
398
+ width = Math.round(dy * ratio.w / ratio.h);
399
+ }
400
+ }
390
401
  const resolution = resolveSnapPoints(
391
402
  { vertical: [x, x + width], horizontal: [y, y + height] },
392
403
  axes,
@@ -435,6 +446,38 @@ function resizeSnapPoints(start, handle, dx, dy) {
435
446
  if (SOUTH_HANDLES.has(handle)) horizontal.push(start.y + start.height + dy);
436
447
  return { vertical, horizontal };
437
448
  }
449
+ function proportionalDrivingAxis(handle, dx, dy) {
450
+ const corner = handleResizesWidth(handle) && handleResizesHeight(handle);
451
+ if (corner) return Math.abs(dx) >= Math.abs(dy) ? "width" : "height";
452
+ return handleResizesWidth(handle) ? "width" : "height";
453
+ }
454
+ function resizeBoxProportional(start, handle, reference, driving, dx, dy, minSize = RESIZE_MIN_SIZE_PX) {
455
+ const west = WEST_HANDLES.has(handle);
456
+ const north = NORTH_HANDLES.has(handle);
457
+ let width;
458
+ let height;
459
+ if (driving === "width") {
460
+ width = Math.max(west ? start.width - dx : start.width + dx, minSize);
461
+ height = Math.max(Math.round(width * reference.h / reference.w), minSize);
462
+ } else {
463
+ height = Math.max(north ? start.height - dy : start.height + dy, minSize);
464
+ width = Math.max(Math.round(height * reference.w / reference.h), minSize);
465
+ }
466
+ return {
467
+ x: west ? start.x + start.width - width : start.x,
468
+ y: north ? start.y + start.height - height : start.y,
469
+ width,
470
+ height
471
+ };
472
+ }
473
+ function resizeProportionalSnapPoints(start, handle, driving, dx, dy) {
474
+ if (driving === "width") {
475
+ const vertical = WEST_HANDLES.has(handle) ? [start.x + dx] : EAST_HANDLES.has(handle) ? [start.x + start.width + dx] : [];
476
+ return { vertical, horizontal: [] };
477
+ }
478
+ const horizontal = NORTH_HANDLES.has(handle) ? [start.y + dy] : SOUTH_HANDLES.has(handle) ? [start.y + start.height + dy] : [];
479
+ return { vertical: [], horizontal };
480
+ }
438
481
  var NO_AXES = { width: false, height: false };
439
482
  function resizableAxesAt(doc, path) {
440
483
  const layer = resolveLayer(doc, path);
@@ -1175,6 +1218,26 @@ function defaultNextTextStyle() {
1175
1218
  lineHeight: base.shape.lineHeight
1176
1219
  };
1177
1220
  }
1221
+ var RATIO_PRESETS = [
1222
+ { id: "free", ratio: null },
1223
+ { id: "1:1", ratio: { w: 1, h: 1 } },
1224
+ { id: "4:3", ratio: { w: 4, h: 3 } },
1225
+ { id: "3:4", ratio: { w: 3, h: 4 } },
1226
+ { id: "16:9", ratio: { w: 16, h: 9 } },
1227
+ { id: "9:16", ratio: { w: 9, h: 16 } }
1228
+ ];
1229
+ function ratioValue(id) {
1230
+ const entry = RATIO_PRESETS.find((candidate) => candidate.id === id);
1231
+ return entry?.ratio ? { ...entry.ratio } : null;
1232
+ }
1233
+ function findPresetEntry(type, presetId) {
1234
+ if (type === "TextLayer") return TEXT_PRESETS.find((candidate) => candidate.id === presetId);
1235
+ if (type === "ImageLayer") return RATIO_PRESETS.find((candidate) => candidate.id === presetId);
1236
+ return void 0;
1237
+ }
1238
+ function defaultSizeForRatio(ratio) {
1239
+ return ratio.w >= ratio.h ? { width: 200, height: Math.round(200 * ratio.h / ratio.w) } : { width: Math.round(200 * ratio.w / ratio.h), height: 200 };
1240
+ }
1178
1241
  function pickNextTextStylePatch(raw) {
1179
1242
  const out = {};
1180
1243
  if (raw === null || typeof raw !== "object") return out;
@@ -1243,7 +1306,8 @@ function createDefaultLayer(type, textDefaults) {
1243
1306
  }
1244
1307
  }
1245
1308
  function createTemplateTable(options) {
1246
- const table = createDefaultLayer("TableLayer");
1309
+ const defaultTable = createDefaultLayer("TableLayer");
1310
+ const table = options.shape ? { ...defaultTable, shape: { ...defaultTable.shape, width: options.shape.width, height: options.shape.height } } : defaultTable;
1247
1311
  const templateBase = createDefaultLayer("TableRowTemplate");
1248
1312
  const cellBase = createDefaultLayer("TableCellLayer");
1249
1313
  const contentBase = createDefaultLayer("TextLayer");
@@ -1676,6 +1740,8 @@ var EditorStore = class {
1676
1740
  drag: null,
1677
1741
  resize: null,
1678
1742
  armedCreate: null,
1743
+ armedCreateRowsPath: "",
1744
+ armedCreatePreset: "",
1679
1745
  create: null,
1680
1746
  editing: null,
1681
1747
  renaming: null,
@@ -1724,6 +1790,8 @@ var EditorStore = class {
1724
1790
  drag: null,
1725
1791
  resize: null,
1726
1792
  armedCreate: null,
1793
+ armedCreateRowsPath: "",
1794
+ armedCreatePreset: "",
1727
1795
  create: null,
1728
1796
  editing: null,
1729
1797
  renaming: null,
@@ -1820,10 +1888,23 @@ var EditorStore = class {
1820
1888
  this.uiValue = { ...this.uiValue, resize: gesture };
1821
1889
  this.notify({ scope: "ui", branch: "resize" });
1822
1890
  }
1823
- /** 建层武装置/解除(null,drag-create 工单 01);标量值等短路(重复武装同型不通知) */
1824
- setArmedCreate(type) {
1825
- if (this.uiValue.armedCreate === type) return;
1826
- this.uiValue = { ...this.uiValue, armedCreate: type };
1891
+ /**
1892
+ * 建层武装置/解除(null,drag-create 工单 01);标量值等短路(重复武装同型
1893
+ * 不通知)。rowsPath(template-table-drag-create 工单 01):模板表风味载荷
1894
+ * 与层型一体写;preset(canvas-web-preset-submenu 工单 01):预设载荷与层型
1895
+ * 一体写——同层型同载荷才短路(重复武装同型换载荷要更新),解除(null)
1896
+ * 一律清载荷(rowsPath 与 preset 双清)。
1897
+ */
1898
+ setArmedCreate(type, rowsPath = "", preset = "") {
1899
+ if (this.uiValue.armedCreate === type && this.uiValue.armedCreateRowsPath === rowsPath && this.uiValue.armedCreatePreset === preset) {
1900
+ return;
1901
+ }
1902
+ this.uiValue = {
1903
+ ...this.uiValue,
1904
+ armedCreate: type,
1905
+ armedCreateRowsPath: type === null ? "" : rowsPath,
1906
+ armedCreatePreset: type === null ? "" : preset
1907
+ };
1827
1908
  this.notify({ scope: "ui", branch: "armedCreate" });
1828
1909
  }
1829
1910
  /** 画拉橡皮筋会话开始/更新/结束(null);会话对象住 ui 分支,落库在 endCreate 单事务 */
@@ -1847,8 +1928,9 @@ var EditorStore = class {
1847
1928
  this.notify({ scope: "ui", branch: "anchorExpanded" });
1848
1929
  }
1849
1930
  /**
1850
- * 新层样式槽逐字段合并(text-defaults 工单 01:捕获/预设/prefs 恢复的统一
1851
- * 写入口):patch 并入现值,五字段值等短路(重复写同值零通知),不进历史。
1931
+ * 新层样式槽逐字段合并(text-defaults 工单 01:捕获/prefs 恢复的统一写入口;
1932
+ * canvas-web-preset-submenu 工单 01 起预设不再写槽):patch 并入现值,五字段
1933
+ * 值等短路(重复写同值零通知),不进历史。
1852
1934
  */
1853
1935
  setNextTextStyle(patch) {
1854
1936
  const next = { ...this.uiValue.nextTextStyle, ...patch };
@@ -2740,8 +2822,17 @@ var EditorSession = class {
2740
2822
  *
2741
2823
  * 拖不动写不动的同门守卫:无会话、目标层被结构编辑移除(悬空路径)整体
2742
2824
  * 空转(不发事务、不发布命中轴)。
2825
+ *
2826
+ * Shift 等比(image-ratio-preset 工单 01,spec 决策 5):仅 ImageLayer 生效
2827
+ * (其余层型 shift 空转走现状——自适应与尺寸耦合面不被触碰),且须双轴皆在
2828
+ * 可缩放面(格内容等强同步面派生轴写不出去,不进等比模式)、起始盒零维空转
2829
+ * (无比例可参照,回落自由)。参照手势起始盒实际宽高比;驱动轴 = 角柄取位
2830
+ * 移较大轴、边中柄取被拖轴;派生维 = round(驱动维 × 短比)(钳后驱动维跟随,
2831
+ * 最小钳位随动);对缘/对角固定照旧;吸附只在驱动轴移动缘候选(派生轴是算
2832
+ * 术产物不独立吸附,钳先于吸附同门)。等比模式双轴都落声明值(派生轴写算
2833
+ * 术积并清 auto 标志),canonicalize 以驱动轴键收口。
2743
2834
  */
2744
- resizeTo(sceneX, sceneY) {
2835
+ resizeTo(sceneX, sceneY, shift = false) {
2745
2836
  const gesture = this.store.ui.resize;
2746
2837
  if (!gesture) return;
2747
2838
  const doc = this.store.doc;
@@ -2751,12 +2842,21 @@ var EditorSession = class {
2751
2842
  const dy = sceneY - gesture.startScene.y;
2752
2843
  const widthActive = handleResizesWidth(gesture.handle);
2753
2844
  const heightActive = handleResizesHeight(gesture.handle);
2845
+ const axes = resizableAxesAt(doc, gesture.path);
2846
+ const proportionalDriving = shift && current.type === "ImageLayer" && gesture.startBox.width > 0 && gesture.startBox.height > 0 && axes.width && axes.height ? proportionalDrivingAxis(gesture.handle, dx, dy) : null;
2754
2847
  const resolution = resolveSnapPoints(
2755
- resizeSnapPoints(gesture.startBox, gesture.handle, dx, dy),
2848
+ proportionalDriving !== null ? resizeProportionalSnapPoints(gesture.startBox, gesture.handle, proportionalDriving, dx, dy) : resizeSnapPoints(gesture.startBox, gesture.handle, dx, dy),
2756
2849
  this.snapAxesForGesture(gesture.path),
2757
2850
  snapThresholdScene(this.store.ui.viewport.zoom)
2758
2851
  );
2759
- const target = resizeBox(
2852
+ const target = proportionalDriving !== null ? resizeBoxProportional(
2853
+ gesture.startBox,
2854
+ gesture.handle,
2855
+ { w: gesture.startBox.width, h: gesture.startBox.height },
2856
+ proportionalDriving,
2857
+ dx + resolution.dx,
2858
+ dy + resolution.dy
2859
+ ) : resizeBox(
2760
2860
  gesture.startBox,
2761
2861
  gesture.handle,
2762
2862
  widthActive ? dx + resolution.dx : dx,
@@ -2767,11 +2867,11 @@ var EditorSession = class {
2767
2867
  const layer = resolveLayer(draft, gesture.path);
2768
2868
  if (!layer) return;
2769
2869
  const shape = layer.shape;
2770
- if (widthActive) {
2870
+ if (widthActive || proportionalDriving !== null) {
2771
2871
  shape.width = target.width;
2772
2872
  shape.autoWidth = false;
2773
2873
  }
2774
- if (heightActive) {
2874
+ if (heightActive || proportionalDriving !== null) {
2775
2875
  shape.height = target.height;
2776
2876
  shape.autoHeight = false;
2777
2877
  }
@@ -2779,7 +2879,7 @@ var EditorSession = class {
2779
2879
  canonicalizeTableSyncInDraft(
2780
2880
  draft,
2781
2881
  gesture.path,
2782
- widthActive ? ["shape", "width"] : ["shape", "height"],
2882
+ proportionalDriving === "height" || proportionalDriving === null && !widthActive ? ["shape", "height"] : ["shape", "width"],
2783
2883
  this.textPolicies
2784
2884
  );
2785
2885
  const resolvedWidth = layerWidth(layer, this.textPolicies);
@@ -2812,24 +2912,38 @@ var EditorSession = class {
2812
2912
  /**
2813
2913
  * 武装建层(面板新增项/层型快捷键的统一入口,CONTEXT「武装」词条):置 ui
2814
2914
  * 分支待命层型,下一次画布按下即开画拉;建层提交或 Esc 即解除——一次性
2815
- * 待命态,无粘性工具(spec 决策 2)。重复武装同型值等短路零通知。
2915
+ * 待命态,无粘性工具(spec 决策 2)。重复武装同型同载荷值等短路零通知。
2816
2916
  *
2817
- * preset(text-defaults 工单 01,CONTEXT「文本预设」词条):文本预设 = 写
2818
- * 新层样式槽的快捷方式——命中档位先把档位值并入槽(v1 只 fontSize),再
2819
- * 武装;仅 TextLayer 生效;未知 id = 跳过写槽、武装照常(评审口径统一:
2820
- * 「空转」只指写槽,武装层型本身仍合法)。
2917
+ * preset(canvas-web-preset-submenu 工单 01,ADR 0021):层型相关预设随武装
2918
+ * 载荷一次性携带、**不再写任何偏好槽**——命中档位把档位 id 写进
2919
+ * ui.armedCreatePreset(endCreate 落库一次性消费,取消路零残留);未知 id =
2920
+ * 不落载荷、武装照常(评审口径统一:「空转」只指载荷,武装层型本身仍合法)。
2921
+ * - TextLayer + 文本预设档:载荷命中 TEXT_PRESETS(endCreate 档位字号直接
2922
+ * 生效,不经新层样式槽——普通武装文本按槽的语义不受染指);
2923
+ * - ImageLayer + 比例预设档:载荷命中 RATIO_PRESETS('free' 档载荷行为等价
2924
+ * 自由;QR 恒方是层型硬钳、无比例语义,spec 豁免面);
2925
+ * - 交叉类型(文本层带比例档/图片层带文本档)各在对方清单查无此档、不落载荷。
2821
2926
  */
2822
2927
  armLayerCreate(type, preset) {
2823
- if (preset !== void 0 && type === "TextLayer") {
2824
- const entry = TEXT_PRESETS.find((candidate) => candidate.id === preset);
2825
- if (entry) this.store.setNextTextStyle({ fontSize: entry.fontSize });
2826
- }
2827
- this.store.setArmedCreate(type);
2928
+ const entry = preset === void 0 ? void 0 : findPresetEntry(type, preset);
2929
+ this.store.setArmedCreate(type, "", entry !== void 0 ? entry.id : "");
2828
2930
  }
2829
- /** 解除建层武装(取消入口):仅清待命态,零副作用。 */
2931
+ /** 解除建层武装(取消入口):仅清待命态(含模板表风味与预设载荷),零副作用。 */
2830
2932
  cancelArmLayerCreate() {
2831
2933
  this.store.setArmedCreate(null);
2832
2934
  }
2935
+ /**
2936
+ * 武装模板表建层(canvas-web-template-table-drag-create 工单 01,spec 决策 3):
2937
+ * 行路径表单确认即武装——rowsPath 随武装进 ui 瞬态载荷(armedCreate 仍为
2938
+ * 'TableLayer'、不新增 LayerType、不进 graph),下一次画拉或死区点击落模板表。
2939
+ * 空串 no-op(内核防御,表单层已拦);重复武装同 rowsPath 值等短路,换
2940
+ * rowsPath 更新载荷。TableLayer 的 G 武装语义(V1 普通表,armLayerCreate 走
2941
+ * 空载荷)不受染指。
2942
+ */
2943
+ armTemplateTableCreate(rowsPath) {
2944
+ if (rowsPath === "") return;
2945
+ this.store.setArmedCreate("TableLayer", rowsPath);
2946
+ }
2833
2947
  /**
2834
2948
  * 开始画拉会话:须已武装(未武装空转防御——画布手势态机的分派前置,此守卫
2835
2949
  * 兜其余调用来源),与拖动/缩放会话互斥(手势起点一次性定死,beginDrag/
@@ -2842,8 +2956,12 @@ var EditorSession = class {
2842
2956
  if (armed === null || this.store.ui.drag || this.store.ui.resize || this.store.ui.create) return false;
2843
2957
  if (!this.store.doc) return false;
2844
2958
  const point = { x: sceneX, y: sceneY };
2959
+ const templateRowsPath = armed === "TableLayer" && this.store.ui.armedCreateRowsPath !== "" ? this.store.ui.armedCreateRowsPath : void 0;
2960
+ const preset = (armed === "TextLayer" || armed === "ImageLayer") && this.store.ui.armedCreatePreset !== "" ? this.store.ui.armedCreatePreset : void 0;
2845
2961
  this.store.setCreate({
2846
2962
  type: armed,
2963
+ templateRowsPath,
2964
+ preset,
2847
2965
  startScene: point,
2848
2966
  currentScene: point,
2849
2967
  rect: { x: sceneX, y: sceneY, width: 0, height: 0 }
@@ -2852,22 +2970,33 @@ var EditorSession = class {
2852
2970
  }
2853
2971
  /**
2854
2972
  * 画拉进行中:橡皮筋求位(spatial/create.createRubberBandRect——两点正规化
2855
- * + QR 钳 height := width + 左右/上下缘各两点进 resolveSnapPoints,吸附数学
2856
- * 零新增)写回会话 rect 供 overlay 直读(虚线矩形 + W×H 气泡),命中轴回写
2857
- * ui.snapAxes 走 GuidesOverlay 既有通道回显(dragTo 同门)。吸附排除面为空
2858
- * (snapAxesForGesture(null)——新层尚未入库,全部可见根层供轴,copyMode
2859
- * 首移同门)。零文档事务:修正只进会话几何,落库在 endCreate。
2973
+ * + QR 钳 height := width / 比例钳长轴驱动 + 左右/上下缘各两点进
2974
+ * resolveSnapPoints,吸附数学零新增)写回会话 rect 供 overlay 直读(虚线
2975
+ * 矩形 + W×H 气泡),命中轴回写 ui.snapAxes 走 GuidesOverlay 既有通道回显
2976
+ * (dragTo 同门)。吸附排除面为空(snapAxesForGesture(null)——新层尚未入库,
2977
+ * 全部可见根层供轴,copyMode 首移同门)。零文档事务:修正只进会话几何,
2978
+ * 落库在 endCreate。
2979
+ *
2980
+ * 建层预设档(canvas-web-preset-submenu 工单 01,ADR 0021,取代建层比例槽):
2981
+ * 仅 ImageLayer 按会话载荷档钳制(QR 层不传 ratio、恒方语义不动,两钳互斥;
2982
+ * 其余层型无比例语义);无载荷或 free 档 = 自由钳(普通武装恒自由)。
2983
+ * Shift 瞬时反转源 = 生效档(载荷档或自由):档具名按 Shift = 临时自由、
2984
+ * 生效档自由(含无载荷)按 Shift = 临时钳 1:1,按住生效松开还原(逐帧求位、
2985
+ * 不写载荷不改武装态,Figma 惯例的手势瞬时语义)。
2860
2986
  */
2861
- createTo(sceneX, sceneY) {
2987
+ createTo(sceneX, sceneY, shift = false) {
2862
2988
  const gesture = this.store.ui.create;
2863
2989
  const doc = this.store.doc;
2864
2990
  if (!gesture || !doc) return;
2991
+ const presetRatio = gesture.type === "ImageLayer" && gesture.preset !== void 0 ? ratioValue(gesture.preset) : null;
2992
+ const ratio = gesture.type === "ImageLayer" ? shift ? presetRatio === null ? { w: 1, h: 1 } : null : presetRatio : null;
2865
2993
  const resolution = createRubberBandRect(
2866
2994
  gesture.type,
2867
2995
  gesture.startScene,
2868
2996
  { x: sceneX, y: sceneY },
2869
2997
  this.snapAxesForGesture(null),
2870
- snapThresholdScene(this.store.ui.viewport.zoom)
2998
+ snapThresholdScene(this.store.ui.viewport.zoom),
2999
+ ratio
2871
3000
  );
2872
3001
  this.store.setCreate({
2873
3002
  ...gesture,
@@ -2881,12 +3010,24 @@ var EditorSession = class {
2881
3010
  * + priority)。先清会话再落文档(commitTextEdit 同门——退出路径级联时幂等):
2882
3011
  * - 死区外:单事务写入「createDefaultLayer 缺省形态 + position 左上角对准
2883
3012
  * 橡皮筋矩形(含吸附修正)+ shape.width/height」并 insertRootLayerInDraft
2884
- * 置顶(priority min−1、push 数组尾);
3013
+ * 置顶(priority min−1、push 数组尾)——尺寸取手势末态钳制结果(含 Shift
3014
+ * 反转末态,钳制源 = 会话预设档,canvas-web-preset-submenu 工单 01);
2885
3015
  * - 死区内(点击兜底,位移按 CREATE_DEAD_ZONE_SCREEN_PX 屏幕 px 随 zoom 折算,
2886
- * ALT_DRAG_DEAD_ZONE_SCREEN_PX 同门):缺省尺寸、左上角对准点击点。
3016
+ * ALT_DRAG_DEAD_ZONE_SCREEN_PX 同门):左上角对准点击点;尺寸按会话预设档
3017
+ * 派生缺省盒(ImageLayer 长边 200 定短边,自由/无载荷 = 工厂 200×150 现状;
3018
+ * 忽略 shift——无位移可供等比约束;其余层型保持工厂缺省)。
2887
3019
  * 落库后自动选中新层并解除武装;返回新层路径(绑定层消费:文本层据此自动
2888
3020
  * 进文本编辑,spec 决策 6)。无会话/无文档空转返回 null。点击兜底不产生
2889
3021
  * 零尺寸层(缺省形态自带可见尺寸)。
3022
+ *
3023
+ * 预设档一次性消费(canvas-web-preset-submenu 工单 01,ADR 0021):文本档
3024
+ * 字号直接生效(不经新层样式槽——字体/行高等槽外字段仍按槽,普通武装语义
3025
+ * 保留);图片档比供死区缺省盒派生。档位随会话捕获,落库后武装与载荷双清
3026
+ * (一次性语义,取消路零残留)。
3027
+ *
3028
+ * 模板表风味(template-table-drag-create 工单 01):会话载荷 templateRowsPath
3029
+ * 非空即分派 createTemplateTable 工厂(死区外 shape = 手势正规化矩形,死区内
3030
+ * 缺省壳 400×120、左上角对准点击点);模板层不自动进文本编辑(文本层专属)。
2890
3031
  */
2891
3032
  endCreate() {
2892
3033
  const gesture = this.store.ui.create;
@@ -2900,9 +3041,23 @@ var EditorSession = class {
2900
3041
  const deadZoneScene = CREATE_DEAD_ZONE_SCREEN_PX / this.store.ui.viewport.zoom;
2901
3042
  const clicked = Math.hypot(dx, dy) <= deadZoneScene;
2902
3043
  const index = { v: -1 };
3044
+ const templateRowsPath = gesture.templateRowsPath;
3045
+ const presetTextEntry = gesture.type === "TextLayer" && gesture.preset !== void 0 ? findPresetEntry(gesture.type, gesture.preset) : void 0;
3046
+ const textDefaults = presetTextEntry ? { ...this.store.ui.nextTextStyle, fontSize: presetTextEntry.fontSize } : this.store.ui.nextTextStyle;
3047
+ const presetRatio = gesture.type === "ImageLayer" && gesture.preset !== void 0 ? ratioValue(gesture.preset) : null;
2903
3048
  this.store.transact((draft) => {
2904
- const layer = createDefaultLayer(gesture.type, this.store.ui.nextTextStyle);
3049
+ const layer = templateRowsPath !== void 0 ? createTemplateTable(
3050
+ clicked ? { rowsPath: templateRowsPath } : {
3051
+ rowsPath: templateRowsPath,
3052
+ shape: { width: gesture.rect.width, height: gesture.rect.height }
3053
+ }
3054
+ ) : createDefaultLayer(gesture.type, textDefaults);
2905
3055
  if (clicked) {
3056
+ if (gesture.type === "ImageLayer" && presetRatio) {
3057
+ const size = defaultSizeForRatio(presetRatio);
3058
+ layer.shape.width = size.width;
3059
+ layer.shape.height = size.height;
3060
+ }
2906
3061
  layer.position = { ...layer.position, x: gesture.startScene.x, y: gesture.startScene.y };
2907
3062
  } else {
2908
3063
  layer.position = { ...layer.position, x: gesture.rect.x, y: gesture.rect.y };
@@ -2917,9 +3072,9 @@ var EditorSession = class {
2917
3072
  return path;
2918
3073
  }
2919
3074
  /**
2920
- * 取消画拉(Esc 语义):清橡皮筋会话、命中轴回显与武装态——零文档写入、
2921
- * 零历史步(拖拽期纯视觉因此取消零回退成本,spec 决策 4)。幂等:无会话时
2922
- * 仅解除武装(待命态 Esc 同走此口)。
3075
+ * 取消画拉(Esc 语义):清橡皮筋会话、命中轴回显与武装态(含模板表风味与
3076
+ * 预设载荷)——零文档写入、零历史步(拖拽期纯视觉因此取消零回退成本,spec
3077
+ * 决策 4)。幂等:无会话时仅解除武装(待命态 Esc 同走此口)。
2923
3078
  */
2924
3079
  cancelCreate() {
2925
3080
  this.store.setCreate(null);
@@ -3083,7 +3238,9 @@ var EditorSession = class {
3083
3238
  /**
3084
3239
  * 只读偏好(保存 prefs 的读取口):与 store.ui 同源,给宿主一个不用下钻
3085
3240
  * store 的口。读 = getEditorPrefs,写 = applyEditorPrefs,对称成对;帧切片
3086
- * 走 restoreEditorState,两不相混。
3241
+ * 走 restoreEditorState,两不相混。建层比例(nextLayerRatio)已退役不在此
3242
+ * 带出(canvas-web-preset-submenu 工单 01,ADR 0021——预设随武装载荷一次性
3243
+ * 携带,prefs 停写)。
3087
3244
  */
3088
3245
  getEditorPrefs() {
3089
3246
  const ui = this.store.ui;
@@ -3097,7 +3254,9 @@ var EditorSession = class {
3097
3254
  * 应用偏好(会话建立时一次性,跨会话恢复 prefs 的写入口):整体替换
3098
3255
  * rulersVisible/anchorExpanded,不进历史;值等写入经 store 短路零通知。
3099
3256
  * 新层样式(text-defaults 工单 01)走宽容读逐字段合并(pickNextTextStylePatch
3100
- * 共享谓词——与捕获同门,缺字/类型不符保留现值)。
3257
+ * 共享谓词——与捕获同门,缺字/类型不符保留现值)。旧快照的 nextLayerRatio
3258
+ * 遗留键宽容忽略(canvas-web-preset-submenu 工单 01:槽退役停读,载荷上多余
3259
+ * 的键无害;schemaVersion 加法演进)。
3101
3260
  */
3102
3261
  applyEditorPrefs(prefs) {
3103
3262
  this.store.setRulersVisible(prefs.rulersVisible);
@@ -4058,19 +4217,6 @@ var EditorSession = class {
4058
4217
  });
4059
4218
  if (index.v >= 0) this.store.setSelection([...rowPath, "cells", index.v]);
4060
4219
  }
4061
- /**
4062
- * 新增模板表(「+」选择器入口,spec §2.1):createTemplateTable 缺省形态 +
4063
- * 置顶 + 自动选中;一次调用 = 一步历史。rowsPath 必填(解码 rows_path_missing
4064
- * 硬约束的对齐兜底,表单校验前置),空串 no-op。
4065
- */
4066
- addTemplateTable(rowsPath) {
4067
- if (!this.store.doc || rowsPath === "") return;
4068
- const index = { v: -1 };
4069
- this.store.transact((draft) => {
4070
- index.v = insertRootLayerInDraft(draft, createTemplateTable({ rowsPath }));
4071
- });
4072
- if (index.v >= 0) this.store.setSelection(["layers", index.v]);
4073
- }
4074
4220
  /**
4075
4221
  * 模板行加格(spec §3.1):addTemplateCellInDraft 重建路径(末格宽/首格行宽、
4076
4222
  * 零高度耦合),自动选中新格;一次调用 = 一步历史。非模板态 no-op。
@@ -4748,6 +4894,7 @@ export {
4748
4894
  PAN_TO_BOX_MARGIN_PX,
4749
4895
  PASTE_OFFSET_PX,
4750
4896
  PHP_TRIM_CHARS,
4897
+ RATIO_PRESETS,
4751
4898
  RESIZE_HANDLES,
4752
4899
  RESIZE_HANDLE_HIT_PX,
4753
4900
  RESIZE_MIN_SIZE_PX,
@@ -4778,12 +4925,14 @@ export {
4778
4925
  createRubberBandRect,
4779
4926
  createTemplateTable,
4780
4927
  defaultNextTextStyle,
4928
+ defaultSizeForRatio,
4781
4929
  deleteLayerInDraft,
4782
4930
  enumerateExpressionCandidates,
4783
4931
  enumerateRowsPathCandidates,
4784
4932
  expressionCallSlotAtEnd,
4785
4933
  expressionDiagnostics,
4786
4934
  expressionFragmentAtCursor,
4935
+ findPresetEntry,
4787
4936
  fitRect,
4788
4937
  fitViewport,
4789
4938
  growRowToCellInDraft,
@@ -4820,14 +4969,18 @@ export {
4820
4969
  phpTrim2 as phpTrim,
4821
4970
  pickNextTextStylePatch,
4822
4971
  prepareRootPaste,
4972
+ proportionalDrivingAxis,
4973
+ ratioValue,
4823
4974
  readSpecField,
4824
4975
  remapPathAfterSplice,
4825
4976
  replaceHitsInDraft,
4826
4977
  resizableAxesAt,
4827
4978
  resizeBox,
4979
+ resizeBoxProportional,
4828
4980
  resizeHandleAt,
4829
4981
  resizeHandlePoint,
4830
4982
  resizeHandlesAt,
4983
+ resizeProportionalSnapPoints,
4831
4984
  resizeSnapPoints,
4832
4985
  resolveLayer,
4833
4986
  resolveRowSchema,