@zzalai/leafer-point-annotation 1.1.3 → 1.1.5

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.
@@ -1 +1 @@
1
- .brush-panel-overlay[data-v-10bc9982]{z-index:1500;background:#0000004d;position:fixed;inset:0}.brush-style-panel[data-v-10bc9982]{z-index:1501;background:#fff;border-radius:10px;min-width:240px;position:fixed;overflow:visible;box-shadow:0 4px 24px #00000026}.panel-header[data-v-10bc9982]{background:var(--leafer-point-color-background-light);border-bottom:1px solid var(--leafer-point-color-border);border-radius:10px 10px 0 0;justify-content:space-between;align-items:center;padding:10px 16px;display:flex}.panel-header span[data-v-10bc9982]{color:var(--leafer-point-color-text);font-size:14px;font-weight:600}.close-btn[data-v-10bc9982]{cursor:pointer;width:24px;height:24px;color:var(--leafer-point-color-text);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;font-size:18px;transition:all .2s;display:flex}.close-btn[data-v-10bc9982]:hover{background:var(--leafer-point-color-border)}.panel-content[data-v-10bc9982]{padding:16px 16px 24px}.config-item[data-v-10bc9982]{align-items:center;margin-bottom:20px;display:flex}.config-item[data-v-10bc9982]:last-child{margin-bottom:0}.config-label[data-v-10bc9982]{color:var(--leafer-point-color-text);text-align:right;min-width:50px;padding-right:5px;font-size:12px;display:block}.config-value[data-v-10bc9982]{color:var(--leafer-point-color-text);width:30px;padding-left:5px;font-size:12px}.color-picker-wrapper[data-v-10bc9982]{width:100%;margin:-10px 0}.config-slider[data-v-10bc9982]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:3px;outline:none;width:200px;height:6px}.config-slider[data-v-10bc9982]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}.config-slider[data-v-10bc9982]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}:root{--leafer-point-color-primary:#007aff;--leafer-point-color-background:#f5f5f5;--leafer-point-color-background-light:#f0f0f0;--leafer-point-color-white:#fff;--leafer-point-color-text:#333;--leafer-point-color-text-secondary:#666;--leafer-point-color-text-tertiary:#999;--leafer-point-color-placeholder:#999;--leafer-point-color-border:#ddd;--leafer-point-color-border-light:#e0e0e0;--leafer-point-color-error:#e74c3c;--leafer-point-color-button:#3498db;--leafer-point-color-button-rgb:52, 152, 219;--leafer-point-color-button-hover:#2980b9;--leafer-point-padding-toolbar:10px;--leafer-point-padding-tool-button:8px;--leafer-point-size-tool-icon:18px;--leafer-point-size-zoom-button:36px;--leafer-point-size-zoom-value:60px;--leafer-point-font-size-hotkey:10px;--leafer-point-padding-hotkey:1px 3px;--leafer-point-padding-error:20px;--leafer-point-padding-error-button:8px 16px;--leafer-point-border-radius-tool-button:4px;--leafer-point-border-radius-hotkey:2px;--leafer-point-border-radius-overlay:8px;--leafer-point-border-radius-zoom:8px;--leafer-point-shadow-tool-button:0 2px 4px #0000001a;--leafer-point-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-point-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-point-shadow-overlay:0 4px 12px #0000001a;--leafer-point-shadow-zoom:0 2px 8px #00000026;--leafer-point-transition-time:.2s;--leafer-point-animation-gradient:2s}.point-annotation[data-v-bbf6bd6d]{width:100%;height:100%}.canvas-container[data-v-bbf6bd6d]{outline:none;width:100%;height:100%;position:relative;overflow:hidden}.point-annotation.has-image .canvas-container[data-v-bbf6bd6d]{height:calc(100% - 55px)}.canvas-container[data-v-bbf6bd6d]:focus{outline:2px solid var(--leafer-point-color-primary);outline-offset:-2px}.loading-overlay[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-background-light);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);z-index:1000;justify-content:center;align-items:center;min-width:100%;min-height:100%;display:flex;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.gradient-animation[data-v-bbf6bd6d]{animation:gradientShift-bbf6bd6d var(--leafer-point-animation-gradient) ease-in-out infinite;opacity:.7;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) 0 0/200% 200%;position:absolute;inset:0}@keyframes gradientShift-bbf6bd6d{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-bbf6bd6d]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:18px;font-weight:500;position:relative}.upload-overlay[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-white);z-index:1000;border:3px dashed var(--leafer-point-color-border);flex-direction:column;justify-content:center;align-items:center;transition:all .2s;display:flex;position:absolute;inset:0}.upload-icon[data-v-bbf6bd6d]{color:var(--leafer-point-color-placeholder);margin-bottom:24px;transform:scale(1.2)}.upload-text[data-v-bbf6bd6d]{color:var(--leafer-point-color-text);margin-bottom:12px;font-size:18px;font-weight:500}.upload-hint[data-v-bbf6bd6d]{color:var(--leafer-point-color-placeholder);margin-bottom:28px;font-size:14px}.upload-button[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;padding:12px 32px;font-size:15px;font-weight:500;transition:all .2s}.upload-button[data-v-bbf6bd6d]:hover{background-color:var(--leafer-point-color-button-hover);transform:translateY(-1px)}.upload-overlay.drag-over[data-v-bbf6bd6d]{border-color:var(--leafer-point-color-button);background-color:rgba(var(--leafer-point-color-button-rgb), .05)}.upload-overlay.drag-over .upload-icon[data-v-bbf6bd6d]{color:var(--leafer-point-color-button)}.error-overlay[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-white);z-index:1000;flex-direction:column;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.error-overlay p[data-v-bbf6bd6d]{color:var(--leafer-point-color-error);margin-bottom:24px;font-size:18px;font-weight:500}.error-overlay button[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;padding:12px 32px;font-size:15px;font-weight:500;transition:all .2s}.error-overlay button[data-v-bbf6bd6d]:hover{background-color:var(--leafer-point-color-button-hover);transform:translateY(-1px)}.zoom-controller[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-zoom);box-shadow:var(--leafer-point-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-bbf6bd6d]{width:var(--leafer-point-size-zoom-button);height:var(--leafer-point-size-zoom-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-bbf6bd6d]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.zoom-button[data-v-bbf6bd6d]:active{background-color:#e0e0e0}.zoom-value[data-v-bbf6bd6d]{min-width:var(--leafer-point-size-zoom-value);height:var(--leafer-point-size-zoom-button);line-height:var(--leafer-point-size-zoom-button);text-align:center;color:var(--leafer-point-color-text);cursor:pointer;border-left:1px solid var(--leafer-point-color-border-light);border-right:1px solid var(--leafer-point-color-border-light);transition:all var(--leafer-point-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-bbf6bd6d]{line-height:1}.zoom-value[data-v-bbf6bd6d]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.toolbar[data-v-bbf6bd6d]{padding:var(--leafer-point-padding-toolbar);background-color:var(--leafer-point-color-background);border-top:1px solid var(--leafer-point-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-bbf6bd6d]{padding:var(--leafer-point-padding-tool-button);border-radius:var(--leafer-point-border-radius-tool-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;box-shadow:var(--leafer-point-shadow-tool-button);border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-bbf6bd6d]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary);box-shadow:var(--leafer-point-shadow-tool-button-hover)}.tool-button[data-v-bbf6bd6d]:active{box-shadow:var(--leafer-point-shadow-tool-button);transform:translateY(1px)}.tool-button.active[data-v-bbf6bd6d]{background-color:var(--leafer-point-color-primary);color:#fff;box-shadow:var(--leafer-point-shadow-tool-button-active)}.hotkey-hint[data-v-bbf6bd6d]{font-size:var(--leafer-point-font-size-hotkey);color:#fff;padding:var(--leafer-point-padding-hotkey);border-radius:var(--leafer-point-border-radius-hotkey);pointer-events:none;white-space:nowrap;background-color:#0009;position:absolute;top:0;right:0}.size-control[data-v-bbf6bd6d]{padding:var(--leafer-point-padding-tool-button);background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-tool-button);box-shadow:var(--leafer-point-shadow-tool-button);align-items:center;gap:8px;display:flex}.size-label[data-v-bbf6bd6d]{color:var(--leafer-point-color-text);white-space:nowrap;font-size:12px}.size-slider[data-v-bbf6bd6d]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:4px;outline:none;width:120px;height:8px}.size-slider[data-v-bbf6bd6d]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-bbf6bd6d]::-webkit-slider-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-bbf6bd6d]::-webkit-slider-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-bbf6bd6d]:focus::-webkit-slider-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-bbf6bd6d]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-bbf6bd6d]::-moz-range-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-bbf6bd6d]::-moz-range-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-bbf6bd6d]:focus::-moz-range-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-bbf6bd6d]:focus{outline:none}.size-value[data-v-bbf6bd6d]{text-align:center;min-width:30px;color:var(--leafer-point-color-primary);font-size:12px;font-weight:600}.app[data-v-374efae5]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-374efae5]{text-align:center;margin-bottom:30px}.editor-container[data-v-374efae5]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.controls[data-v-374efae5]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-374efae5]{margin-bottom:15px}label[data-v-374efae5]{margin-bottom:5px;font-weight:700;display:block}input[data-v-374efae5]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}.mask-options[data-v-374efae5]{gap:15px;margin-bottom:10px;display:flex}.mask-options label[data-v-374efae5]{align-items:center;gap:5px;font-weight:400;display:flex}.mask-options select[data-v-374efae5]{border:1px solid #ddd;border-radius:4px;padding:4px 8px}button[data-v-374efae5]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-374efae5]:hover{background-color:#0069d9}.output[data-v-374efae5]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-374efae5]{white-space:pre-wrap;word-wrap:break-word;background-color:#fff;border-radius:4px;max-height:300px;padding:15px;overflow-y:auto}.status[data-v-374efae5]{background-color:#f5f5f5;border-radius:8px;padding:20px}.multi-layer-row[data-v-374efae5]{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:8px;display:flex}.multi-layer-row>label[data-v-374efae5]{align-items:center;gap:6px;margin:0;font-weight:400;display:flex}.multi-layer-row>select[data-v-374efae5]{border:1px solid #ddd;border-radius:4px;padding:6px 10px;font-size:14px}.multi-layer-row>button[data-v-374efae5]{margin-right:0}.layer-info[data-v-374efae5]{color:#555;font-size:14px}.layer-info b[data-v-374efae5]{color:#007bff}.subtle[data-v-374efae5]{color:#888;margin:0 0 8px;font-size:13px}.active-btn[data-v-374efae5]{background-color:var(--leafer-point-color-primary,#409eff);color:#fff;border-color:var(--leafer-point-color-primary,#409eff)}.active-btn[data-v-374efae5]:hover{background-color:var(--leafer-point-color-primary,#409eff);opacity:.9}.point-list[data-v-374efae5]{flex-direction:column;gap:6px;max-height:280px;display:flex;overflow-y:auto}.point-list-row[data-v-374efae5]{background-color:#f9f9f9;border:1px solid #eee;border-radius:4px;align-items:center;gap:10px;padding:6px 8px;display:flex}.point-num[data-v-374efae5]{color:#007bff;min-width:32px;font-size:14px;font-weight:700}.label-input[data-v-374efae5]{border:1px solid #ddd;border-radius:4px;flex:1;padding:4px 8px;font-size:14px}.label-input[data-v-374efae5]:focus{border-color:#007bff;outline:none}.point-id[data-v-374efae5]{color:#aaa;min-width:80px;font-size:12px}.upload-log[data-v-374efae5]{color:#333;word-break:break-all;white-space:pre-wrap;background-color:#f9f9f9;border:1px solid #eee;border-radius:4px;max-height:200px;margin-top:10px;padding:10px 12px;font-size:13px;overflow-y:auto}
1
+ .brush-panel-overlay[data-v-10bc9982]{z-index:1500;background:#0000004d;position:fixed;inset:0}.brush-style-panel[data-v-10bc9982]{z-index:1501;background:#fff;border-radius:10px;min-width:240px;position:fixed;overflow:visible;box-shadow:0 4px 24px #00000026}.panel-header[data-v-10bc9982]{background:var(--leafer-point-color-background-light);border-bottom:1px solid var(--leafer-point-color-border);border-radius:10px 10px 0 0;justify-content:space-between;align-items:center;padding:10px 16px;display:flex}.panel-header span[data-v-10bc9982]{color:var(--leafer-point-color-text);font-size:14px;font-weight:600}.close-btn[data-v-10bc9982]{cursor:pointer;width:24px;height:24px;color:var(--leafer-point-color-text);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;font-size:18px;transition:all .2s;display:flex}.close-btn[data-v-10bc9982]:hover{background:var(--leafer-point-color-border)}.panel-content[data-v-10bc9982]{padding:16px 16px 24px}.config-item[data-v-10bc9982]{align-items:center;margin-bottom:20px;display:flex}.config-item[data-v-10bc9982]:last-child{margin-bottom:0}.config-label[data-v-10bc9982]{color:var(--leafer-point-color-text);text-align:right;min-width:50px;padding-right:5px;font-size:12px;display:block}.config-value[data-v-10bc9982]{color:var(--leafer-point-color-text);width:30px;padding-left:5px;font-size:12px}.color-picker-wrapper[data-v-10bc9982]{width:100%;margin:-10px 0}.config-slider[data-v-10bc9982]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:3px;outline:none;width:200px;height:6px}.config-slider[data-v-10bc9982]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}.config-slider[data-v-10bc9982]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;border:2px solid #fff;border-radius:50%;width:16px;height:16px;box-shadow:0 2px 4px #0003}:root{--leafer-point-color-primary:#007aff;--leafer-point-color-background:#f5f5f5;--leafer-point-color-background-light:#f0f0f0;--leafer-point-color-white:#fff;--leafer-point-color-text:#333;--leafer-point-color-text-secondary:#666;--leafer-point-color-text-tertiary:#999;--leafer-point-color-placeholder:#999;--leafer-point-color-border:#ddd;--leafer-point-color-border-light:#e0e0e0;--leafer-point-color-error:#e74c3c;--leafer-point-color-button:#3498db;--leafer-point-color-button-rgb:52, 152, 219;--leafer-point-color-button-hover:#2980b9;--leafer-point-padding-toolbar:10px;--leafer-point-padding-tool-button:8px;--leafer-point-size-tool-icon:18px;--leafer-point-size-zoom-button:36px;--leafer-point-size-zoom-value:60px;--leafer-point-font-size-hotkey:10px;--leafer-point-padding-hotkey:1px 3px;--leafer-point-padding-error:20px;--leafer-point-padding-error-button:8px 16px;--leafer-point-border-radius-tool-button:4px;--leafer-point-border-radius-hotkey:2px;--leafer-point-border-radius-overlay:8px;--leafer-point-border-radius-zoom:8px;--leafer-point-shadow-tool-button:0 2px 4px #0000001a;--leafer-point-shadow-tool-button-active:0 2px 4px #007aff4d;--leafer-point-shadow-tool-button-hover:0 4px 6px #0000001a;--leafer-point-shadow-overlay:0 4px 12px #0000001a;--leafer-point-shadow-zoom:0 2px 8px #00000026;--leafer-point-transition-time:.2s;--leafer-point-animation-gradient:2s}.point-annotation[data-v-d7009af0]{width:100%;height:100%}.canvas-container[data-v-d7009af0]{outline:none;width:100%;height:100%;position:relative;overflow:hidden}.point-annotation.has-image .canvas-container[data-v-d7009af0]{height:calc(100% - 55px)}.canvas-container[data-v-d7009af0]:focus{outline:2px solid var(--leafer-point-color-primary);outline-offset:-2px}.loading-overlay[data-v-d7009af0]{background-color:var(--leafer-point-color-background-light);border-radius:var(--leafer-point-border-radius-overlay);box-shadow:var(--leafer-point-shadow-overlay);z-index:1000;justify-content:center;align-items:center;min-width:100%;min-height:100%;display:flex;position:absolute;top:50%;left:50%;overflow:hidden;transform:translate(-50%,-50%)}.gradient-animation[data-v-d7009af0]{animation:gradientShift-d7009af0 var(--leafer-point-animation-gradient) ease-in-out infinite;opacity:.7;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) 0 0/200% 200%;position:absolute;inset:0}@keyframes gradientShift-d7009af0{0%{background-position:0%}50%{background-position:100%}to{background-position:0%}}.loading-text[data-v-d7009af0]{z-index:1;color:#fff;text-shadow:0 2px 4px #0003;font-size:18px;font-weight:500;position:relative}.upload-overlay[data-v-d7009af0]{background-color:var(--leafer-point-color-white);z-index:1000;border:3px dashed var(--leafer-point-color-border);flex-direction:column;justify-content:center;align-items:center;transition:all .2s;display:flex;position:absolute;inset:0}.upload-icon[data-v-d7009af0]{color:var(--leafer-point-color-placeholder);margin-bottom:24px;transform:scale(1.2)}.upload-text[data-v-d7009af0]{color:var(--leafer-point-color-text);margin-bottom:12px;font-size:18px;font-weight:500}.upload-hint[data-v-d7009af0]{color:var(--leafer-point-color-placeholder);margin-bottom:28px;font-size:14px}.upload-button[data-v-d7009af0]{background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;padding:12px 32px;font-size:15px;font-weight:500;transition:all .2s}.upload-button[data-v-d7009af0]:hover{background-color:var(--leafer-point-color-button-hover);transform:translateY(-1px)}.upload-overlay.drag-over[data-v-d7009af0]{border-color:var(--leafer-point-color-button);background-color:rgba(var(--leafer-point-color-button-rgb), .05)}.upload-overlay.drag-over .upload-icon[data-v-d7009af0]{color:var(--leafer-point-color-button)}.error-overlay[data-v-d7009af0]{background-color:var(--leafer-point-color-white);z-index:1000;flex-direction:column;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.error-overlay p[data-v-d7009af0]{color:var(--leafer-point-color-error);margin-bottom:24px;font-size:18px;font-weight:500}.error-overlay button[data-v-d7009af0]{background-color:var(--leafer-point-color-button);color:#fff;border-radius:var(--leafer-point-border-radius-tool-button);cursor:pointer;border:none;padding:12px 32px;font-size:15px;font-weight:500;transition:all .2s}.error-overlay button[data-v-d7009af0]:hover{background-color:var(--leafer-point-color-button-hover);transform:translateY(-1px)}.zoom-controller[data-v-d7009af0]{background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-zoom);box-shadow:var(--leafer-point-shadow-zoom);z-index:100;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;overflow:hidden}.zoom-button[data-v-d7009af0]{width:var(--leafer-point-size-zoom-button);height:var(--leafer-point-size-zoom-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;border:none;justify-content:center;align-items:center;display:flex;position:relative}.zoom-button[data-v-d7009af0]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.zoom-button[data-v-d7009af0]:active{background-color:#e0e0e0}.zoom-value[data-v-d7009af0]{min-width:var(--leafer-point-size-zoom-value);height:var(--leafer-point-size-zoom-button);line-height:var(--leafer-point-size-zoom-button);text-align:center;color:var(--leafer-point-color-text);cursor:pointer;border-left:1px solid var(--leafer-point-color-border-light);border-right:1px solid var(--leafer-point-color-border-light);transition:all var(--leafer-point-transition-time) ease;font-size:14px;font-weight:500;position:relative}.zoom-value .hotkey-hint[data-v-d7009af0]{line-height:1}.zoom-value[data-v-d7009af0]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary)}.toolbar[data-v-d7009af0]{padding:var(--leafer-point-padding-toolbar);background-color:var(--leafer-point-color-background);border-top:1px solid var(--leafer-point-color-border);justify-content:center;align-items:center;gap:10px;display:flex}.tool-button[data-v-d7009af0]{padding:var(--leafer-point-padding-tool-button);border-radius:var(--leafer-point-border-radius-tool-button);background-color:var(--leafer-point-color-white);color:var(--leafer-point-color-text);cursor:pointer;transition:all var(--leafer-point-transition-time) ease;box-shadow:var(--leafer-point-shadow-tool-button);border:none;justify-content:center;align-items:center;display:flex;position:relative}.tool-button[data-v-d7009af0]:hover{background-color:var(--leafer-point-color-background-light);color:var(--leafer-point-color-primary);box-shadow:var(--leafer-point-shadow-tool-button-hover)}.tool-button[data-v-d7009af0]:active{box-shadow:var(--leafer-point-shadow-tool-button);transform:translateY(1px)}.tool-button.active[data-v-d7009af0]{background-color:var(--leafer-point-color-primary);color:#fff;box-shadow:var(--leafer-point-shadow-tool-button-active)}.hotkey-hint[data-v-d7009af0]{font-size:var(--leafer-point-font-size-hotkey);color:#fff;padding:var(--leafer-point-padding-hotkey);border-radius:var(--leafer-point-border-radius-hotkey);pointer-events:none;white-space:nowrap;background-color:#0009;position:absolute;top:0;right:0}.size-control[data-v-d7009af0]{padding:var(--leafer-point-padding-tool-button);background-color:var(--leafer-point-color-white);border-radius:var(--leafer-point-border-radius-tool-button);box-shadow:var(--leafer-point-shadow-tool-button);align-items:center;gap:8px;display:flex}.size-label[data-v-d7009af0]{color:var(--leafer-point-color-text);white-space:nowrap;font-size:12px}.size-slider[data-v-d7009af0]{appearance:none;cursor:pointer;background:#e0e0e0;border-radius:4px;outline:none;width:120px;height:8px}.size-slider[data-v-d7009af0]::-webkit-slider-thumb{appearance:none;background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-d7009af0]::-webkit-slider-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-d7009af0]::-webkit-slider-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-d7009af0]:focus::-webkit-slider-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-d7009af0]::-moz-range-thumb{background:var(--leafer-point-color-primary);cursor:pointer;width:18px;height:18px;transition:all var(--leafer-point-transition-time) ease;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 6px #0000004d}.size-slider[data-v-d7009af0]::-moz-range-thumb:hover{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-d7009af0]::-moz-range-thumb:active{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-d7009af0]:focus::-moz-range-thumb{background:var(--leafer-point-color-primary-hover);transform:scale(1.15);box-shadow:0 3px 8px #0006}.size-slider[data-v-d7009af0]:focus{outline:none}.size-value[data-v-d7009af0]{text-align:center;min-width:30px;color:var(--leafer-point-color-primary);font-size:12px;font-weight:600}.app[data-v-62faa61b]{max-width:1200px;margin:0 auto;padding:20px;font-family:Arial,sans-serif}h1[data-v-62faa61b]{text-align:center;margin-bottom:30px}.editor-container[data-v-62faa61b]{border:1px solid #ddd;border-radius:8px;width:100%;height:600px;margin-bottom:30px;overflow:hidden}.editor-container.multi-instance[data-v-62faa61b]{gap:12px;height:auto;display:flex}.editor-container.multi-instance[data-v-62faa61b]>.point-annotation,.editor-container.multi-instance>.point-annotation[data-v-62faa61b]{border:1px solid #ddd;border-radius:8px;flex:1;min-width:0;height:600px;overflow:hidden}.controls[data-v-62faa61b]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}.control-group[data-v-62faa61b]{margin-bottom:15px}label[data-v-62faa61b]{margin-bottom:5px;font-weight:700;display:block}input[data-v-62faa61b]{border:1px solid #ddd;border-radius:4px;width:100%;margin-bottom:10px;padding:8px}.mask-options[data-v-62faa61b]{gap:15px;margin-bottom:10px;display:flex}.mask-options label[data-v-62faa61b]{align-items:center;gap:5px;font-weight:400;display:flex}.mask-options select[data-v-62faa61b]{border:1px solid #ddd;border-radius:4px;padding:4px 8px}button[data-v-62faa61b]{color:#fff;cursor:pointer;background-color:#007bff;border:none;border-radius:4px;margin-right:10px;padding:8px 16px}button[data-v-62faa61b]:hover{background-color:#0069d9}.output[data-v-62faa61b]{background-color:#f5f5f5;border-radius:8px;margin-bottom:30px;padding:20px}pre[data-v-62faa61b]{white-space:pre-wrap;word-wrap:break-word;background-color:#fff;border-radius:4px;max-height:300px;padding:15px;overflow-y:auto}.status[data-v-62faa61b]{background-color:#f5f5f5;border-radius:8px;padding:20px}.multi-layer-row[data-v-62faa61b]{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:8px;display:flex}.multi-layer-row>label[data-v-62faa61b]{align-items:center;gap:6px;margin:0;font-weight:400;display:flex}.multi-layer-row>select[data-v-62faa61b]{border:1px solid #ddd;border-radius:4px;padding:6px 10px;font-size:14px}.multi-layer-row>button[data-v-62faa61b]{margin-right:0}.layer-info[data-v-62faa61b]{color:#555;font-size:14px}.layer-info b[data-v-62faa61b]{color:#007bff}.subtle[data-v-62faa61b]{color:#888;margin:0 0 8px;font-size:13px}.active-btn[data-v-62faa61b]{background-color:var(--leafer-point-color-primary,#409eff);color:#fff;border-color:var(--leafer-point-color-primary,#409eff)}.active-btn[data-v-62faa61b]:hover{background-color:var(--leafer-point-color-primary,#409eff);opacity:.9}.point-list[data-v-62faa61b]{flex-direction:column;gap:6px;max-height:280px;display:flex;overflow-y:auto}.point-list-row[data-v-62faa61b]{background-color:#f9f9f9;border:1px solid #eee;border-radius:4px;align-items:center;gap:10px;padding:6px 8px;display:flex}.point-num[data-v-62faa61b]{color:#007bff;min-width:32px;font-size:14px;font-weight:700}.label-input[data-v-62faa61b]{border:1px solid #ddd;border-radius:4px;flex:1;padding:4px 8px;font-size:14px}.label-input[data-v-62faa61b]:focus{border-color:#007bff;outline:none}.point-id[data-v-62faa61b]{color:#aaa;min-width:80px;font-size:12px}.upload-log[data-v-62faa61b]{color:#333;word-break:break-all;white-space:pre-wrap;background-color:#f9f9f9;border:1px solid #eee;border-radius:4px;max-height:200px;margin-top:10px;padding:10px 12px;font-size:13px;overflow-y:auto}
package/docs/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Leafer Point Annotation</title>
8
- <script type="module" crossorigin src="./assets/index-BcqmlFff.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-dq8tjOSG.css">
8
+ <script type="module" crossorigin src="./assets/index-B2EnBW5l.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-BtyValpk.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zzalai/leafer-point-annotation",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "A Vue3 component for point annotation and brush painting on images using LeaferJS, supporting COCO/YOLO/JSON export, designed for AI model training dataset annotation",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -152,6 +152,8 @@ interface OptionsSource {
152
152
  // - initBrushLayers 跳过 canvas 创建
153
153
  // - mask 导出相关方法返回 null/{}
154
154
  // - 删除确认文案不包含"笔刷"
155
+ enableHotkeys?: boolean // 默认 false;设为 true 时才绑定 tinykeys 快捷键
156
+ // (v/p/b/e/Ctrl+Z/Ctrl+Y/Delete/Ctrl+±/Ctrl+0/Alt)
155
157
  }
156
158
  ```
157
159
 
@@ -0,0 +1,503 @@
1
+ # 多实例共享撤销/重做栈 — 设计与实现文档
2
+
3
+ > **版本**: v1.0 | **状态**: 方案已确定,待实现 | **对应组件**: `src/components/PointAnnotation.vue`
4
+ >
5
+ > **适用场景**: 业务上需要多个 PointAnnotation 编辑器共用一个 undo/redo 栈
6
+ > (例如:在编辑器 A 创建点标注 → 在编辑器 B 涂抹笔刷 → 按一次 Ctrl+Z 撤销 B 的涂抹 → 再按一次 Ctrl+Z 撤销 A 的点标注)
7
+
8
+ ---
9
+
10
+ ## 1. 设计思路
11
+
12
+ ### 1.1 核心机制:Vue 的 provide / inject
13
+
14
+ - **父组件**:`provide(KEY, new CommandManager(200))` — 创建一个共享的撤销/重做管理器
15
+ - **子组件(每个 PointAnnotation)**:`inject(KEY, null)` — 优先使用父组件注入的共享实例
16
+ - **无注入时**:子组件自动创建本地独立的 CommandManager(**100% 向后兼容**)
17
+
18
+ ### 1.2 为什么不需要修改 BrushCommands.ts / PointCommands.ts?
19
+
20
+ 当前的命令对象(`BrushSnapshotCommand`、`AddPointCommand`、`RemovePointCommand`)已经基于**对象引用**实现,命令内部只操作它被创建时传入的具体对象(某实例的 `canvasBrush`、`pointLayer`、`pointAnnotations`)。
21
+
22
+ 当共享 CommandManager 执行 undo 时,命令会自动操作属于那个实例的内部对象——**天然支持跨实例**,无需改动。
23
+
24
+ ### 1.3 全局快捷键的归属
25
+
26
+ - **独立模式(默认)**:每个 PointAnnotation 自己通过 tinykeys 监听 `Ctrl+Z` / `Ctrl+Y`
27
+ - **共享模式**:子组件不绑定这两个快捷键,**由父组件统一监听**,执行 `sharedManager.undo()` 后再让所有子实例调用 `refreshUI()` 刷新各自 UI
28
+
29
+ ---
30
+
31
+ ## 2. 精确修改清单
32
+
33
+ **修改文件**:`src/components/PointAnnotation.vue`(仅此一个文件)
34
+ **无需修改**:`src/utils/BrushCommands.ts`、`src/utils/PointCommands.ts`、`src/App.vue`(按需增加演示)
35
+
36
+ ---
37
+
38
+ ### 修改 1:import 区域 — 增加 inject 与 inject key
39
+
40
+ **位置**:第 272 行附近(第一个 `import from "vue"` 处)
41
+
42
+ **旧代码**:
43
+ ```ts
44
+ import { ref, onMounted, onUnmounted, nextTick, computed, watch } from "vue";
45
+ import {
46
+ App,
47
+ ImageEvent,
48
+ PointerEvent,
49
+ ZoomEvent,
50
+ Image,
51
+ Group,
52
+ } from "leafer-ui";
53
+ ```
54
+
55
+ **新代码**:
56
+ ```ts
57
+ import { ref, onMounted, onUnmounted, nextTick, computed, watch, inject } from "vue";
58
+
59
+ // 共享撤销/重做栈:provide/inject key
60
+ // 父组件:provide(POINT_ANNOTATION_COMMAND_MANAGER_KEY, new CommandManager(200))
61
+ // 多个 PointAnnotation 子组件将自动共享同一个 undo/redo 栈
62
+ export const POINT_ANNOTATION_COMMAND_MANAGER_KEY = 'pointAnnotationCommandManager';
63
+
64
+ import {
65
+ App,
66
+ ImageEvent,
67
+ PointerEvent,
68
+ ZoomEvent,
69
+ Image,
70
+ Group,
71
+ } from "leafer-ui";
72
+ ```
73
+
74
+ ---
75
+
76
+ ### 修改 2:CommandManager 声明 — 从 let 改为 inject + local 组合
77
+
78
+ **位置**:第 525 行附近(原 `// 撤销/重做管理器` 处)
79
+
80
+ **旧代码**:
81
+ ```ts
82
+ // 撤销/重做管理器
83
+ let commandManager: CommandManager | null = null;
84
+
85
+ // 多实例支持:tinykeys 解绑函数
86
+ let hotkeysUnsubscribe: (() => void) | null = null;
87
+ ```
88
+
89
+ **新代码**:
90
+ ```ts
91
+ // 撤销/重做管理器
92
+ // 共享模式:优先从父组件 inject 共享的 CommandManager(多实例共用一个 undo/redo 栈)
93
+ const injectedCommandManager = inject<CommandManager | null>(POINT_ANNOTATION_COMMAND_MANAGER_KEY, null);
94
+ const isSharedCommandManager = injectedCommandManager !== null;
95
+ let localCommandManager: CommandManager | null = null;
96
+ const getCommandManager = () => injectedCommandManager ?? localCommandManager;
97
+
98
+ // 多实例支持:tinykeys 解绑函数
99
+ let hotkeysUnsubscribe: (() => void) | null = null;
100
+ ```
101
+
102
+ **关键点**:
103
+ - `injectedCommandManager` 可能是 `null`(独立模式)或一个共享实例(共享模式)
104
+ - `isSharedCommandManager` 是布尔值,用于 tinykeys 绑定决策
105
+ - `getCommandManager()` 统一返回「当前应该使用的」CommandManager,调用方无需知道是共享还是独立
106
+ - 全文所有 `commandManager.xxx` 的调用全部替换为 `getCommandManager()!.xxx`(见下文修改 6)
107
+
108
+ ---
109
+
110
+ ### 修改 3:onMounted — 共享模式下不新建本地实例
111
+
112
+ **位置**:原第 1024-1025 行附近(onMounted 内 `// 初始化撤销/重做管理器` 处)
113
+
114
+ **旧代码**:
115
+ ```ts
116
+ // 初始化撤销/重做管理器
117
+ commandManager = new CommandManager(100);
118
+ ```
119
+
120
+ **新代码**:
121
+ ```ts
122
+ // 初始化撤销/重做管理器
123
+ // 共享模式:使用父组件注入的实例;独立模式:创建本地实例
124
+ if (!isSharedCommandManager) {
125
+ localCommandManager = new CommandManager(100);
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ### 修改 4:tinykeys — 共享模式下不绑定 Ctrl+Z / Ctrl+Y
132
+
133
+ **位置**:原第 1054-1065 行附近(tinykeys 配置对象内)
134
+
135
+ **旧代码**:
136
+ ```ts
137
+ "$mod+KeyZ": (event: KeyboardEvent) => {
138
+ if (!isCanvasFocused.value && !isMouseOverCanvas.value) return;
139
+ event.preventDefault();
140
+ event.stopPropagation();
141
+ undo();
142
+ },
143
+ "$mod+KeyY": (event: KeyboardEvent) => {
144
+ if (!isCanvasFocused.value && !isMouseOverCanvas.value) return;
145
+ event.preventDefault();
146
+ event.stopPropagation();
147
+ redo();
148
+ },
149
+ ```
150
+
151
+ **新代码**:
152
+ ```ts
153
+ // 共享模式:undo/redo 由父组件统一监听,避免多实例重复响应
154
+ // (独立模式下保持组件内部快捷键)
155
+ ...(isSharedCommandManager ? {} : {
156
+ "$mod+KeyZ": (event: KeyboardEvent) => {
157
+ if (!isCanvasFocused.value && !isMouseOverCanvas.value) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ undo();
161
+ },
162
+ "$mod+KeyY": (event: KeyboardEvent) => {
163
+ if (!isCanvasFocused.value && !isMouseOverCanvas.value) return;
164
+ event.preventDefault();
165
+ event.stopPropagation();
166
+ redo();
167
+ },
168
+ }),
169
+ ```
170
+
171
+ **说明**:用展开运算符实现「条件性包含」两个快捷键。共享模式下这两项被替换为空对象,由父组件统一管理。
172
+
173
+ ---
174
+
175
+ ### 修改 5:undo/redo — 抽取 refreshUI,改用 getCommandManager
176
+
177
+ **位置**:原第 1738-1752 行附近
178
+
179
+ **旧代码**:
180
+ ```ts
181
+ const undo = () => {
182
+ if (commandManager?.canUndo()) {
183
+ commandManager.undo();
184
+ renumberSequenceNumbers();
185
+ emit("pointChange", [...pointAnnotations.value]);
186
+ }
187
+ };
188
+
189
+ const redo = () => {
190
+ if (commandManager?.canRedo()) {
191
+ commandManager.redo();
192
+ renumberSequenceNumbers();
193
+ emit("pointChange", [...pointAnnotations.value]);
194
+ }
195
+ };
196
+ ```
197
+
198
+ **新代码**:
199
+ ```ts
200
+ // 刷新本实例的 UI:重新编号 + 通知父组件
201
+ // 在共享模式下,父组件调用 sharedManager.undo() 后,
202
+ // 需要对每个子实例调用此方法,确保 UI 和 emit 同步
203
+ const refreshUI = () => {
204
+ renumberSequenceNumbers();
205
+ emit("pointChange", [...pointAnnotations.value]);
206
+ };
207
+
208
+ const undo = () => {
209
+ const cm = getCommandManager();
210
+ if (cm?.canUndo()) {
211
+ cm.undo();
212
+ refreshUI();
213
+ }
214
+ };
215
+
216
+ const redo = () => {
217
+ const cm = getCommandManager();
218
+ if (cm?.canRedo()) {
219
+ cm.redo();
220
+ refreshUI();
221
+ }
222
+ };
223
+ ```
224
+
225
+ **关键点**:`refreshUI()` 会暴露到 defineExpose 中(见修改 7),父组件在共享模式下调用 `sharedManager.undo()` 后需要 `for...of` 所有子实例逐个调用 `refreshUI()`。
226
+
227
+ ---
228
+
229
+ ### 修改 6:全文所有 `commandManager.xxx` → `getCommandManager()!.xxx`(共 7 处)
230
+
231
+ 以下按行号精确列出每一处的替换。每处的模式都是:
232
+
233
+ ```
234
+ if (commandManager) { → if (getCommandManager()) {
235
+ ... → ...
236
+ commandManager.executeCommand(...) → getCommandManager()!.executeCommand(...)
237
+ } → }
238
+ ```
239
+
240
+ **6A. handleBrushUp — 笔刷抬起后保存快照**
241
+
242
+ 位置:原第 1398-1400 行
243
+ ```ts
244
+ // 旧
245
+ if (commandManager && brushSnapshotLayer && canvasBrushesByLayer.value[brushSnapshotLayer] && brushSnapshotBeforeDraw) {
246
+ const snapshotCommand = new BrushSnapshotCommand(canvasBrushesByLayer.value[brushSnapshotLayer], brushSnapshotBeforeDraw);
247
+ commandManager.executeCommand(snapshotCommand);
248
+ // 新
249
+ if (getCommandManager() && brushSnapshotLayer && canvasBrushesByLayer.value[brushSnapshotLayer] && brushSnapshotBeforeDraw) {
250
+ const snapshotCommand = new BrushSnapshotCommand(canvasBrushesByLayer.value[brushSnapshotLayer], brushSnapshotBeforeDraw);
251
+ getCommandManager()!.executeCommand(snapshotCommand);
252
+ ```
253
+
254
+ **6B. 添加点标注**
255
+
256
+ 位置:原第 1549-1551 行
257
+ ```ts
258
+ // 旧
259
+ if (commandManager) {
260
+ const addCommand = new AddPointCommand(pointLayer, pointElement, pointAnnotations.value, pointData);
261
+ commandManager.executeCommand(addCommand);
262
+ } else {
263
+ // 新
264
+ const cm = getCommandManager();
265
+ if (cm) {
266
+ const addCommand = new AddPointCommand(pointLayer, pointElement, pointAnnotations.value, pointData);
267
+ cm.executeCommand(addCommand);
268
+ } else {
269
+ ```
270
+
271
+ **6C. 删除选中点(editor select 删除)**
272
+
273
+ 位置:原第 1609-1611 行
274
+ ```ts
275
+ // 旧
276
+ if (commandManager) {
277
+ const removeCommand = new RemovePointCommand(pointLayer, element, pointAnnotations.value);
278
+ commandManager.executeCommand(removeCommand);
279
+ } else {
280
+ // 新
281
+ if (getCommandManager()) {
282
+ const removeCommand = new RemovePointCommand(pointLayer, element, pointAnnotations.value);
283
+ getCommandManager()!.executeCommand(removeCommand);
284
+ } else {
285
+ ```
286
+
287
+ **6D. clearBrush — 清除当前图层的笔刷**
288
+
289
+ 位置:原第 1657-1660 行
290
+ ```ts
291
+ // 旧
292
+ if (commandManager) {
293
+ const beforeSnapshot = activeCanvasBrush.value.getImageData();
294
+ const snapshotCommand = new BrushSnapshotCommand(activeCanvasBrush.value, beforeSnapshot, true);
295
+ commandManager.executeCommand(snapshotCommand);
296
+ }
297
+ // 新
298
+ if (getCommandManager()) {
299
+ const beforeSnapshot = activeCanvasBrush.value.getImageData();
300
+ const snapshotCommand = new BrushSnapshotCommand(activeCanvasBrush.value, beforeSnapshot, true);
301
+ getCommandManager()!.executeCommand(snapshotCommand);
302
+ }
303
+ ```
304
+
305
+ **6E. clearAllBrushLayers — 清除所有图层的笔刷**
306
+
307
+ 位置:原第 1672-1675 行
308
+ ```ts
309
+ // 旧
310
+ if (commandManager) {
311
+ const beforeSnapshot = brush.getImageData();
312
+ const snapshotCommand = new BrushSnapshotCommand(brush, beforeSnapshot, true);
313
+ commandManager.executeCommand(snapshotCommand);
314
+ }
315
+ // 新
316
+ if (getCommandManager()) {
317
+ const beforeSnapshot = brush.getImageData();
318
+ const snapshotCommand = new BrushSnapshotCommand(brush, beforeSnapshot, true);
319
+ getCommandManager()!.executeCommand(snapshotCommand);
320
+ }
321
+ ```
322
+
323
+ **6F. createBrushFromPoints — 从标注点轨迹生成笔刷区域**
324
+
325
+ 位置:原第 1731-1733 行
326
+ ```ts
327
+ // 旧
328
+ if (commandManager) {
329
+ const snapshotCommand = new BrushSnapshotCommand(brush, beforeSnapshot);
330
+ commandManager.executeCommand(snapshotCommand);
331
+ }
332
+ // 新
333
+ if (getCommandManager()) {
334
+ const snapshotCommand = new BrushSnapshotCommand(brush, beforeSnapshot);
335
+ getCommandManager()!.executeCommand(snapshotCommand);
336
+ }
337
+ ```
338
+
339
+ **6G. removePointAnnotation — 按索引删除指定点(ref API 调用)**
340
+
341
+ 位置:原第 1790-1792 行
342
+ ```ts
343
+ // 旧
344
+ if (commandManager) {
345
+ const removeCommand = new RemovePointCommand(pointLayer, element as any, pointAnnotations.value);
346
+ commandManager.executeCommand(removeCommand);
347
+ } else {
348
+ // 新
349
+ if (getCommandManager()) {
350
+ const removeCommand = new RemovePointCommand(pointLayer, element as any, pointAnnotations.value);
351
+ getCommandManager()!.executeCommand(removeCommand);
352
+ } else {
353
+ ```
354
+
355
+ ---
356
+
357
+ ### 修改 7:defineExpose — 暴露 3 个新方法
358
+
359
+ **位置**:原第 1867-1869 行附近(defineExpose 的末尾 `});` 前)
360
+
361
+ 在 `updateBrushStyle,` 之后、`});` 之前插入:
362
+
363
+ ```ts
364
+ updateBrushStyle,
365
+ // 共享撤销/重做栈支持
366
+ refreshUI, // 父组件调用 sharedManager.undo()/redo() 后,调用此方法刷新本实例 UI 并 emit pointChange
367
+ getCommandManager, // 获取当前使用的 CommandManager(共享或本地)
368
+ isSharedCommandManager: () => isSharedCommandManager, // 是否使用共享模式
369
+ });
370
+ ```
371
+
372
+ ---
373
+
374
+ ## 3. 修改完成后:父组件使用示例
375
+
376
+ 在任意父组件中实现以下代码,即可开启多实例共享撤销栈:
377
+
378
+ ```vue
379
+ <script setup lang="ts">
380
+ import { provide, ref, onMounted, onUnmounted } from 'vue'
381
+ import PointAnnotation, { POINT_ANNOTATION_COMMAND_MANAGER_KEY } from '@/components/PointAnnotation.vue'
382
+ import { CommandManager } from '@zzalai/leafer-undo-redo'
383
+ // @ts-ignore — tinykeys 类型声明问题
384
+ import { tinykeys } from 'tinykeys'
385
+
386
+ // ============================================================
387
+ // 1️⃣ 创建共享的 CommandManager,所有子实例共享一个 undo/redo 栈
388
+ // ============================================================
389
+ const sharedManager = new CommandManager(200)
390
+ provide(POINT_ANNOTATION_COMMAND_MANAGER_KEY, sharedManager)
391
+
392
+ // ============================================================
393
+ // 2️⃣ 子组件 ref(用于共享模式下 undo/redo 后调用 refreshUI)
394
+ // ============================================================
395
+ const ann1 = ref<InstanceType<typeof PointAnnotation> | null>(null)
396
+ const ann2 = ref<InstanceType<typeof PointAnnotation> | null>(null)
397
+
398
+ // ============================================================
399
+ // 3️⃣ 父组件统一监听 Ctrl+Z / Ctrl+Y
400
+ // (子组件在共享模式下不绑定这些快捷键)
401
+ // ============================================================
402
+ let hotkeysUnsubscribe: (() => void) | null = null
403
+
404
+ onMounted(() => {
405
+ hotkeysUnsubscribe = tinykeys(window, {
406
+ '$mod+KeyZ': (e) => {
407
+ e.preventDefault()
408
+ e.stopPropagation()
409
+ // 先在共享栈中执行撤销
410
+ sharedManager.undo()
411
+ // 关键:让所有子实例刷新 UI 和 emit pointChange
412
+ // (即使某个子实例的命令不是这次 undo 的目标,调用 refreshUI 也无副作用
413
+ // ——renumberSequenceNumbers 是幂等的,pointChange 只是重新 emit 一次)
414
+ ann1.value?.refreshUI()
415
+ ann2.value?.refreshUI()
416
+ },
417
+ '$mod+KeyY': (e) => {
418
+ e.preventDefault()
419
+ e.stopPropagation()
420
+ sharedManager.redo()
421
+ ann1.value?.refreshUI()
422
+ ann2.value?.refreshUI()
423
+ },
424
+ })
425
+ })
426
+
427
+ onUnmounted(() => {
428
+ if (hotkeysUnsubscribe) {
429
+ hotkeysUnsubscribe()
430
+ hotkeysUnsubscribe = null
431
+ }
432
+ })
433
+ </script>
434
+
435
+ <template>
436
+ <div style="display: flex; gap: 12px;">
437
+ <PointAnnotation
438
+ ref="ann1"
439
+ :options="{ enableBrush: true }"
440
+ :imageSource="{ url: 'https://example.com/image1.png' }"
441
+ />
442
+ <PointAnnotation
443
+ ref="ann2"
444
+ :options="{ enableBrush: true }"
445
+ :imageSource="{ url: 'https://example.com/image2.png' }"
446
+ />
447
+ </div>
448
+ </template>
449
+ ```
450
+
451
+ ---
452
+
453
+ ## 4. 验证步骤
454
+
455
+ 实现完成后,按以下步骤自测:
456
+
457
+ 1. `pnpm run build:all` — 确认 TypeScript 类型、CSS、构建产物全部通过
458
+ 2. `pnpm run dev` 启动开发服务器
459
+ 3. 打开包含两个并排 PointAnnotation 的演示页
460
+ 4. **左编辑器**:创建 2 个点标注 → 观察控制台 emit 正常
461
+ 5. **右编辑器**:用笔刷涂抹一块区域 → 观察控制台 emit 正常
462
+ 6. 按一次 `Ctrl+Z` → 右编辑器的涂抹被撤销 ✅
463
+ 7. 按一次 `Ctrl+Z` → 左编辑器最后一个点被撤销 ✅
464
+ 8. 按 `Ctrl+Y`(重做) → 左编辑器的点恢复 ✅
465
+ 9. **不提供 provide(POINT_ANNOTATION_COMMAND_MANAGER_KEY, ...) 时**,两个编辑器应该各自独立的 undo/redo 栈(验证向后兼容)
466
+
467
+ ---
468
+
469
+ ## 5. 兼容性与风险评估
470
+
471
+ | 风险点 | 评估 | 应对方式 |
472
+ |--------|------|---------|
473
+ | 独立模式行为变化 | ✅ 无变化 | 无 inject 时自动创建本地 CommandManager,行为与原版一致 |
474
+ | 共享模式下 refreshUI 多次调用 | ✅ 安全 | renumberSequenceNumbers 是幂等操作;多次 emit pointChange 只是数据重复广播,无副作用 |
475
+ | `POINT_ANNOTATION_COMMAND_MANAGER_KEY` 命名冲突 | ✅ 极低 | 使用足够具体的字符串,且使用组件导出的常量 |
476
+ | 共享栈容量限制 | ⚠️ 需注意 | 父组件创建时自行指定 `new CommandManager(200)`,容量按业务场景评估 |
477
+ | 父组件忘记调用 `refreshUI()` | ⚠️ 需要文档约束 | 共享模式下 undo/redo 由父组件驱动,必须调用所有子实例的 `refreshUI()` |
478
+
479
+ ---
480
+
481
+ ## 6. 实现用时预估
482
+
483
+ | 步骤 | 预估耗时 | 说明 |
484
+ |------|---------|------|
485
+ | 修改 1-7 代码 | 约 30 分钟 | 按本文档逐段替换 |
486
+ | 更新 App.vue 增加演示用例 | 约 15 分钟 | 复制第 3 节示例代码 |
487
+ | 构建 + 自测 | 约 15 分钟 | 运行 `pnpm run build:all` + 手动自测 |
488
+ | **总计** | **约 1 小时** | |
489
+
490
+ ---
491
+
492
+ ## 7. 变更后暴露给外部的新 API
493
+
494
+ | 名称 | 类型 | 说明 |
495
+ |------|------|------|
496
+ | `POINT_ANNOTATION_COMMAND_MANAGER_KEY` | `string` (export) | provide/inject 的 key,父组件 import 后传入 `provide` |
497
+ | `refreshUI()` | `ref` 方法 | 刷新本实例 UI(重编号 + emit pointChange),共享模式下父组件在 undo/redo 后调用 |
498
+ | `getCommandManager()` | `ref` 方法 | 返回当前使用的 CommandManager(共享实例或本地实例) |
499
+ | `isSharedCommandManager()` | `ref` 方法 | 返回布尔值,标识当前是否处于共享模式 |
500
+
501
+ ---
502
+
503
+ _EOF_