Integrate Stable Diffusion with the Imaginate layer (#784)
* Add AI Artist layer * WIP add a button to download the rendered folder under an AI Artist layer * Successfully download the correct image * Break out image downloading JS into helper function * Change file download from using data URLs to blob URLs * WIP rasterize to blob * Remove dimensions from AI Artist layer * Successfully draw rasterized image on layer after calculation * Working txt2img generation based on user prompt * Add img2img and the main parameters * Fix ability to rasterize multi-depth documents with blob URL images by switching them to base64 * Fix test * Rasterize with artboard background color * Allow aspect ratio stretch of AI Artist images * Add automatic resolution choosing * Add a terminate button, and make the lifecycle more robust * Add negative prompt * Add range bounds for parameter inputs * Add seed * Add tiling and restore faces * Add server status check, server hostname customization, and resizing layer to fit AI Artist resolution * Fix background color of infinite canvas rasterization * Escape prompt text sent in the JSON * Revoke blob URLs when cleared/replaced to reduce memory leak * Fix welcome screen logo color * Add PreferencesMessageHandler * Add persistent storage of preferences * Fix crash introduced in previous commit when moving mouse on page load * Add tooltips to the AI Artist layer properties * Integrate AI Artist tool into the raster section of the tool shelf * Add a refresh button to the connection status * Fix crash when generating and switching to a different document tab * Add persistent image storage to AI Artist layers and fix duplication bugs * Add a generate with random seed button * Simplify and standardize message names * Majorly improve robustness of networking code * Fix race condition causing default server hostname to show disconnected when app loads with AI Artist layer selected (probably, not confirmed fixed) * Clean up messages and function calls by changing arguments into structs * Update API to more recent server commit * Add support for picking the sampling method * Add machinery for filtering selected layers with type * Replace placeholder button icons * Improve the random icon by tilting the dice * Use selected_layers() instead of repeating that code * Fix borrow error * Change message flow in progress towards fixing #797 * Allow loading image on non-active document (fixes #797) * Reduce code duplication with rasterization * Add AI Artist tool and layer icons, and remove ugly node layer icon style * Rename "AI Artist" codename to "Imaginate" feature name Co-authored-by: otdavies <oliver@psyfer.io> Co-authored-by: 0hypercube <0hypercube@gmail.com>
@@ -1,11 +1,11 @@
|
||||
<svg width="937" height="240" viewBox="0 0 937 240" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<path fill="#ffffff" d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" />
|
||||
<path fill="#ffffff" d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" />
|
||||
<path fill="#ffffff" d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" />
|
||||
<path fill="#ffffff" d="M732.15,154.68c-0.64-2.97-0.91-6-0.81-9.03v-39.22c0-7.05-1.57-12.18-4.72-15.38c-3.15-3.2-8.08-4.8-14.81-4.8c-4.06,0.01-8.07,0.84-11.8,2.44c-3.08,1.21-6.03,2.75-8.79,4.57c-3.07,2.01-5.99,4.25-8.71,6.72V61.55c0-5.21,0.08-9.52,0.24-12.94c0.16-3.42,0.3-5.94,0.41-7.57L682.11,40l-35.45,16.42l1.66,3.82c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.32,2.85-0.33c0.96-0.07,1.92,0.22,2.68,0.81c0.6,0.55,0.9,1.63,0.9,3.26v82.63c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99c-0.64-2.97-0.91-6-0.82-9.03v-37.92c2.72-1.87,5.71-3.29,8.87-4.23c2.26-0.61,4.58-0.94,6.92-0.98c3.79,0,6.18,1,7.16,3.01c1.06,2.43,1.56,5.08,1.46,7.73v32.39c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99" />
|
||||
<path fill="#ffffff" d="M624.97,90.71c-4.3-2.92-9.37-4.48-14.57-4.48c-5.74-0.16-11.38,1.43-16.19,4.56c-4.26,2.76-7.67,6.65-9.85,11.23h-0.32c0-3.26,0.12-6.35,0.39-9.49c0.14-2.07,0.38-4.14,0.73-6.18l-0.98-0.98l-33.84,15.68l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.81c0.6,0.54,0.9,1.63,0.9,3.25v73.9c0.08,3.02-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.72-2.86,1.11-4.39,1.14V200h43.12v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.96-2.18-3.33-3.99c-0.64-2.97-0.91-6-0.81-9.03v-16.76c1.52,0.22,3.17,0.38,4.96,0.49s3.77,0.16,5.94,0.16c5.18-0.03,10.33-0.8,15.3-2.28c5.21-1.52,10.1-4.01,14.4-7.32c4.5-3.5,8.15-7.98,10.66-13.1c2.71-5.37,4.07-11.96,4.07-19.78c0-7.81-1.36-14.49-4.07-20.02C633.4,98.33,629.66,93.92,624.97,90.71 M608.94,150.61c-3.26,5.04-7.27,7.57-12.04,7.57c-5.21,0-9.33-2.39-12.37-7.16v-43.3c1.7-1.75,3.75-3.11,6.02-3.99c2.03-0.79,4.18-1.2,6.35-1.22c4.77,0,8.79,2.31,12.04,6.92c3.26,4.61,4.88,11.64,4.88,21.08C613.82,138.86,612.19,145.57,608.94,150.61" />
|
||||
<path fill="#ffffff" d="M541.31,150.61c-1.17,0.45-2.41,0.7-3.66,0.73c-1.95,0-3.25-0.68-3.91-2.03c-0.74-1.83-1.07-3.81-0.98-5.78v-35.48c0-12.25-7.16-19.5-19.95-21.8c-8.97-1.62-19.39-1.04-28.28,0.57c-5.06,0.92-10.37,2.79-13.57,5.49v23.95h3.71c0.91-5.48,3.36-10.58,7.07-14.72c3.2-3.81,7.96-5.97,12.94-5.86c3.8,0,6.75,1.11,8.87,3.34c2.12,2.23,3.17,5.89,3.17,10.99v8.63c-13.78,3.69-23.95,7.76-30.52,12.21s-9.85,10.25-9.85,17.42c-0.06,4.5,1.47,8.88,4.31,12.36c2.87,3.58,7.29,5.37,13.27,5.37c4.5-0.01,8.92-1.16,12.86-3.34c4.18-2.27,7.62-5.69,9.93-9.85h0.33c0.95,3.66,3.1,6.9,6.1,9.2c2.87,2.12,6.97,3.17,12.29,3.17c4.71,0.08,9.34-1.19,13.35-3.66c4.15-2.73,7.43-6.6,9.44-11.15l-2.6-2.6C544.39,148.99,542.93,149.96,541.31,150.61 M506.73,146.3c-1.27,1.36-2.72,2.54-4.31,3.5c-1.74,1.05-3.75,1.58-5.78,1.54c-2.11,0.12-4.16-0.75-5.53-2.36c-1.32-1.63-2.02-3.68-1.95-5.78c0.09-1.95,0.5-3.88,1.22-5.7c1.09-2.66,2.82-5.01,5.05-6.84c2.55-2.28,6.32-4.12,11.31-5.53L506.73,146.3z" />
|
||||
<path fill="#ffffff" d="M440.68,91.63c-4.8,1.93-9.07,4.75-11.91,9.87h-0.33c-0.02-2.98,0.11-5.96,0.41-8.92c0.13-2.13,0.37-4.25,0.73-6.35l-0.98-0.98l-33.85,15.79l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.82c0.59,0.54,0.89,1.63,0.9,3.25v37.44c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h43.13v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.97-2.18-3.34-3.99c-0.64-2.97-0.91-6-0.82-9.03v-36.29c2.1-1.79,4.53-3.15,7.16-3.99c2.49-0.72,5.06-1.08,7.65-1.06c2.42,0.01,4.78,0.68,6.84,1.95c2.17,1.3,3.71,5.12,4.48,10h4.1V92.3C455.3,89.03,446.61,89.25,440.68,91.63" />
|
||||
<path fill="#ffffff" d="M344.13,115.53c2.68,0.05,5.32,0.57,7.81,1.55c1.73,0.81,2.9,2.6,3.5,5.37c0.72,4.38,1.02,8.82,0.9,13.26c0,3.8-0.04,6.29-0.2,9.22c-0.16,2.93-0.39,4.51-1.58,6.47c-1.63,2.71-4.43,4-7.41,4.59c-2.7,0.57-5.46,0.87-8.22,0.9c-6.29,0-12.7-1.98-16.81-5.14c-5.27-4.05-9.38-11.35-12.04-19.92c-2.4-8.27-3.58-16.84-3.51-25.45c0-14.54,4.01-24.17,9.38-31.43c5.46-7.37,14.61-11.25,25-11.89c4.13-0.21,8.27,0.21,12.28,1.25c3.63,1.12,7.4,2.65,10.43,6.07c3.03,3.42,4.67,7.11,6.85,13.4h3.74v-24.9c-4.86-1.84-9.87-3.25-14.97-4.23c-5.73-1.18-11.56-1.78-17.41-1.79c-8.11-0.06-16.17,1.23-23.85,3.82c-7.23,2.44-13.91,6.25-19.69,11.23c-5.77,5.04-10.36,11.29-13.43,18.31c-3.38,7.91-5.05,16.46-4.88,25.07c0,10.96,2.39,20.57,7.16,28.81c4.6,8.07,11.36,14.7,19.53,19.12c8.5,4.57,18.02,6.89,27.67,6.76c7.53,0.11,15.02-0.97,22.22-3.18c5.71-1.74,11.2-4.14,16.36-7.16c3.26-1.87,6.32-4.08,9.11-6.59c-0.63-2.67-1.01-5.4-1.14-8.14c-0.11-2.61-0.16-5.37-0.16-8.3v-9.44c0-2.82,0.3-4.77,0.9-5.86c0.66-1.12,1.87-1.81,3.17-1.79v-3.74h-40.7V115.53z" />
|
||||
<path fill="#ffffff" d="M231.18,218.98l-0.07-0.69c-0.86-9.39-11.15-121.38-11.18-121.86c-0.23-2.84-1.07-5.6-2.45-8.09c-0.03-0.09-0.07-0.17-0.11-0.25l-0.06-0.15l-0.03,0.03l-0.02-0.01l0.04-0.02L205.5,67.5L172.31,10c-3.58-6.19-10.18-10-17.33-10H64.99c-7.14,0-13.74,3.81-17.32,10l-45,77.93c-3.57,6.19-3.57,13.81,0,20l45,77.93c3.57,6.19,10.17,10,17.32,10h89.99c3.86-0.03,7.63-1.19,10.85-3.32l38.59,27.68c-6.97-2.18-14.18-3.47-21.47-3.83c-18.11-0.87-71.2-0.28-131.42,4.63c-24.71,2.01-36.39,7.88-35.03,9.03c3.49,2.98,7.62,4.16,28.2,4.08c18.32-0.06,71.65,1.91,87.76,2.9c11.41,0.71,23.41,2.88,32.04,2.97c9.2-0.12,18.37-0.82,27.48-2.1c13.74-1.89,31.96-5.7,36.15-10.77C230.34,225.03,231.47,222.02,231.18,218.98z M62.49,24.32c1.67-2.55,4.45-4.16,7.5-4.33h79.99c3.04,0.17,5.81,1.77,7.49,4.31l33.26,57.61c-4.99,5.2-9.32,11-12.89,17.26l-24.77,2.75L138.3,122c-7.21-0.04-14.4,0.82-21.4,2.54L60.77,27.31L62.49,24.32z M69.99,175.86c-3.05-0.17-5.83-1.78-7.5-4.33l-40-69.27c-1.37-2.72-1.37-5.94,0-8.66l26.73-46.28l59.6,103.24l0.04-0.02c0.69,1.24,1.64,2.31,2.79,3.15l30.93,22.18L69.99,175.86z M186.75,182.93l-57.9-41.53c6.39-1.39,12.91-2.09,19.45-2.09l14.77-20.07l24.77-2.75c3.26-5.66,7.13-10.95,11.52-15.79l7.03,70.9C198.07,170.71,190.13,175.29,186.75,182.93z M81.64,154.71c1.49,2.33,0.8,5.42-1.52,6.91c-2.33,1.49-5.42,0.8-6.91-1.52c-0.08-0.12-0.15-0.25-0.22-0.38l-35-60.61c-1.49-2.33-0.8-5.42,1.52-6.91c2.33-1.49,5.42-0.8,6.91,1.52c0.08,0.12,0.15,0.25,0.22,0.38L81.64,154.71z" />
|
||||
<path d="M934.29,139.3c-3.08,2.94-6.82,5.09-10.91,6.27c-3.49,1.06-7.1,1.63-10.74,1.71c-6.08,0.08-11.98-2.06-16.6-6.02c-4.78-4.01-7.49-10.63-8.14-19.86l48.01-6.02c0-8.68-2.58-15.71-7.73-21.08c-5.16-5.37-12.72-8.06-22.7-8.06c-7.19-0.04-14.29,1.57-20.75,4.72c-6.37,3.07-11.75,7.86-15.54,13.83c-3.91,6.08-5.86,13.46-5.86,22.14c0,8.03,1.76,14.98,5.29,20.83c3.41,5.76,8.38,10.44,14.32,13.51c6.21,3.19,13.11,4.81,20.1,4.72c9.01,0,16.14-2.2,21.41-6.59c5.51-4.74,9.78-10.74,12.45-17.5L934.29,139.3z M891.64,99.01c2.28-3.85,5.26-5.78,8.95-5.78c3.79,0,6.48,1.84,8.06,5.53c1.68,4.2,2.59,8.66,2.69,13.18l-23.6,2.93C888.06,108.15,889.37,102.86,891.64,99.01" />
|
||||
<path d="M844.61,151.33c-7.06,0-10.58-4.34-10.58-13.02v-34.5c0-4.34,2.17-6.51,6.51-6.51h14.65v-8.62h-21.16c0-4.12,0.05-8.19,0.16-12.21c0.11-4.01,0.59-11.63,0.91-15.76l-25.49,11.81v16.16h-9.77v8.62h9.77v44.27c0,7.16,2.01,13.02,6.02,17.58c4.01,4.56,9.87,6.83,17.58,6.84c4.07,0.13,8.11-0.71,11.8-2.44c3.03-1.49,5.72-3.6,7.89-6.18c1.98-2.37,3.62-5,4.88-7.81l-2.6-2.6C852.42,149.81,848.59,151.4,844.61,151.33" />
|
||||
<path d="M783.25,154.67c-0.64-2.97-0.91-6-0.81-9.03v-38.9c0-5.21,0.08-9.52,0.24-12.94s0.3-5.94,0.41-7.57l-0.98-0.98l-35.48,16.44l1.63,3.74c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.31,2.85-0.32c0.97-0.07,1.92,0.22,2.69,0.81c0.59,0.54,0.89,1.63,0.9,3.26v37.43c0.08,3.03-0.14,6.05-0.65,9.03c-0.44,2.01-1.2,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.39,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.39-6.02-1.14C784.64,157.85,783.56,156.38,783.25,154.67 M771.04,77.28c3.74,0.07,7.35-1.44,9.93-4.15c2.64-2.59,4.11-6.15,4.07-9.85c0.03-3.72-1.44-7.3-4.07-9.93c-2.56-2.75-6.17-4.29-9.93-4.23c-3.81-0.09-7.48,1.45-10.09,4.23c-2.64,2.63-4.1,6.21-4.07,9.93c0.02,7.75,6.32,14.02,14.07,14C770.98,77.29,771.01,77.29,771.04,77.28" />
|
||||
<path d="M732.15,154.68c-0.64-2.97-0.91-6-0.81-9.03v-39.22c0-7.05-1.57-12.18-4.72-15.38c-3.15-3.2-8.08-4.8-14.81-4.8c-4.06,0.01-8.07,0.84-11.8,2.44c-3.08,1.21-6.03,2.75-8.79,4.57c-3.07,2.01-5.99,4.25-8.71,6.72V61.55c0-5.21,0.08-9.52,0.24-12.94c0.16-3.42,0.3-5.94,0.41-7.57L682.11,40l-35.45,16.42l1.66,3.82c1.09-0.4,2.2-0.73,3.34-0.98c0.94-0.21,1.89-0.32,2.85-0.33c0.96-0.07,1.92,0.22,2.68,0.81c0.6,0.55,0.9,1.63,0.9,3.26v82.63c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99c-0.64-2.97-0.91-6-0.82-9.03v-37.92c2.72-1.87,5.71-3.29,8.87-4.23c2.26-0.61,4.58-0.94,6.92-0.98c3.79,0,6.18,1,7.16,3.01c1.06,2.43,1.56,5.08,1.46,7.73v32.39c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h41.5v-3.74c-2.06,0-4.1-0.38-6.02-1.14c-1.54-0.81-2.62-2.28-2.93-3.99" />
|
||||
<path d="M624.97,90.71c-4.3-2.92-9.37-4.48-14.57-4.48c-5.74-0.16-11.38,1.43-16.19,4.56c-4.26,2.76-7.67,6.65-9.85,11.23h-0.32c0-3.26,0.12-6.35,0.39-9.49c0.14-2.07,0.38-4.14,0.73-6.18l-0.98-0.98l-33.84,15.68l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.81c0.6,0.54,0.9,1.63,0.9,3.25v73.9c0.08,3.02-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.72-2.86,1.11-4.39,1.14V200h43.12v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.96-2.18-3.33-3.99c-0.64-2.97-0.91-6-0.81-9.03v-16.76c1.52,0.22,3.17,0.38,4.96,0.49s3.77,0.16,5.94,0.16c5.18-0.03,10.33-0.8,15.3-2.28c5.21-1.52,10.1-4.01,14.4-7.32c4.5-3.5,8.15-7.98,10.66-13.1c2.71-5.37,4.07-11.96,4.07-19.78c0-7.81-1.36-14.49-4.07-20.02C633.4,98.33,629.66,93.92,624.97,90.71 M608.94,150.61c-3.26,5.04-7.27,7.57-12.04,7.57c-5.21,0-9.33-2.39-12.37-7.16v-43.3c1.7-1.75,3.75-3.11,6.02-3.99c2.03-0.79,4.18-1.2,6.35-1.22c4.77,0,8.79,2.31,12.04,6.92c3.26,4.61,4.88,11.64,4.88,21.08C613.82,138.86,612.19,145.57,608.94,150.61" />
|
||||
<path d="M541.31,150.61c-1.17,0.45-2.41,0.7-3.66,0.73c-1.95,0-3.25-0.68-3.91-2.03c-0.74-1.83-1.07-3.81-0.98-5.78v-35.48c0-12.25-7.16-19.5-19.95-21.8c-8.97-1.62-19.39-1.04-28.28,0.57c-5.06,0.92-10.37,2.79-13.57,5.49v23.95h3.71c0.91-5.48,3.36-10.58,7.07-14.72c3.2-3.81,7.96-5.97,12.94-5.86c3.8,0,6.75,1.11,8.87,3.34c2.12,2.23,3.17,5.89,3.17,10.99v8.63c-13.78,3.69-23.95,7.76-30.52,12.21s-9.85,10.25-9.85,17.42c-0.06,4.5,1.47,8.88,4.31,12.36c2.87,3.58,7.29,5.37,13.27,5.37c4.5-0.01,8.92-1.16,12.86-3.34c4.18-2.27,7.62-5.69,9.93-9.85h0.33c0.95,3.66,3.1,6.9,6.1,9.2c2.87,2.12,6.97,3.17,12.29,3.17c4.71,0.08,9.34-1.19,13.35-3.66c4.15-2.73,7.43-6.6,9.44-11.15l-2.6-2.6C544.39,148.99,542.93,149.96,541.31,150.61 M506.73,146.3c-1.27,1.36-2.72,2.54-4.31,3.5c-1.74,1.05-3.75,1.58-5.78,1.54c-2.11,0.12-4.16-0.75-5.53-2.36c-1.32-1.63-2.02-3.68-1.95-5.78c0.09-1.95,0.5-3.88,1.22-5.7c1.09-2.66,2.82-5.01,5.05-6.84c2.55-2.28,6.32-4.12,11.31-5.53L506.73,146.3z" />
|
||||
<path d="M440.68,91.63c-4.8,1.93-9.07,4.75-11.91,9.87h-0.33c-0.02-2.98,0.11-5.96,0.41-8.92c0.13-2.13,0.37-4.25,0.73-6.35l-0.98-0.98l-33.85,15.79l1.63,3.74c1.49-0.4,3.02-0.62,4.56-0.65c0.97-0.07,1.92,0.22,2.69,0.82c0.59,0.54,0.89,1.63,0.9,3.25v37.44c0.08,3.03-0.14,6.05-0.65,9.03c-0.43,2.01-1.19,3.34-2.28,3.99c-1.35,0.73-2.86,1.12-4.4,1.14v3.74h43.13v-3.74c-2.46,0.01-4.9-0.38-7.24-1.14c-1.71-0.68-2.97-2.18-3.34-3.99c-0.64-2.97-0.91-6-0.82-9.03v-36.29c2.1-1.79,4.53-3.15,7.16-3.99c2.49-0.72,5.06-1.08,7.65-1.06c2.42,0.01,4.78,0.68,6.84,1.95c2.17,1.3,3.71,5.12,4.48,10h4.1V92.3C455.3,89.03,446.61,89.25,440.68,91.63" />
|
||||
<path d="M344.13,115.53c2.68,0.05,5.32,0.57,7.81,1.55c1.73,0.81,2.9,2.6,3.5,5.37c0.72,4.38,1.02,8.82,0.9,13.26c0,3.8-0.04,6.29-0.2,9.22c-0.16,2.93-0.39,4.51-1.58,6.47c-1.63,2.71-4.43,4-7.41,4.59c-2.7,0.57-5.46,0.87-8.22,0.9c-6.29,0-12.7-1.98-16.81-5.14c-5.27-4.05-9.38-11.35-12.04-19.92c-2.4-8.27-3.58-16.84-3.51-25.45c0-14.54,4.01-24.17,9.38-31.43c5.46-7.37,14.61-11.25,25-11.89c4.13-0.21,8.27,0.21,12.28,1.25c3.63,1.12,7.4,2.65,10.43,6.07c3.03,3.42,4.67,7.11,6.85,13.4h3.74v-24.9c-4.86-1.84-9.87-3.25-14.97-4.23c-5.73-1.18-11.56-1.78-17.41-1.79c-8.11-0.06-16.17,1.23-23.85,3.82c-7.23,2.44-13.91,6.25-19.69,11.23c-5.77,5.04-10.36,11.29-13.43,18.31c-3.38,7.91-5.05,16.46-4.88,25.07c0,10.96,2.39,20.57,7.16,28.81c4.6,8.07,11.36,14.7,19.53,19.12c8.5,4.57,18.02,6.89,27.67,6.76c7.53,0.11,15.02-0.97,22.22-3.18c5.71-1.74,11.2-4.14,16.36-7.16c3.26-1.87,6.32-4.08,9.11-6.59c-0.63-2.67-1.01-5.4-1.14-8.14c-0.11-2.61-0.16-5.37-0.16-8.3v-9.44c0-2.82,0.3-4.77,0.9-5.86c0.66-1.12,1.87-1.81,3.17-1.79v-3.74h-40.7V115.53z" />
|
||||
<path d="M231.18,218.98l-0.07-0.69c-0.86-9.39-11.15-121.38-11.18-121.86c-0.23-2.84-1.07-5.6-2.45-8.09c-0.03-0.09-0.07-0.17-0.11-0.25l-0.06-0.15l-0.03,0.03l-0.02-0.01l0.04-0.02L205.5,67.5L172.31,10c-3.58-6.19-10.18-10-17.33-10H64.99c-7.14,0-13.74,3.81-17.32,10l-45,77.93c-3.57,6.19-3.57,13.81,0,20l45,77.93c3.57,6.19,10.17,10,17.32,10h89.99c3.86-0.03,7.63-1.19,10.85-3.32l38.59,27.68c-6.97-2.18-14.18-3.47-21.47-3.83c-18.11-0.87-71.2-0.28-131.42,4.63c-24.71,2.01-36.39,7.88-35.03,9.03c3.49,2.98,7.62,4.16,28.2,4.08c18.32-0.06,71.65,1.91,87.76,2.9c11.41,0.71,23.41,2.88,32.04,2.97c9.2-0.12,18.37-0.82,27.48-2.1c13.74-1.89,31.96-5.7,36.15-10.77C230.34,225.03,231.47,222.02,231.18,218.98z M62.49,24.32c1.67-2.55,4.45-4.16,7.5-4.33h79.99c3.04,0.17,5.81,1.77,7.49,4.31l33.26,57.61c-4.99,5.2-9.32,11-12.89,17.26l-24.77,2.75L138.3,122c-7.21-0.04-14.4,0.82-21.4,2.54L60.77,27.31L62.49,24.32z M69.99,175.86c-3.05-0.17-5.83-1.78-7.5-4.33l-40-69.27c-1.37-2.72-1.37-5.94,0-8.66l26.73-46.28l59.6,103.24l0.04-0.02c0.69,1.24,1.64,2.31,2.79,3.15l30.93,22.18L69.99,175.86z M186.75,182.93l-57.9-41.53c6.39-1.39,12.91-2.09,19.45-2.09l14.77-20.07l24.77-2.75c3.26-5.66,7.13-10.95,11.52-15.79l7.03,70.9C198.07,170.71,190.13,175.29,186.75,182.93z M81.64,154.71c1.49,2.33,0.8,5.42-1.52,6.91c-2.33,1.49-5.42,0.8-6.91-1.52c-0.08-0.12-0.15-0.25-0.22-0.38l-35-60.61c-1.49-2.33-0.8-5.42,1.52-6.91c2.33-1.49,5.42-0.8,6.91,1.52c0.08,0.12,0.15,0.25,0.22,0.38L81.64,154.71z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.6,13.6C9.5,14.5,8.7,15,8,15c-0.5,0-1-0.2-1.3-0.6L9.6,13.6z M6.3,12.5c0,0.3,0,0.6,0.1,1c0,0,0,0,0,0.1l3.3-0.8c0-0.5,0.1-0.8,0.1-1.1L6.3,12.5z M8,2c2.3,0,4.1,1.9,4.1,4.2c0,2.5-1.5,3.1-2.1,4.5c0,0,0,0.1,0,0.1l-3.8,0.9c-0.1-0.4-0.2-0.7-0.3-1C5.4,9.3,3.9,8.7,3.9,6.2c0,0,0,0,0,0C3.9,3.9,5.7,2,8,2C8,2,8,2,8,2z M8.2,8.9C8,8.7,7.3,9.2,7.1,9.5c-0.2,0.3-0.2,0.7,0.1,1c0.5,0.1,0.9,0,1.2-0.4C8.6,9.7,8.6,9.2,8.2,8.9z M8.2,8c0.3-0.7,1.2-4.7,1.2-4.7C8.2,2.7,7.1,4.2,7.1,4.2C7,5.5,7.1,6.8,7.2,8.1C7.6,8.2,7.9,8.1,8.2,8L8.2,8z" />
|
||||
<polygon points="3,1 1,1 0,1 0,2 0,4 1,4 1,2 3,2" />
|
||||
<polygon points="15,1 13,1 13,2 15,2 15,4 16,4 16,2 16,1" />
|
||||
<polygon points="1,14 1,12 0,12 0,14 0,15 1,15 3,15 3,14" />
|
||||
<polygon points="15,12 15,14 13,14 13,15 15,15 16,15 16,14 16,12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M15.9,9.6L14,2.7c-0.5-1.9-2.5-3.1-4.4-2.5L2.7,2C0.7,2.5-0.4,4.5,0.1,6.4L2,13.3c0.5,1.9,2.5,3.1,4.4,2.5l6.9-1.9C15.3,13.5,16.4,11.5,15.9,9.6z M2.3,5.9C2.1,5.1,2.5,4.3,3.3,4.1c0.8-0.2,1.6,0.3,1.8,1.1C5.4,5.9,4.9,6.8,4.1,7C3.3,7.2,2.5,6.7,2.3,5.9z M5.9,13.7c-0.8,0.2-1.6-0.3-1.8-1.1c-0.2-0.8,0.3-1.6,1.1-1.8c0.8-0.2,1.6,0.3,1.8,1.1C7.2,12.7,6.7,13.5,5.9,13.7z M8.4,9.4C7.6,9.7,6.8,9.2,6.6,8.4C6.3,7.6,6.8,6.8,7.6,6.6c0.8-0.2,1.6,0.3,1.8,1.1C9.7,8.4,9.2,9.2,8.4,9.4z M9,4.1c-0.2-0.8,0.3-1.6,1.1-1.8c0.8-0.2,1.6,0.3,1.8,1.1c0.2,0.8-0.3,1.6-1.1,1.8C10.1,5.4,9.2,4.9,9,4.1z M12.7,11.9c-0.8,0.2-1.6-0.3-1.8-1.1c-0.2-0.8,0.3-1.6,1.1-1.8c0.8-0.2,1.6,0.3,1.8,1.1C13.9,10.9,13.5,11.7,12.7,11.9z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 765 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<polygon points="12.6,0 15.5,5 9.7,5" />
|
||||
<path d="M2.2,8H0.8C0.8,4,4,0.8,8,0.8c2,0,3.9,0.8,5.3,2.3l-1,1C11.2,2.9,9.6,2.2,8,2.2C4.8,2.2,2.2,4.8,2.2,8z" />
|
||||
<path d="M8,15.2c-2,0-3.9-0.8-5.3-2.3l1-1c1.1,1.2,2.6,1.9,4.3,1.9c3.2,0,5.8-2.6,5.8-5.8h1.4C15.2,12,12,15.2,8,15.2z" />
|
||||
<polygon points="3.4,16 0.5,11 6.3,11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M8,15.2C4,15.2,0.8,12,0.8,8C0.8,4,4,0.8,8,0.8c2,0,3.9,0.8,5.3,2.3l-1,1C11.2,2.9,9.6,2.2,8,2.2C4.8,2.2,2.2,4.8,2.2,8s2.6,5.8,5.8,5.8s5.8-2.6,5.8-5.8h1.4C15.2,12,12,15.2,8,15.2z" />
|
||||
<polygon points="12.6,0 15.5,5 9.7,5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 300 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<polygon points="3,1 1,1 0,1 0,2 0,4 1,4 1,2 3,2" />
|
||||
<polygon points="15,1 13,1 13,2 15,2 15,4 16,4 16,2 16,1" />
|
||||
<polygon points="1,14 1,12 0,12 0,14 0,15 1,15 3,15 3,14" />
|
||||
<polygon points="15,12 15,14 13,14 13,15 15,15 16,15 16,14 16,12" />
|
||||
<path d="M12,5v6H4V5H12 M13,4H3v8h10V4L13,4z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<polygon points="0,8 2.4,12.2 4.9,8" />
|
||||
<path d="M8.8,15.2c-1.2,0-2.5-0.3-3.6-1L5.9,13c2.8,1.6,6.3,0.6,7.9-2.1c0.8-1.3,1-2.9,0.6-4.4S13,3.8,11.7,3C8.9,1.4,5.4,2.3,3.8,5.1C3,6.5,2.8,8.2,3.3,9.7l-1.3,0.4c-0.6-1.9-0.4-4,0.6-5.8C4.5,1,9-0.2,12.4,1.8c1.7,1,2.9,2.5,3.4,4.4C16.2,8,16,9.9,15,11.6C13.7,13.9,11.3,15.2,8.8,15.2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 394 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M14,8c0-0.3,0-0.7-0.1-1l1.8-1.7l-1.5-2.6l-2.4,0.7c-0.5-0.4-1.1-0.8-1.7-1L9.5,0h-3L5.9,2.4C5.3,2.6,4.7,3,4.2,3.4L1.8,2.7L0.3,5.3L2.1,7C2,7.3,2,7.7,2,8s0,0.7,0.1,1l-1.8,1.7l1.5,2.6l2.4-0.7c0.5,0.4,1.1,0.8,1.7,1L6.5,16h3l0.6-2.4c0.6-0.2,1.2-0.6,1.7-1l2.4,0.7l1.5-2.6L13.9,9C14,8.7,14,8.3,14,8z M8,11c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S9.7,11,8,11z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 429 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path class="color-solid" d="M10.1,22.1c0.5,0.6,1.3,0.9,1.9,0.9c1,0,2.3-0.7,2.4-2L10.1,22.1z" />
|
||||
<path class="color-solid" d="M9.5,19.4c0,0.4,0,0.9,0.1,1.5c0,0,0,0.1,0,0.1l4.8-1.2c0-0.7,0.1-1.2,0.2-1.6L9.5,19.4z" />
|
||||
<path class="color-raster" d="M12,4c-3.3,0-6,2.8-6,6.1c0,0,0,0,0,0c0,3.6,2.2,4.5,3,6.6c0.2,0.5,0.3,1,0.4,1.5l5.5-1.4c0,0,0-0.1,0-0.1c0.8-2.1,3-3,3-6.6C18,6.8,15.3,4,12,4C12,4,12,4,12,4z M12.5,15.8c-0.4,0.6-1.1,0.9-1.7,0.7c-0.4-0.4-0.4-1-0.1-1.5c0.3-0.5,1.3-1.2,1.7-0.9C12.8,14.5,12.9,15.3,12.5,15.8z M12.3,12.7c-0.5,0.2-1,0.3-1.5,0.2c-0.2-1.9-0.3-3.8-0.2-5.7c0,0,1.7-2.2,3.5-1.4C14.2,5.8,12.8,11.8,12.3,12.7L12.3,12.7z" />
|
||||
<path class="color-solid" d="M3.8,12.7l-1.4,0.6c-0.4,0.2-0.6,0.6-0.4,1c0.1,0.3,0.4,0.5,0.7,0.5c0.1,0,0.2,0,0.3-0.1l1.4-0.6c0.4-0.2,0.6-0.6,0.4-1S4.2,12.5,3.8,12.7z" />
|
||||
<path class="color-solid" d="M4.3,6.1L3,5.5c-0.4-0.2-0.8,0-1,0.4c-0.2,0.4,0,0.8,0.4,1l1.4,0.6c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.6-0.2,0.7-0.5C4.9,6.7,4.7,6.3,4.3,6.1z" />
|
||||
<path class="color-solid" d="M19.9,7.6c0.1,0,0.2,0,0.3-0.1l1.4-0.6c0.4-0.2,0.6-0.6,0.4-1c-0.2-0.4-0.6-0.6-1-0.4l-1.4,0.6c-0.4,0.2-0.6,0.6-0.4,1C19.4,7.4,19.6,7.6,19.9,7.6z" />
|
||||
<path class="color-solid" d="M21.6,13.3l-1.4-0.6c-0.4-0.2-0.8,0-1,0.4c-0.2,0.4,0,0.8,0.4,1l1.4,0.6c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.6-0.2,0.7-0.5C22.2,13.9,22,13.4,21.6,13.3z" />
|
||||
<path class="color-solid" d="M8.8,0.5c-0.2-0.4-0.6-0.6-1-0.4c-0.4,0.2-0.6,0.6-0.4,1L8,2.4c0.1,0.3,0.4,0.5,0.7,0.5c0.1,0,0.2,0,0.3-0.1c0.4-0.2,0.6-0.6,0.4-1L8.8,0.5z" />
|
||||
<path class="color-solid" d="M16.2,0.1c-0.4-0.2-0.8,0-1,0.4l-0.6,1.4c-0.2,0.4,0,0.8,0.4,1c0.1,0,0.2,0.1,0.3,0.1c0.3,0,0.6-0.2,0.7-0.5l0.6-1.4C16.7,0.7,16.5,0.2,16.2,0.1z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -217,7 +217,6 @@ img {
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { createBlobManager } from "@/io-managers/blob";
|
||||
import { createClipboardManager } from "@/io-managers/clipboard";
|
||||
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
|
||||
import { createInputManager } from "@/io-managers/input";
|
||||
@@ -236,7 +235,6 @@ import { createEditor, type Editor } from "@/wasm-communication/editor";
|
||||
import MainWindow from "@/components/window/MainWindow.vue";
|
||||
|
||||
const managerDestructors: {
|
||||
createBlobManager?: () => void;
|
||||
createClipboardManager?: () => void;
|
||||
createHyperlinkManager?: () => void;
|
||||
createInputManager?: () => void;
|
||||
@@ -285,7 +283,6 @@ export default defineComponent({
|
||||
async mounted() {
|
||||
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
|
||||
Object.assign(managerDestructors, {
|
||||
createBlobManager: createBlobManager(this.editor),
|
||||
createClipboardManager: createClipboardManager(this.editor),
|
||||
createHyperlinkManager: createHyperlinkManager(this.editor),
|
||||
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
class="row"
|
||||
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
|
||||
:style="{ height: virtualScrollingEntryHeight || '20px' }"
|
||||
:title="tooltip"
|
||||
@click="() => !entry.disabled && onEntryClick(entry)"
|
||||
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
|
||||
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
|
||||
@@ -184,6 +185,7 @@ const MenuList = defineComponent({
|
||||
interactive: { type: Boolean as PropType<boolean>, default: false },
|
||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||
virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -45,17 +45,14 @@
|
||||
@click.alt="(e: MouseEvent) => e.stopPropagation()"
|
||||
>
|
||||
<LayoutRow class="layer-type-icon">
|
||||
<IconLabel v-if="listing.entry.layerType === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" />
|
||||
<IconLabel v-else-if="listing.entry.layerType === 'Image'" :icon="'NodeImage'" :iconStyle="'Node'" title="Image" />
|
||||
<IconLabel v-else-if="listing.entry.layerType === 'Shape'" :icon="'NodeShape'" :iconStyle="'Node'" title="Shape" />
|
||||
<IconLabel v-else-if="listing.entry.layerType === 'Text'" :icon="'NodeText'" :iconStyle="'Node'" title="Path" />
|
||||
<IconLabel :icon="layerTypeData(listing.entry.layerType).icon" :title="layerTypeData(listing.entry.layerType).name" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
||||
<input
|
||||
data-text-input
|
||||
type="text"
|
||||
:value="listing.entry.name"
|
||||
:placeholder="listing.entry.layerType"
|
||||
:placeholder="layerTypeData(listing.entry.layerType).name"
|
||||
:disabled="!listing.editingName"
|
||||
@blur="() => onEditLayerNameDeselect(listing)"
|
||||
@keydown.esc="onEditLayerNameDeselect(listing)"
|
||||
@@ -268,7 +265,16 @@
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
import { platformIsMac } from "@/utility-functions/platform";
|
||||
import { type LayerPanelEntry, defaultWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructure, UpdateLayerTreeOptionsLayout } from "@/wasm-communication/messages";
|
||||
import {
|
||||
type LayerType,
|
||||
type LayerTypeData,
|
||||
type LayerPanelEntry,
|
||||
defaultWidgetLayout,
|
||||
UpdateDocumentLayerDetails,
|
||||
UpdateDocumentLayerTreeStructure,
|
||||
UpdateLayerTreeOptionsLayout,
|
||||
layerTypeData,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
@@ -484,6 +490,9 @@ export default defineComponent({
|
||||
|
||||
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
|
||||
},
|
||||
layerTypeData(layerType: LayerType): LayerTypeData {
|
||||
return layerTypeData(layerType) || { name: "Error", icon: "NodeText" };
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeImage'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeImage'" />
|
||||
<TextLabel>Image</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMask'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeMask'" />
|
||||
<TextLabel>Mask</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
@@ -69,7 +69,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeTransform'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeTransform'" />
|
||||
<TextLabel>Transform</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeMotionBlur'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeMotionBlur'" />
|
||||
<TextLabel>Motion Blur</TextLabel>
|
||||
</div>
|
||||
<div class="arguments">
|
||||
@@ -110,7 +110,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeShape'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeShape'" />
|
||||
<TextLabel>Shape</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBrushwork'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeBrushwork'" />
|
||||
<TextLabel>Brushwork</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeBlur'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeBlur'" />
|
||||
<TextLabel>Blur</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +152,7 @@
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<IconLabel :icon="'NodeGradient'" :iconStyle="'Node'" />
|
||||
<IconLabel :icon="'NodeGradient'" />
|
||||
<TextLabel>Gradient</TextLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,12 +21,6 @@
|
||||
.options-bar {
|
||||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.widget-row > .icon-label:first-of-type {
|
||||
border-radius: 2px;
|
||||
background: var(--color-node-background);
|
||||
fill: var(--color-node-icon);
|
||||
}
|
||||
}
|
||||
|
||||
.sections {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="popover-button">
|
||||
<IconButton :action="() => onClick()" :icon="icon" :size="16" data-hover-menu-spawner />
|
||||
<IconButton :action="() => onClick()" :icon="icon" :size="16" data-hover-menu-spawner :tooltip="tooltip" />
|
||||
<FloatingMenu v-model:open="open" :type="'Popover'" :direction="'Bottom'">
|
||||
<slot></slot>
|
||||
</FloatingMenu>
|
||||
@@ -58,6 +58,7 @@ import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<() => void>, required: false },
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:data-emphasized="emphasized || undefined"
|
||||
:data-disabled="disabled || undefined"
|
||||
data-text-button
|
||||
:title="tooltip"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
@click="(e: MouseEvent) => action(e)"
|
||||
>
|
||||
@@ -71,23 +72,6 @@ import { type IconName } from "@/utility-functions/icons";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
|
||||
export type TextButtonWidget = {
|
||||
tooltip?: string;
|
||||
message?: string | object;
|
||||
callback?: () => void;
|
||||
props: {
|
||||
kind: "TextButton";
|
||||
label: string;
|
||||
icon?: string;
|
||||
emphasized?: boolean;
|
||||
minWidth?: number;
|
||||
disabled?: boolean;
|
||||
|
||||
// Callbacks
|
||||
// `action` is used via `IconButtonWidget.callback`
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
label: { type: String as PropType<string>, required: true },
|
||||
@@ -95,6 +79,7 @@ export default defineComponent({
|
||||
emphasized: { type: Boolean as PropType<boolean>, default: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Callbacks
|
||||
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
|
||||
|
||||
@@ -61,10 +61,14 @@
|
||||
.body {
|
||||
margin: 0 4px;
|
||||
|
||||
.text-label {
|
||||
.text-label:first-of-type {
|
||||
flex: 0 0 30%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
class="dropdown-box"
|
||||
:class="{ disabled, open }"
|
||||
:style="{ minWidth: `${minWidth}px` }"
|
||||
:title="tooltip"
|
||||
@click="() => !disabled && (open = true)"
|
||||
@blur="(e: FocusEvent) => blur(e)"
|
||||
@keydown="(e: KeyboardEvent) => keydown(e)"
|
||||
@@ -115,6 +116,7 @@ export default defineComponent({
|
||||
drawIcon: { type: Boolean as PropType<boolean>, default: false },
|
||||
interactive: { type: Boolean as PropType<boolean>, default: true },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
:title="tooltip"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
@@ -26,6 +27,7 @@
|
||||
v-model="inputValue"
|
||||
:spellcheck="spellcheck"
|
||||
:disabled="disabled"
|
||||
:title="tooltip"
|
||||
@focus="() => $emit('textFocused')"
|
||||
@blur="() => $emit('textChanged')"
|
||||
@change="() => $emit('textChanged')"
|
||||
@@ -55,6 +57,7 @@
|
||||
padding: 3px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:not(.disabled) label {
|
||||
@@ -129,6 +132,7 @@ export default defineComponent({
|
||||
spellcheck: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" tabindex="0" @click="toggleOpen" @keydown="keydown" data-hover-menu-spawner>
|
||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" :title="tooltip" tabindex="0" @click="toggleOpen" @keydown="keydown" data-hover-menu-spawner>
|
||||
<span>{{ activeEntry?.value || "" }}</span>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
@@ -87,6 +87,7 @@ export default defineComponent({
|
||||
fontStyle: { type: String as PropType<string>, required: true },
|
||||
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
:label="label"
|
||||
:spellcheck="false"
|
||||
:disabled="disabled"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
:tooltip="tooltip"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
@@ -107,6 +109,8 @@ export default defineComponent({
|
||||
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
||||
incrementFactor: { type: Number as PropType<number>, default: 1 },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
|
||||
// Callbacks
|
||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||
@@ -122,7 +126,7 @@ export default defineComponent({
|
||||
onTextFocused() {
|
||||
if (this.value === undefined) this.text = "";
|
||||
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
||||
else this.text = `${this.value}${this.unit}`;
|
||||
else this.text = `${this.value}${unPluralize(this.unit, this.value)}`;
|
||||
|
||||
this.editing = true;
|
||||
|
||||
@@ -201,7 +205,7 @@ export default defineComponent({
|
||||
|
||||
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
||||
|
||||
return `${displayValue}${this.unit}`;
|
||||
return `${displayValue}${unPluralize(this.unit, value)}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -222,4 +226,9 @@ export default defineComponent({
|
||||
},
|
||||
components: { FieldInput },
|
||||
});
|
||||
|
||||
function unPluralize(unit: string, value: number): string {
|
||||
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
|
||||
return unit;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
:textarea="true"
|
||||
class="text-area-input"
|
||||
:class="{ 'has-label': label }"
|
||||
v-model:value="inputValue"
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
v-model:value="inputValue"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
@@ -27,6 +28,7 @@ export default defineComponent({
|
||||
value: { type: String as PropType<string>, required: true },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
:label="label"
|
||||
:spellcheck="true"
|
||||
:disabled="disabled"
|
||||
:tooltip="tooltip"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
@textFocused="() => onTextFocused()"
|
||||
@textChanged="() => onTextChanged()"
|
||||
@cancelTextChange="() => onCancelTextChange()"
|
||||
@@ -31,6 +33,8 @@ export default defineComponent({
|
||||
value: { type: String as PropType<string>, required: true },
|
||||
label: { type: String as PropType<string>, required: false },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LayoutRow :class="['icon-label', iconSizeClass, iconStyleClass]">
|
||||
<LayoutRow :class="['icon-label', iconSizeClass]" :title="tooltip">
|
||||
<component :is="icon" />
|
||||
</LayoutRow>
|
||||
</template>
|
||||
@@ -23,35 +23,25 @@
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.node-style {
|
||||
border-radius: 2px;
|
||||
background: var(--color-node-background);
|
||||
fill: var(--color-node-icon);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
import { type IconName, type IconStyle, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
|
||||
import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
icon: { type: String as PropType<IconName>, required: true },
|
||||
iconStyle: { type: String as PropType<IconStyle | undefined>, required: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
computed: {
|
||||
iconSizeClass(): string {
|
||||
return `size-${ICONS[this.icon].size}`;
|
||||
},
|
||||
iconStyleClass(): string {
|
||||
if (!this.iconStyle || this.iconStyle === "Normal") return "";
|
||||
return `${this.iconStyle.toLowerCase()}-style`;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
LayoutRow,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span class="text-label" :class="{ bold, italic, multiline, 'table-align': tableAlign }">
|
||||
<span class="text-label" :class="{ bold, italic, multiline, 'table-align': tableAlign }" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" :title="tooltip">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
@@ -37,7 +37,9 @@ export default defineComponent({
|
||||
bold: { type: Boolean as PropType<boolean>, default: false },
|
||||
italic: { type: Boolean as PropType<boolean>, default: false },
|
||||
tableAlign: { type: Boolean as PropType<boolean>, default: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
multiline: { type: Boolean as PropType<boolean>, default: false },
|
||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
<LayoutRow class="tab-bar" data-tab-bar :class="{ 'min-widths': tabMinWidths }">
|
||||
<LayoutRow class="tab-group" :scrollableX="true">
|
||||
<LayoutRow
|
||||
class="tab"
|
||||
:class="{ active: tabIndex === tabActiveIndex }"
|
||||
data-tab
|
||||
v-for="(tabLabel, tabIndex) in tabLabels"
|
||||
:key="tabIndex"
|
||||
class="tab"
|
||||
:class="{ active: tabIndex === tabActiveIndex }"
|
||||
:title="tabLabel.tooltip || null"
|
||||
@click="(e: MouseEvent) => (e?.stopPropagation(), clickAction?.(tabIndex))"
|
||||
@click.middle="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))"
|
||||
data-tab
|
||||
>
|
||||
<span>{{ tabLabel }}</span>
|
||||
<span>{{ tabLabel.name }}</span>
|
||||
<IconButton :action="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
|
||||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
@@ -31,7 +32,7 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton :label="'New Document:'" :icon="'File'" :action="() => newDocument()" />
|
||||
<TextButton :label="'New Document'" :icon="'File'" :action="() => newDocument()" />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(true), { key: 'KeyN', label: 'N' }]]" />
|
||||
@@ -39,7 +40,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton :label="'Open Document:'" :icon="'Folder'" :action="() => openDocument()" />
|
||||
<TextButton :label="'Open Document'" :icon="'Folder'" :action="() => openDocument()" />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(false), { key: 'KeyO', label: 'O' }]]" />
|
||||
@@ -244,7 +245,7 @@ export default defineComponent({
|
||||
props: {
|
||||
tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
|
||||
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
|
||||
tabLabels: { type: Array as PropType<string[]>, required: true },
|
||||
tabLabels: { type: Array as PropType<{ name: string; tooltip?: string }[]>, required: true },
|
||||
tabActiveIndex: { type: Number as PropType<number>, required: true },
|
||||
panelType: { type: String as PropType<PanelTypes>, required: false },
|
||||
clickAction: { type: Function as PropType<(index: number) => void>, required: false },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
|
||||
:tabCloseButtons="true"
|
||||
:tabMinWidths="true"
|
||||
:tabLabels="portfolio.state.documents.map((doc) => doc.displayName)"
|
||||
:tabLabels="portfolio.state.documents.map((doc) => ({ name: doc.displayName, tooltip: doc.id }))"
|
||||
:clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)"
|
||||
:closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
|
||||
:tabActiveIndex="portfolio.state.activeDocumentIndex"
|
||||
@@ -16,17 +16,17 @@
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible">
|
||||
<Panel :panelType="'NodeGraph'" :tabLabels="['Node Graph']" :tabActiveIndex="0" />
|
||||
<Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
|
||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17">
|
||||
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.2">
|
||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
|
||||
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" />
|
||||
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
|
||||
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
|
||||
<Panel :panelType="'LayerTree'" :tabLabels="['Layer Tree']" :tabActiveIndex="0" />
|
||||
<Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import { UpdateImageData } from "@/wasm-communication/messages";
|
||||
|
||||
export function createBlobManager(editor: Editor): void {
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
updateImageData.imageData.forEach(async (element) => {
|
||||
// Using updateImageData.imageData.buffer returns undefined for some reason?
|
||||
const buffer = new Uint8Array(element.imageData.values()).buffer;
|
||||
const blob = new Blob([buffer], { type: element.mime });
|
||||
|
||||
// TODO: Call `URL.revokeObjectURL` at the appropriate time to avoid a memory leak
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
const image = await createImageBitmap(blob);
|
||||
|
||||
editor.instance.setImageBlobUrl(element.path, blobURL, image.width, image.height);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -25,8 +25,8 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
|
||||
function preparePanicDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
|
||||
const widgets: WidgetLayout = {
|
||||
layout: [
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, multiline: false }, 0n)] },
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, multiline: true }, 1n)] },
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: header, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" }, 0n)] },
|
||||
{ rowWidgets: [new Widget({ kind: "TextLabel", value: details, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" }, 1n)] },
|
||||
],
|
||||
layoutTarget: undefined,
|
||||
};
|
||||
|
||||
@@ -1,99 +1,155 @@
|
||||
import { type PortfolioState } from "@/state-providers/portfolio";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument } from "@/wasm-communication/messages";
|
||||
import { TriggerIndexedDbWriteDocument, TriggerIndexedDbRemoveDocument, TriggerSavePreferences, TriggerLoadAutoSaveDocuments, TriggerLoadPreferences } from "@/wasm-communication/messages";
|
||||
|
||||
const GRAPHITE_INDEXED_DB_VERSION = 2;
|
||||
const GRAPHITE_INDEXED_DB_NAME = "graphite-indexed-db";
|
||||
const GRAPHITE_AUTO_SAVE_STORE = "auto-save-documents";
|
||||
|
||||
const GRAPHITE_AUTO_SAVE_STORE = { name: "auto-save-documents", keyPath: "details.id" };
|
||||
const GRAPHITE_EDITOR_PREFERENCES_STORE = { name: "editor-preferences", keyPath: "key" };
|
||||
|
||||
const GRAPHITE_INDEXEDDB_STORES = [GRAPHITE_AUTO_SAVE_STORE, GRAPHITE_EDITOR_PREFERENCES_STORE];
|
||||
|
||||
const GRAPHITE_AUTO_SAVE_ORDER_KEY = "auto-save-documents-order";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export async function createPersistenceManager(editor: Editor, portfolio: PortfolioState): Promise<() => void> {
|
||||
function storeDocumentOrder(): void {
|
||||
// Make sure to store as string since JSON does not play nice with BigInt
|
||||
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString());
|
||||
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
|
||||
}
|
||||
export function createPersistenceManager(editor: Editor, portfolio: PortfolioState): () => void {
|
||||
async function initialize(): Promise<IDBDatabase> {
|
||||
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
|
||||
return new Promise<IDBDatabase>((resolve) => {
|
||||
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
|
||||
|
||||
async function removeDocument(id: string): Promise<void> {
|
||||
const db = await databaseConnection;
|
||||
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
|
||||
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).delete(id);
|
||||
storeDocumentOrder();
|
||||
}
|
||||
// Handle a version mismatch if `GRAPHITE_INDEXED_DB_VERSION` is now higher than what was saved in the database
|
||||
dbOpenRequest.onupgradeneeded = (): void => {
|
||||
const db = dbOpenRequest.result;
|
||||
|
||||
async function closeDatabaseConnection(): Promise<void> {
|
||||
const db = await databaseConnection;
|
||||
db.close();
|
||||
}
|
||||
// Wipe out all stores when a request is made to upgrade the database version to a newer one
|
||||
GRAPHITE_INDEXEDDB_STORES.forEach((store) => {
|
||||
if (db.objectStoreNames.contains(store.name)) db.deleteObjectStore(store.name);
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => {
|
||||
const db = await databaseConnection;
|
||||
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readwrite");
|
||||
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).put(autoSaveDocument);
|
||||
storeDocumentOrder();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => {
|
||||
removeDocument(removeAutoSaveDocument.documentId);
|
||||
});
|
||||
db.createObjectStore(store.name, { keyPath: store.keyPath });
|
||||
});
|
||||
};
|
||||
|
||||
// Open the IndexedDB database connection and save it to this variable, which is a promise that resolves once the connection is open
|
||||
const databaseConnection = new Promise<IDBDatabase>((resolve) => {
|
||||
const dbOpenRequest = indexedDB.open(GRAPHITE_INDEXED_DB_NAME, GRAPHITE_INDEXED_DB_VERSION);
|
||||
|
||||
dbOpenRequest.onupgradeneeded = (): void => {
|
||||
const db = dbOpenRequest.result;
|
||||
// Wipes out all auto-save data on upgrade
|
||||
if (db.objectStoreNames.contains(GRAPHITE_AUTO_SAVE_STORE)) {
|
||||
db.deleteObjectStore(GRAPHITE_AUTO_SAVE_STORE);
|
||||
}
|
||||
|
||||
db.createObjectStore(GRAPHITE_AUTO_SAVE_STORE, { keyPath: "details.id" });
|
||||
};
|
||||
|
||||
dbOpenRequest.onerror = (): void => {
|
||||
const errorText = stripIndents`
|
||||
// Handle some other error by presenting it to the user
|
||||
dbOpenRequest.onerror = (): void => {
|
||||
const errorText = stripIndents`
|
||||
Documents won't be saved across reloads and later visits.
|
||||
This may be caused by Firefox's private browsing mode.
|
||||
|
||||
Error on opening IndexDB:
|
||||
${dbOpenRequest.error}
|
||||
`;
|
||||
editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText);
|
||||
};
|
||||
editor.instance.errorDialog("Document auto-save doesn't work in this browser", errorText);
|
||||
};
|
||||
|
||||
dbOpenRequest.onsuccess = (): void => {
|
||||
resolve(dbOpenRequest.result);
|
||||
};
|
||||
});
|
||||
|
||||
databaseConnection.then(async (db) => {
|
||||
// Open auto-save documents
|
||||
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE, "readonly");
|
||||
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE).getAll();
|
||||
await new Promise((resolve): void => {
|
||||
request.onsuccess = (): void => {
|
||||
const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = request.result;
|
||||
|
||||
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
|
||||
const orderedSavedDocuments = documentOrder
|
||||
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
|
||||
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
|
||||
|
||||
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
|
||||
orderedSavedDocuments.forEach((doc: TriggerIndexedDbWriteDocument) => {
|
||||
if (doc.version === currentDocumentVersion) {
|
||||
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
|
||||
} else {
|
||||
removeDocument(doc.details.id);
|
||||
}
|
||||
});
|
||||
resolve(undefined);
|
||||
// Resolve the promise on a successful opening of the database connection
|
||||
dbOpenRequest.onsuccess = (): void => {
|
||||
resolve(dbOpenRequest.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function storeDocumentOrder(): void {
|
||||
// Make sure to store as string since JSON does not play nice with BigInt
|
||||
const documentOrder = portfolio.state.documents.map((doc) => doc.id.toString());
|
||||
window.localStorage.setItem(GRAPHITE_AUTO_SAVE_ORDER_KEY, JSON.stringify(documentOrder));
|
||||
}
|
||||
|
||||
async function removeDocument(id: string, db: IDBDatabase): Promise<void> {
|
||||
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite");
|
||||
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).delete(id);
|
||||
storeDocumentOrder();
|
||||
}
|
||||
|
||||
async function loadAutoSaveDocuments(db: IDBDatabase): Promise<void> {
|
||||
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
||||
const promise = new Promise<void>((resolve): void => {
|
||||
promiseResolve = resolve;
|
||||
});
|
||||
|
||||
// Open auto-save documents
|
||||
const transaction = db.transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readonly");
|
||||
const request = transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).getAll();
|
||||
|
||||
request.onsuccess = (): void => {
|
||||
const previouslySavedDocuments: TriggerIndexedDbWriteDocument[] = request.result;
|
||||
|
||||
const documentOrder: string[] = JSON.parse(window.localStorage.getItem(GRAPHITE_AUTO_SAVE_ORDER_KEY) || "[]");
|
||||
const orderedSavedDocuments = documentOrder
|
||||
.map((id) => previouslySavedDocuments.find((autoSave) => autoSave.details.id === id))
|
||||
.filter((x) => x !== undefined) as TriggerIndexedDbWriteDocument[];
|
||||
|
||||
const currentDocumentVersion = editor.instance.graphiteDocumentVersion();
|
||||
orderedSavedDocuments.forEach(async (doc: TriggerIndexedDbWriteDocument) => {
|
||||
if (doc.version === currentDocumentVersion) {
|
||||
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
|
||||
} else {
|
||||
await removeDocument(doc.details.id, db);
|
||||
}
|
||||
});
|
||||
|
||||
promiseResolve();
|
||||
};
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
async function loadPreferences(db: IDBDatabase): Promise<void> {
|
||||
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
||||
const promise = new Promise<void>((resolve): void => {
|
||||
promiseResolve = resolve;
|
||||
});
|
||||
|
||||
// Open auto-save documents
|
||||
const transaction = db.transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readonly");
|
||||
const request = transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).getAll();
|
||||
|
||||
request.onsuccess = (): void => {
|
||||
const preferenceEntries: { key: string; value: unknown }[] = request.result;
|
||||
|
||||
const preferences: Record<string, unknown> = {};
|
||||
preferenceEntries.forEach(({ key, value }) => {
|
||||
preferences[key] = value;
|
||||
});
|
||||
|
||||
editor.instance.loadPreferences(JSON.stringify(preferences));
|
||||
|
||||
promiseResolve();
|
||||
};
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbWriteDocument, async (autoSaveDocument) => {
|
||||
const transaction = (await databaseConnection).transaction(GRAPHITE_AUTO_SAVE_STORE.name, "readwrite");
|
||||
transaction.objectStore(GRAPHITE_AUTO_SAVE_STORE.name).put(autoSaveDocument);
|
||||
|
||||
storeDocumentOrder();
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerIndexedDbRemoveDocument, async (removeAutoSaveDocument) => {
|
||||
await removeDocument(removeAutoSaveDocument.documentId, await databaseConnection);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerLoadAutoSaveDocuments, async () => {
|
||||
await loadAutoSaveDocuments(await databaseConnection);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerSavePreferences, async (preferences) => {
|
||||
Object.entries(preferences.preferences).forEach(async ([key, value]) => {
|
||||
const storedObject = { key, value };
|
||||
|
||||
const transaction = (await databaseConnection).transaction(GRAPHITE_EDITOR_PREFERENCES_STORE.name, "readwrite");
|
||||
transaction.objectStore(GRAPHITE_EDITOR_PREFERENCES_STORE.name).put(storedObject);
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerLoadPreferences, async () => {
|
||||
await loadPreferences(await databaseConnection);
|
||||
});
|
||||
|
||||
return closeDatabaseConnection;
|
||||
const databaseConnection = initialize();
|
||||
|
||||
// Destructor
|
||||
return () => {
|
||||
databaseConnection.then((connection) => connection.close());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,19 @@ import { initWasm } from "@/wasm-communication/editor";
|
||||
|
||||
import App from "@/App.vue";
|
||||
|
||||
// Browser app entry point
|
||||
(async (): Promise<void> => {
|
||||
// Confirm the browser is compatible before initializing the application
|
||||
if (!checkBrowserCompatibility()) return;
|
||||
|
||||
// Initialize the WASM module for the editor backend
|
||||
await initWasm();
|
||||
|
||||
// Initialize the Vue application
|
||||
createApp(App).mount("#app");
|
||||
})();
|
||||
|
||||
function checkBrowserCompatibility(): boolean {
|
||||
if (!("BigUint64Array" in window)) {
|
||||
const body = document.body;
|
||||
const message = stripIndents`
|
||||
@@ -23,12 +35,9 @@ import App from "@/App.vue";
|
||||
JavaScript API must be supported by the browser for Graphite to function.)</p>
|
||||
`;
|
||||
body.innerHTML = message + body.innerHTML;
|
||||
return;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize the WASM module for the editor backend
|
||||
await initWasm();
|
||||
|
||||
// Initialize the Vue application
|
||||
createApp(App).mount("#app");
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
import { downloadFileText, downloadFileBlob, upload } from "@/utility-functions/files";
|
||||
import { imaginateGenerate, imaginateCheckConnection, imaginateTerminate } from "@/utility-functions/imaginate";
|
||||
import { rasterizeSVG } from "@/utility-functions/rasterization";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import {
|
||||
@@ -10,8 +11,13 @@ import {
|
||||
TriggerImport,
|
||||
TriggerOpenDocument,
|
||||
TriggerRasterDownload,
|
||||
TriggerImaginateGenerate,
|
||||
TriggerImaginateTerminate,
|
||||
TriggerImaginateCheckServerStatus,
|
||||
UpdateActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateImageData,
|
||||
TriggerRevokeBlobUrl,
|
||||
} from "@/wasm-communication/messages";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
@@ -55,6 +61,47 @@ export function createPortfolioState(editor: Editor) {
|
||||
// Have the browser download the file to the user's disk
|
||||
downloadFileBlob(name, blob);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerImaginateCheckServerStatus, async (triggerImaginateCheckServerStatus) => {
|
||||
const { hostname } = triggerImaginateCheckServerStatus;
|
||||
|
||||
imaginateCheckConnection(hostname, editor);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerImaginateGenerate, async (triggerImaginateGenerate) => {
|
||||
const { documentId, layerPath, hostname, refreshFrequency, baseImage, parameters } = triggerImaginateGenerate;
|
||||
|
||||
// Handle img2img mode
|
||||
let image: Blob | undefined;
|
||||
if (parameters.denoisingStrength !== undefined && baseImage !== undefined) {
|
||||
// Rasterize the SVG to an image file
|
||||
image = await rasterizeSVG(baseImage.svg, baseImage.size[0], baseImage.size[1], "image/png");
|
||||
|
||||
const blobURL = URL.createObjectURL(image);
|
||||
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, baseImage.size[0], baseImage.size[1]);
|
||||
}
|
||||
|
||||
imaginateGenerate(parameters, image, hostname, refreshFrequency, documentId, layerPath, editor);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerImaginateTerminate, async (triggerImaginateTerminate) => {
|
||||
const { documentId, layerPath, hostname } = triggerImaginateTerminate;
|
||||
|
||||
imaginateTerminate(hostname, documentId, layerPath, editor);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
updateImageData.imageData.forEach(async (element) => {
|
||||
const buffer = new Uint8Array(element.imageData.values()).buffer;
|
||||
const blob = new Blob([buffer], { type: element.mime });
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
const image = await createImageBitmap(blob);
|
||||
|
||||
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.width, image.height);
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(TriggerRevokeBlobUrl, async (triggerRevokeBlobUrl) => {
|
||||
URL.revokeObjectURL(triggerRevokeBlobUrl.url);
|
||||
});
|
||||
|
||||
return {
|
||||
state: readonly(state) as typeof state,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
/* eslint-disable quotes */
|
||||
|
||||
export function escapeJSON(str: string): string {
|
||||
return str
|
||||
.replace(/[\\]/g, "\\\\")
|
||||
.replace(/[\"]/g, '\\"')
|
||||
.replace(/[\/]/g, "\\/")
|
||||
.replace(/[\b]/g, "\\b")
|
||||
.replace(/[\f]/g, "\\f")
|
||||
.replace(/[\n]/g, "\\n")
|
||||
.replace(/[\r]/g, "\\r")
|
||||
.replace(/[\t]/g, "\\t");
|
||||
}
|
||||
@@ -52,3 +52,31 @@ export async function upload<T extends "text" | "data">(acceptedExtensions: stri
|
||||
}
|
||||
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
|
||||
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : never;
|
||||
|
||||
export function blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = (): void => resolve(typeof reader.result === "string" ? reader.result : "");
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export async function replaceBlobURLsWithBase64(svg: string): Promise<string> {
|
||||
const splitByBlobs = svg.split(/(?<=")(blob:.*?)(?=")/);
|
||||
const onlyBlobs = splitByBlobs.filter((_, i) => i % 2 === 1);
|
||||
|
||||
const onlyBlobsConverted = onlyBlobs.map(async (blobURL) => {
|
||||
const data = await fetch(blobURL);
|
||||
const dataBlob = await data.blob();
|
||||
return blobToBase64(dataBlob);
|
||||
});
|
||||
const base64Images = await Promise.all(onlyBlobsConverted);
|
||||
|
||||
const substituted = splitByBlobs.map((segment, i) => {
|
||||
if (i % 2 === 0) return segment;
|
||||
|
||||
const blobsIndex = Math.floor(i / 2);
|
||||
return base64Images[blobsIndex];
|
||||
});
|
||||
return substituted.join("");
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ import NodeColorCorrection from "@/../assets/icon-16px-solid/node-color-correcti
|
||||
import NodeFolder from "@/../assets/icon-16px-solid/node-folder.svg";
|
||||
import NodeGradient from "@/../assets/icon-16px-solid/node-gradient.svg";
|
||||
import NodeImage from "@/../assets/icon-16px-solid/node-image.svg";
|
||||
import NodeImaginate from "@/../assets/icon-16px-solid/node-imaginate.svg";
|
||||
import NodeMagicWand from "@/../assets/icon-16px-solid/node-magic-wand.svg";
|
||||
import NodeMask from "@/../assets/icon-16px-solid/node-mask.svg";
|
||||
import NodeMotionBlur from "@/../assets/icon-16px-solid/node-motion-blur.svg";
|
||||
@@ -109,6 +110,12 @@ import NodeShape from "@/../assets/icon-16px-solid/node-shape.svg";
|
||||
import NodeText from "@/../assets/icon-16px-solid/node-text.svg";
|
||||
import NodeTransform from "@/../assets/icon-16px-solid/node-transform.svg";
|
||||
import Paste from "@/../assets/icon-16px-solid/paste.svg";
|
||||
import Random from "@/../assets/icon-16px-solid/random.svg";
|
||||
import Regenerate from "@/../assets/icon-16px-solid/regenerate.svg";
|
||||
import Reload from "@/../assets/icon-16px-solid/reload.svg";
|
||||
import Rescale from "@/../assets/icon-16px-solid/rescale.svg";
|
||||
import Reset from "@/../assets/icon-16px-solid/reset.svg";
|
||||
import Settings from "@/../assets/icon-16px-solid/settings.svg";
|
||||
import Trash from "@/../assets/icon-16px-solid/trash.svg";
|
||||
import ViewModeNormal from "@/../assets/icon-16px-solid/view-mode-normal.svg";
|
||||
import ViewModeOutline from "@/../assets/icon-16px-solid/view-mode-outline.svg";
|
||||
@@ -142,6 +149,7 @@ const SOLID_16PX = {
|
||||
FlipVertical: { component: FlipVertical, size: 16 },
|
||||
Folder: { component: Folder, size: 16 },
|
||||
GraphiteLogo: { component: GraphiteLogo, size: 16 },
|
||||
NodeImaginate: { component: NodeImaginate, size: 16 },
|
||||
NodeArtboard: { component: NodeArtboard, size: 16 },
|
||||
NodeBlur: { component: NodeBlur, size: 16 },
|
||||
NodeBrushwork: { component: NodeBrushwork, size: 16 },
|
||||
@@ -157,6 +165,12 @@ const SOLID_16PX = {
|
||||
NodeText: { component: NodeText, size: 16 },
|
||||
NodeTransform: { component: NodeTransform, size: 16 },
|
||||
Paste: { component: Paste, size: 16 },
|
||||
Random: { component: Random, size: 16 },
|
||||
Regenerate: { component: Regenerate, size: 16 },
|
||||
Reload: { component: Reload, size: 16 },
|
||||
Rescale: { component: Rescale, size: 16 },
|
||||
Reset: { component: Reset, size: 16 },
|
||||
Settings: { component: Settings, size: 16 },
|
||||
Trash: { component: Trash, size: 16 },
|
||||
ViewModeNormal: { component: ViewModeNormal, size: 16 },
|
||||
ViewModeOutline: { component: ViewModeOutline, size: 16 },
|
||||
@@ -205,6 +219,7 @@ import RasterBrushTool from "@/../assets/icon-24px-two-tone/raster-brush-tool.sv
|
||||
import RasterCloneTool from "@/../assets/icon-24px-two-tone/raster-clone-tool.svg";
|
||||
import RasterDetailTool from "@/../assets/icon-24px-two-tone/raster-detail-tool.svg";
|
||||
import RasterHealTool from "@/../assets/icon-24px-two-tone/raster-heal-tool.svg";
|
||||
import RasterImaginateTool from "@/../assets/icon-24px-two-tone/raster-imaginate-tool.svg";
|
||||
import RasterPatchTool from "@/../assets/icon-24px-two-tone/raster-patch-tool.svg";
|
||||
import RasterRelightTool from "@/../assets/icon-24px-two-tone/raster-relight-tool.svg";
|
||||
import VectorEllipseTool from "@/../assets/icon-24px-two-tone/vector-ellipse-tool.svg";
|
||||
@@ -220,10 +235,11 @@ import VectorTextTool from "@/../assets/icon-24px-two-tone/vector-text-tool.svg"
|
||||
const TWO_TONE_24PX = {
|
||||
GeneralArtboardTool: { component: GeneralArtboardTool, size: 24 },
|
||||
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: 24 },
|
||||
GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
|
||||
GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
|
||||
GeneralFillTool: { component: GeneralFillTool, size: 24 },
|
||||
GeneralGradientTool: { component: GeneralGradientTool, size: 24 },
|
||||
GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
|
||||
GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
|
||||
RasterImaginateTool: { component: RasterImaginateTool, size: 24 },
|
||||
RasterBrushTool: { component: RasterBrushTool, size: 24 },
|
||||
RasterCloneTool: { component: RasterCloneTool, size: 24 },
|
||||
RasterDetailTool: { component: RasterDetailTool, size: 24 },
|
||||
@@ -256,7 +272,6 @@ export const ICON_COMPONENTS = Object.fromEntries(Object.entries(ICONS).map(([na
|
||||
|
||||
export type IconName = keyof typeof ICONS;
|
||||
export type IconSize = undefined | 12 | 16 | 24 | 32;
|
||||
export type IconStyle = "Normal" | "Node";
|
||||
|
||||
// The following helper type declarations allow us to avoid manually maintaining the `IconName` type declaration as a string union paralleling the keys of the
|
||||
// icon definitions. It lets TypeScript do that for us. Our goal is to define the big key-value pair of icons by constraining its values, but inferring its keys.
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
import { escapeJSON } from "@/utility-functions/escape";
|
||||
import { blobToBase64 } from "@/utility-functions/files";
|
||||
import { type RequestResult, requestWithUploadDownloadProgress } from "@/utility-functions/network";
|
||||
import { stripIndents } from "@/utility-functions/strip-indents";
|
||||
import { type Editor } from "@/wasm-communication/editor";
|
||||
import { type ImaginateGenerationParameters } from "@/wasm-communication/messages";
|
||||
|
||||
const MAX_POLLING_RETRIES = 4;
|
||||
const SERVER_STATUS_CHECK_TIMEOUT = 5000;
|
||||
const SAMPLING_MODES_POLLING_UNSUPPORTED = ["DPM fast", "DPM adaptive"];
|
||||
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
let terminated = false;
|
||||
|
||||
let generatingAbortRequest: XMLHttpRequest | undefined;
|
||||
let pollingAbortController = new AbortController();
|
||||
let statusAbortController = new AbortController();
|
||||
|
||||
// PUBLICLY CALLABLE FUNCTIONS
|
||||
|
||||
export async function imaginateGenerate(
|
||||
parameters: ImaginateGenerationParameters,
|
||||
image: Blob | undefined,
|
||||
hostname: string,
|
||||
refreshFrequency: number,
|
||||
documentId: bigint,
|
||||
layerPath: BigUint64Array,
|
||||
editor: Editor
|
||||
): Promise<void> {
|
||||
// Ignore a request to generate a new image while another is already being generated
|
||||
if (generatingAbortRequest !== undefined) return;
|
||||
|
||||
terminated = false;
|
||||
|
||||
// Immediately set the progress to 0% so the backend knows to update its layout
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, 0, "Beginning");
|
||||
|
||||
// Initiate a request to the computation server
|
||||
const discloseUploadingProgress = (progress: number): void => {
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, progress * 100, "Uploading");
|
||||
};
|
||||
const { uploaded, result, xhr } = await generate(discloseUploadingProgress, hostname, image, parameters);
|
||||
generatingAbortRequest = xhr;
|
||||
|
||||
try {
|
||||
// Wait until the request is fully uploaded, which could be slow if the img2img source is large and the user is on a slow connection
|
||||
await uploaded;
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, 0, "Generating");
|
||||
|
||||
// Begin polling for updates to the in-progress image generation at the specified interval
|
||||
// Don't poll if the chosen interval is 0, or if the chosen sampling method does not support polling
|
||||
if (refreshFrequency > 0 && !SAMPLING_MODES_POLLING_UNSUPPORTED.includes(parameters.samplingMethod)) {
|
||||
const interval = Math.max(refreshFrequency * 1000, 500);
|
||||
scheduleNextPollingUpdate(interval, Date.now(), 0, editor, hostname, documentId, layerPath, parameters.resolution);
|
||||
}
|
||||
|
||||
// Wait for the final image to be returned by the initial request containing either the full image or the last frame if it was terminated by the user
|
||||
const { body, status } = await result;
|
||||
if (status < 200 || status > 299) {
|
||||
throw new Error(`Request to server failed to return a 200-level status code (${status})`);
|
||||
}
|
||||
|
||||
// Extract the final image from the response and convert it to a data blob
|
||||
// Highly unstable API
|
||||
const base64 = JSON.parse(body)?.data[0]?.[0] as string | undefined;
|
||||
if (typeof base64 !== "string" || !base64.startsWith("data:image/png;base64,")) throw new Error("Could not read final image result from server response");
|
||||
const blob = await (await fetch(base64)).blob();
|
||||
|
||||
// Send the backend an updated status
|
||||
const percent = terminated ? undefined : 100;
|
||||
const newStatus = terminated ? "Terminated" : "Idle";
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, percent, newStatus);
|
||||
|
||||
// Send the backend a blob URL for the final image
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, parameters.resolution[0], parameters.resolution[1]);
|
||||
|
||||
// Send the backend the blob data to be stored persistently in the layer
|
||||
const u8Array = new Uint8Array(await blob.arrayBuffer());
|
||||
editor.instance.setImaginateImageData(documentId, layerPath, u8Array);
|
||||
} catch {
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, undefined, "Terminated");
|
||||
|
||||
await imaginateCheckConnection(hostname, editor);
|
||||
}
|
||||
|
||||
abortAndResetGenerating();
|
||||
abortAndResetPolling();
|
||||
}
|
||||
|
||||
export async function imaginateTerminate(hostname: string, documentId: bigint, layerPath: BigUint64Array, editor: Editor): Promise<void> {
|
||||
terminated = true;
|
||||
abortAndResetPolling();
|
||||
|
||||
try {
|
||||
await terminate(hostname);
|
||||
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, undefined, "Terminating");
|
||||
} catch {
|
||||
abortAndResetGenerating();
|
||||
abortAndResetPolling();
|
||||
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, undefined, "Terminated");
|
||||
|
||||
await imaginateCheckConnection(hostname, editor);
|
||||
}
|
||||
}
|
||||
|
||||
export async function imaginateCheckConnection(hostname: string, editor: Editor): Promise<void> {
|
||||
const serverReached = await checkConnection(hostname);
|
||||
editor.instance.setImaginateServerStatus(serverReached);
|
||||
}
|
||||
|
||||
// ABORTING AND RESETTING HELPERS
|
||||
|
||||
function abortAndResetGenerating(): void {
|
||||
generatingAbortRequest?.abort();
|
||||
generatingAbortRequest = undefined;
|
||||
}
|
||||
|
||||
function abortAndResetPolling(): void {
|
||||
pollingAbortController.abort();
|
||||
pollingAbortController = new AbortController();
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
// POLLING IMPLEMENTATION DETAILS
|
||||
|
||||
function scheduleNextPollingUpdate(
|
||||
interval: number,
|
||||
timeoutBegan: number,
|
||||
pollingRetries: number,
|
||||
editor: Editor,
|
||||
hostname: string,
|
||||
documentId: bigint,
|
||||
layerPath: BigUint64Array,
|
||||
resolution: [number, number]
|
||||
): void {
|
||||
// Pick a future time that keeps to the user-requested interval if possible, but on slower connections will go as fast as possible without overlapping itself
|
||||
const nextPollTimeGoal = timeoutBegan + interval;
|
||||
const timeFromNow = Math.max(0, nextPollTimeGoal - Date.now());
|
||||
|
||||
timer = setTimeout(async () => {
|
||||
const nextTimeoutBegan = Date.now();
|
||||
|
||||
try {
|
||||
const [blob, percentComplete] = await pollImage(hostname);
|
||||
if (terminated) return;
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
editor.instance.setImaginateBlobURL(documentId, layerPath, blobURL, resolution[0], resolution[1]);
|
||||
editor.instance.setImaginateGeneratingStatus(documentId, layerPath, percentComplete, "Generating");
|
||||
|
||||
scheduleNextPollingUpdate(interval, nextTimeoutBegan, 0, editor, hostname, documentId, layerPath, resolution);
|
||||
} catch {
|
||||
if (generatingAbortRequest === undefined) return;
|
||||
|
||||
if (pollingRetries + 1 > MAX_POLLING_RETRIES) {
|
||||
abortAndResetGenerating();
|
||||
abortAndResetPolling();
|
||||
|
||||
await imaginateCheckConnection(hostname, editor);
|
||||
} else {
|
||||
scheduleNextPollingUpdate(interval, nextTimeoutBegan, pollingRetries + 1, editor, hostname, documentId, layerPath, resolution);
|
||||
}
|
||||
}
|
||||
}, timeFromNow);
|
||||
}
|
||||
|
||||
// API COMMUNICATION FUNCTIONS
|
||||
// These are highly unstable APIs that will need to be updated very frequently, so we currently assume usage of this exact commit from the server:
|
||||
// https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/7d6042b908c064774ee10961309d396eabdc6c4a
|
||||
|
||||
function endpoint(hostname: string): string {
|
||||
// Highly unstable API
|
||||
return `${hostname}api/predict/`;
|
||||
}
|
||||
|
||||
async function pollImage(hostname: string): Promise<[Blob, number]> {
|
||||
// Highly unstable API
|
||||
const result = await fetch(endpoint(hostname), {
|
||||
signal: pollingAbortController.signal,
|
||||
headers: {
|
||||
accept: "*/*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
referrer: hostname,
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
body: stripIndents`
|
||||
{
|
||||
"fn_index":3,
|
||||
"data":[],
|
||||
"session_hash":"0000000000"
|
||||
}`,
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "omit",
|
||||
});
|
||||
const json = await result.json();
|
||||
// Highly unstable API
|
||||
const percentComplete = Math.abs(Number(json.data[0].match(/(?<="width:).*?(?=%")/)[0])); // The API sometimes returns negative values presumably due to a bug
|
||||
// Highly unstable API
|
||||
const base64 = json.data[2];
|
||||
|
||||
if (typeof base64 !== "string" || !base64.startsWith("data:image/png;base64,")) return Promise.reject();
|
||||
|
||||
const blob = await (await fetch(base64)).blob();
|
||||
|
||||
return [blob, percentComplete];
|
||||
}
|
||||
|
||||
async function generate(
|
||||
discloseUploadingProgress: (progress: number) => void,
|
||||
hostname: string,
|
||||
image: Blob | undefined,
|
||||
parameters: ImaginateGenerationParameters
|
||||
): Promise<{
|
||||
uploaded: Promise<void>;
|
||||
result: Promise<RequestResult>;
|
||||
xhr?: XMLHttpRequest;
|
||||
}> {
|
||||
let body;
|
||||
if (image === undefined || parameters.denoisingStrength === undefined) {
|
||||
// Highly unstable API
|
||||
body = stripIndents`
|
||||
{
|
||||
"fn_index":13,
|
||||
"data":[
|
||||
"${escapeJSON(parameters.prompt)}",
|
||||
"${escapeJSON(parameters.negativePrompt)}",
|
||||
"None",
|
||||
"None",
|
||||
${parameters.samples},
|
||||
"${parameters.samplingMethod}",
|
||||
${parameters.restoreFaces},
|
||||
${parameters.tiling},
|
||||
1,
|
||||
1,
|
||||
${parameters.cfgScale},
|
||||
${parameters.seed},
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
${parameters.resolution[1]},
|
||||
${parameters.resolution[0]},
|
||||
false,
|
||||
0.7,
|
||||
0,
|
||||
0,
|
||||
"None",
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
"",
|
||||
"Seed",
|
||||
"",
|
||||
"Nothing",
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
""
|
||||
],
|
||||
"session_hash":"0000000000"
|
||||
}`;
|
||||
} else {
|
||||
const sourceImageBase64 = await blobToBase64(image);
|
||||
|
||||
// Highly unstable API
|
||||
body = stripIndents`
|
||||
{
|
||||
"fn_index":33,
|
||||
"data":[
|
||||
0,
|
||||
"${escapeJSON(parameters.prompt)}",
|
||||
"${escapeJSON(parameters.negativePrompt)}",
|
||||
"None",
|
||||
"None",
|
||||
"${sourceImageBase64}",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"Draw mask",
|
||||
${parameters.samples},
|
||||
"${parameters.samplingMethod}",
|
||||
4,
|
||||
"fill",
|
||||
${parameters.restoreFaces},
|
||||
${parameters.tiling},
|
||||
1,
|
||||
1,
|
||||
${parameters.cfgScale},
|
||||
${parameters.denoisingStrength},
|
||||
${parameters.seed},
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
${parameters.resolution[1]},
|
||||
${parameters.resolution[0]},
|
||||
"Just resize",
|
||||
false,
|
||||
32,
|
||||
"Inpaint masked",
|
||||
"",
|
||||
"",
|
||||
"None",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
50,
|
||||
true,
|
||||
1,
|
||||
0,
|
||||
false,
|
||||
4,
|
||||
1,
|
||||
"",
|
||||
128,
|
||||
8,
|
||||
["left","right","up","down"],
|
||||
1,
|
||||
0.05,
|
||||
128,
|
||||
4,
|
||||
"fill",
|
||||
["left","right","up","down"],
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
"",
|
||||
"",
|
||||
64,
|
||||
"None",
|
||||
"Seed",
|
||||
"",
|
||||
"Nothing",
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
"",
|
||||
""
|
||||
],
|
||||
"session_hash":"0000000000"
|
||||
}`;
|
||||
}
|
||||
|
||||
// Prepare a promise that will resolve after the outbound request upload is complete
|
||||
let uploadedResolve: () => void;
|
||||
let uploadedReject: () => void;
|
||||
const uploaded = new Promise<void>((resolve, reject): void => {
|
||||
uploadedResolve = resolve;
|
||||
uploadedReject = reject;
|
||||
});
|
||||
|
||||
// Fire off the request and, once the outbound request upload is complete, resolve the promise we defined above
|
||||
const uploadProgress = (progress: number): void => {
|
||||
if (progress < 1) {
|
||||
discloseUploadingProgress(progress);
|
||||
} else {
|
||||
uploadedResolve();
|
||||
}
|
||||
};
|
||||
const [result, xhr] = requestWithUploadDownloadProgress(endpoint(hostname), "POST", body, uploadProgress, abortAndResetPolling);
|
||||
result.catch(() => uploadedReject());
|
||||
|
||||
// Return the promise that resolves when the request upload is complete, the promise that resolves when the response download is complete, and the XHR so it can be aborted
|
||||
return { uploaded, result, xhr };
|
||||
}
|
||||
|
||||
async function terminate(hostname: string): Promise<void> {
|
||||
const body = stripIndents`
|
||||
{
|
||||
"fn_index":2,
|
||||
"data":[],
|
||||
"session_hash":"0000000000"
|
||||
}`;
|
||||
|
||||
await fetch(endpoint(hostname), {
|
||||
headers: {
|
||||
accept: "*/*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
referrer: hostname,
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
body,
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "omit",
|
||||
});
|
||||
}
|
||||
|
||||
async function checkConnection(hostname: string): Promise<boolean> {
|
||||
statusAbortController.abort();
|
||||
statusAbortController = new AbortController();
|
||||
|
||||
const timeout = setTimeout(() => statusAbortController.abort(), SERVER_STATUS_CHECK_TIMEOUT);
|
||||
|
||||
const body = stripIndents`
|
||||
{
|
||||
"fn_index":100,
|
||||
"data":[],
|
||||
"session_hash":"0000000000"
|
||||
}`;
|
||||
|
||||
try {
|
||||
await fetch(endpoint(hostname), {
|
||||
signal: statusAbortController.signal,
|
||||
headers: {
|
||||
accept: "*/*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
referrer: hostname,
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
body,
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "omit",
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export type RequestResult = { body: string; status: number };
|
||||
|
||||
// Special implementation using the legacy XMLHttpRequest API that provides callbacks to get:
|
||||
// - Calls with the percent progress uploading the request to the server
|
||||
// - Calls when downloading the result from the server, after the server has begun streaming back the response data
|
||||
// It returns a tuple of the promise as well as the XHR which can be used to call the `.abort()` method on it.
|
||||
export function requestWithUploadDownloadProgress(
|
||||
url: string,
|
||||
method: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH",
|
||||
body: string,
|
||||
uploadProgress: (progress: number) => void,
|
||||
downloadOccurring: () => void
|
||||
): [Promise<RequestResult>, XMLHttpRequest | undefined] {
|
||||
let xhrValue: XMLHttpRequest | undefined;
|
||||
const promise = new Promise<RequestResult>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener("progress", (e) => uploadProgress(e.loaded / e.total));
|
||||
xhr.addEventListener("progress", () => downloadOccurring());
|
||||
xhr.addEventListener("load", () => resolve({ status: xhr.status, body: xhr.responseText }));
|
||||
xhr.addEventListener("abort", () => resolve({ status: xhr.status, body: xhr.responseText }));
|
||||
xhr.addEventListener("error", () => reject(new Error("Request error")));
|
||||
xhr.open(method, url, true);
|
||||
xhr.setRequestHeader("accept", "*/*");
|
||||
xhr.setRequestHeader("accept-language", "en-US,en;q=0.9");
|
||||
xhr.setRequestHeader("content-type", "application/json");
|
||||
|
||||
xhrValue = xhr;
|
||||
|
||||
xhr.send(body);
|
||||
});
|
||||
|
||||
return [promise, xhrValue];
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { replaceBlobURLsWithBase64 } from "@/utility-functions/files";
|
||||
|
||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||
export function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||
let promiseResolve: (value: Blob | PromiseLike<Blob>) => void | undefined;
|
||||
let promiseReject: () => void | undefined;
|
||||
const promise = new Promise<Blob>((resolve, reject) => {
|
||||
@@ -21,9 +23,12 @@ export function rasterizeSVG(svg: string, width: number, height: number, mime: s
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// This SVG rasterization scheme has the limitation that it cannot access blob URLs, so they must be inlined to base64 URLs
|
||||
const svgWithBase64Images = await replaceBlobURLsWithBase64(svg);
|
||||
|
||||
// Create a blob URL for our SVG
|
||||
const image = new Image();
|
||||
const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
|
||||
const svgBlob = new Blob([svgWithBase64Images], { type: "image/svg+xml;charset=utf-8" });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
image.onload = (): void => {
|
||||
// Draw our SVG to the canvas
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Transform, Type, plainToClass } from "class-transformer";
|
||||
|
||||
import { type IconName, type IconSize, type IconStyle } from "@/utility-functions/icons";
|
||||
import { type IconName, type IconSize } from "@/utility-functions/icons";
|
||||
import { type WasmEditorInstance, type WasmRawInstance } from "@/wasm-communication/editor";
|
||||
|
||||
import type MenuList from "@/components/floating-menus/MenuList.vue";
|
||||
@@ -165,7 +165,8 @@ export class UpdateDocumentArtboards extends JsMessage {
|
||||
readonly svg!: string;
|
||||
}
|
||||
|
||||
const TupleToVec2 = Transform(({ value }: { value: [number, number] }) => ({ x: value[0], y: value[1] }));
|
||||
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
|
||||
const BigIntTupleToNumberTuple = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : [Number(value[0]), Number(value[1])]));
|
||||
|
||||
export type XY = { x: number; y: number };
|
||||
|
||||
@@ -215,6 +216,10 @@ export class TriggerFileDownload extends JsMessage {
|
||||
readonly name!: string;
|
||||
}
|
||||
|
||||
export class TriggerLoadAutoSaveDocuments extends JsMessage {}
|
||||
|
||||
export class TriggerLoadPreferences extends JsMessage {}
|
||||
|
||||
export class TriggerOpenDocument extends JsMessage {}
|
||||
|
||||
export class TriggerImport extends JsMessage {}
|
||||
@@ -232,8 +237,73 @@ export class TriggerRasterDownload extends JsMessage {
|
||||
readonly size!: XY;
|
||||
}
|
||||
|
||||
export class TriggerImaginateCheckServerStatus extends JsMessage {
|
||||
readonly hostname!: string;
|
||||
}
|
||||
|
||||
export class TriggerImaginateGenerate extends JsMessage {
|
||||
@Type(() => ImaginateGenerationParameters)
|
||||
readonly parameters!: ImaginateGenerationParameters;
|
||||
|
||||
@Type(() => ImaginateBaseImage)
|
||||
readonly baseImage!: ImaginateBaseImage | undefined;
|
||||
|
||||
readonly hostname!: string;
|
||||
|
||||
readonly refreshFrequency!: number;
|
||||
|
||||
readonly documentId!: bigint;
|
||||
|
||||
readonly layerPath!: BigUint64Array;
|
||||
}
|
||||
|
||||
export class ImaginateBaseImage {
|
||||
readonly svg!: string;
|
||||
|
||||
readonly size!: [number, number];
|
||||
}
|
||||
|
||||
export class ImaginateGenerationParameters {
|
||||
readonly seed!: number;
|
||||
|
||||
readonly samples!: number;
|
||||
|
||||
readonly samplingMethod!: string;
|
||||
|
||||
readonly denoisingStrength!: number | undefined;
|
||||
|
||||
readonly cfgScale!: number;
|
||||
|
||||
readonly prompt!: string;
|
||||
|
||||
readonly negativePrompt!: string;
|
||||
|
||||
@BigIntTupleToNumberTuple
|
||||
readonly resolution!: [number, number];
|
||||
|
||||
readonly restoreFaces!: boolean;
|
||||
|
||||
readonly tiling!: boolean;
|
||||
}
|
||||
|
||||
export class TriggerImaginateTerminate extends JsMessage {
|
||||
readonly documentId!: bigint;
|
||||
|
||||
readonly layerPath!: BigUint64Array;
|
||||
|
||||
readonly hostname!: string;
|
||||
}
|
||||
|
||||
export class TriggerRefreshBoundsOfViewports extends JsMessage {}
|
||||
|
||||
export class TriggerRevokeBlobUrl extends JsMessage {
|
||||
readonly url!: string;
|
||||
}
|
||||
|
||||
export class TriggerSavePreferences extends JsMessage {
|
||||
readonly preferences!: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class DocumentChanged extends JsMessage {}
|
||||
|
||||
export class UpdateDocumentLayerTreeStructure extends JsMessage {
|
||||
@@ -313,8 +383,10 @@ export class DisplayEditableTextbox extends JsMessage {
|
||||
}
|
||||
|
||||
export class UpdateImageData extends JsMessage {
|
||||
@Type(() => ImageData)
|
||||
readonly imageData!: ImageData[];
|
||||
readonly documentId!: bigint;
|
||||
|
||||
@Type(() => ImaginateImageData)
|
||||
readonly imageData!: ImaginateImageData[];
|
||||
}
|
||||
|
||||
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
||||
@@ -327,7 +399,8 @@ export class UpdateDocumentLayerDetails extends JsMessage {
|
||||
export class LayerPanelEntry {
|
||||
name!: string;
|
||||
|
||||
tooltip!: string;
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
|
||||
visible!: boolean;
|
||||
|
||||
@@ -348,9 +421,26 @@ export class LayerMetadata {
|
||||
selected!: boolean;
|
||||
}
|
||||
|
||||
export type LayerType = "Folder" | "Image" | "Shape" | "Text";
|
||||
export type LayerType = "Imaginate" | "Folder" | "Image" | "Shape" | "Text";
|
||||
|
||||
export class ImageData {
|
||||
export type LayerTypeData = {
|
||||
name: string;
|
||||
icon: IconName;
|
||||
};
|
||||
|
||||
export function layerTypeData(layerType: LayerType): LayerTypeData | undefined {
|
||||
const entries: Record<string, LayerTypeData> = {
|
||||
Imaginate: { name: "Imaginate", icon: "NodeImaginate" },
|
||||
Folder: { name: "Folder", icon: "NodeFolder" },
|
||||
Image: { name: "Image", icon: "NodeImage" },
|
||||
Shape: { name: "Shape", icon: "NodeShape" },
|
||||
Text: { name: "Text", icon: "NodeText" },
|
||||
};
|
||||
|
||||
return entries[layerType];
|
||||
}
|
||||
|
||||
export class ImaginateImageData {
|
||||
readonly path!: BigUint64Array;
|
||||
|
||||
readonly mime!: string;
|
||||
@@ -400,7 +490,8 @@ export class CheckboxInput extends WidgetProps {
|
||||
|
||||
icon!: IconName;
|
||||
|
||||
tooltip!: string;
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class ColorInput extends WidgetProps {
|
||||
@@ -412,7 +503,8 @@ export class ColorInput extends WidgetProps {
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
tooltip!: string;
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
type MenuEntryCommon = {
|
||||
@@ -435,6 +527,7 @@ export type MenuListEntry = MenuEntryCommon & {
|
||||
shortcutRequiresLock?: boolean;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
font?: URL;
|
||||
ref?: InstanceType<typeof MenuList>;
|
||||
};
|
||||
@@ -449,6 +542,9 @@ export class DropdownInput extends WidgetProps {
|
||||
interactive!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class FontInput extends WidgetProps {
|
||||
@@ -459,6 +555,9 @@ export class FontInput extends WidgetProps {
|
||||
isStyle!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class IconButton extends WidgetProps {
|
||||
@@ -468,13 +567,15 @@ export class IconButton extends WidgetProps {
|
||||
|
||||
active!: boolean;
|
||||
|
||||
tooltip!: string;
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class IconLabel extends WidgetProps {
|
||||
icon!: IconName;
|
||||
|
||||
iconStyle!: IconStyle | undefined;
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export type IncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
|
||||
@@ -501,6 +602,11 @@ export class NumberInput extends WidgetProps {
|
||||
incrementFactor!: number;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class OptionalInput extends WidgetProps {
|
||||
@@ -508,7 +614,8 @@ export class OptionalInput extends WidgetProps {
|
||||
|
||||
icon!: IconName;
|
||||
|
||||
tooltip!: string;
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class PopoverButton extends WidgetProps {
|
||||
@@ -518,6 +625,9 @@ export class PopoverButton extends WidgetProps {
|
||||
header!: string;
|
||||
|
||||
text!: string;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export type RadioEntryData = {
|
||||
@@ -560,6 +670,9 @@ export class TextAreaInput extends WidgetProps {
|
||||
label!: string | undefined;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class TextButton extends WidgetProps {
|
||||
@@ -572,6 +685,9 @@ export class TextButton extends WidgetProps {
|
||||
minWidth!: number;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export type TextButtonWidget = {
|
||||
@@ -585,6 +701,7 @@ export type TextButtonWidget = {
|
||||
emphasized?: boolean;
|
||||
minWidth?: number;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
|
||||
// Callbacks
|
||||
// `action` is used via `IconButtonWidget.callback`
|
||||
@@ -597,6 +714,11 @@ export class TextInput extends WidgetProps {
|
||||
label!: string | undefined;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class TextLabel extends WidgetProps {
|
||||
@@ -610,7 +732,12 @@ export class TextLabel extends WidgetProps {
|
||||
|
||||
tableAlign!: boolean;
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
multiline!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => (value.length > 0 ? value : undefined))
|
||||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export type PivotPosition = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight";
|
||||
@@ -851,15 +978,22 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
DisplayEditableTextbox,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerAboutGraphiteLocalizedCommitDate,
|
||||
TriggerOpenDocument,
|
||||
TriggerImaginateCheckServerStatus,
|
||||
TriggerImaginateGenerate,
|
||||
TriggerImaginateTerminate,
|
||||
TriggerFileDownload,
|
||||
TriggerFontLoad,
|
||||
TriggerImport,
|
||||
TriggerIndexedDbRemoveDocument,
|
||||
TriggerIndexedDbWriteDocument,
|
||||
TriggerLoadAutoSaveDocuments,
|
||||
TriggerLoadPreferences,
|
||||
TriggerOpenDocument,
|
||||
TriggerPaste,
|
||||
TriggerRasterDownload,
|
||||
TriggerRefreshBoundsOfViewports,
|
||||
TriggerRevokeBlobUrl,
|
||||
TriggerSavePreferences,
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
TriggerViewportResize,
|
||||
|
||||
@@ -10,11 +10,11 @@ use editor::application::Editor;
|
||||
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
|
||||
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
||||
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
|
||||
use editor::messages::portfolio::document::utility_types::misc::Platform;
|
||||
use editor::messages::portfolio::utility_types::{ImaginateServerStatus, Platform};
|
||||
use editor::messages::prelude::*;
|
||||
use graphene::color::Color;
|
||||
use graphene::layers::imaginate_layer::ImaginateStatus;
|
||||
use graphene::LayerId;
|
||||
use graphene::Operation;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_wasm_bindgen::{self, from_value};
|
||||
@@ -168,6 +168,13 @@ impl JsEditorHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadPreferences)]
|
||||
pub fn load_preferences(&self, preferences: String) {
|
||||
let message = PreferencesMessage::Load { preferences };
|
||||
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = selectDocument)]
|
||||
pub fn select_document(&self, document_id: u64) {
|
||||
let message = PortfolioMessage::SelectDocument { document_id };
|
||||
@@ -427,11 +434,71 @@ impl JsEditorHandle {
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Sends the blob url generated by js
|
||||
#[wasm_bindgen(js_name = setImageBlobUrl)]
|
||||
pub fn set_image_blob_url(&self, path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
||||
let dimensions = (width, height);
|
||||
let message = Operation::SetImageBlobUrl { path, blob_url, dimensions };
|
||||
/// Sends the blob URL generated by JS to the Image layer
|
||||
#[wasm_bindgen(js_name = setImageBlobURL)]
|
||||
pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
||||
let resolution = (width, height);
|
||||
let message = PortfolioMessage::SetImageBlobUrl {
|
||||
document_id,
|
||||
layer_path,
|
||||
blob_url,
|
||||
resolution,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
|
||||
#[wasm_bindgen(js_name = setImaginateImageData)]
|
||||
pub fn set_imaginate_image_data(&self, document_id: u64, layer_path: Vec<LayerId>, image_data: Vec<u8>) {
|
||||
let message = PortfolioMessage::ImaginateSetImageData { document_id, layer_path, image_data };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document
|
||||
#[wasm_bindgen(js_name = setImaginateBlobURL)]
|
||||
pub fn set_imaginate_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
|
||||
let resolution = (width, height);
|
||||
let message = PortfolioMessage::ImaginateSetBlobUrl {
|
||||
document_id,
|
||||
layer_path,
|
||||
blob_url,
|
||||
resolution,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Notifies the Imaginate layer of a new percentage of completion and whether or not it's currently generating
|
||||
#[wasm_bindgen(js_name = setImaginateGeneratingStatus)]
|
||||
pub fn set_imaginate_generating_status(&self, document_id: u64, path: Vec<LayerId>, percent: Option<f64>, status: String) {
|
||||
let status = match status.as_str() {
|
||||
"Idle" => ImaginateStatus::Idle,
|
||||
"Beginning" => ImaginateStatus::Beginning,
|
||||
"Uploading" => ImaginateStatus::Uploading(percent.expect("Percent needs to be supplied to set ImaginateStatus::Uploading")),
|
||||
"Generating" => ImaginateStatus::Generating,
|
||||
"Terminating" => ImaginateStatus::Terminating,
|
||||
"Terminated" => ImaginateStatus::Terminated,
|
||||
_ => panic!("Invalid string from JS for ImaginateStatus, received: {}", status),
|
||||
};
|
||||
|
||||
let percent = if matches!(status, ImaginateStatus::Uploading(_)) { None } else { percent };
|
||||
|
||||
let message = PortfolioMessage::ImaginateSetGeneratingStatus { document_id, path, percent, status };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Notifies the editor that the Imaginate server is available or unavailable
|
||||
#[wasm_bindgen(js_name = setImaginateServerStatus)]
|
||||
pub fn set_imaginate_server_status(&self, available: bool) {
|
||||
let message: Message = match available {
|
||||
true => PortfolioMessage::ImaginateSetServerStatus {
|
||||
status: ImaginateServerStatus::Connected,
|
||||
}
|
||||
.into(),
|
||||
false => PortfolioMessage::ImaginateSetServerStatus {
|
||||
status: ImaginateServerStatus::Unavailable,
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
|
||||