.2869047221311500:9c5759a5394aa1b51101e64d032d75de_69e614bbf51efcfb0ecaa3bf.69e62868f51efcfb0ecaa543.69e62867d90ca3ecca53d17c:Trae CN.T(2026/4/20 21:21:44)Trae 4#374
Conversation
实现参考图片功能,包括加载、显示/隐藏、调整透明度和移除参考图片 添加相关CSS样式和菜单选项 创建ReferenceImage类处理参考图片的显示和交互
修改ReferenceImage的显示/隐藏逻辑,使用CSS类.hidden替代直接调用jQuery的hide/show方法
There was a problem hiding this comment.
Pull request overview
Adds a “Reference Image” overlay feature to the JS Paint canvas so users can load an image/PDF to trace over, toggle visibility, adjust opacity, and remove it.
Changes:
- Introduces a new
ReferenceImageon-canvas object with resize handles and a move handle. - Adds View menu entries to load/show/adjust opacity/remove the reference image.
- Adds CSS styling for reference image layering and RTL support.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| styles/layout.css | Adds .reference-image styling and stacking behavior. |
| styles/layout.rtl.css | Mirrors .reference-image styling for RTL layout. |
| src/menus.js | Adds “Reference Image” submenu under View and wires menu actions. |
| src/functions.js | Implements reference image load/toggle/remove/opacity UI and exports. |
| src/app-state.js | Adds global singleton reference_image state. |
| src/ReferenceImage.js | New on-canvas object implementation for displaying/moving/resizing the reference image. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| MENU_DIVIDER, | ||
| { | ||
| emoji_icon: "📷", | ||
| label: localize("&Reference Image"), | ||
| submenu: [ | ||
| { | ||
| label: localize("&Load Reference Image") + "...", | ||
| speech_recognition: [ | ||
| "load reference image", "load reference", | ||
| "import reference image", "import reference", | ||
| "open reference image", "open reference", | ||
| ], | ||
| action: () => { load_reference_image(); }, | ||
| description: localize("Loads a reference image to trace over."), | ||
| }, | ||
| { | ||
| label: localize("&Show Reference Image"), | ||
| speech_recognition: [ | ||
| "toggle reference image", "toggle reference", | ||
| "show reference image", "show reference", | ||
| "hide reference image", "hide reference", | ||
| ], | ||
| enabled: () => has_reference_image(), | ||
| checkbox: { | ||
| toggle: () => { toggle_reference_image(); }, | ||
| check: () => is_reference_image_visible(), | ||
| }, | ||
| description: localize("Shows or hides the reference image."), | ||
| }, | ||
| { | ||
| label: localize("Reference Image &Opacity") + "...", | ||
| speech_recognition: [ | ||
| "change reference image opacity", "change reference opacity", | ||
| "adjust reference image opacity", "adjust reference opacity", | ||
| "reference image transparency", "reference transparency", | ||
| ], | ||
| enabled: () => has_reference_image(), | ||
| action: () => { show_reference_image_opacity_window(); }, | ||
| description: localize("Adjusts the opacity of the reference image."), | ||
| }, | ||
| { | ||
| label: localize("&Remove Reference Image"), | ||
| speech_recognition: [ | ||
| "remove reference image", "remove reference", | ||
| "delete reference image", "delete reference", | ||
| "clear reference image", "clear reference", | ||
| ], | ||
| enabled: () => has_reference_image(), | ||
| action: () => { remove_reference_image(); }, | ||
| description: localize("Removes the reference image."), | ||
| }, | ||
| ], | ||
| }, |
There was a problem hiding this comment.
New View > Reference Image menu actions (load/toggle/opacity/remove) aren’t covered by Cypress tests, while the repo already has menu interaction helpers in cypress/integration/visual-tests.spec.js. Add an e2e test that loads a reference image fixture, verifies it appears, toggles visibility, adjusts opacity, and removes it (and verifies menu items enable/disable based on has_reference_image()).
| .reference-image { | ||
| z-index: 2.5; | ||
| display: block !important; | ||
| box-sizing: border-box; | ||
| -moz-box-sizing: border-box; | ||
| pointer-events: none; |
There was a problem hiding this comment.
z-index: 2.5 is not valid per the CSS spec (z-index takes an integer). Some browsers will ignore the declaration, which can break stacking of the reference image relative to .main-canvas (z-index: 2) and .selection/.textbox/.helper-layer (z-index: 3). Use an integer z-index and adjust stacking via DOM order or by shifting the surrounding z-index values (e.g., keep reference image at 2 and ensure it’s appended after .main-canvas, or move .main-canvas to 1 and reference image to 2).
| .reference-image { | ||
| z-index: 2.5; | ||
| display: block !important; | ||
| box-sizing: border-box; | ||
| -moz-box-sizing: border-box; | ||
| pointer-events: none; | ||
| } |
There was a problem hiding this comment.
z-index: 2.5 is not valid per the CSS spec (z-index takes an integer). If the browser drops this rule, the reference image may end up behind the main canvas or above selections inconsistently. Use an integer z-index and adjust stacking using DOM order or integer z-index steps.
| async function load_reference_image() { | ||
| const { file } = await systemHooks.showOpenFileDialog({ formats: image_formats }); | ||
| if (file.type.match(/^image|application\/pdf/)) { |
There was a problem hiding this comment.
systemHooks.showOpenFileDialog can reject/throw when the user cancels (Electron implementation explicitly throws; showOpenFilePicker also rejects). This function doesn’t handle that, so canceling can cause an unhandled rejection. Wrap the dialog call in a try/catch and return early on cancel.
| const { file } = await systemHooks.showOpenFileDialog({ formats: image_formats }); | ||
| if (file.type.match(/^image|application\/pdf/)) { | ||
| read_image_file(file, (error, info) => { | ||
| if (error) { |
There was a problem hiding this comment.
Don’t gate reference image loading on file.type. In Electron, readBlobFromHandle constructs new File([...], fileName) without a MIME type, so file.type is an empty string and this will incorrectly show “not a valid bitmap” for valid images/PDFs. Prefer calling read_image_file(file, ...) directly and rely on its content-based detection/error handling (optionally fall back to checking file.name extension when needed).
| const rect = this.canvas.getBoundingClientRect(); | ||
| const cx = e.clientX - rect.left; | ||
| const cy = e.clientY - rect.top; | ||
| mox = ~~(cx / rect.width * this.canvas.width); | ||
| moy = ~~(cy / rect.height * this.canvas.height); |
There was a problem hiding this comment.
The move-handle drag offset is calculated using this.canvas.getBoundingClientRect(), but the pointerdown occurs on .reference-move-handle (positioned outside the canvas). This can produce negative/incorrect offsets and cause the reference image to jump when dragging. Compute the offset from the reference image’s current position instead (e.g., on pointerdown: const m = to_canvas_coords(e); mox = m.x - this.x; moy = m.y - this.y;).
| const rect = this.canvas.getBoundingClientRect(); | |
| const cx = e.clientX - rect.left; | |
| const cy = e.clientY - rect.top; | |
| mox = ~~(cx / rect.width * this.canvas.width); | |
| moy = ~~(cy / rect.height * this.canvas.height); | |
| const m = to_canvas_coords(e); | |
| mox = m.x - this.x; | |
| moy = m.y - this.y; |
| this.handles = new Handles({ | ||
| $handles_container: this.$el, | ||
| $object_container: $canvas_area, | ||
| outset: 2, | ||
| get_rect: () => ({ x: this.x, y: this.y, width: this.width, height: this.height }), | ||
| set_rect: ({ x, y, width, height }) => { | ||
| this.x = x; | ||
| this.y = y; | ||
| this._resize(width, height); | ||
| this.position(); | ||
| }, | ||
| get_ghost_offset_left: () => parseFloat($canvas_area.css("padding-left")) + 1, | ||
| get_ghost_offset_top: () => parseFloat($canvas_area.css("padding-top")) + 1, | ||
| }); |
There was a problem hiding this comment.
Handles registers global $G.on("resize theme-load", update_handle) listeners but doesn’t provide a way to unregister them. Creating/destroying reference images repeatedly will accumulate window-level handlers (and this class also adds its own resize handler in addition to OnCanvasObject’s). Consider adding a destroy() method to Handles that removes its $G listeners and calling it from ReferenceImage.destroy(). Also consider avoiding the extra resize listener by overriding position() to also update the move-handle position, relying on OnCanvasObject’s resize handler.
No description provided.