diff --git a/.gitignore b/.gitignore index 7469b03..30e7267 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ examples/*/dist yarn-error.log .turbo +.vercel diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a14d56e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 0000000..17de935 --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,6 @@ +## Conventions + +- all exports from `commands.ts` are treated and registered as commands. The `id` is autogenerated from `${manifest.id}.commands.${exportname} +- commands can be categorized by `_`. As such `block_bringToFront` belong to category `block` +- id and category can also be manually overwritten in the `manifest` +- translation keys are the id of the command. use `en.json` \ No newline at end of file diff --git a/LIST_OF_PLUGINS.md b/LIST_OF_PLUGINS.md new file mode 100644 index 0000000..bcda3f4 --- /dev/null +++ b/LIST_OF_PLUGINS.md @@ -0,0 +1,193 @@ +# Commands + +- [ ] Useful + - [ ] Background removal + - [ ] Segment everything: Sam based auto segmenter + - [ ] Bake image + - [ ] Upscale image + + +- [ ] Convert to + - [ ] Convert to Vector + - [ ] Convert to Bitmap + +- [ ] Vector on Path: Convert any shape and bind to path. How do we keep the original block? + +- [ ] Turn into // Rethink Semantics This should change existing blocks without loss of date + - [ ] Turn into Image: Turns a block into a graphics block with the image as fill, + - [ ] Turn into Vector + - [ ] Turn into Graphics + + - [ ] Turn into Group + - [ ] Turn into Page + + +- [ ] Zoom + - [ ] Fit Page + - [ ] Fit Selection + - [ ] Zoom to 100% + +- [ ] Block Lifecylce + - [x] Duplicate Selected + - [x] Delete Selected + + - [ ] Copy Selected + - [ ] Paste Selected + - [ ] Group Selected + - [ ] Frame Selected + + - [ ] Merge Selected (Union Boolean Op) + - [ ] Subtract Selected (Difference Boolean Op) + - [ ] Intersect Selected + - [ ] Exclude Selected + +- [ ] Copy Properties: Copies them only onces + - [ ] Copy Transform Properties to {Selected, Clipboard} + - [ ] Copy Position Properties to {Selected, Clipboard} + - [ ] Copy Rotation Properties to {Selected, Clipboard} + - [ ] Copy Size (Width, Height) Properties to {Selected, Clipboard} + - [ ] Copy Fill Content to {Selected, Clipboard} + - [ ] Copy Text Content to {Selected, Clipboard} + - [ ] Copy Appearance Properties to {Selected, Clipboard} + - [ ] Copy Stroke Properties to {Selected, Clipboard} + - [ ] Copy Adjustment properties to {Selected, Clipboard} + - [ ] Copy Filter Properties to {Selected, Clipboard} + +- [ ] Sync Properties: Sync keeps them sync and updates both in a two way fassion + - [ ] Sync Transform Properties to {Selected, Clipboard} + - [ ] Sync Position Properties to {Selected, Clipboard} + - [ ] Sync Rotation Properties to {Selected, Clipboard} + - [ ] Sync Size (Width, Height) Properties to {Selected, Clipboard} + - [ ] Sync Fill Content to {Selected, Clipboard} + - [ ] Sync Text Content to {Selected, Clipboard} + - [ ] Sync Appearance Properties to {Selected, Clipboard} + - [ ] Sync Stroke Properties to {Selected, Clipboard} + - [ ] Sync Adjustment properties to {Selected, Clipboard} + - [ ] Sync Filter Properties to {Selected, Clipboard} + + +- [ ] Block Editing + - [ ] Auto-Crop Image + - [ ] Adjust Image + - [ ] Apply Filter + - [ ] Apply Blur + - [ ] Apply Effect + +- [ ] Create New Block + - [ ] Create New Graphic + - [ ] Create New Text + - [ ] Create New Shape + + - [ ] Create New Group from Selection + - [ ] Create New Page from Selection + - [ ] Create new Scene from Selection + + +- [ ] Add from Library + - [ ] Add Image from Library + - [ ] Add Text from Library + - [ ] Add Sticker from Library + + + +- [ ] Replace /Change + - [ ] Replace Image + - [ ] Replace Shape + - [ ] Replace Font + - [ ] Replace Text + + - [ ] Change Fill to Image + - [ ] Change Fill to Video + - [ ] Change Fill to Color + - [ ] Change Fill to Gradient + + +- [ ] Add: Open the libraries and choose + - [ ] Add ${List all Asset Libraries} + +- [x] Save As + - [x] Save {Scene, Selected} As {SVG, PNG, JPEG, PDF, Component} to {Clipboard, File, Console, S3, Asset Library} + + +- [ ] Share to + - [ ] Share to Zapier + - [ ] Share to LinkedIn + - [ ] Share to Twitter/X + - [ ] + +- [ ] Auto-(mation) + - [ ] Auto-crop + - [ ] Auto-Adjust + +- [ ] Text + - [ ] Summarize Text + - [ ] Translate Text to {en,de} + - [ ] Correct Text Spelling + +- [ ] Effects + - [ ] Custom effect + +- [ ] Generators + - [ ] QR Code + - [ ] Map + - [ ] AI Image + - [ ] Star Rating + +- [ ] Uploader + - [ ] upload to S3 + - [ ] upload to localStorage + - [ ] ... +- [ ] Importer + - [ ] SVG Importer + +- [ ] Layouting + - [ ] Auto-Resize Parent: Parent block always resizes to the child block automatically + - [ ] Layout all in group in stack + +- [ ] Solutions + - [ ] Studio: Loads all plugins for a studio solution + - [ ] Photo + - [ ] Video + + +## Asset Sources / Libraries +- [ ] Assets + - [ ] Giphy + - [ ] Unsplkash + - [ ] Getty + - [ ] Soundstripe + - [ ] Pexels + - [ ] Demo Images + - [ ] ... +- [ ] Designer + - [ ] Component Library + + +- [ ] Experimental/Fun + +- [ ] Import/Upload + - [ ] Import Design File + - [ ] Import Asset + + +- [ ] Editor Functionality + - [ ] Component Library + - [ ] CommandPalette + - [ ] Layerlist + - [ ] Metadata Editor + + + +## Debugging +- [ ] Debugging + - [x] CommandPalette + - [ ] Print Metadata + - [ ] {Print, Save,, Meta} + + + + +## Other +- [ ] Reduce colors to 1...n +- [ ] Subtitle STR colors +- [ ] Upload scenes, Upload components \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..12ad073 --- /dev/null +++ b/TODO.md @@ -0,0 +1,61 @@ +- [ ] How can I list all plugins that are active +- [ ] How can I deactivate a plugin later completely? `enableFeatures`, but this must be per "command". A plugin might contribute multiple features? +- [ ] we should pass the PluginContext with separate `imgly: {engine?: ImglyEngine, editor?: ImglyEditor, ui?: ImglyUI` +- [ ] We can already establish the name 'imgly" for the PLUGINS +- [ ] `unstable_getCanvasMenuOrder` should maybe be called `unstable_getCanvasMenuEntries` +- [ ] `unstable_setCanvasMenuOrder` should maybe be called `unstable_setCanvasMenuEntries` +- [ ] `unstable_enableFeatures` what is it used for. the button is not displayed when I check it internally +- [ ] `unstable_enableFeatures` should get the blocks it should evaluate it for. It's not always the selected ones in every scenario. +- [ ] `enable_features` could probably better be named `enableFeatureInContext()` +- [ ] What is the intention of the `builder.Button` first parameter, where is the id used later? +- [ ] (Exkurs) How can I change the type of a block to another. E.g. Change a Graphic Block into a Group Block for Vectorizer and the ID should stay the same and all properties that are relevant. "Turn into" +- [ ] The separation of ui and engine is sometimes too hard to use. I propose not to make it dependent on initUI and init. But more like lifecycles in general and always pass {ui?, engine, editor} +- [ ] `upload` should maybe be part of the asset sources and/or part of engine + +- [ ] `listPanels` function is missing to know which panels exists and are registers +- [ ] `registerComponents` should get `cesdk` and not only `engine` +- [ ] **Naming correction** + - [ ] IMGLY = { ui: IMGLYUI, engine: IMGLYEngine } + - [ ] CESDK = { ui: CESDKUI, engine: CESDKEngine } + - Get rid of high level functions maybe +- [ ] SDK should use dynamic import internally to save space and also not load on non supported platforms +- [ ] Commands should be define as should be `func params {context: {engine, ui}, params: any}` +- [ ] `hasMetadata` should be `hasMetaDataKey` because it's referring to an entry or item not if the whole metadata is existing +- [ ] `lifetime/unsubscribe` is totally missing from plugin apu. E.g. VSCode offers a `subscribe` to register all that need to be called when the plugin is `unloaded` + + +- [ ] Clarify semantics of `label`, `titel`, and `id` + - [ ] Label is used for translation +- it's `block.export` but not `scene.export` as I would expect +- `pages` should have design units +- `pages` should have their type e.g. video vs static +- `docuements` are just groups and as such leverage multi-editing + + +- How to work with `scopes` and `features`. Can I define custom scopes? Do we handle scopes in `enabledFeature +- There seems to be no API like `findAllScopes()` to enumerate scopes +- Is there an option to add customs scopes. +- `isEnableFeature` should be evaluated by the UI and the commands already? +- I think scopes +- `block.ungroup(bId)` should return the Ids of the items in the group +- block has no `getScope` and `setScope` to define the hierarchies. Here is an issue when dealing with hiearchies. We need to have the same thing available as with global scopes + - Editor : Allow, Defer, Deny + - Scene: Allow, Defer, Deny + - Collection: Allow, Defer, Deny + - Element: Allow, (Defer,) Deny + + +- `getEffects` api seems unsimilar to all `findAllScopes` etc. Maybe +- `UploadCallbackContext` not found in exports + + +- `MultiSelection` across multiple pages does not work!!! +- `Engine disposed` is logged everytime + + +- `Unsubribe` mechanism. We need to know and be able todo cleanup of commands. E.g. removeCommand and than also cleanup all dependencies +- In VSCode every register function also returns the "unsubscribe" and "free" function. + + +- `stroke/join` properties are not exposed it seems if you list all properties +- Default Stroke grey seems not a good choice as default \ No newline at end of file diff --git a/examples/imgly-components-source.zip b/examples/imgly-components-source.zip new file mode 100644 index 0000000..b3462a0 Binary files /dev/null and b/examples/imgly-components-source.zip differ diff --git a/examples/web/package.json b/examples/web/package.json index 5e38457..8d659e2 100644 --- a/examples/web/package.json +++ b/examples/web/package.json @@ -5,11 +5,19 @@ "type": "module", "scripts": { "dev": "vite --clearScreen=false", - "build": "tsc && vite build" + "build": "tsc && vite build", + "preview": "vite preview" }, "dependencies": { - "@cesdk/cesdk-js": "^1.20.0", + "@cesdk/cesdk-js": "^1.21.0", + "@imgly/plugin-core": "*", + "@imgly/plugin-background-removal-web": "*", + "@imgly/plugin-vectorizer": "*", + "@imgly/plugin-design-essentials": "*", + "@imgly/plugin-documents": "*", + "lodash": "^4.17.21", "react": "^18.2.0", + "react-cmdk": "^1.3.9", "react-dom": "^18.2.0" }, "devDependencies": { @@ -22,6 +30,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "typescript": "^5.2.2", - "vite": "^5.0.8" + "vite": "^5.1.1" } } diff --git a/examples/web/src/App.tsx b/examples/web/src/App.tsx index 5505297..1d33543 100644 --- a/examples/web/src/App.tsx +++ b/examples/web/src/App.tsx @@ -1,38 +1,203 @@ -import { useRef } from "react"; +// use DENO best practice and move all externals into externals.ts + +import { useRef, useState } from "react"; +import CreativeEditorSDKComponent from "./components/CreativeEditorSDK"; import CreativeEditorSDK from "@cesdk/cesdk-js"; -import addPlugins from "./addPlugins"; + +// React UI Components +import { CommandPalette } from "./components/CommandPalette" +// Utils +import { downloadBlocks } from "./utils/download"; + +// IMGLY Plugins + + +// Plugins +// import BackgroundRemovalPlugin from '@imgly/plugin-background-removal-web'; +import VectorizerPlugin from '@imgly/plugin-vectorizer'; +import DesignBatteriesPlugin from "@imgly/plugin-design-essentials"; +import DocumentPlugin from "@imgly/plugin-documents"; +import { PluginContext } from "@imgly/plugin-core"; + + +declare global { + interface Window { imgly: PluginContext } +} + function App() { - const cesdk = useRef(); - return ( -
{ - if (domElement != null) { - CreativeEditorSDK.create(domElement, { - license: import.meta.env.VITE_CESDK_LICENSE_KEY, - callbacks: { onUpload: "local" }, - }).then(async (instance) => { - // @ts-ignore - window.cesdk = instance; - cesdk.current = instance; - - // Do something with the instance of CreativeEditor SDK, for example: - // Populate the asset library with default / demo asset sources. - await Promise.all([ - instance.addDefaultAssetSources(), - instance.addDemoAssetSources({ sceneMode: "Design" }), - addPlugins(instance), - ]); - await instance.createDesignScene(); - }); - } else if (cesdk.current != null) { - cesdk.current.dispose(); + // const cesdkRef = useRef(); + const contextRef = useRef(); + const [commandItems, setCommandItems] = useState>([]) + const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false) + + + const commandPaletteButton = (params: { builder: any }) => { + params + .builder! + .Button("plugin.imgly.commandpalette.id", { + label: "plugin.imgly.commandpalette.label", + icon: undefined, + isActive: isCommandPaletteOpen, + isLoading: false, + isDisabled: isCommandPaletteOpen, + loadingProgress: undefined, // creates infinite spinner + onClick: () => { + setIsCommandPaletteOpen(true) } - }} - >
+ }); + } + + + + const [config, _setConfig] = useState( + { + "license": import.meta.env.VITE_CESDK_LICENSE_KEY, + "callbacks.onUpload": 'local', + "callbacks.onDownload": "download", + "callbacks.onSave": async (str: string) => downloadBlocks(contextRef.current!.engine.block, [new Blob([str])], { mimeType: 'application/imgly' }), + "callbacks.onExport": async (blobs: Array, options: any) => downloadBlocks(contextRef.current!.engine.block, blobs, { mimeType: options.mimeType, pages: options.pages }), + // "callbacks.onLoad": , + // devMode: true, + "theme": "dark", + "role": 'Creator', + "ui.hide": false, + "ui.elements.view": "advanced", + "ui.elements.navigation.action.save": true, + "ui.elements.navigation.action.load": true, + "ui.elements.navigation.action.export": true, + }) + + + const initCallback = async (cesdk: CreativeEditorSDK) => { + + const imgly = new PluginContext(cesdk) + window.imgly = imgly + + + // Init Scene Programatically + await cesdk.createDesignScene(); + cesdk.engine.scene.setDesignUnit("Pixel"); // + + + const vectorizerPlugin = VectorizerPlugin(imgly, {}) + const commandsPlugin = DesignBatteriesPlugin(imgly, {}) + const documentPlugin = DocumentPlugin(imgly, {}) + // Register Plguins + await Promise.all([ + cesdk.addDefaultAssetSources(), + cesdk.addDemoAssetSources({ sceneMode: "Design" }), + cesdk.unstable_addPlugin(commandsPlugin), + cesdk.unstable_addPlugin(vectorizerPlugin), + cesdk.unstable_addPlugin(documentPlugin) + + ]); + + + + // Ui components + imgly.ui?.unstable_registerComponent("plugin.imgly.commandpalette", commandPaletteButton); + + imgly.i18n.setTranslations({ en: { "plugin.imgly.commandpalette.label": "✨ Run .." } }) + // Canvas Menu + const canvasMenuItems = imgly.ui?.unstable_getCanvasMenuOrder() ?? [] + const newCanvasMenuItems = ["plugin.imgly.commandpalette", ...canvasMenuItems]; + imgly.ui?.unstable_setCanvasMenuOrder(newCanvasMenuItems) + + + + // Bind our react command paltte to cesdk command palettes are listen on new commands being created + imgly.engine.event.subscribe([], (events) => { + events + .forEach(_ => { + setCommandItems(generateItems(imgly)) + }) + }) + imgly.commands.subscribe("register", (_label: string) => setCommandItems(generateItems(imgly))) + imgly.commands.subscribe("unregister", (_label: string) => setCommandItems(generateItems(imgly))) + + setCommandItems(generateItems(imgly)) + } + + + return ( + <> + setIsCommandPaletteOpen(val)} /> + + + ); } +const generateItems = (ctx: PluginContext) => { + return [...generateBlockHierarchy(ctx), ...generateCommandItems(ctx), ...generateProperyItems(ctx)] +} + +const generateBlockHierarchy = (ctx: PluginContext) => { + const blocks = ctx.engine.block.findAll() + + return blocks.map((bId: number) => { + const titel = ctx.engine.block.getName(bId) || ctx.engine.block.getUUID(bId).toString() + return { + id: bId, + children: titel, + kind: "block", + group: "Hierarchy", + showType: false, + onClick: () => ctx.engine.block.select(bId) + } + }) +} + +const generateProperyItems = (ctx: PluginContext) => { + const { block } = ctx.engine + const bIds = block.findAllSelected() + const bId = bIds[0] + if (!bId) return [] // for now + + const props = bIds.flatMap((bId: number) => block.findAllProperties(bId)) + const uniqueProps = Array.from(new Set(props)) + + return uniqueProps.map((p) => { + const titel = p + const value = 42 + + return { + id: bId, + children: titel, + kind: "property", + group: "Properties", + showType: false, + onClick: () => prompt(`Change ${p} to`, value.toString()) + } + }) +} + + +const generateCommandItems = (ctx: PluginContext): Array => { + const cmds = ctx + .commands! + .listCommands() + + return cmds + .map((cmdId: string) => { + const titel = ctx.i18n.translate(cmdId) // this comes from the metadata + const desc = ctx.commands.getCommandDescription(cmdId) + if (titel === undefined) throw new Error(`No translation found for command ${cmdId}`) + return { + id: cmdId, + children: titel, + kind: "command", + group: desc?.category || "Commands", + showType: false, + onClick: async () => { + await ctx.commands!.executeCommand(cmdId, {}) + } + } + }) +} + + + export default App; diff --git a/examples/web/src/addPlugins.ts b/examples/web/src/addPlugins.ts deleted file mode 100644 index 305a42f..0000000 --- a/examples/web/src/addPlugins.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type CreativeEditorSDK from '@cesdk/cesdk-js'; - -import BackgroundRemovalPlugin from '@imgly/plugin-background-removal-web'; -import VectorizerPlugin from '@imgly/plugin-vectorizer-web'; - -async function addPlugins(cesdk: CreativeEditorSDK) { - try { - cesdk.unstable_addPlugin(VectorizerPlugin()); - cesdk.unstable_addPlugin(BackgroundRemovalPlugin()); - } catch (error) { - console.error('Could not add all plugins: ', error); - } -} - -export default addPlugins; diff --git a/examples/web/src/components/CommandPalette.tsx b/examples/web/src/components/CommandPalette.tsx new file mode 100644 index 0000000..63d86a5 --- /dev/null +++ b/examples/web/src/components/CommandPalette.tsx @@ -0,0 +1,105 @@ +import "react-cmdk/dist/cmdk.css"; +import CMDK, { filterItems, getItemIndex } from "react-cmdk"; +import { useState, useEffect } from "react"; + + +const CommandPrefix = "!" +const BlockPrefix = "#" +const PropertyPrefix = "@" + +import { groupBy } from "lodash"; + +// https://github.com/albingroen/react-cmdk +type Params = { items: Array, isOpen: boolean, setIsOpen: (val: boolean) => void } +export const CommandPalette = (params: Params) => { + const [page, _setPage] = useState<"root">("root"); + const [search, setSearch] = useState(CommandPrefix); + const { isOpen, setIsOpen } = params + const { items } = params + + + + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if ( + (navigator?.userAgent?.toLowerCase().includes("mac") + ? e.metaKey + : e.ctrlKey) && + e.key === "k" + ) { + e.preventDefault(); + e.stopPropagation(); + + setIsOpen(!isOpen); + } + } + + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + // Support prefixes + + let refinedSearch = search + let refinedItems = items + + if (search.startsWith(CommandPrefix)) { + refinedSearch = search.substring(CommandPrefix.length).trim() + refinedItems = items.filter((item) => item.kind === "command") + } + else if (search.startsWith(BlockPrefix)) { + refinedSearch = search.substring(BlockPrefix.length).trim() + refinedItems = items.filter((item) => item.kind === "block") + } + + else if (search.startsWith(PropertyPrefix)) { + refinedSearch = search.substring(PropertyPrefix.length).trim() + refinedItems = items.filter((item) => item.kind === "property") + } else { + refinedItems = items.filter((item) => item.kind === "command") + } + + const grouped = groupBy(refinedItems, "group") + const filteredItems = filterItems(Object.keys(grouped).map((key) => { + return { + heading: key, + id: key, + items: grouped[key] ?? [] + } + }), refinedSearch, { filterOnListHeading: true}); + + return ( + + + {filteredItems.length ? ( + filteredItems.map((list) => ( + + {list.items.map(({ id, ...rest }) => ( + + ))} + + )) + ) : ( + + )} + + + + ); +} + +/// helper + diff --git a/examples/web/src/components/CreativeEditorSDK.tsx b/examples/web/src/components/CreativeEditorSDK.tsx new file mode 100644 index 0000000..35e53e4 --- /dev/null +++ b/examples/web/src/components/CreativeEditorSDK.tsx @@ -0,0 +1,44 @@ +import { useRef, useState, useEffect } from "react"; +import { type Configuration } from "@cesdk/cesdk-js"; +import type CreativeEditorSDK from "@cesdk/cesdk-js"; +import { unflatten } from "../utils/flatten"; + +type Props = { + config: Configuration, callback: (cesdk: CreativeEditorSDK) => Promise +} +export default function CreativeEditorSDKComponent(props: Props) { + const cesdk_container = useRef(null); + + const [_, setCesdk] = useState(); + + useEffect(() => { + if (!cesdk_container.current) return; + + let cleanedUp = false; + let instance: CreativeEditorSDK; + import("@cesdk/cesdk-js") + .then((module) => module.default.create(cesdk_container!.current!, unflatten(props.config))) + .then(async (instance) => { + if (cleanedUp) { + instance.dispose(); + return; + } + setCesdk(instance); + await props.callback(instance); + } + ); + + return () => { + cleanedUp = true; + instance?.dispose(); + setCesdk(undefined); + }; + }, [cesdk_container, props.config]); + + return ( +
+ ); +} diff --git a/examples/web/src/deps.ts b/examples/web/src/deps.ts new file mode 100644 index 0000000..e69de29 diff --git a/examples/web/src/imgly.ts b/examples/web/src/imgly.ts new file mode 100644 index 0000000..e69de29 diff --git a/examples/web/src/utils/cesdk.ts b/examples/web/src/utils/cesdk.ts new file mode 100644 index 0000000..44df8e1 --- /dev/null +++ b/examples/web/src/utils/cesdk.ts @@ -0,0 +1,18 @@ +import CreativeEditorSDK from "@cesdk/cesdk-js"; + + +export const readPropValue = (cesdk: CreativeEditorSDK, id: number, propKey: string, propType: string) => { + try { + switch (propType.toLowerCase()) { + case "string": return cesdk.engine.block.getString(id, propKey); + case "float": return cesdk.engine.block.getFloat(id, propKey); + case "double": return cesdk.engine.block.getDouble(id, propKey); + case "color": return cesdk.engine.block.getColor(id, propKey); + case "bool": return cesdk.engine.block.getBool(id, propKey); + case "enum": return cesdk.engine.block.getEnum(id, propKey); + } + } catch(e){ + console.warn("Error reading property value: ", e); + } + return undefined; +}; diff --git a/examples/web/src/utils/download.ts b/examples/web/src/utils/download.ts new file mode 100644 index 0000000..5cde074 --- /dev/null +++ b/examples/web/src/utils/download.ts @@ -0,0 +1,29 @@ +import { BlockAPI } from "@cesdk/cesdk-js"; + + +export function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); +} +export const downloadBlocks = (block: BlockAPI, blobs: Blob[], options: { mimeType: string; pages?: number[]; }) => { + const postfix = options.mimeType.split("/")[1]; + const pageIds = options.pages ?? []; + + blobs.forEach((blob, index) => { + const pageId = pageIds[index]; + let pageName = `page-${index}`; + if (pageId) { + const name = block.getName(pageId); + pageName = name?.length ? name : pageName; + } + const filename = `${pageName}.${postfix}`; + downloadBlob(blob, filename); + }); + return Promise.resolve(); +}; + + diff --git a/examples/web/src/utils/flatten.ts b/examples/web/src/utils/flatten.ts new file mode 100644 index 0000000..03dfe56 --- /dev/null +++ b/examples/web/src/utils/flatten.ts @@ -0,0 +1,46 @@ +export function flatten(obj: any, prefix = ''): any { + const flattened = {}; + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const propName = prefix ? `${prefix}.${key}` : key; + + if (typeof obj[key] === 'object' && obj[key] !== null) { + Object.assign(flattened, flatten(obj[key], propName)); + } else { + // @ts-ignore + flattened[propName] = obj[key]; + } + } + } + + return flattened; +} + + +export function unflatten(obj: any): any { + const unflattened = {}; + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const keys = key.split('.'); + + let currentObj = unflattened; + for (let i = 0; i < keys.length - 1; i++) { + const nestedKey = keys[i]; + if (!currentObj.hasOwnProperty(nestedKey)) { + // @ts-ignore + currentObj[nestedKey] = {}; + } + // @ts-ignore + currentObj = currentObj[nestedKey]; + } + + // @ts-ignore + currentObj[keys[keys.length - 1]] = value; + } + } + + return unflattened; +} \ No newline at end of file diff --git a/examples/web/tsconfig.json b/examples/web/tsconfig.json index a7fc6fb..3d69b79 100644 --- a/examples/web/tsconfig.json +++ b/examples/web/tsconfig.json @@ -20,6 +20,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], + "include": ["src", "../imgly-components-source", "../../packages/design-batteries/src/commands", "../../packages/design-batteries/src/types", "../../packages/design-batteries/src/utils", "../../packages/design-batteries/src/commands/index.ts", "../../packages/design-batteries/src/index.ts", "../../packages/design-batteries/src/PluginManifest.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/examples/web/vercel.json.disabled b/examples/web/vercel.json.disabled new file mode 100644 index 0000000..8381922 --- /dev/null +++ b/examples/web/vercel.json.disabled @@ -0,0 +1,17 @@ +{ + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cross-Origin-Opener-Policy", + "value": "same-origin" + }, + { + "key": "Cross-Origin-Embedder-Policy", + "value": "require-corp" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/web/vite.config.ts b/examples/web/vite.config.ts index 7fd700f..0129c15 100644 --- a/examples/web/vite.config.ts +++ b/examples/web/vite.config.ts @@ -1,13 +1,25 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import { createLogger } from "./vite/logger"; + + // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + build: { + chunkSizeWarningLimit: 4096, + sourcemap: true + }, + worker: { + format: "es" // Default was "iife" but then import.meta.url for importing worker does not worker + }, server: { headers: { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }, }, + customLogger: createLogger() }); + diff --git a/examples/web/vite/logger.js b/examples/web/vite/logger.js new file mode 100644 index 0000000..7eda5a3 --- /dev/null +++ b/examples/web/vite/logger.js @@ -0,0 +1,14 @@ +import { createLogger as createViteLogger } from "vite"; + + +export const createLogger = () => { + // We create a custom logger in order to filter messages that are persistent + const logger = createViteLogger(); + const originalWarning = logger.warn; + + logger.warn = (msg, options) => { + if (msg.includes('[plugin:vite:resolve]')) return; + originalWarning(msg, options); + }; + return logger; +} \ No newline at end of file diff --git a/package.json b/package.json index af70b89..c776115 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,13 @@ "name": "imgly-plugins", "version": "0.0.0", "workspaces": [ - "packages/*", - "examples/*" + "examples/web", + "packages/background-removal", + "packages/plugin-core", + "packages/plugin-design-essentials", + "packages/plugin-documents", + "packages/plugin-vectorizer" + ], "scripts": { "build": "turbo run build --force", diff --git a/packages/background-removal/esbuild/config.mjs b/packages/background-removal/esbuild/config.mjs index 43cf4fa..cfc780d 100644 --- a/packages/background-removal/esbuild/config.mjs +++ b/packages/background-removal/esbuild/config.mjs @@ -7,6 +7,12 @@ const packageJson = JSON.parse( await readFile(new URL('../package.json', import.meta.url)) ); +const dependencies = Object.keys(packageJson.dependencies) +const peerDependencies = Object.keys(packageJson.peerDependencies) + +const externals = [...dependencies, ...peerDependencies] + + console.log( chalk.yellow('Building version: '), chalk.green(packageJson.version) @@ -21,8 +27,8 @@ const configs = [ minify: true, bundle: true, sourcemap: true, - external: ['@imgly/background-removal', '@cesdk/cesdk-js', 'lodash'], - platform: 'browser', + external: externals, + platform: 'node', format: 'esm', outfile: 'dist/index.mjs', plugins: [ diff --git a/packages/background-removal/package.json b/packages/background-removal/package.json index f27f946..83d0c3e 100644 --- a/packages/background-removal/package.json +++ b/packages/background-removal/package.json @@ -14,7 +14,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/imgly/plugin-background-removal-web.git" + "url": "git+https://github.com/imgly/plugins.git" }, "license": "SEE LICENSE IN LICENSE.md", "author": { @@ -57,7 +57,6 @@ "types:create": "tsc --emitDeclarationOnly" }, "devDependencies": { - "@cesdk/cesdk-js": "~1.20.0", "@types/ndarray": "^1.0.14", "chalk": "^5.3.0", "concurrently": "^8.2.2", @@ -66,7 +65,7 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "@cesdk/cesdk-js": "~1.20.0" + "@cesdk/cesdk-js": "~1.21.0" }, "dependencies": { "@imgly/background-removal": "~1.4", diff --git a/packages/background-removal/src/registerComponents.ts b/packages/background-removal/src/registerComponents.ts index ac4039b..a0821bd 100644 --- a/packages/background-removal/src/registerComponents.ts +++ b/packages/background-removal/src/registerComponents.ts @@ -36,6 +36,7 @@ export function registerComponents(cesdk: CreativeEditorSDK) { return; } + // Why is that needed. The feature enable should already handle that const [id] = engine.block.findAllSelected(); if (!cesdk.engine.block.hasFill(id)) return; diff --git a/packages/vectorizer/LICENSE.md b/packages/plugin-core/LICENSE.md similarity index 100% rename from packages/vectorizer/LICENSE.md rename to packages/plugin-core/LICENSE.md diff --git a/packages/vectorizer/README.md b/packages/plugin-core/README.md similarity index 88% rename from packages/vectorizer/README.md rename to packages/plugin-core/README.md index e4197b5..6acc73e 100644 --- a/packages/vectorizer/README.md +++ b/packages/plugin-core/README.md @@ -7,8 +7,8 @@ This plugin introduces a vectorizer for the CE.SDK editor. You can install the plugin via npm or yarn. Use the following commands to install the package: ``` -yarn add @imgly/plugin-vectorizer-web -npm install @imgly/plugin-vectorizer-web +yarn add @imgly/plugin-vectorizer +npm install @imgly/plugin-vectorizer ``` ## Usage @@ -18,7 +18,7 @@ canvas menu entry for every block with an image fill. ```typescript import CreativeEditorSDK from '@cesdk/cesdk-js'; -import VectorizerPlugin from '@imgly/plugin-vectorizer-web'; +import VectorizerPlugin from '@imgly/plugin-vectorizer'; const config = { license: '', diff --git a/packages/plugin-core/STRUCTURE.md b/packages/plugin-core/STRUCTURE.md new file mode 100644 index 0000000..13b1472 --- /dev/null +++ b/packages/plugin-core/STRUCTURE.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-core/TODO.md b/packages/plugin-core/TODO.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/vectorizer/esbuild/config.mjs b/packages/plugin-core/esbuild/config.mjs similarity index 76% rename from packages/vectorizer/esbuild/config.mjs rename to packages/plugin-core/esbuild/config.mjs index e60ac60..37b437c 100644 --- a/packages/vectorizer/esbuild/config.mjs +++ b/packages/plugin-core/esbuild/config.mjs @@ -7,6 +7,11 @@ const packageJson = JSON.parse( await readFile(new URL('../package.json', import.meta.url)) ); + +const dependencies = Object.keys(packageJson.dependencies) +const peerDependencies = Object.keys(packageJson.peerDependencies) +const externals = [...dependencies, ...peerDependencies] + console.log( chalk.yellow('Building version: '), chalk.green(packageJson.version) @@ -14,17 +19,18 @@ console.log( const configs = [ { - entryPoints: ['src/index.ts'], + entryPoints: ['src/index.ts', "src/worker.ts"], define: { PLUGIN_VERSION: `"${packageJson.version}"` }, minify: true, bundle: true, sourcemap: true, - external: ['@cesdk/cesdk-js', 'lodash', "node:path", "fs", "url"], - platform: 'browser', + external: externals, + platform: 'node', format: 'esm', - outfile: 'dist/index.mjs', + outdir: 'dist', + outExtension: { '.js': '.mjs' }, plugins: [ { name: 'reporter', diff --git a/packages/vectorizer/esbuild/global.d.ts b/packages/plugin-core/esbuild/global.d.ts similarity index 100% rename from packages/vectorizer/esbuild/global.d.ts rename to packages/plugin-core/esbuild/global.d.ts diff --git a/packages/plugin-core/manifest.json b/packages/plugin-core/manifest.json new file mode 100644 index 0000000..a218d68 --- /dev/null +++ b/packages/plugin-core/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "pluginapi-polyfill", + "version": "0.0.1", + "publisher": "IMG.LY GmbH", + "icon": null, + "license": "MIT", + "categories": [ + "polyfill" + ], + "contributes": {} +} \ No newline at end of file diff --git a/packages/plugin-core/package.json b/packages/plugin-core/package.json new file mode 100644 index 0000000..84a7ad7 --- /dev/null +++ b/packages/plugin-core/package.json @@ -0,0 +1,66 @@ +{ + "name": "@imgly/plugin-core", + "version": "0.1.0", + "description": "Polyfill for API plugin for the CE.SDK editor", + "keywords": [ + "CE.SDK", + "plugin", + "polyfill" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/imgly/plugins.git" + }, + "license": "SEE LICENSE IN LICENSE.md", + "author": { + "name": "IMG.LY GmbH", + "email": "support@img.ly", + "url": "https://img.ly" + }, + "bugs": { + "email": "support@img.ly" + }, + "source": "./src/index.ts", + "module": "./dist/index.mjs", + "types": "./types/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "types": "./types/index.d.ts" + } + }, + "homepage": "https://img.ly", + "files": [ + "LICENSE.md", + "README.md", + "CHANGELOG.md", + "dist/", + "types/", + "bin/" + ], + "scripts": { + "start": "npm run watch", + "clean": "npx rimraf dist && npx rimraf types", + "build": "yarn run types:create && node scripts/build.mjs", + "dev": "yarn run types:create && node scripts/watch.mjs", + "publish:latest": "npm run clean && npm run build && npm publish --tag latest --access public", + "publish:next": "npm run clean && npm run build && npm publish --tag next --access public", + "check:all": "concurrently -n lint,type,pretty \"yarn check:lint\" \"yarn check:type\" \"yarn check:pretty\"", + "check:lint": "eslint --max-warnings 0 './src/**/*.{ts,tsx}'", + "check:pretty": "prettier --list-different './src/**/*.{ts,tsx}'", + "check:type": "tsc --noEmit", + "types:create": "tsc --emitDeclarationOnly" + }, + "devDependencies": { + "chalk": "^5.3.0", + "concurrently": "^8.2.2", + "esbuild": "^0.19.11", + "eslint": "^8.51.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@cesdk/cesdk-js": "~1.21.0" + }, + "dependencies": { + } +} diff --git a/packages/vectorizer/scripts/build.mjs b/packages/plugin-core/scripts/build.mjs similarity index 100% rename from packages/vectorizer/scripts/build.mjs rename to packages/plugin-core/scripts/build.mjs diff --git a/packages/vectorizer/scripts/watch.mjs b/packages/plugin-core/scripts/watch.mjs similarity index 100% rename from packages/vectorizer/scripts/watch.mjs rename to packages/plugin-core/scripts/watch.mjs diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts new file mode 100644 index 0000000..c82128a --- /dev/null +++ b/packages/plugin-core/src/index.ts @@ -0,0 +1,7 @@ +import { PluginContext } from "./plugin/PluginContext" + +export { Commands, type CommandCallback, type CommandDescription } from "./plugin/Commands" +export { I18N } from "./plugin/I18n" +export { type Logger } from "./plugin/Logger" +export { PluginContext } from "./plugin/PluginContext" + diff --git a/packages/plugin-core/src/plugin/Commands.ts b/packages/plugin-core/src/plugin/Commands.ts new file mode 100644 index 0000000..dbf73d8 --- /dev/null +++ b/packages/plugin-core/src/plugin/Commands.ts @@ -0,0 +1,65 @@ +import { PluginContext } from './PluginContext'; +import { Subscribable } from './Subscribable'; + +export type CommandCallback = (ctx: PluginContext, params: any) => Promise | any; +export type CommandArgs = { blockIds?: number[] } + +export type CommandEvents = "register" | "unregister" + +export type CommandDescription = { + id?: string, + category?: string + args?: any, //JSONSchema + returns?: any // JSONSchema +} + +export class Commands extends Subscribable { + #entries = new Map() + #descs = new Map() + #ctx: PluginContext; + + constructor(ctx: PluginContext) { + super() + this.#ctx = ctx + } + + listCommands() { + return Array.from(this.#entries.keys()); + } + + registerCommand(label: string, callback: CommandCallback, description: CommandDescription) { + this.#entries.set(label, callback); + this.#descs.set(label, description || {}); + this.notify("register", label) + return () => this.unregisterCommand(label) + } + + unregisterCommand(label: string) { + this.notify("unregister", label) + this.#entries.delete(label); + this.#descs.delete(label); + } + + getCommandCallback(label: string) { + return this.#entries.get(label); + } + getCommandDescription(label: string) { + return this.#descs.get(label); + } + + + async executeCommand

(cmd: string, params: P): Promise { + + const command = this.#entries.get(cmd); + if (command) { + + // this.#ctx.ui?.showNotification({ message: `Running command: ${cmd}`, type: "info" }) + return await command(this.#ctx, params); + } else { + // this.#ctx.ui?.showNotification({ message: `Command not found: ${cmd}`, type: "info" }) + } + } +} + + + diff --git a/packages/plugin-core/src/plugin/I18n.ts b/packages/plugin-core/src/plugin/I18n.ts new file mode 100644 index 0000000..e0681f7 --- /dev/null +++ b/packages/plugin-core/src/plugin/I18n.ts @@ -0,0 +1,62 @@ +import { PluginContext } from './PluginContext'; +import { Subscribable } from './Subscribable'; + + +import { merge } from 'lodash'; +import { flatten } from '../utils/flatten'; + +type Translation = Record +type Translations = { [locale: string]: Translation; } + +export class I18N extends Subscribable<"register", Translations> { + #translations: any = {}; + #locale: string = navigator.language ?? "en"; + #ctx: PluginContext + + + constructor(ctx: PluginContext) { + super() + this.#ctx = ctx + } + + setTranslations(translations: Translations) { + this.#translations = merge(this.#translations, flatten(translations)); + this.notify("register", translations) + } + + translate(key: K, fallback: string | undefined = undefined) { + const translation = this.findTranslation(key, this.#locale) + if (!translation) { + this.#ctx.logger?.warn(`Translation in "${this.#locale}" for key ${key as string} not found!`); + } + return translation ?? fallback ?? key; + } + + findTranslation(key: K, language?: string) { + const [lang, region] = this.#locale.split('-'); + const langLookup = lang.concat('.', key as string); + const langAndRegionLookup = lang.concat('.', key as string); + return this.#translations[langAndRegionLookup] || this.#translations[langLookup] + } + + hasTranslation(key: K, language?: string): boolean { + const locale = language ?? this.#locale; + const lookup = locale.concat('.', key as string); + return !!this.findTranslation(key, language) + } + + setLocale(locale: string) { + this.#locale = locale; + } + locale() { + return this.#locale; + } + + locales() { + return navigator.languages + } + t = this.translate.bind(this) +} + + +export type I18NType = { i18n?: I18N } diff --git a/packages/plugin-core/src/plugin/Logger.ts b/packages/plugin-core/src/plugin/Logger.ts new file mode 100644 index 0000000..03cfdd4 --- /dev/null +++ b/packages/plugin-core/src/plugin/Logger.ts @@ -0,0 +1,9 @@ +export interface Logger { + log: (message: string) => void , + trace: (message: string) => void + debug: (message: string) => void, + info: (message: string) => void, + warn: (message: string) => void, + error: (message: string) => void, + + } \ No newline at end of file diff --git a/packages/plugin-core/src/plugin/PluginContext.ts b/packages/plugin-core/src/plugin/PluginContext.ts new file mode 100644 index 0000000..f8230c2 --- /dev/null +++ b/packages/plugin-core/src/plugin/PluginContext.ts @@ -0,0 +1,45 @@ +import { type CreativeEngine } from "@cesdk/cesdk-js"; +import CreativeEditorSDK from "@cesdk/cesdk-js"; +import { Logger } from "./Logger"; + +import { Commands } from "./Commands"; +import { I18N } from "./I18n"; + +// this is not exposed yet +type UserInterfaceAPI = typeof CreativeEditorSDK.prototype.ui; +type FeatureAPI = typeof CreativeEditorSDK.prototype.feature; + + + +type I18NContributions = { + [key: string]: string +} +type ManifestContributions = { + i18n?: I18NContributions +} +type Manifest = { + contributes: ManifestContributions +} + + +export class PluginContext { + logger?: Logger; + engine: CreativeEngine; + ui?: UserInterfaceAPI; + i18n: I18N; + commands: Commands; + feature: FeatureAPI; + constructor(cesdk: CreativeEditorSDK) { + this.engine = cesdk.engine; + this.ui = cesdk.ui; + this.i18n = new I18N(this as PluginContext) + this.commands = new Commands(this as PluginContext) + this.feature = cesdk.feature; + const _unsubscribe = this.i18n.subscribe("register", (translations) => { + cesdk.setTranslations(translations); + } ) + + } + +}; + diff --git a/packages/plugin-core/src/plugin/Subscribable.ts b/packages/plugin-core/src/plugin/Subscribable.ts new file mode 100644 index 0000000..cab0333 --- /dev/null +++ b/packages/plugin-core/src/plugin/Subscribable.ts @@ -0,0 +1,24 @@ + +export class Subscribable { + #subscribers: Array<{ filter: E[], callback: (event: D) => void }> = [] + + subscribe(toEvent: E, callback: (label: D) => void): () => void { + const entry = { filter: [toEvent], callback } + this.#subscribers.push(entry); + return () => { + const idx = this.#subscribers.indexOf(entry); + if (idx > -1) { + this.#subscribers.splice(idx, 1); + } + } + } + + notify(event: E, data: D) { + this.#subscribers.forEach(({ filter, callback }) => { + if (filter.includes(event)) { + callback(data); + } + }) + } + +} \ No newline at end of file diff --git a/packages/plugin-core/src/utils/flatten.ts b/packages/plugin-core/src/utils/flatten.ts new file mode 100644 index 0000000..03dfe56 --- /dev/null +++ b/packages/plugin-core/src/utils/flatten.ts @@ -0,0 +1,46 @@ +export function flatten(obj: any, prefix = ''): any { + const flattened = {}; + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const propName = prefix ? `${prefix}.${key}` : key; + + if (typeof obj[key] === 'object' && obj[key] !== null) { + Object.assign(flattened, flatten(obj[key], propName)); + } else { + // @ts-ignore + flattened[propName] = obj[key]; + } + } + } + + return flattened; +} + + +export function unflatten(obj: any): any { + const unflattened = {}; + + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const keys = key.split('.'); + + let currentObj = unflattened; + for (let i = 0; i < keys.length - 1; i++) { + const nestedKey = keys[i]; + if (!currentObj.hasOwnProperty(nestedKey)) { + // @ts-ignore + currentObj[nestedKey] = {}; + } + // @ts-ignore + currentObj = currentObj[nestedKey]; + } + + // @ts-ignore + currentObj[keys[keys.length - 1]] = value; + } + } + + return unflattened; +} \ No newline at end of file diff --git a/packages/plugin-core/src/worker.ts b/packages/plugin-core/src/worker.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/vectorizer/tsconfig.json b/packages/plugin-core/tsconfig.json similarity index 84% rename from packages/vectorizer/tsconfig.json rename to packages/plugin-core/tsconfig.json index eb78ece..07e826a 100644 --- a/packages/vectorizer/tsconfig.json +++ b/packages/plugin-core/tsconfig.json @@ -5,10 +5,11 @@ "module": "es2020", "lib": ["es2018", "dom"], "moduleResolution": "node", + "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "declaration": true, - "declarationDir": "dist/", + "declarationDir": "types/", "skipLibCheck": true }, "include": ["src/**/*", "esbuild/global.d.ts"], diff --git a/packages/plugin-core/types/index.d.ts b/packages/plugin-core/types/index.d.ts new file mode 100644 index 0000000..7588cca --- /dev/null +++ b/packages/plugin-core/types/index.d.ts @@ -0,0 +1,4 @@ +export { Commands, type CommandCallback, type CommandDescription } from "./plugin/Commands"; +export { I18N } from "./plugin/I18n"; +export { type Logger } from "./plugin/Logger"; +export { PluginContext } from "./plugin/PluginContext"; diff --git a/packages/plugin-core/types/manifest.d.ts b/packages/plugin-core/types/manifest.d.ts new file mode 100644 index 0000000..e1ead83 --- /dev/null +++ b/packages/plugin-core/types/manifest.d.ts @@ -0,0 +1,4 @@ +declare const _default: { + id: string; +}; +export default _default; diff --git a/packages/plugin-core/types/plugin/Commands.d.ts b/packages/plugin-core/types/plugin/Commands.d.ts new file mode 100644 index 0000000..89cc3e0 --- /dev/null +++ b/packages/plugin-core/types/plugin/Commands.d.ts @@ -0,0 +1,23 @@ +import { PluginContext } from './PluginContext'; +import { Subscribable } from './Subscribable'; +export type CommandCallback = (ctx: PluginContext, params: any) => Promise | any; +export type CommandArgs = { + blockIds?: number[]; +}; +export type CommandEvents = "register" | "unregister"; +export type CommandDescription = { + id?: string; + category?: string; + args?: any; + returns?: any; +}; +export declare class Commands extends Subscribable { + #private; + constructor(ctx: PluginContext); + listCommands(): string[]; + registerCommand(label: string, callback: CommandCallback, description: CommandDescription): () => void; + unregisterCommand(label: string): void; + getCommandCallback(label: string): CommandCallback | undefined; + getCommandDescription(label: string): CommandDescription | undefined; + executeCommand

(cmd: string, params: P): Promise; +} diff --git a/packages/plugin-core/types/plugin/I18n.d.ts b/packages/plugin-core/types/plugin/I18n.d.ts new file mode 100644 index 0000000..ba0b245 --- /dev/null +++ b/packages/plugin-core/types/plugin/I18n.d.ts @@ -0,0 +1,22 @@ +import { PluginContext } from './PluginContext'; +import { Subscribable } from './Subscribable'; +type Translation = Record; +type Translations = { + [locale: string]: Translation; +}; +export declare class I18N extends Subscribable<"register", Translations> { + #private; + constructor(ctx: PluginContext); + setTranslations(translations: Translations): void; + translate(key: K, fallback?: string | undefined): any; + findTranslation(key: K, language?: string): any; + hasTranslation(key: K, language?: string): boolean; + setLocale(locale: string): void; + locale(): string; + locales(): readonly string[]; + t: (key: K, fallback?: string | undefined) => any; +} +export type I18NType = { + i18n?: I18N; +}; +export {}; diff --git a/packages/plugin-core/types/plugin/Logger.d.ts b/packages/plugin-core/types/plugin/Logger.d.ts new file mode 100644 index 0000000..27f1a10 --- /dev/null +++ b/packages/plugin-core/types/plugin/Logger.d.ts @@ -0,0 +1,8 @@ +export interface Logger { + log: (message: string) => void; + trace: (message: string) => void; + debug: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; +} diff --git a/packages/plugin-core/types/plugin/PluginContext.d.ts b/packages/plugin-core/types/plugin/PluginContext.d.ts new file mode 100644 index 0000000..5ea39f2 --- /dev/null +++ b/packages/plugin-core/types/plugin/PluginContext.d.ts @@ -0,0 +1,17 @@ +import { type CreativeEngine } from "@cesdk/cesdk-js"; +import CreativeEditorSDK from "@cesdk/cesdk-js"; +import { Logger } from "./Logger"; +import { Commands } from "./Commands"; +import { I18N } from "./I18n"; +type UserInterfaceAPI = typeof CreativeEditorSDK.prototype.ui; +type FeatureAPI = typeof CreativeEditorSDK.prototype.feature; +export declare class PluginContext { + logger?: Logger; + engine: CreativeEngine; + ui?: UserInterfaceAPI; + i18n: I18N; + commands: Commands; + feature: FeatureAPI; + constructor(cesdk: CreativeEditorSDK); +} +export {}; diff --git a/packages/plugin-core/types/plugin/Subscribable.d.ts b/packages/plugin-core/types/plugin/Subscribable.d.ts new file mode 100644 index 0000000..12aefba --- /dev/null +++ b/packages/plugin-core/types/plugin/Subscribable.d.ts @@ -0,0 +1,5 @@ +export declare class Subscribable { + #private; + subscribe(toEvent: E, callback: (label: D) => void): () => void; + notify(event: E, data: D): void; +} diff --git a/packages/plugin-core/types/utils/flatten.d.ts b/packages/plugin-core/types/utils/flatten.d.ts new file mode 100644 index 0000000..f9433a9 --- /dev/null +++ b/packages/plugin-core/types/utils/flatten.d.ts @@ -0,0 +1,2 @@ +export declare function flatten(obj: any, prefix?: string): any; +export declare function unflatten(obj: any): any; diff --git a/packages/plugin-core/types/worker.d.ts b/packages/plugin-core/types/worker.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/plugin-design-essentials/.gitignore b/packages/plugin-design-essentials/.gitignore new file mode 100644 index 0000000..9060c20 --- /dev/null +++ b/packages/plugin-design-essentials/.gitignore @@ -0,0 +1,11 @@ +node_modules +packages/*/dist +examples/*/dist +.env.local + +.DS_Store +yarn-error.log + +.turbo +.vercel +types \ No newline at end of file diff --git a/packages/plugin-design-essentials/esbuild/config.mjs b/packages/plugin-design-essentials/esbuild/config.mjs new file mode 100644 index 0000000..37b437c --- /dev/null +++ b/packages/plugin-design-essentials/esbuild/config.mjs @@ -0,0 +1,58 @@ +import chalk from 'chalk'; +import { readFile } from 'fs/promises'; + +// import packageJson from '../package.json' assert { type: 'json' }; +// Avoid the Experimental Feature warning when using the above. +const packageJson = JSON.parse( + await readFile(new URL('../package.json', import.meta.url)) +); + + +const dependencies = Object.keys(packageJson.dependencies) +const peerDependencies = Object.keys(packageJson.peerDependencies) +const externals = [...dependencies, ...peerDependencies] + +console.log( + chalk.yellow('Building version: '), + chalk.green(packageJson.version) +); + +const configs = [ + { + entryPoints: ['src/index.ts', "src/worker.ts"], + define: { + PLUGIN_VERSION: `"${packageJson.version}"` + }, + minify: true, + bundle: true, + sourcemap: true, + external: externals, + platform: 'node', + format: 'esm', + outdir: 'dist', + outExtension: { '.js': '.mjs' }, + plugins: [ + { + name: 'reporter', + setup(build) { + build.onEnd((result) => { + console.log( + `[${new Date().toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false + })}] Build ${ + result.errors.length + ? chalk.red('failed') + : chalk.green('succeeded') + }` + ); + }); + } + } + ] + } +]; + +export default configs; diff --git a/packages/plugin-design-essentials/esbuild/global.d.ts b/packages/plugin-design-essentials/esbuild/global.d.ts new file mode 100644 index 0000000..de80fd8 --- /dev/null +++ b/packages/plugin-design-essentials/esbuild/global.d.ts @@ -0,0 +1,3 @@ +// These constants here are added by the base esbuild config + +declare const PLUGIN_VERSION: string; diff --git a/packages/plugin-design-essentials/manifest.json b/packages/plugin-design-essentials/manifest.json new file mode 100644 index 0000000..2c550fa --- /dev/null +++ b/packages/plugin-design-essentials/manifest.json @@ -0,0 +1,83 @@ +{ + "id": "design-batteries", + "version": "1.0.0", + "publisher": "IMG.LY GmbH", + "icon": null, + "categories": [], + "contributes": { + "commands": { + "blockDelete": { + "category": "block" + }, + "blockDuplicate": { + "category": "block" + }, + "blockGroup": { + "category": "block" + }, + "blockUngroup": { + "category": "block" + }, + "exportComponentToFile": { + "category": "export" + }, + "exportJsonToClipboard": { + "category": "export" + }, + "exportJsonToFile": { + "category": "export" + }, + "exportJpegToFile": { + "category": "export" + }, + "exportPdfToFile": { + "category": "export" + }, + "exportPngToClipboard": { + "category": "export" + }, + "exportPngToFile": { + "category": "export" + }, + "exportRgba8ToFile": { + "category": "export" + }, + "exportSceneToClipboard": { + "category": "export" + }, + "exportSceneToFile": { + "category": "export" + }, + "exportWebpToFile": { + "category": "export" + }, + "imageFitModeContain": { + "category": "image" + }, + "imageFitModeCover": { + "category": "image" + }, + "imageFitModeCrop": { + "category": "image" + }, + "layoutHorizontally": { + "category": "layout" + }, + "layoutMasonry": { + "category": "layout" + }, + "layoutVertically": { + "category": "layout" + }, + "playground": { + "category": "playground" + }, + "productSetInstagram": { + "category": "product" + }, + "syncBlockAppearance": { + "category": "sync" + } + } + } +} \ No newline at end of file diff --git a/packages/vectorizer/package.json b/packages/plugin-design-essentials/package.json similarity index 64% rename from packages/vectorizer/package.json rename to packages/plugin-design-essentials/package.json index e23dc4a..c034b02 100644 --- a/packages/vectorizer/package.json +++ b/packages/plugin-design-essentials/package.json @@ -1,5 +1,5 @@ { - "name": "@imgly/plugin-vectorizer-web", + "name": "@imgly/plugin-design-essentials", "version": "0.1.0", "description": "Vectorizer plugin for the CE.SDK editor", "keywords": [ @@ -10,7 +10,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/imgly/plugin-vectorizer-web.git" + "url": "git+https://github.com/imgly/plugins.git" }, "license": "SEE LICENSE IN LICENSE.md", "author": { @@ -23,28 +23,29 @@ }, "source": "./src/index.ts", "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", + "types": "./types/index.d.ts", "exports": { ".": { "import": "./dist/index.mjs", - "types": "./dist/index.d.ts" + "types": "./types/index.d.ts" } }, - "homepage": "https://img.ly/products/creative-sdk", + "homepage": "https://img.ly", "files": [ "LICENSE.md", "README.md", "CHANGELOG.md", "dist/", + "types/", "bin/" ], "scripts": { "start": "npm run watch", - "clean": "npx rimraf dist", - "build": "npm run clean && node scripts/build.mjs && yarn run types:create", - "dev": "npm run clean && node scripts/watch.mjs", - "publish:latest": "npm run build && npm publish --tag latest --access public", - "publish:next": "npm run build && npm publish --tag next --access public", + "clean": "npx rimraf dist && npx rimraf types", + "build": "yarn run clean && yarn run types:create && node scripts/build.mjs", + "dev": "yarn run types:create && node scripts/watch.mjs", + "publish:latest": "npm run clean && npm run build && npm publish --tag latest --access public", + "publish:next": "npm run clean && npm run build && npm publish --tag next --access public", "check:all": "concurrently -n lint,type,pretty \"yarn check:lint\" \"yarn check:type\" \"yarn check:pretty\"", "check:lint": "eslint --max-warnings 0 './src/**/*.{ts,tsx}'", "check:pretty": "prettier --list-different './src/**/*.{ts,tsx}'", @@ -52,8 +53,6 @@ "types:create": "tsc --emitDeclarationOnly" }, "devDependencies": { - "@cesdk/cesdk-js": "~1.20.0", - "@types/ndarray": "^1.0.14", "chalk": "^5.3.0", "concurrently": "^8.2.2", "esbuild": "^0.19.11", @@ -61,10 +60,10 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "@cesdk/cesdk-js": "~1.20.0" + "@cesdk/cesdk-js": "~1.21.0" }, "dependencies": { - "@imgly/vectorizer": "^0.1.0-rc4", + "@imgly/plugin-core": "*", "lodash": "^4.17.21" } } diff --git a/packages/plugin-design-essentials/scripts/build.mjs b/packages/plugin-design-essentials/scripts/build.mjs new file mode 100644 index 0000000..13d12e1 --- /dev/null +++ b/packages/plugin-design-essentials/scripts/build.mjs @@ -0,0 +1,5 @@ +import * as esbuild from 'esbuild'; + +import configs from '../esbuild/config.mjs'; + +await Promise.all(configs.map(async (config) => await esbuild.build(config))); diff --git a/packages/plugin-design-essentials/scripts/watch.mjs b/packages/plugin-design-essentials/scripts/watch.mjs new file mode 100644 index 0000000..15dbb21 --- /dev/null +++ b/packages/plugin-design-essentials/scripts/watch.mjs @@ -0,0 +1,19 @@ +import chalk from 'chalk'; +import * as esbuild from 'esbuild'; + +import configs from '../esbuild/config.mjs'; + +console.log( + `[${new Date().toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false + })}] ${chalk.green('Watching...')}` +); + +const contexts = await Promise.all( + configs.map((config) => esbuild.context(config)) +); + +await Promise.any(contexts.map((ctx) => ctx.watch())); diff --git a/packages/plugin-design-essentials/src/PluginManifest.ts b/packages/plugin-design-essentials/src/PluginManifest.ts new file mode 100644 index 0000000..4eb5576 --- /dev/null +++ b/packages/plugin-design-essentials/src/PluginManifest.ts @@ -0,0 +1,10 @@ +import Manifest from '../manifest.json'; +import { CommandCallback } from '../../plugin-core/types'; + + +export type Contributions = typeof Manifest;["contributes"] +export type CommandContributions = keyof typeof Manifest["contributes"]["commands"] +export type CommandImports = Record + +export const PluginManifest = Manifest + diff --git a/packages/plugin-design-essentials/src/commands/__template.ts b/packages/plugin-design-essentials/src/commands/__template.ts new file mode 100644 index 0000000..ca26232 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/__template.ts @@ -0,0 +1,4 @@ +// // @ts-ignore +// export const __template = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + +// } \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/block.ts b/packages/plugin-design-essentials/src/commands/block.ts new file mode 100644 index 0000000..63663b4 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/block.ts @@ -0,0 +1,119 @@ +import { PluginContext } from "@imgly/plugin-core"; +import { setTransform } from "../utils/setTransform"; +import { createDefaultBlockByType } from "../utils/createDefaultBlockByType"; + + +export const blockDelete = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + ctx.engine.block.isValid(id) && ctx.engine.block.destroy(id) + }); +} + +export const blockDuplicate = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + block.isValid(id); + const newBlock = block.duplicate(id); + const parent = block.getParent(id); + if (parent && block.isValid(parent)) { + block.appendChild(parent, newBlock); + } + block.setSelected(newBlock, true); // should return the previous state + block.setSelected(id, false); + + }); +} + + +export const blockRename = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + block.isValid(id); + const name = block.getName(id) + const newName = prompt("Block name", name); + if (newName) { + block.setName(id, newName); + } + }); +} +export const blockBringForward = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + block.bringForward(id); + }); +} + +export const blockSendBackward = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + block.sendBackward(id); + }); +} + +export const blockBringToFront = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + block.bringToFront(id); + }); +} + +export const blockSendToBack = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + blockIds.forEach((id: number) => { + block.sendToBack(id); + }) +} + + +export const blockCreateGraphic = async (ctx: PluginContext, _params: { blockIds?: number[] }) => { + + const { block, scene } = ctx.engine; + // const { blockIds = block.findAllSelected() } = params; + const setTransform = (blockId: number, transform: any) => { + const { x, y, width, height } = transform + x && block.setPositionX(bId, x) + y && block.setPositionY(bId, y) + width && block.setWidth(bId, width) + height && block.setHeight(bId, height) + } + const pId = scene.getCurrentPage() ?? scene.get()! + + const bId = createDefaultBlockByType(ctx, "graphic") + const width = block.getFrameWidth(pId) / 2.0 + const height = block.getFrameHeight(pId) / 2.0 + const x = width - width / 2.0 + const y = height - height / 2.0 + setTransform(bId, { x, y, width, height }) + block.appendChild(pId, bId); + block.setSelected(bId, true) +} + + +export const blockCreateText = async (ctx: PluginContext, _params: { blockIds?: number[] }) => { + const { block, scene } = ctx.engine; + // const { blockIds = block.findAllSelected() } = params; + + const pId = scene.getCurrentPage() ?? scene.get()! + + const bId = createDefaultBlockByType(ctx, "text") + + const width = block.getFrameWidth(pId) / 2.0 + const height = block.getFrameHeight(pId) / 2.0 + const x = width - width / 2.0 + const y = height - height / 2.0 + setTransform(ctx, bId, { x, y, width, height }) + block.appendChild(pId, bId); + block.setSelected(bId, true) +} + + + + diff --git a/packages/plugin-design-essentials/src/commands/components.ts b/packages/plugin-design-essentials/src/commands/components.ts new file mode 100644 index 0000000..7b47648 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/components.ts @@ -0,0 +1,85 @@ +import { MimeType } from "@cesdk/cesdk-js"; +import { PluginContext } from "../../../plugin-core/types"; +import { downloadBlob, loadAsBlob } from "../utils/download"; + +import { inferBlockName } from "../utils/computeBlockName"; +import { exportBlockAs } from "../utils/exportBlockAs"; + +export const exportComponentToFile = async (ctx: PluginContext, params: { blockIds?: number[]; }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + const componentData = await Promise.all(blockIds.map(async (bId) => { + + const thumbnail = await exportBlockAs(ctx, { blockIds: [bId], mimeType: "image/png" as MimeType, width: 256, height: 256 }); + const cesdk = await exportBlockAs(ctx, { blockIds: [bId], mimeType: "application/x-cesdk" }); + return { + thumbnail: thumbnail[0], + cesdk: cesdk[0], + + // zip: zip[0], + }; + })); + + componentData.forEach((data, index) => { + const blockId = blockIds[index]; + const filename = inferBlockName(ctx.engine.block, blockId); + downloadBlob(data.thumbnail, `${filename}.png`); + downloadBlob(data.cesdk, `${filename}.cesdk`); + }); +}; + +export const importComponent = async (ctx: PluginContext, _params: { blockIds?: number[]; }) => { + const { engine } = ctx; + const { scene, block } = engine; + + const data = await loadAsBlob(); + const str = await data.text(); + const bIds = await ctx.engine.block.loadFromString(str); + + const pId = scene.getCurrentPage()!; + bIds.forEach((bId) => { + const name = ctx.engine.block.getName(bId) || ctx.engine.block.getUUID(bId); + const type = ctx.engine.block.getType(bId); + const isGroup = type === "//ly.img.ubq/group"; + console.log("Inserting Block", name); + console.log("Block Type", type); + if (isGroup) { // // ugly workaround for groups after loading. How does duplicate work? + const childIds = block.getChildren(bId); + block.ungroup(bId) + childIds.forEach((childId) => { + block.appendChild(pId, childId); + }) + block.group(childIds); + } else{ + block.appendChild(pId, bId); + } + }); + + + + + +}; +export const exportComponentLibrary = async (ctx: PluginContext, _params: { blockIds?: number[]; }) => { + const { block } = ctx.engine; + const libs = block.findByType("//ly.img.ubq/page"); + + libs.forEach(async (pId) => { + const pName = block.getName(pId) || block.getUUID(pId); + const bIds = block.getChildren(pId); + bIds.forEach(async (bId) => { + const bName = block.getName(bId) || block.getUUID(bId); + const thumbnail = await exportBlockAs(ctx, { blockIds: [bId], mimeType: "image/png" as MimeType, width: 256, height: 256 }); + const cesdk = await exportBlockAs(ctx, { blockIds: [bId], mimeType: "application/x-cesdk" }); + const filename = `${pName}-${bName}`; + downloadBlob(thumbnail[0], `${filename}.png`); + downloadBlob(cesdk[0], `${filename}.cesdk`); + }); + }); + + +}; + + + diff --git a/packages/plugin-design-essentials/src/commands/container.ts b/packages/plugin-design-essentials/src/commands/container.ts new file mode 100644 index 0000000..7a980e3 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/container.ts @@ -0,0 +1,32 @@ +import { PluginContext } from "../../../plugin-core/types"; + + +export const blockGroup = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { + blockIds = block.findAllSelected(), + } = params; + if (blockIds.length < 1) return; + + const group = block.group(blockIds); + blockIds.forEach((id: number) => block.isSelected(id) && block.setSelected(id, false)); + block.setSelected(group, true); +} + + +export const blockUngroup = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + + const { block } = ctx.engine; + const { + blockIds = block.findAllSelected(), + } = params; + if (blockIds.length !== 1) return; + + const blockId = blockIds[0]; + const isSelected = block.isSelected(blockId); + if (block.getType(blockId) !== '//ly.img.ubq/group') return; + const childIds = block.getChildren(blockId); + block.ungroup(blockId); // Note – ungroup should return the IDs + childIds.forEach((id: number) => block.setSelected(id, isSelected)); +} + diff --git a/packages/plugin-design-essentials/src/commands/debug.ts b/packages/plugin-design-essentials/src/commands/debug.ts new file mode 100644 index 0000000..68143fc --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/debug.ts @@ -0,0 +1,161 @@ +import { PluginContext } from "@imgly/plugin-core"; +import { readBlockProperty } from "../utils/cesdk"; + + +export const debugLogBlockProperties = async (ctx: PluginContext, params: { blockIds: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((bId: number) => { + const props = block.findAllProperties(bId) + const propDefinition = new Map() + props.forEach((propKey: string) => { + if (!block.isPropertyReadable(propKey)) return; + const propType = block.getPropertyType(propKey) + const propValue = readBlockProperty(block, bId, propKey, propType) + propDefinition.set(propKey, propValue) + }) + console.debug("Properties for block", bId, JSON.stringify(Object.fromEntries(propDefinition.entries()), null, 2)) + }) +} + + + +export const debugLogBlockCrop = (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((id: number) => { + const x = block.getCropTranslationX(id) + const y = block.getCropTranslationY(id) + const scaleX = block.getCropScaleX(id) + const scaleY = block.getCropScaleY(id) + const fillMode = block.getContentFillMode(id) + const crop = { + x, y, scaleX, scaleY, fillMode + } + console.debug("Crop for block", id, JSON.stringify(crop, null, 2)) + }); + +} + +export const debugLogBlockMetadata = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + + blockIds.forEach((id: number) => { + const keys = block.findAllMetadata(id) + if (keys.length === 0) { + console.debug("No metadata for block", id) + return + } + const map = new Map() + keys.forEach((key: string) => { + const metadata = block.getMetadata(id, key) + const obj = JSON.parse(metadata) + map.set(key, obj) + }) + console.debug("Metadata for block", id, JSON.stringify(Object.fromEntries(map.entries()), null, 2)) + }) + +} + +export const debugLogBlockFill = (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((bId: number) => { + const fId = block.getFill(bId) + const fillType = block.getType(fId) + + const props = block.findAllProperties(fId); + + const propDefinition = new Map() + props.forEach((propKey: string) => { + + if (!block.isPropertyReadable(propKey)) return; + const propValue = readBlockProperty(block, fId, propKey) + propDefinition.set(propKey, propValue) + }) + console.debug("Fill for block", bId, JSON.stringify(Object.fromEntries(propDefinition.entries()), null, 2)) + }); +} + +export const debugLogBlockEffects = (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((bId: number) => { + const eIds = block.getEffects(bId) + + eIds.forEach((eId: number) => { + const props = block.findAllProperties(eId); + + const propDefinition = new Map() + props.forEach((propKey: string) => { + + if (!block.isPropertyReadable(propKey)) return; + const propValue = readBlockProperty(block, eId, propKey) + propDefinition.set(propKey, propValue) + }) + console.debug(`Effects ${eId} for block`, bId, JSON.stringify(Object.fromEntries(propDefinition.entries()), null, 2)) + }) + + + }); +} + + + +export const debugLogBlockBlur = (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((bId: number) => { + if (!block.hasBlur(bId)) { + console.log("No blur for block", bId) + return + }; + const eId = block.getBlur(bId) + if (!block.isValid(eId)) { // that is an error source + console.log("No valid blur for block", bId) + return + }; + + const props = block.findAllProperties(eId); + + const propDefinition = new Map() + props.forEach((propKey: string) => { + + if (!block.isPropertyReadable(propKey)) return; + const propValue = readBlockProperty(block, eId, propKey) + propDefinition.set(propKey, propValue) + + + }) + console.debug(`Blur for block`, bId, JSON.stringify(Object.fromEntries(propDefinition.entries()), null, 2)) + + }); +} + +export const debugLogSceneHierarchy = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block, scene } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + const sId = scene.get()! + + const blockInfo = (bId: number) => { + const name = block.getName(bId) || block.getUUID(bId).toString() + const type = block.getType(bId) + const cIds = block.getChildren(bId) + const children = cIds.map(blockInfo) + const hierarchy = {name, type, id: bId, children} + return hierarchy + } + + const hierarchy = blockInfo(sId); + console.debug("Scene Hierarchy", JSON.stringify(hierarchy, null, 2)) + + + +} diff --git a/packages/plugin-design-essentials/src/commands/export.ts b/packages/plugin-design-essentials/src/commands/export.ts new file mode 100644 index 0000000..fe0abd8 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/export.ts @@ -0,0 +1,74 @@ +import { PluginContext } from "../../../plugin-core/types"; +import { MimeType } from "@cesdk/cesdk-js"; +import { downloadBlob } from "../utils/download"; +import { exportBlockAs } from "../utils/exportBlockAs"; + + + + +export const exportPngToClipboard = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "image/png" as MimeType })).map((blob) => { + return new ClipboardItem({ + ["image/png"]: blob, + }, { presentationStyle: "attachment" }); + }) + await navigator.clipboard.write(items); +}; + +export const exportSceneToClipboard = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "application/x-cesdk" })).map((blob) => { + return new ClipboardItem({ + ["text/plain"]: blob, + }, { presentationStyle: "attachment" }); + }) + await navigator.clipboard.write(items); +}; + +export const exportJsonToClipboard = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "application/json" })).map((blob) => { + return new ClipboardItem({ + ["text/plain"]: blob, + }, { presentationStyle: "attachment" }); + }) + await navigator.clipboard.write(items); +}; + +export const exportPngToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "image/png" as MimeType })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.png`) }); +} + +export const exportJpegToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "image/jpeg" as MimeType })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.jpeg`) }); +} + +export const exportWebpToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "image/webp" as MimeType })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.webp`) }); +} + +export const exportPdfToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "application/pdf" as MimeType })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.pdf`) }); +} + + +export const exportRgba8ToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "application/octet-stream" as MimeType })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.rgba8`) }); +} + + +export const exportSceneToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "application/x-cesdk" })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.cesdk`) }); +} + + +export const exportJsonToFile = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const items = (await exportBlockAs(ctx, { ...params, mimeType: "application/json" })) + items.forEach((blob, index) => { downloadBlob(blob, `block-${index}.json`) }); +} + + diff --git a/packages/plugin-design-essentials/src/commands/i18n.ts b/packages/plugin-design-essentials/src/commands/i18n.ts new file mode 100644 index 0000000..86d43f2 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/i18n.ts @@ -0,0 +1,21 @@ + +import { downloadBlob } from "../utils/download"; +import { PluginContext } from "../../../plugin-core/types"; // Add this import statement + +export const i18nDownloadMissingCommandTranslations = (ctx: PluginContext) => { + const { i18n, commands } = ctx; + + const missingCommandTranslations: [string, string][] = commands + .listCommands() + .map((cmd: string): [string, string] | null => { + return i18n.hasTranslation(cmd) ? null : [cmd, cmd.split(".").pop()]; + }) + .filter(Boolean) as [string, string][]; + + if (missingCommandTranslations.length > 0) { + const ob = Object.fromEntries(missingCommandTranslations); + const json = JSON.stringify(ob, null, 2); + const blob = new Blob([json], { type: "application/json" }); + downloadBlob(blob, `${i18n.locale()}.json`); + } +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/image.ts b/packages/plugin-design-essentials/src/commands/image.ts new file mode 100644 index 0000000..bb89625 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/image.ts @@ -0,0 +1,16 @@ +import { PluginContext } from "@imgly/plugin-core"; +import { ContentFillMode } from "@cesdk/cesdk-js" + + +const imageFitWithMode = (ctx: PluginContext, params: { blockIds?: number[], fillMode: ContentFillMode }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected(), fillMode } = params; + blockIds.forEach((id: number) => { + if (!block.hasContentFillMode(id)) return; + block.setContentFillMode(id, fillMode) + }) +} + +export const imageFitModeCrop = async (ctx: PluginContext, params: { blockIds?: number[] }) => imageFitWithMode(ctx, { ...params, fillMode: 'Crop' }) +export const imageFitModeCover = async (ctx: PluginContext, params: { blockIds?: number[] }) => imageFitWithMode(ctx, { ...params, fillMode: 'Cover' }) +export const imageFitModeContain = async (ctx: PluginContext, params: { blockIds?: number[] }) => imageFitWithMode(ctx, { ...params, fillMode: 'Contain' }) diff --git a/packages/plugin-design-essentials/src/commands/import.ts b/packages/plugin-design-essentials/src/commands/import.ts new file mode 100644 index 0000000..67a14bd --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/import.ts @@ -0,0 +1,2 @@ +// .txt, .cesdk, .png, .jpg, .jpeg, .webp, .pdf, +// .csv ... generate a new page from each row and replace the content of the blocks with the values from the row \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/index.ts b/packages/plugin-design-essentials/src/commands/index.ts new file mode 100644 index 0000000..e2b8d4d --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/index.ts @@ -0,0 +1,18 @@ +export * from "./block"; +export * from "./export"; +export * from "./image"; +export * from "./container"; +export * from "./layout"; + +export * from "./debug"; +export * from "./plugins"; +export * from "./components"; +export * from "./playground"; +export * from "./i18n"; +export * from "./panels"; +export * from "./turnInto"; + +export * from "./replicate.io"; + +// for convenience I splitted the files into smaller files +export * from "./shape"; \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/layout.ts b/packages/plugin-design-essentials/src/commands/layout.ts new file mode 100644 index 0000000..a548320 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/layout.ts @@ -0,0 +1,104 @@ +import { PluginContext } from "../../../plugin-core/types"; +import { toSafeInteger } from "lodash"; +import { computeMultiSelectionBounds } from "../utils/computeMultiSelectionBounds"; + +export const layoutHorizontally = async (ctx: PluginContext, params: { blockIds?: number[]; padding?: number; }) => { + const { block } = ctx.engine; + const { + blockIds = block.findAllSelected(), padding = 0 + } = params; + + const isGroup = (blockIds.length === 1 && block.getType(blockIds[0]) !== '//ly.img.ubq/group'); + const isMultiSelection = blockIds.length > 1; + + if (!isGroup && !isMultiSelection) { + return; + }; + + const children = isGroup ? block.getChildren(blockIds[0]) : blockIds; + if (children.length === 0) return; + + + let curXPos = block.getPositionX(children[0]); + let curYPos = block.getPositionY(children[0]); + children.forEach((childId: number) => { + block.setPositionY(childId, curYPos); + block.setPositionX(childId, curXPos); + const width = block.getFrameWidth(childId); + curXPos += width; + curXPos += padding; + }); +}; + +export const layoutVertically = async (ctx: PluginContext, params: { blockIds?: number[]; padding?: number; }) => { + const { block } = ctx.engine; + const { + blockIds = block.findAllSelected(), padding = 0 + } = params; + const isGroup = (blockIds.length === 1 && block.getType(blockIds[0]) !== '//ly.img.ubq/group'); + const isMultiSelection = blockIds.length > 1; + + if (!isGroup && !isMultiSelection) { + return; + }; + + const children = isGroup ? block.getChildren(blockIds[0]) : blockIds; + if (children.length === 0) return; + + let curXPos = block.getPositionX(children[0]); + let curYPos = block.getPositionY(children[0]); + children.forEach((childId: number) => { + block.setPositionX(childId, curXPos); + block.setPositionY(childId, curYPos); + const height = block.getFrameHeight(childId); + curYPos += height; + curYPos += padding; + }); +}; + + +export const layoutMasonry = async (ctx: PluginContext, params: { blockIds?: number[]; cols?: number; paddingX?: number; paddingY?: number; }) => { + const { block } = ctx.engine; + let { + blockIds = block.findAllSelected(), paddingX = 16, paddingY = 16, cols = 2 + } = params; + + + cols = toSafeInteger(prompt("Enter the number of columns", "2")); + const isGroup = (blockIds.length === 1 && block.getType(blockIds[0]) !== '//ly.img.ubq/group'); + const isMultiSelection = blockIds.length > 1; + + if (!isGroup && !isMultiSelection) { + return; + }; + + const children = isGroup ? block.getChildren(blockIds[0]) : blockIds; + const groupWidth = isGroup ? block.getFrameWidth(blockIds[0]) : computeMultiSelectionBounds(ctx, blockIds).width; + const childWidth = groupWidth / cols - paddingX; + + console.log(children); + let rowHeights: Array = []; + for (let i = 0; i < cols; i++) { + rowHeights.push(0); + } + + let curXPos = block.getPositionX(children[0]); + let curYPos = block.getPositionY(children[0]); + children.forEach((childId: number) => { + const w = block.getFrameWidth(childId); + const h = block.getFrameHeight(childId); + const aspect = h / w; + const newWidth = childWidth; + const newHeight = aspect * newWidth; + block.setWidth(childId, newWidth); + block.setHeight(childId, newHeight); + // get column with the "lowest" height + const minIndex = rowHeights.indexOf(Math.min(...rowHeights)); + console.log(minIndex, rowHeights[minIndex]); + const xPos = curXPos + minIndex * (childWidth + paddingX); + const yPos = curYPos + rowHeights[minIndex]; + rowHeights[minIndex] += newHeight + paddingY; + block.setPositionX(childId, xPos); + block.setPositionY(childId, yPos); + }); +}; diff --git a/packages/plugin-design-essentials/src/commands/old_debug.ts b/packages/plugin-design-essentials/src/commands/old_debug.ts new file mode 100644 index 0000000..5b714ea --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/old_debug.ts @@ -0,0 +1,203 @@ +// import CreativeEditorSDK from "@cesdk/cesdk-js"; +// import { type CommandsType } from "@imgly/plugin-commands-polyfill"; +// import { readPropValue } from "../utils/cesdk"; + + + +// export const registerDebugCommands = (cesdk: CreativeEditorSDK & CommandsType) => { +// const { asset, block, variable, editor, scene } = cesdk.engine +// const commands = cesdk.engine.commands!; + +// commands.registerCommand("imgly.debug.log.metadata", async (params: { blockIds?: number[] }) => { +// const blockIds = params.blockIds ?? block.findAllSelected() + +// blockIds.forEach((id: number) => { +// const keys = block.findAllMetadata(id) +// if (keys.length === 0) { +// console.debug("No metadata found for block", id) +// return +// } +// keys.forEach((key: string) => { +// const metadata = block.getMetadata(id, key) +// const obj = JSON.parse(metadata) +// console.debug(key, obj) +// }) +// }) +// }) + +// commands.registerCommand("imgly.debug.clear.metadata", async (params: { blockIds?: number[] }) => { +// const blockIds = params.blockIds ?? block.findAllSelected() +// blockIds.forEach((id: number) => { +// block.findAllMetadata(id) +// .forEach((key: string) => { +// block.setMetadata(id, key, "") +// }) +// }) +// }) + +// commands.registerCommand("imgly.debug.log.block", async (params: { blockIds?: number[] }) => { +// const blockIds = params.blockIds ?? block.findAllSelected() + +// blockIds.forEach((id: number) => { +// const props = block.findAllProperties(id) +// const propDefinition = new Map() +// props.forEach((propKey: string) => { +// if (!block.isPropertyReadable(propKey)) return; +// const propType = block.getPropertyType(propKey) +// const propValue = readPropValue(cesdk, id, propKey, propType) +// propDefinition.set(propKey, { type: propType, value: propValue }) + +// }) + +// console.debug("Properties for block", id, propDefinition) +// }) +// }) + + + +// commands.registerCommand("imgly.debug.log.fill", async (params: { blockIds?: number[] }) => { +// const blockIds = params.blockIds ?? block.findAllSelected() +// blockIds.forEach((bId: number) => { +// const fId = block.getFill(bId) +// if (!block.isValid(fId)) { +// console.debug("No fill found for block", bId) +// return +// }; + +// const props = block.findAllProperties(fId) +// const propDefinition = new Map() +// props.forEach((propKey: string) => { +// console.debug("Reading propKey", propKey) +// if (!block.isPropertyReadable(propKey)) return; + +// const propType = block.getPropertyType(propKey) +// const propValue = readPropValue(cesdk, fId, propKey, propType) +// propDefinition.set(propKey, { type: propType, value: propValue }) + +// }) + +// console.debug("Fill properties for block", bId, propDefinition) +// }) +// }) + + + +// commands.registerCommand("imgly.debug.log.assets", async (_params: { blockIds?: number[] }) => { +// // const blockIds = params.blockIds ?? block.findAllSelected() + +// const entries = asset.findAllSources() +// const definition = new Map() +// entries.forEach((key: string) => { +// const types = asset.getSupportedMimeTypes(key) +// const groups = variable.getString(key) +// definition.set(key, { types, groups }) +// }) +// console.debug("Assets", definition) +// }) + + +// commands.registerCommand("imgly.debug.log.variables", async (_params: { blockIds?: number[] }) => { +// const vars = variable.findAll() +// const definition = new Map() +// vars.forEach((key: string) => { +// const value = variable.getString(key) + +// definition.set(key, { type: "String", value: value }) +// }) +// console.debug("Variables", definition) + +// }) + + +// commands.registerCommand("imgly.debug.log.editor.settings", async (_params: { blockIds?: number[] }) => { +// const entries = editor.findAllSettings() +// const definition = new Map() +// entries.forEach((key: string) => { +// const type = editor.getSettingType(key) +// const value = undefined; //editor.getSettingValue(key) +// definition.set(key, { type, value }) +// }) +// console.debug("Settings", definition) + +// }) + + + +// commands.registerCommand("imgly.debug.log.scene", async (_params: { blockIds?: number[] }) => { +// console.debug("Settings", { +// designUnit: scene.getDesignUnit(), +// mode: scene.getMode(), +// }) + +// }) + +// commands.registerCommand("imgly.debug.log.scopes", async (params: { blockIds?: number[] }) => { +// // https://img.ly/docs/cesdk/engine/guides/scopes/ +// const scopeNames = [ +// "layer/move", +// "layer/resize", +// "layer/rotate", +// "layer/crop", +// "layer/clipping", +// "layer/opacity", +// "layer/blendMode", +// "layer/visibility", +// "appearance/adjustments", +// "appearance/filter", +// "appearance/effect", +// "appearance/blur", +// "appearance/shadow", +// "lifecycle/destroy", // delete +// "lifecycle/duplicate", +// "editor/add", +// "editor/select", +// "fill/change", +// "stroke/change", +// "shape/change", +// "text/edit", +// // "text/change", // would be replace from library +// "text/character" +// ] +// const definition = new Map() +// scopeNames.forEach((scope: string) => { +// const value = editor.getGlobalScope(scope) +// definition.set(scope,{value}) +// }) +// console.debug("GlobalScopes", definition) + +// const blockIds = params.blockIds ?? block.findAllSelected() +// blockIds.forEach((id: number) => { +// const definition = new Map() +// scopeNames.forEach((scope: string) => { +// const value = block.isAllowedByScope(id, scope) +// definition.set(scope,{value}) +// }) +// console.debug("Scopes for block", id, definition) +// }) +// }) + + + + +// commands.registerCommand("imgly.debug.log.effects", async (params: { blockIds?: number[] }) => { +// const blockIds = params.blockIds ?? block.findAllSelected() +// blockIds.forEach((id: number) => { +// block.getEffects(id).forEach((eId: number) => { +// const props = block.findAllProperties(eId); +// let propDefinition = new Map() +// props.forEach((propKey: string) => { +// if (!block.isPropertyReadable(propKey)) return; +// const propType = block.getPropertyType(propKey) +// const propValue = readPropValue(cesdk, eId, propKey, propType) +// propDefinition.set(propKey, { type: propType, value: propValue }) + +// }) + +// console.debug("Effect for block", id, propDefinition) +// }) + +// }) + +// }) + +// } \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/panels.ts b/packages/plugin-design-essentials/src/commands/panels.ts new file mode 100644 index 0000000..3da6b3e --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/panels.ts @@ -0,0 +1,61 @@ +import { PluginContext } from '@imgly/plugin-core' + +const panelIds = [ + '//ly.img.panel/settings', + + '//ly.img.panel/inspector', + '//ly.img.panel/inspector/placeholderSettings', + + '//ly.img.panel/assetLibrary', + '//ly.img.panel/assetLibrary.replace', + + '//ly.img.panel/inspector/image/adjustments', + '//ly.img.panel/inspector/image/filters', + '//ly.img.panel/inspector/image/effects', + '//ly.img.panel/inspector/image/blurs', + + '//ly.img.panel/inspector/crop', + '//ly.img.panel/inspector/adjustments', + '//ly.img.panel/inspector/filters', + '//ly.img.panel/inspector/effects', + '//ly.img.panel/inspector/blur', + '//ly.img.panel/inspector/trim', + '//ly.img.panel/inspector/fill', + '//ly.img.panel/inspector/fill/color', + '//ly.img.panel/inspector/text/properties', + '//ly.img.panel/inspector/stroke/properties', + '//ly.img.panel/inspector/stroke/color', + '//ly.img.panel/inspector/shadow', + '//ly.img.panel/inspector/shadow/color', + '//ly.img.panel/inspector/transform', + '//ly.img.panel/inspector/editColor', + '//ly.img.panel/inspector/colorLibrary', + ]; + +export const panelSettingsOpen = async (ctx: PluginContext) => { + const { ui } = ctx; + ui?.openPanel("//ly.img.panel/settings") +} + +export const panelSettingsClose = async (ctx: PluginContext) => { + const { ui } = ctx; + ui?.closePanel("//ly.img.panel/settings") +} + + + +// export const panelInspectorOpen = async (ctx: PluginContext) => { +// const { ui } = ctx; +// ui?.openPanel("//ly.img.panel/inspector") +// } + +// export const panelInspectorClose = async (ctx: PluginContext) => { +// const { ui } = ctx; +// ui?.closePanel("//ly.img.panel/inspector") +// } + + +// export const panelAssetLibraryOpen = async (ctx: PluginContext) => { +// const { ui } = ctx; +// ui?.openPanel("//ly.img.panel/asset-library") +// } diff --git a/packages/plugin-design-essentials/src/commands/playground.ts b/packages/plugin-design-essentials/src/commands/playground.ts new file mode 100644 index 0000000..79bf1e7 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/playground.ts @@ -0,0 +1,339 @@ +// EXPORT AS LIBRARY +// SYNC BY NAME - two pages, same name + +import { PluginContext } from "../../../plugin-core/types"; +import { readBlockProperty, setBlockProperty } from "../utils/cesdk"; +import { CreativeEngine } from "@cesdk/cesdk-js"; + +const propKeys = [ + 'alwaysOnBottom', + 'alwaysOnTop', + 'blend/mode', 'blur/enabled', 'clipped', + // 'contentFill/mode', + // 'transformLocked', + // 'crop/rotation', 'crop/scaleRatio', 'crop/scaleX', 'crop/scaleY', 'crop/translationX', 'crop/translationY', + // 'position/x', 'position/x/mode', 'position/y', 'position/y/mode', 'rotation', + 'dropShadow/blurRadius/x', 'dropShadow/blurRadius/y', 'dropShadow/clip', 'dropShadow/color', 'dropShadow/enabled', 'dropShadow/offset/x', 'dropShadow/offset/y', + 'fill/enabled', + 'opacity', + 'placeholder/enabled', + 'playback/duration', 'playback/timeOffset', + 'stroke/color', 'stroke/cornerGeometry', 'stroke/enabled', 'stroke/position', 'stroke/style', 'stroke/width', + 'visible'] + + +const syncBlockProperties = (ctx: PluginContext, sourceId: number, destIds: number[], whiteList?: string[], blackList?: string[]) => { + const { block } = ctx.engine; + if (!block.isValid(sourceId)) return + + const propertyKeys = block.findAllProperties(sourceId) + .filter((key: string) => block.isPropertyReadable(key)) + .filter((key: string) => { + if (whiteList && !whiteList.includes(key)) return false + if (blackList && blackList.includes(key)) return false + return true + }) + + propertyKeys.forEach((propertyKey: string) => { + const sourceValue = readBlockProperty(block, sourceId, propertyKey) + destIds.forEach((receiverBlockId: number) => { + if (!block.isValid(receiverBlockId)) return + if (sourceId === receiverBlockId) return; + const receiverValue = readBlockProperty(block, receiverBlockId, propertyKey) + if (receiverValue === sourceValue) return; + setBlockProperty(block, receiverBlockId, propertyKey, sourceValue) + }) + }) +} + + + +// name syntax = "label=appearance(other), rotation(other)" +export const syncBlockAppearance = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block, event } = ctx.engine; + let { blockIds = block.findAllSelected() } = params + console.log("syncBlocks", block.findAllProperties(blockIds[0])) + const propWhiteList: string[] = propKeys + const propBlackList: string[] = [] + // better would be to add a meta data + // const sync = { + // uuid: // appearance: + + // } + + const unsubscribe = event.subscribe(blockIds, (events) => { + events.forEach((event) => { + const bId = event.block; + switch (event.type) { + case 'Created': { + // throw new Error("Not implemented") + break; + } + case 'Updated': { + syncBlockProperties(ctx, bId, blockIds, propWhiteList, propBlackList) + break; + } + case "Destroyed": { + if (blockIds.includes(bId)) { + blockIds.splice(blockIds.indexOf(bId), 1) + } + if (blockIds.length === 1) { + unsubscribe() + } + break; + } + } + }) + }) +} + + + +const products = { + "instagram_story": { + kind: "story", + resolution: { + width: "1080px", height: "1920px" + }, + }, +} + +export const myNewFunctionForTheEditor = async (ctx: PluginContext, _params: { blockIds?: number[] }) => { + const { block, scene } = ctx.engine; + + const pId = scene.getCurrentPage()! + + const bId = block.create("//ly.img.ubq/text") + block.setName(bId, "Hello World") + block.setTextColor(bId, { r: 1, g: 0, b: 0, a: 1 }) + block.replaceText(bId, "Hello World") + + + block.appendChild(pId, bId) + +} + + + + + + +export const productSetInstagram = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + const resolution = products.instagram_story.resolution; + const width = parseValueWithUnit(resolution.width) + const height = parseValueWithUnit(resolution.height) + if (!width || !height) return + + blockIds.forEach((id: number) => { + const widthInDu = unitToDesignUnit(width.value, width.unit, ctx) + const heightInDu = unitToDesignUnit(height.value, height.unit, ctx) + if (!widthInDu || !heightInDu) return + block.setHeight(id, heightInDu) + // block.setHeightMode(id, unitToMode(height.unit)) + block.setWidth(id, widthInDu) + // block.setWidthMode(id, unitToMode(width.unit)) + }) +} + +export const playground = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const { block, event } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + // check distance to parent edges + // type _Distance = { + // top: number; + // left: number; + // centerX: number; + // centerY: number; + // bottom: number; + // right: number; + // } + type Anchors = { + top: boolean; + left: boolean; + centerX: boolean; + centerY: boolean; + bottom: boolean; + right: boolean; + } + + const calcDistance = (fromId: number, toId: number) => { + const bId = fromId + const pId = toId + const bX = block.getGlobalBoundingBoxX(bId) + const bY = block.getGlobalBoundingBoxY(bId) + + const bWidth = block.getGlobalBoundingBoxWidth(bId) + const bheight = block.getGlobalBoundingBoxHeight(bId) + const bCenterX = bX + bWidth / 2 + const bCenterY = bY + bheight / 2 + + const pX = block.getGlobalBoundingBoxX(pId) + const pY = block.getGlobalBoundingBoxY(pId) + const pWidth = block.getGlobalBoundingBoxWidth(pId) + const pheight = block.getGlobalBoundingBoxHeight(pId) + const pCenterX = pX + pWidth / 2 + const pCenterY = pY + pheight / 2 + + const left = pX - bX + const top = pY - bY + const right = pX + pWidth - bX - bWidth + const bottom = pY + pheight - bY - bheight + const centerX = bCenterX - pCenterX + const centerY = bCenterY - pCenterY + + return { left, top, centerX, centerY, right, bottom } + } + + // const distances = blockIds.map((bId: number): Distance => { + // return calcDistance(bId, block.getParent(bId)); + // }) + + + event.subscribe(blockIds, (events) => { + events.forEach((event) => { + switch (event.type) { + case "Created": break; + case "Updated": { + const pId = block.getParent(event.block)! + const dist = calcDistance(event.block, pId) + const height = block.getGlobalBoundingBoxHeight(pId) + const width = block.getGlobalBoundingBoxWidth(pId) + console.log(width, height, dist) + const distInPercent = { + top: (dist.top / height) * 100, + left: (dist.left / width) * 100, + right: (dist.right / width) * 100, + bottom: (dist.bottom / height) * 100, + centerX: (dist.centerX / width) * 100, + centerY: (dist.centerY / height) * 100, + }; + console.log(JSON.stringify(distInPercent, null, 2)); + + const threshold = 1 // percent + const anchors: Anchors = { + top: Math.abs(distInPercent.top) < threshold, + left: Math.abs(distInPercent.left) < threshold, + right: Math.abs(distInPercent.right) < threshold, + bottom: Math.abs(distInPercent.bottom) < threshold, + centerX: Math.abs(distInPercent.centerX) < threshold, + centerY: Math.abs(distInPercent.centerY) < threshold, + } + console.log(JSON.stringify(anchors, null, 2)); + break; + } + case "Destroyed": break; + } + }) + + }) + + // snap to edge with offset + + +} + + + + + + + + + + + + +// Utils + + +type ValueWithUnit = { + value: number; + unit: string; +}; + +function parseValueWithUnit(string: string | number): ValueWithUnit | null { + const isNumber = typeof string === "number" + if (isNumber) { + return { + value: string, + unit: "" + } + } + const regex = /^(\d+(?:\.\d+)?)([a-z%]*)$/i; + const match = string.match(regex); + + if (match) { + return { + value: parseFloat(match[1]), + unit: match[2], + }; + } + return null; +} + +// function unitToMode(unit: string) { +// switch (unit) { +// case "%": return "Percent" +// case "px": +// case "mm": +// case "in": +// default: return "Absolute" +// } +// } + + +function unitToDesignUnit(value: number, unit: string, ctx: PluginContext) { + switch (unit) { + case "%": return value + case "px": return pixelToDesignUnit(ctx.engine, value) + case "mm": return mmToDesignUnit(ctx.engine, value) + case "in": return inToDesignUnit(ctx.engine, value) + } +} + + + + + +const inToDesignUnit = (engine: CreativeEngine, inch: number) => { + const sceneId = engine.scene.get()! + const sceneUnit = engine.block.getEnum(sceneId, 'scene/designUnit'); + const dpi = engine.block.getFloat(sceneId, 'scene/dpi') + + switch (sceneUnit) { + case "Millimeter": return inch * 2.54; + case "Inch": return inch; + case "Pixel": return inch * dpi; + } + +}; + + +const mmToDesignUnit = (engine: CreativeEngine, mm: number) => { + const sceneId = engine.scene.get()! + const sceneUnit = engine.block.getEnum(sceneId, 'scene/designUnit'); + const dpi = engine.block.getFloat(sceneId, 'scene/dpi') + + switch (sceneUnit) { + case "Millimeter": return mm; + case "Inch": return mm / 25.4; + case "Pixel": return mm / 25.4 * dpi; + } + +}; + + +const pixelToDesignUnit = (engine: CreativeEngine, pixel: number) => { + const sceneId = engine.scene.get()! + const sceneUnit = engine.block.getEnum(sceneId, 'scene/designUnit'); + const dpi = engine.block.getFloat(sceneId, 'scene/dpi') + + switch (sceneUnit) { + case "Millimeter": return pixel * 25.4 / dpi; + case "Inch": return pixel / dpi; + case "Pixel": return pixel; + } +}; diff --git a/packages/plugin-design-essentials/src/commands/plugins.ts b/packages/plugin-design-essentials/src/commands/plugins.ts new file mode 100644 index 0000000..4207616 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/plugins.ts @@ -0,0 +1,27 @@ +import { PluginContext } from "../../../plugin-core/types"; + +export const pluginRegisterCustomPanel = async (ctx: PluginContext, _params: { blockIds?: number[] }) => { + const { ui } = ctx; + ui?.unstable_registerCustomPanel('ly.img.foo', (domElement) => { + domElement.appendChild(document.createTextNode('Hello World')); + return () => { + console.log('Apps disposer called'); + }; + }); + + ui?.openPanel('ly.img.foo'); +} + + + +export const pluginOpenCustomPanel = async (ctx: PluginContext, _params: { blockIds?: number[] }) => { + const { ui } = ctx; + ui?.openPanel('ly.img.foo'); +} + + + +export const command = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + + +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/replicate.io.ts b/packages/plugin-design-essentials/src/commands/replicate.io.ts new file mode 100644 index 0000000..1c0f482 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/replicate.io.ts @@ -0,0 +1,159 @@ +import { PluginContext } from "@imgly/plugin-core"; + +// TODOS +// https://replicate.com/stability-ai/stable-diffusion-img2img?prediction=63trbdrbookprhnq3eoap6iwz4 +// https://replicate.com/pharmapsychotic/clip-interrogator +// https://replicate.com/batouresearch/high-resolution-controlnet-tile/examples#jq5gj2dbjg6j6l3ih7hf3jfuoa + +// https://replicate.com/mistralai/mixtral-8x7b-instruct-v0.1 +const REPLICATE_API_TOKEN = "r8_Y7Qt7U8vkF8QBVDJ9RvWTQNuebwVLBp2qvvBT" + + +const REPLICATE_HEADERS = { + "Content-Type": 'application/json', + "Authorization": `Token ${REPLICATE_API_TOKEN}` +} + +const proxyForCors = (url: string) => 'https://corsproxy.io/?' + encodeURIComponent(url); + +const MODELS = { + "face2sticker": { + version: "764d4827ea159608a07cdde8ddf1c6000019627515eb02b6b449695fd547e5ef", + input: (iInput) => ({ + "steps": 20, + "width": 1024, + "height": 1024, + "upscale": false, + "upscale_steps": 10, + "negative_prompt": "", + "prompt_strength": 4.5, + "ip_adapter_noise": 0.5, + "ip_adapter_weight": 0.2, + "instant_id_strength": 0.7, + ...iInput + }) + }, + "sdxl": { + version: "39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", + input: (iInput) => ({ + "width": 1024, + "height": 1024, + "refine": "expert_ensemble_refiner", + "scheduler": "K_EULER", + "num_outputs": 1, + "lora_scale": 0.6, + "guidance_scale": 7.5, + "apply_watermark": false, + "high_noise_frac": 0.8, + "negative_prompt": "", + "prompt_strength": 0.8, + "num_inference_steps": 25, + ...iInput + }) + }, + "playgroundai/playground-v2-1024px-aesthetic": { + version: "42fe626e41cc811eaf02c94b892774839268ce1994ea778eba97103fe1ef51b8", + input: (iInput) => ({ + width: 1024, + height: 1024, + scheduler: "K_EULER_ANCESTRAL", + guidance_scale: 3, + apply_watermark: false, + negative_prompt: "", + num_inference_steps: 50, + ...iInput + }) + + } +} + +export const replicateSDXL = async (ctx: PluginContext, params: { blockIds?: number[]; }) => { + const { block } = ctx.engine; + let { + blockIds = block.findAllSelected() + } = params; + const isGroup = (blockIds.length === 1 && block.getType(blockIds[0]) === '//ly.img.ubq/group'); + blockIds = isGroup ? block.getChildren(blockIds[0]) : blockIds; + + const iIds = blockIds.filter((id: number) => { + if (!block.hasFill(id)) return false + const fId = block.getFill(id) + if (!block.isValid(fId)) return false + const fType = block.getType(fId) + return fType === "//ly.img.ubq/fill/image" + }) + + const fIds = iIds.map((id: number) => [id, block.getFill(id)]) + fIds.forEach(async ([bId, fId]) => { + // fake busy + //enssure we have preview + const imageFillUri = block.getString(fId, 'fill/image/imageFileURI'); + const name = block.getName(bId) + block.setString(fId, 'fill/image/previewFileURI', block.getString(fId, 'fill/image/previewFileURI') ?? block.getString(fId, 'fill/image/imageFileURI')); + block.setString(fId, 'fill/image/imageFileURI', ''); + block.setSourceSet(fId, 'fill/image/sourceSet', []); + + + const iPrompt = (name.length === 0) ? prompt("Enter a prompt", name) : name + const iImage = imageFillUri + const iMask = undefined + + + if (!name) block.setName(bId, iPrompt) + // const model = "playgroundai/playground-v2-1024px-aesthetic" + const model = "sdxl" + // const model = "face2sticker" + const replicateIoVersion = MODELS[model].version + const replicateIoinput = MODELS[model].input({ prompt: iPrompt, mask: iMask, image: iImage }) + const images = await callReplicateIo(replicateIoVersion, replicateIoinput) + + const image = images[0] // for now we assume one image but we could gather multiple in batch + block.setString(fId, 'fill/image/imageFileURI', image); + block.setSourceSet(fId, 'fill/image/sourceSet', []); + }) + +} + + + +const callReplicateIo = async (version: string, input: any) => { + const data = { + version, + input + } + + const url = proxyForCors("https://api.replicate.com/v1/predictions") + const res = await fetch(url, { + method: 'POST', + // mode: "no-cors", + headers: REPLICATE_HEADERS, + body: JSON.stringify(data) + }) + + + const json = await res.json() + return await waitForReplicate(json) +} + +const waitForReplicate = async (json: any): Promise => { + return new Promise((resolve, reject) => { + const interval = 1000 + const timeout = setInterval(async () => { + // console.log("Checking status") + const url = proxyForCors(json.urls.get) + const statusRes = await fetch(url, { headers: REPLICATE_HEADERS }) + const statusJson = await statusRes.json() + console.log(statusJson) + if (statusJson.error) { + clearInterval(timeout) + reject(statusJson.error) + } + if (statusJson.status === "succeeded") { + clearInterval(timeout) + const { output } = statusJson + resolve(output?.map((o: string) => proxyForCors(o))) + + } + }, interval) + }) +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/shape.ts b/packages/plugin-design-essentials/src/commands/shape.ts new file mode 100644 index 0000000..ea08159 --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/shape.ts @@ -0,0 +1,20 @@ +import { PluginContext } from "@imgly/plugin-core"; + + +// each export must be a command +export const shapeSetEllipse = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const {block} = ctx.engine; + const {blockIds = block.findAllSelected()} = params; + + + blockIds.forEach((bId: number) => { + if (!block.hasShape(bId)) return; + const sId = block.getShape(bId); + block.isValid(sId) && block.destroy(sId); + + const eId = block.createShape("//ly.img.ubq/shape/ellipse"); + block.setShape(bId, eId); + }) + + +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/commands/turnInto.ts b/packages/plugin-design-essentials/src/commands/turnInto.ts new file mode 100644 index 0000000..38fae9b --- /dev/null +++ b/packages/plugin-design-essentials/src/commands/turnInto.ts @@ -0,0 +1,24 @@ +import { PluginContext } from "@imgly/plugin-core"; +import { turnBlockInto } from "../utils/turnBlockInto"; + +export const blockTurnIntoGraphic = (ctx: PluginContext, params: { blockIds: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((id) => turnBlockInto(ctx, "graphic", id)) + +} + +export const blockTurnIntoText = (ctx: PluginContext, params: { blockIds: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((id) => turnBlockInto(ctx, "text", id)) +} + +export const blockTurnIntoPage = (ctx: PluginContext, params: { blockIds: number[] }) => { + const { block } = ctx.engine; + const { blockIds = block.findAllSelected() } = params; + + blockIds.forEach((id) => turnBlockInto(ctx, "page", id)) +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/index.ts b/packages/plugin-design-essentials/src/index.ts new file mode 100644 index 0000000..efb525e --- /dev/null +++ b/packages/plugin-design-essentials/src/index.ts @@ -0,0 +1,84 @@ + + +import { PluginContext, CommandDescription } from '../../plugin-core/types'; + + +import { CommandImports, CommandContributions, PluginManifest } from './PluginManifest'; + +export interface PluginConfiguration { } + +const registerTranslation = (ctx: PluginContext, translations: { [key: string]: any } = {}) => { + ctx.i18n.setTranslations(translations) +} + +const registerCommands = (ctx: PluginContext, imports: CommandImports) => { + for (const command in imports) { + const callback = imports[command as CommandContributions] + + let desc: CommandDescription = PluginManifest.contributes.commands[command as CommandContributions]; + desc ??= {}; + desc.id ??= `${PluginManifest.id}.commands.${command as string}`; + const [category, name] = (command as string).split("_") + desc.category ??= name ? category : "Commands"; + ctx.commands.registerCommand( + desc.id, + async (params: any) => await callback(ctx, params), + desc + ); + } +} + +const loadTranslation = async (ctx: PluginContext, locales: readonly string[] = ctx.i18n.locales()) => { + const translations = await Promise.all(locales.map(async (locale) => { + try { + const translations = await import(`./locale/${locale}.json`) + return { [locale]: translations.default } + } catch (e) { + // when loading of the file fails + return { [locale]: {} } + } + })) + + translations.forEach((t) => registerTranslation(ctx, t)) +} + + +const loadCommands = async (ctx: PluginContext) => { + const commands = await import("./commands") + await registerCommands(ctx, commands) +} + + +const registerPanels = async (ctx: PluginContext, panels: any) => { + for (const panel in panels) { + const id = `${PluginManifest.id}.panels.${panel}` + // ctx.ui?.unstable_registerPanel(panel, ({ builder: any }) => { + // return panels[panel](ctx, builder) + + // }) + } + +} + +const loadPanels = async (ctx: PluginContext) => { + // const panels = await import("./panels/layers") + // await registerPanels(ctx, panels) +} + + + +export const activate = async (ctx: PluginContext) => { + await loadTranslation(ctx) + await loadCommands(ctx) + await loadPanels(ctx) +} + + +export default (ctx: PluginContext, _config: PluginConfiguration) => { + return { + async initializeUserInterface() { + await activate(ctx) + } + }; +}; + diff --git a/packages/plugin-design-essentials/src/locale/en.json b/packages/plugin-design-essentials/src/locale/en.json new file mode 100644 index 0000000..8c8f11f --- /dev/null +++ b/packages/plugin-design-essentials/src/locale/en.json @@ -0,0 +1,54 @@ +{ + "design-batteries.commands.blockBringForward": "Bring Block Forward", + "design-batteries.commands.blockBringToFront": "Bring Block to Front", + "design-batteries.commands.blockCreateText": "Create Text Block", + "design-batteries.commands.blockDelete": "Delete Block", + "design-batteries.commands.blockDuplicate": "Duplicate Block", + "design-batteries.commands.blockGroup": "Group Blocks", + "design-batteries.commands.blockRename": "Rename Block", + "design-batteries.commands.blockSendBackward": "Send Block Backward", + "design-batteries.commands.blockSendToBack": "Send Block to Back", + "design-batteries.commands.blockTurnIntoGraphic": "Turn Block into Graphic", + "design-batteries.commands.blockTurnIntoPage": "Turn Block into Page", + "design-batteries.commands.blockTurnIntoText": "Turn Block into Text", + "design-batteries.commands.blockUngroup": "Ungroup Blocks", + "design-batteries.commands.createDefaultBlockByType": "Create Default Block by Type", + "design-batteries.commands.debugLogBlockBlur": "Debug Log Block Blur", + "design-batteries.commands.debugLogBlockCrop": "Debug Log Block Crop", + "design-batteries.commands.debugLogBlockEffects": "Debug Log Block Effects", + "design-batteries.commands.debugLogBlockFill": "Debug Log Block Fill", + "design-batteries.commands.debugLogBlockMetadata": "Debug Log Block Metadata", + "design-batteries.commands.debugLogBlockProperties": "Debug Log Block Properties", + "design-batteries.commands.debugLogSceneHierarchy": "Debug Log Scene Hierarchy", + "design-batteries.commands.exportComponentLibrary": "Export Component Library", + "design-batteries.commands.exportComponentToFile": "Export Component to File", + "design-batteries.commands.exportJsonToClipboard": "Export JSON to Clipboard", + "design-batteries.commands.exportJsonToFile": "Export JSON to File", + "design-batteries.commands.exportJpegToFile": "Export JPEG to File", + "design-batteries.commands.exportPdfToFile": "Export PDF to File", + "design-batteries.commands.exportPngToClipboard": "Export PNG to Clipboard", + "design-batteries.commands.exportPngToFile": "Export PNG to File", + "design-batteries.commands.exportRgba8ToFile": "Export Raw RGBA8 to File", + "design-batteries.commands.exportSceneToClipboard": "Export Scene to Clipboard", + "design-batteries.commands.exportSceneToFile": "Export Scene to File", + "design-batteries.commands.exportWebpToFile": "Export WEBP to File", + "design-batteries.commands.i18nDownloadMissingCommandTranslations": "Download Missing Command Translations", + "design-batteries.commands.imageFitModeContain": "Set Image Fit Mode to Contain", + "design-batteries.commands.imageFitModeCrop": "Set Image Fit Mode to Crop", + "design-batteries.commands.imageFitModeCover": "Set Image Fit Mode to Cover", + "design-batteries.commands.importComponent": "Import Component", + "design-batteries.commands.layoutHorizontally": "Layout Horizontally", + "design-batteries.commands.layoutMasonry": "Layout Masonry", + "design-batteries.commands.layoutVertically": "Layout Vertically", + "design-batteries.commands.myNewFunctionForTheEditor": "Super Duper Function", + "design-batteries.commands.panelSettingsClose": "Close Settings Panel", + "design-batteries.commands.panelSettingsOpen": "Open Settings Panel", + "design-batteries.commands.pluginOpenCustomPanel": "Open Custom Panel", + "design-batteries.commands.pluginRegisterAndOpenCustomPanel": "Register and Open Custom Panel", + "design-batteries.commands.pluginRegisterCustomPanel": "Register Custom Panel", + "design-batteries.commands.productSetInstagram": "Set Instagram Product", + "design-batteries.commands.replicateSDXL": "Replicate SDXL", + "design-batteries.commands.syncBlockAppearance": "Sync Block Appearance", + "design-batteries.commands.syncBlocks": "Sync Blocks", + "design-batteries.commands.shapeSetEllipse": "Set Ellipse Shape" +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/panels/index.ts b/packages/plugin-design-essentials/src/panels/index.ts new file mode 100644 index 0000000..0e84be7 --- /dev/null +++ b/packages/plugin-design-essentials/src/panels/index.ts @@ -0,0 +1 @@ +// export * from "./layers"; \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/panels/layers.ts b/packages/plugin-design-essentials/src/panels/layers.ts new file mode 100644 index 0000000..473a5d6 --- /dev/null +++ b/packages/plugin-design-essentials/src/panels/layers.ts @@ -0,0 +1,19 @@ +// import { PluginContext } from '@imgly/plugin-core'; + + +// export const layerList = async (ctx: PluginContext, builder: any, params: any) => { +// const { block } = ctx.engine; +// const { Button, Section, Separator } = builder; +// const blockIds = block.findAllSelected(); + +// Section('layer.section', { +// title: 'Layers', +// children: () => +// blockIds.forEach((bId: number) => +// Button(bId.toString(), { +// label: block.getName(bId) || block.getUUID(bId).toString(), +// onClick: () => block.select(bId) +// })) +// }); + +// } diff --git a/packages/plugin-design-essentials/src/utils/cesdk.ts b/packages/plugin-design-essentials/src/utils/cesdk.ts new file mode 100644 index 0000000..d1c6def --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/cesdk.ts @@ -0,0 +1,39 @@ +import { BlockAPI } from "@cesdk/cesdk-js"; + + +export const readBlockProperty = (block: BlockAPI, id: number, propKey: string, propType?: string) => { + const blacklist = ["fill/solid/color", "fill/image/sourceSet"] + if (blacklist.includes(propKey)) return undefined; + if (!propType) propType = block.getPropertyType(propKey) + try { + switch (propType.toLowerCase()) { + case "string": return block.getString(id, propKey); + case "float": return block.getFloat(id, propKey); + case "double": return block.getDouble(id, propKey); + case "color": return block.getColor(id, propKey); + case "bool": return block.getBool(id, propKey); + case "enum": return block.getEnum(id, propKey); + } + } catch (e) { + console.warn("Error reading property value: ", e); + } + return undefined; +}; + + +export const setBlockProperty = (block: BlockAPI, id: number, propKey: string, propValue: any, propType?: string) => { + if (!propType) propType = block.getPropertyType(propKey) + try { + switch (propType.toLowerCase()) { + case "string": return block.setString(id, propKey, propValue); + case "float": return block.setFloat(id, propKey, propValue); + case "double": return block.setDouble(id, propKey, propValue); + case "color": return block.setColor(id, propKey, propValue); + case "bool": return block.setBool(id, propKey, propValue); + case "enum": return block.setEnum(id, propKey, propValue); + } + } catch (e) { + console.warn("Error writing property: ", propKey, propType, propValue); + } + return undefined; +}; diff --git a/packages/plugin-design-essentials/src/utils/computeBlockName.ts b/packages/plugin-design-essentials/src/utils/computeBlockName.ts new file mode 100644 index 0000000..a730434 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/computeBlockName.ts @@ -0,0 +1,8 @@ +import { BlockAPI } from "@cesdk/cesdk-js"; + +export const inferBlockName = (block: BlockAPI, blockId: number) => { + + const uuid = block.getUUID(blockId); + const name = block.getName(blockId) + return name || uuid || blockId +} \ No newline at end of file diff --git a/packages/plugin-design-essentials/src/utils/computeMultiSelectionBounds.ts b/packages/plugin-design-essentials/src/utils/computeMultiSelectionBounds.ts new file mode 100644 index 0000000..d529d12 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/computeMultiSelectionBounds.ts @@ -0,0 +1,20 @@ +import { PluginContext } from "../../../plugin-core/types"; + +export const computeMultiSelectionBounds = (ctx: PluginContext, blockIds: number[]) => { + + const { block } = ctx.engine; + const bounds = blockIds.map((id: number) => { + return { + x: block.getFrameX(id), + y: block.getFrameY(id), + width: block.getFrameWidth(id), + height: block.getFrameHeight(id) + }; + }); + + const x = Math.min(...bounds.map(b => b.x)); + const y = Math.min(...bounds.map(b => b.y)); + const width = Math.max(...bounds.map(b => b.x + b.width)) - x; + const height = Math.max(...bounds.map(b => b.y + b.height)) - y; + return { x, y, width, height }; +}; diff --git a/packages/plugin-design-essentials/src/utils/createDefaultBlockByType.ts b/packages/plugin-design-essentials/src/utils/createDefaultBlockByType.ts new file mode 100644 index 0000000..88ca0f4 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/createDefaultBlockByType.ts @@ -0,0 +1,30 @@ +import { PluginContext } from "@imgly/plugin-core"; + +// UTILS +export const createDefaultBlockByType = (ctx: PluginContext, type: string) => { + const { block } = ctx.engine; + switch (type) { + + case "graphic": { + const bId = block.create("graphic"); + const sId = block.createShape("rect"); + const fId = block.createFill("//ly.img.ubq/fill/image"); + block.setShape(bId, sId); + block.setFill(bId, fId); + block.setName(bId, type.toUpperCase()); + return bId; + } + case "page": { + const bId = block.create("page"); + block.setName(bId, type.toUpperCase()); + return bId; + } + case "text": { + const bId = block.create("graphic"); + block.replaceText(bId, "Hello World"); + block.setName(bId, type.toUpperCase()); + return bId; + } + default: throw new Error("Invalid type"); + } +}; diff --git a/packages/plugin-design-essentials/src/utils/download.ts b/packages/plugin-design-essentials/src/utils/download.ts new file mode 100644 index 0000000..f83c9c5 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/download.ts @@ -0,0 +1,60 @@ +import CreativeEditorSDK from "@cesdk/cesdk-js"; + + + +export async function loadAsBlob(filter: string = "*") { + return new Promise((resolve, reject) => { + const upload = document.createElement("input"); + upload.setAttribute("type", "file"); + upload.setAttribute("accept", filter) + upload.setAttribute("style", "display: none") + upload.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const buffer = e.target?.result + if (buffer instanceof ArrayBuffer) { + const blob = new Blob([buffer]); + upload.remove() + resolve(blob) + } else { + upload.remove() + reject(new Error("Invalid buffer")) + } + } + reader.readAsArrayBuffer(file); + } + } + + upload.click() + }) +} + +export function downloadBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + link.remove(); + URL.revokeObjectURL(url); +} +export const downloadBlocks = (cesdk: CreativeEditorSDK, blobs: Blob[], options: { mimeType: string; pages?: number[]; }) => { + const postfix = options.mimeType.split("/")[1]; + const pageIds = options.pages ?? []; + + blobs.forEach((blob, index) => { + const pageId = pageIds[index]; + let pageName = `page-${index}`; + if (pageId) { + const name = cesdk.engine.block.getName(pageId); + pageName = name?.length ? name : pageName; + } + const filename = `${pageName}.${postfix}`; + downloadBlob(blob, filename); + }); + return Promise.resolve(); +}; + + diff --git a/packages/plugin-design-essentials/src/utils/exportBlockAs.ts b/packages/plugin-design-essentials/src/utils/exportBlockAs.ts new file mode 100644 index 0000000..3185100 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/exportBlockAs.ts @@ -0,0 +1,34 @@ +import { PluginContext } from "../../../plugin-core/types"; +import { type MimeType } from "@cesdk/cesdk-js"; + +export const exportBlockAs = async (ctx: PluginContext, params: { blockIds?: number[], mimeType?: MimeType | 'application/x-cesdk' | 'application/json', width?: number, height?: number }) => { + + const { block } = ctx.engine; + const { + blockIds = block.findAllSelected(), + mimeType = "image/png" as MimeType, + width, + height + } = params; + blockIds.length === 0 && blockIds.push(ctx.engine.scene.get()!); + return await Promise.all(blockIds.map(async (bId: number) => { + switch (mimeType) { + case "application/x-cesdk": { + const str = await block.saveToString([bId]); + return new Blob([str], { type: mimeType }); + + } + case "application/json": { + const str = await block.saveToString([bId]); + const json = str.substring(4) + const decoded = atob(json) + return new Blob([decoded], { type: mimeType }); + + } + default: + return await block.export(bId, mimeType, { targetHeight: height, targetWidth: width }); + } + + })); +}; + diff --git a/packages/plugin-design-essentials/src/utils/getTransform.ts b/packages/plugin-design-essentials/src/utils/getTransform.ts new file mode 100644 index 0000000..6cbd969 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/getTransform.ts @@ -0,0 +1,12 @@ +import { PluginContext } from "@imgly/plugin-core"; + +export const getTransform = (ctx: PluginContext, bId: number) => { + const { block } = ctx.engine; + return { + x: block.getPositionX(bId), + y: block.getPositionY(bId), + width: block.getFrameWidth(bId), + height: block.getFrameHeight(bId) + }; + +}; diff --git a/packages/plugin-design-essentials/src/utils/setTransform.ts b/packages/plugin-design-essentials/src/utils/setTransform.ts new file mode 100644 index 0000000..fa6c938 --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/setTransform.ts @@ -0,0 +1,10 @@ +import { PluginContext } from "@imgly/plugin-core"; + +export const setTransform = (ctx: PluginContext, bId: number, transform: any) => { + const { block } = ctx.engine; + const { x, y, width, height } = transform; + x && block.setPositionX(bId, x); + y && block.setPositionY(bId, y); + width && block.setWidth(bId, width); + height && block.setHeight(bId, height); +}; diff --git a/packages/plugin-design-essentials/src/utils/turnBlockInto.ts b/packages/plugin-design-essentials/src/utils/turnBlockInto.ts new file mode 100644 index 0000000..5cb81be --- /dev/null +++ b/packages/plugin-design-essentials/src/utils/turnBlockInto.ts @@ -0,0 +1,37 @@ +import { PluginContext } from "@imgly/plugin-core"; +import { getTransform } from "./getTransform"; +import { setTransform } from "./setTransform"; +import { createDefaultBlockByType } from "../utils/createDefaultBlockByType"; + +export const turnBlockInto = (ctx: PluginContext, toType: string, id: number) => { + const { block, scene } = ctx.engine; + + const bId = createDefaultBlockByType(ctx, toType); + + if (block.hasFill(id)) { + const fId = block.duplicate(block.getFill(id)); + block.hasFill(bId) && block.setFill(bId, fId); + } + if (block.hasShape(id)) { + const sId = block.duplicate(block.getShape(id)); + block.hasShape(bId) && block.setShape(bId, sId); + } + + setTransform(ctx, bId, getTransform(ctx, id)); + + if (toType === "page") { + console.log("Turning into page"); + let pId = scene.get()!; + const cIds = block.getChildren(pId); + const sId = cIds.find((cId) => block.getType(cId) === "//ly.img.ubq/stack") + const hasStack = sId !== -1 + console.log("Has stack", hasStack); + pId = hasStack ? sId : pId; + block.appendChild(pId, bId); + } else { + const pId = block.getParent(id) ?? scene.getCurrentPage() ?? scene.get()!; + block.appendChild(pId, bId); + } + + block.destroy(id); +}; diff --git a/packages/plugin-design-essentials/src/worker.ts b/packages/plugin-design-essentials/src/worker.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/plugin-design-essentials/tsconfig.json b/packages/plugin-design-essentials/tsconfig.json new file mode 100644 index 0000000..0d7577f --- /dev/null +++ b/packages/plugin-design-essentials/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": false, + "target": "es2017", + "module": "es2020", + "lib": ["es2018", "dom"], + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "declaration": true, + "declarationDir": "types/", + "skipLibCheck": true + }, + "include": ["src/**/*", "esbuild/global.d.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/plugin-documents/.gitignore b/packages/plugin-documents/.gitignore new file mode 100644 index 0000000..9060c20 --- /dev/null +++ b/packages/plugin-documents/.gitignore @@ -0,0 +1,11 @@ +node_modules +packages/*/dist +examples/*/dist +.env.local + +.DS_Store +yarn-error.log + +.turbo +.vercel +types \ No newline at end of file diff --git a/packages/plugin-documents/esbuild/config.mjs b/packages/plugin-documents/esbuild/config.mjs new file mode 100644 index 0000000..37b437c --- /dev/null +++ b/packages/plugin-documents/esbuild/config.mjs @@ -0,0 +1,58 @@ +import chalk from 'chalk'; +import { readFile } from 'fs/promises'; + +// import packageJson from '../package.json' assert { type: 'json' }; +// Avoid the Experimental Feature warning when using the above. +const packageJson = JSON.parse( + await readFile(new URL('../package.json', import.meta.url)) +); + + +const dependencies = Object.keys(packageJson.dependencies) +const peerDependencies = Object.keys(packageJson.peerDependencies) +const externals = [...dependencies, ...peerDependencies] + +console.log( + chalk.yellow('Building version: '), + chalk.green(packageJson.version) +); + +const configs = [ + { + entryPoints: ['src/index.ts', "src/worker.ts"], + define: { + PLUGIN_VERSION: `"${packageJson.version}"` + }, + minify: true, + bundle: true, + sourcemap: true, + external: externals, + platform: 'node', + format: 'esm', + outdir: 'dist', + outExtension: { '.js': '.mjs' }, + plugins: [ + { + name: 'reporter', + setup(build) { + build.onEnd((result) => { + console.log( + `[${new Date().toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false + })}] Build ${ + result.errors.length + ? chalk.red('failed') + : chalk.green('succeeded') + }` + ); + }); + } + } + ] + } +]; + +export default configs; diff --git a/packages/plugin-documents/esbuild/global.d.ts b/packages/plugin-documents/esbuild/global.d.ts new file mode 100644 index 0000000..de80fd8 --- /dev/null +++ b/packages/plugin-documents/esbuild/global.d.ts @@ -0,0 +1,3 @@ +// These constants here are added by the base esbuild config + +declare const PLUGIN_VERSION: string; diff --git a/packages/plugin-documents/manifest.json b/packages/plugin-documents/manifest.json new file mode 100644 index 0000000..9ee9cf1 --- /dev/null +++ b/packages/plugin-documents/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "documents", + "version": "0.0.1", + "publisher": "IMG.LY GmbH", + "icon": null, + "categories": [], + "contributes": { + "commands": { + + }, + "i18n": {} + } +} \ No newline at end of file diff --git a/packages/plugin-documents/package.json b/packages/plugin-documents/package.json new file mode 100644 index 0000000..0efdba4 --- /dev/null +++ b/packages/plugin-documents/package.json @@ -0,0 +1,68 @@ +{ + "name": "@imgly/plugin-documents", + "version": "0.1.0", + "keywords": [ + "CE.SDK", + "IMG.LY", + "plugin" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/imgly/plugins.git" + }, + "license": "SEE LICENSE IN LICENSE.md", + "author": { + "name": "IMG.LY GmbH", + "email": "support@img.ly", + "url": "https://img.ly" + }, + "bugs": { + "email": "support@img.ly" + }, + "source": "./src/index.ts", + "module": "./dist/index.mjs", + "types": "./types/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "types": "./types/index.d.ts" + } + }, + "homepage": "https://img.ly", + "files": [ + "LICENSE.md", + "README.md", + "CHANGELOG.md", + "dist/", + "types/", + "bin/" + ], + "scripts": { + "start": "npm run watch", + "clean": "npx rimraf dist && npx rimraf types", + "build": "yarn run clean && yarn run types:create && node scripts/build.mjs", + "dev": "yarn run types:create && node scripts/watch.mjs", + "publish:latest": "npm run clean && npm run build && npm publish --tag latest --access public", + "publish:next": "npm run clean && npm run build && npm publish --tag next --access public", + "check:all": "concurrently -n lint,type,pretty \"yarn check:lint\" \"yarn check:type\" \"yarn check:pretty\"", + "check:lint": "eslint --max-warnings 0 './src/**/*.{ts,tsx}'", + "check:pretty": "prettier --list-different './src/**/*.{ts,tsx}'", + "check:type": "tsc --noEmit", + "types:create": "tsc --emitDeclarationOnly" + }, + "devDependencies": { + "chalk": "^5.3.0", + "concurrently": "^8.2.2", + "esbuild": "^0.19.11", + "eslint": "^8.51.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@cesdk/cesdk-js": "~1.21.0" + }, + "dependencies": { + "@imgly/plugin-core": "*", + "lodash": "^4.17.21", + "yjs": "^13.6.14" + } +} diff --git a/packages/plugin-documents/scripts/build.mjs b/packages/plugin-documents/scripts/build.mjs new file mode 100644 index 0000000..13d12e1 --- /dev/null +++ b/packages/plugin-documents/scripts/build.mjs @@ -0,0 +1,5 @@ +import * as esbuild from 'esbuild'; + +import configs from '../esbuild/config.mjs'; + +await Promise.all(configs.map(async (config) => await esbuild.build(config))); diff --git a/packages/plugin-documents/scripts/watch.mjs b/packages/plugin-documents/scripts/watch.mjs new file mode 100644 index 0000000..15dbb21 --- /dev/null +++ b/packages/plugin-documents/scripts/watch.mjs @@ -0,0 +1,19 @@ +import chalk from 'chalk'; +import * as esbuild from 'esbuild'; + +import configs from '../esbuild/config.mjs'; + +console.log( + `[${new Date().toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false + })}] ${chalk.green('Watching...')}` +); + +const contexts = await Promise.all( + configs.map((config) => esbuild.context(config)) +); + +await Promise.any(contexts.map((ctx) => ctx.watch())); diff --git a/packages/plugin-documents/src/PluginManifest.ts b/packages/plugin-documents/src/PluginManifest.ts new file mode 100644 index 0000000..f60f3b4 --- /dev/null +++ b/packages/plugin-documents/src/PluginManifest.ts @@ -0,0 +1,10 @@ +import Manifest from '../manifest.json'; +import { CommandCallback } from '@imgly/plugin-core'; + + +export type Contributions = typeof Manifest;["contributes"] +export type CommandContributions = keyof typeof Manifest["contributes"]["commands"] +export type CommandImports = Record + +export const PluginManifest = Manifest + diff --git a/packages/plugin-documents/src/commands/__template.ts b/packages/plugin-documents/src/commands/__template.ts new file mode 100644 index 0000000..bad2b6a --- /dev/null +++ b/packages/plugin-documents/src/commands/__template.ts @@ -0,0 +1,3 @@ +// // @ts-ignore +// const __template = async (ctx: PluginContext, params: { blockIds?: number[] }) => { +// } \ No newline at end of file diff --git a/packages/plugin-documents/src/commands/index.ts b/packages/plugin-documents/src/commands/index.ts new file mode 100644 index 0000000..7d9a23a --- /dev/null +++ b/packages/plugin-documents/src/commands/index.ts @@ -0,0 +1,139 @@ +"use strict"; + +import { PluginContext } from "@imgly/plugin-core"; + +import { isEqual } from 'lodash' + +import * as Y from 'yjs' + +const documents = new Map() + +type YJson = Y.Map + +class Transform extends Y.Map { + constructor() { + super() + } + set x(value: number) { + this.set("x", value) + } + set y(value: number) { + this.set("y", value) + } +} + + +class DesignDocument { + doc: Y.Doc; + properties: YJson; + + // shape + // stroke + // style + attachments: Blob[] // images, files, etc + + + constructor() { + this.doc = new Y.Doc({ + meta: { + "content-type": "application/json" + } + }) + this.properties = this.doc.getMap('properties'); + this.properties.set("transform", new Transform()) + this.opacity = 1 + console.log("guid", this.doc.guid) + } + + transact(fn: () => void) { + this.doc.transact(fn) + } + + set opacity(value: number) { + // if (this.opacity === value) return + this.properties.set("opacity", value) + } + + // getter + get opacity(): number | undefined { + return this.properties.get("opacity") + } + + get transform(): Transform | undefined { + return this.properties.get("transform") + } + set transform(value: { x?: number, y?: number }) { + value.x && (this.transform.x = value.x) + value.y && (this.transform.y = value.y) + } +} + +export const documentDemo = async (ctx: PluginContext, params: { blockIds?: number[] }) => { + const design = new DesignDocument() + const design2 = new DesignDocument() + + { + console.log(design.doc.meta, design2.doc.meta) + const hash = await sha256(Y.encodeStateVector(design.doc)) + const hash2 = await sha256(Y.encodeStateVector(design2.doc)) + console.log(hash, hash2) + + } + + + // sync https://docs.yjs.dev/api/document-updates + design.doc.on('update', (update, origin, doc) => { + //call get state vector from others + const stateVector2 = Y.encodeStateVector(design2.doc) + const hash = sha256(stateVector2) + // encode state as update + const diff12 = Y.encodeStateAsUpdate(design.doc, stateVector2) + // send to other and apply + Y.applyUpdate(design2.doc, diff12) + }) + + design2.doc.on('update', (update, origin, doc) => { + const stateVector1 = Y.encodeStateVector(design.doc) + const diff21 = Y.encodeStateAsUpdate(design2.doc, stateVector1) + Y.applyUpdate(design.doc, diff21) + }) + + + + const eventCallback = (events, transaction) => { + events.forEach((event: Y.YMapEvent) => { + const path = event.path + event.keysChanged.forEach(key => { + const newData = event.keys.get(key) + const oldValue = newData?.oldValue + const newValue = event.target.get(key) + const hasChanged = !isEqual(oldValue, newValue) + if (hasChanged) { + const fullPath = new Array(path) + fullPath.push(key) + console.log(event.target.doc.guid, fullPath.join("."), oldValue, newValue) + } + }) + + + }) + } + design.properties.observeDeep(eventCallback) + design2.properties.observeDeep(eventCallback) + + design.opacity = 0.5 + design2.opacity = 0.95 + + console.log(design.opacity) + console.log(design2.opacity) + + const hash = await sha256(Y.encodeStateVector(design.doc)) + const hash2 = await sha256(Y.encodeStateVector(design2.doc)) + console.log(hash, hash2) +} + + +async function sha256(data: Uint8Array) { + const hash = await window.crypto.subtle.digest('SHA-256', data); + return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); +} \ No newline at end of file diff --git a/packages/plugin-documents/src/index.ts b/packages/plugin-documents/src/index.ts new file mode 100644 index 0000000..d728fe9 --- /dev/null +++ b/packages/plugin-documents/src/index.ts @@ -0,0 +1,86 @@ + + +import { PluginContext, CommandDescription } from '@imgly/plugin-core'; + + +import { CommandImports, CommandContributions, PluginManifest } from './PluginManifest'; + +export interface PluginConfiguration { } + +const registerTranslation = (ctx: PluginContext, translations: { [key: string]: any } = {}) => { + ctx.i18n.setTranslations(translations) +} + + +const loadCommands = async (ctx: PluginContext, imports: any) => { + + await registerCommands(ctx, imports) +} + +const registerCommands = (ctx: PluginContext, imports: any) => { + for (const command in imports) { + const callback = imports[command as CommandContributions] + + let desc: CommandDescription = PluginManifest.contributes.commands[command as CommandContributions]; + desc ??= {}; + desc.id ??= `${PluginManifest.id}.commands.${command as string}`; + const [category, name] = (command as string).split("_") + desc.category ??= name ? category : "Commands"; + console.log("registerCommand", command as string) + ctx.commands.registerCommand( + desc.id, + async (params: any) => await callback(ctx, params), + desc + ); + } +} + +const loadTranslation = async (ctx: PluginContext, locales: readonly string[] = ctx.i18n.locales()) => { + const translations = await Promise.all(locales.map(async (locale) => { + try { + const translations = await import(`./locale/${locale}.json`) + return { [locale]: translations.default } + } catch (e) { + // when loading of the file fails + return { [locale]: {} } + } + })) + + translations.forEach((t) => registerTranslation(ctx, t)) +} + + + +const registerPanels = async (ctx: PluginContext, panels: any) => { + for (const panel in panels) { + const id = `${PluginManifest.id}.panels.${panel}` + // ctx.ui?.unstable_registerPanel(panel, ({ builder: any }) => { + // return panels[panel](ctx, builder) + + // }) + } + +} + +const loadPanels = async (ctx: PluginContext) => { + // const panels = await import("./panels/layers") + // await registerPanels(ctx, panels) +} + + + +export const activate = async (ctx: PluginContext) => { + await loadTranslation(ctx) + await loadCommands(ctx, await import('./commands')) + await loadPanels(ctx) +} + + +export default (ctx: PluginContext, _config: PluginConfiguration) => { + return { + async initializeUserInterface() { + await activate(ctx) + } + }; +}; + diff --git a/packages/plugin-documents/src/locale/en.json b/packages/plugin-documents/src/locale/en.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/packages/plugin-documents/src/locale/en.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/plugin-documents/src/panels/index.ts b/packages/plugin-documents/src/panels/index.ts new file mode 100644 index 0000000..0e84be7 --- /dev/null +++ b/packages/plugin-documents/src/panels/index.ts @@ -0,0 +1 @@ +// export * from "./layers"; \ No newline at end of file diff --git a/packages/plugin-documents/src/panels/layers.ts b/packages/plugin-documents/src/panels/layers.ts new file mode 100644 index 0000000..473a5d6 --- /dev/null +++ b/packages/plugin-documents/src/panels/layers.ts @@ -0,0 +1,19 @@ +// import { PluginContext } from '@imgly/plugin-core'; + + +// export const layerList = async (ctx: PluginContext, builder: any, params: any) => { +// const { block } = ctx.engine; +// const { Button, Section, Separator } = builder; +// const blockIds = block.findAllSelected(); + +// Section('layer.section', { +// title: 'Layers', +// children: () => +// blockIds.forEach((bId: number) => +// Button(bId.toString(), { +// label: block.getName(bId) || block.getUUID(bId).toString(), +// onClick: () => block.select(bId) +// })) +// }); + +// } diff --git a/packages/plugin-documents/src/worker.ts b/packages/plugin-documents/src/worker.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/plugin-documents/tsconfig.json b/packages/plugin-documents/tsconfig.json new file mode 100644 index 0000000..0d7577f --- /dev/null +++ b/packages/plugin-documents/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": false, + "target": "es2017", + "module": "es2020", + "lib": ["es2018", "dom"], + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "declaration": true, + "declarationDir": "types/", + "skipLibCheck": true + }, + "include": ["src/**/*", "esbuild/global.d.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/plugin-vectorizer/LICENSE.md b/packages/plugin-vectorizer/LICENSE.md new file mode 100644 index 0000000..a099036 --- /dev/null +++ b/packages/plugin-vectorizer/LICENSE.md @@ -0,0 +1 @@ +TBD diff --git a/packages/plugin-vectorizer/PLAYGROUND.md b/packages/plugin-vectorizer/PLAYGROUND.md new file mode 100644 index 0000000..f9a21fa --- /dev/null +++ b/packages/plugin-vectorizer/PLAYGROUND.md @@ -0,0 +1,180 @@ + +Define feature flags + +Feature Enabled +Feature Permissions + + + + + +```JS +// define +feature.register('featureName') +feature.unregister('featureName') + +// if the feature does not exist it always evaluates to flse +feature.setEnabled('featureName', true) +feature.setEnabled('featureName', false) +feature.setEnabled('featureName', (context) => { return true +}); + +feature.setPermission('featureName', (context) => { return true; +}) +feature.setPermission('featureName', (context) => { return true; +}) +feature.setPermission('featureName', (context) => { return true; +}) + +feature.setSupported('featureName', (context) => { return true +}) + +feature.can(featureName, context) { + if (!this.isSupported(featureName)) return; + if (!this.isAllowed(featureName)) return; + if (!this.isEnabled(featureName)) return; +} + +``` + + +```JS + // check if simd is suppored +feature.register("simd"); +feature.setEnabled("simd", (ctx) => { + if (feature.canUse("browser", ctx)) + return (typeof SIMD !== "undefined"); + }); + + +``` + +feature.isEnabled + + + +# Example share featur +```JS + +// we can register a unique identifier that has potentially dependencies to other features +feature.register("my.command", (ctx) => {}, ["fill", {"opt": { optional: true}}]) + + +// the integrator +feature.setPermission("share.dialog", (ctx) => {}) +feature.setEnabled("share.dialog", (ctx) => {}) + +// register dependecy that also need to be checked + +// can will check if the feature and the dependencies are met + +if (feature.can("my.command",ctx)) { + feature.run("my.command", args, ctx) +} + +feature + .try("my.command")? + .run(args, ctx) + +feature.wrap(func, ["feature"]) { + return () => { + // check features + return T | undefined + } +} + + +``` + + +```JS + +// only run when imgly scope img.ly arrange is set and whatever is in there evaluates to true +feature.register("vectorize", ctx => { + if (ctx.blockIds) // do some checks on the thing +}, ["imgly.arrange", "imgly.block.setPositionX"]) + + +function vectorize(image) { + +} + +export const vectorize = feature.wrap(vectorize, ["vectorize"]) + +//Vectorize will only run if feature is enabled + + +feature.setPermission("vectorize", "deny") // no one will be able to run it +feature.setEnabled("vectorize", true) + + +// checkout cancancan + +FEATURES/PERMISSIONS/SCOPES +// checkout Abilities +// results = Allow +class Abilities { + + #permission = Map() // featureName -> (func: boolean) | boolean + #enabled = Map() // featureName -> (func: boolean) | boolean + #definition = Map() // featureName -> (func: boolean) | boolean + #dependencies = Map() // featureName -> (func: boolean) | boolean + + + // all need allow, defer, deny + + can(featureName, ctx) => { + if (this.#enabled[featureName] && (!this.#enabled[featureName]?.(ctx))) return false; + if (this.#enabled[featureName] && (!this.#permission[featureName]?.(ctx)())) return false; + if (this.#enabled[featureName] && (!this.#definition[featureName]?.(ctx)())() return false; + // when decision s deferred to this feature then don't care + // when deicison is false + const deps = this.dependencies[featureName]?? [] + if (!deps.every(dep => this.isEnabled(dep, ctx))) return false + return true + } + +} + + +abilities.define("read", {blockIds} => { + if (!blockIds) +}) + +abilities.can("read", {blockIds}) + +abilities.define("block.setPositionX", ([id, value] => { + if (!id) return "deny" + // if (!value) we don't care about the value here +// can cehck wether or not to run +})) +abilities.define("block.setPositionX", ["block.arrange"]) + +// put a lock in front of setPosition +// this modifies the prototype +abilities.lock("block.setPositionX", setPosition, Block) + + +abilities.setPermission("block.setPosition") + +block.setPosition.can?(blockId) + +block.setPosition.allowed?(BlockId) + + +block.setPosition?.(blockId, value) +// check feature avaiability else use +blcok = ability.guard(block) //this will create a proxy to the object with each function being overwritten + +if (!block) return +if (!block.setPosition) return +if (!block.getPosition) return +if (!block.value) return + + +//enabled +//permission +//other + + +``` \ No newline at end of file diff --git a/packages/plugin-vectorizer/README.md b/packages/plugin-vectorizer/README.md new file mode 100644 index 0000000..6acc73e --- /dev/null +++ b/packages/plugin-vectorizer/README.md @@ -0,0 +1,40 @@ +# IMG.LY CE.SDK Plugin Vectorizer + +This plugin introduces a vectorizer for the CE.SDK editor. + +## Installation + +You can install the plugin via npm or yarn. Use the following commands to install the package: + +``` +yarn add @imgly/plugin-vectorizer +npm install @imgly/plugin-vectorizer +``` + +## Usage + +Adding the plugin to CE.SDK will automatically add a vectorizer +canvas menu entry for every block with an image fill. + +```typescript +import CreativeEditorSDK from '@cesdk/cesdk-js'; +import VectorizerPlugin from '@imgly/plugin-vectorizer'; + +const config = { + license: '', + callbacks: { + // Please note that the vectorizer plugin depends on an correctly + // configured upload. 'local' will work for local testing, but in + // production you will need something stable. Please take a look at: + // https://img.ly/docs/cesdk/ui/guides/upload-images/ + onUpload: 'local' + } +}; + +const cesdk = await CreativeEditorSDK.create(container, config); +await cesdk.addDefaultAssetSources(), + await cesdk.addDemoAssetSources({ sceneMode: 'Design' }), + await cesdk.unstable_addPlugin(VectorizerPlugin()); + +await cesdk.createDesignScene(); +``` diff --git a/packages/plugin-vectorizer/STRUCTURE.md b/packages/plugin-vectorizer/STRUCTURE.md new file mode 100644 index 0000000..13b1472 --- /dev/null +++ b/packages/plugin-vectorizer/STRUCTURE.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-vectorizer/TODO.md b/packages/plugin-vectorizer/TODO.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/plugin-vectorizer/esbuild/config.mjs b/packages/plugin-vectorizer/esbuild/config.mjs new file mode 100644 index 0000000..37b437c --- /dev/null +++ b/packages/plugin-vectorizer/esbuild/config.mjs @@ -0,0 +1,58 @@ +import chalk from 'chalk'; +import { readFile } from 'fs/promises'; + +// import packageJson from '../package.json' assert { type: 'json' }; +// Avoid the Experimental Feature warning when using the above. +const packageJson = JSON.parse( + await readFile(new URL('../package.json', import.meta.url)) +); + + +const dependencies = Object.keys(packageJson.dependencies) +const peerDependencies = Object.keys(packageJson.peerDependencies) +const externals = [...dependencies, ...peerDependencies] + +console.log( + chalk.yellow('Building version: '), + chalk.green(packageJson.version) +); + +const configs = [ + { + entryPoints: ['src/index.ts', "src/worker.ts"], + define: { + PLUGIN_VERSION: `"${packageJson.version}"` + }, + minify: true, + bundle: true, + sourcemap: true, + external: externals, + platform: 'node', + format: 'esm', + outdir: 'dist', + outExtension: { '.js': '.mjs' }, + plugins: [ + { + name: 'reporter', + setup(build) { + build.onEnd((result) => { + console.log( + `[${new Date().toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false + })}] Build ${ + result.errors.length + ? chalk.red('failed') + : chalk.green('succeeded') + }` + ); + }); + } + } + ] + } +]; + +export default configs; diff --git a/packages/plugin-vectorizer/esbuild/global.d.ts b/packages/plugin-vectorizer/esbuild/global.d.ts new file mode 100644 index 0000000..de80fd8 --- /dev/null +++ b/packages/plugin-vectorizer/esbuild/global.d.ts @@ -0,0 +1,3 @@ +// These constants here are added by the base esbuild config + +declare const PLUGIN_VERSION: string; diff --git a/packages/plugin-vectorizer/manifest.json b/packages/plugin-vectorizer/manifest.json new file mode 100644 index 0000000..d277b70 --- /dev/null +++ b/packages/plugin-vectorizer/manifest.json @@ -0,0 +1,67 @@ +{ + "id": "vectorizer", + "version": "1.0.0", + "publisher": "IMG.LY GmbH", + "icon": "", + "license": "", + "pricing": "free", + "payment": { + "oneTime": 0, + "subscription": 0, + "usage": 0 + }, + "engines": { + "imgly": ">2.0.0" + }, + "categories": [ + "image", + "vector" + ], + "contributes": { + "ui": { + "button": { + "id": "vectorizer.ui.button", + "label": "vectorizer.ui.button" + } + }, + "commands": { + "vectorize": { + "id": "vectorizer.commands.vectorize", + "label": "vectorizer.commands.vectorize", + "group": "convert", + "args": [ + { + "type": "number", + "format": "", + "ui": "slider" + } + ], + "returns": {} + } + }, + "i18n": { + "vectorizer.commands.vectorize": "Convert into Vectorpath", + "vectorizer.commands.category": "Vectorizer", + "vectorizer.config.title": "Vectorizer", + "vectorizer.config.enabled": "Enable Vectorizer", + "vectorizer.config.enabled.description": "Enable or disable the vectorizer" + }, + "errors": { + "validationError": { + "id": "vectorizer.errors.validationError", + "message": "vectorizer.errors.validationError.message" + } + }, + "config": { + "type": "object", + "title": "vectorizer.config.titel", + "settings": { + "enabled": { + "type": "boolean", + "default": true, + "description": "vectorizer.config.settings.enabled.description" + } + } + } + } +} \ No newline at end of file diff --git a/packages/plugin-vectorizer/package.json b/packages/plugin-vectorizer/package.json new file mode 100644 index 0000000..6ac69e3 --- /dev/null +++ b/packages/plugin-vectorizer/package.json @@ -0,0 +1,70 @@ +{ + "name": "@imgly/plugin-vectorizer", + "version": "0.1.0", + "description": "Vectorizer plugin for the CE.SDK editor", + "keywords": [ + "CE.SDK", + "plugin", + "svg", + "vectorizer" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/imgly/plugins.git" + }, + "license": "SEE LICENSE IN LICENSE.md", + "author": { + "name": "IMG.LY GmbH", + "email": "support@img.ly", + "url": "https://img.ly" + }, + "bugs": { + "email": "support@img.ly" + }, + "source": "./src/index.ts", + "module": "./dist/index.mjs", + "types": "./types/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "types": "./types/index.d.ts" + } + }, + "homepage": "https://img.ly", + "files": [ + "LICENSE.md", + "README.md", + "CHANGELOG.md", + "dist/", + "types/", + "bin/" + ], + "scripts": { + "start": "npm run watch", + "clean": "npx rimraf dist && npx rimraf types", + "build": "yarn run clean && yarn run types:create && node scripts/build.mjs", + "dev": "yarn run types:create && node scripts/watch.mjs", + "publish:latest": "npm run clean && npm run build && npm publish --tag latest --access public", + "publish:next": "npm run clean && npm run build && npm publish --tag next --access public", + "check:all": "concurrently -n lint,type,pretty \"yarn check:lint\" \"yarn check:type\" \"yarn check:pretty\"", + "check:lint": "eslint --max-warnings 0 './src/**/*.{ts,tsx}'", + "check:pretty": "prettier --list-different './src/**/*.{ts,tsx}'", + "check:type": "tsc --noEmit", + "types:create": "tsc --emitDeclarationOnly" + }, + "devDependencies": { + "chalk": "^5.3.0", + "concurrently": "^8.2.2", + "esbuild": "^0.19.11", + "eslint": "^8.51.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@cesdk/cesdk-js": "~1.21.0" + }, + "dependencies": { + "@imgly/vectorizer": "~0.1.0-rc6", + "@imgly/plugin-core": "*", + "lodash": "^4.17.21" + } +} diff --git a/packages/plugin-vectorizer/scripts/build.mjs b/packages/plugin-vectorizer/scripts/build.mjs new file mode 100644 index 0000000..13d12e1 --- /dev/null +++ b/packages/plugin-vectorizer/scripts/build.mjs @@ -0,0 +1,5 @@ +import * as esbuild from 'esbuild'; + +import configs from '../esbuild/config.mjs'; + +await Promise.all(configs.map(async (config) => await esbuild.build(config))); diff --git a/packages/plugin-vectorizer/scripts/watch.mjs b/packages/plugin-vectorizer/scripts/watch.mjs new file mode 100644 index 0000000..15dbb21 --- /dev/null +++ b/packages/plugin-vectorizer/scripts/watch.mjs @@ -0,0 +1,19 @@ +import chalk from 'chalk'; +import * as esbuild from 'esbuild'; + +import configs from '../esbuild/config.mjs'; + +console.log( + `[${new Date().toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false + })}] ${chalk.green('Watching...')}` +); + +const contexts = await Promise.all( + configs.map((config) => esbuild.context(config)) +); + +await Promise.any(contexts.map((ctx) => ctx.watch())); diff --git a/packages/plugin-vectorizer/src/PluginManifest.ts b/packages/plugin-vectorizer/src/PluginManifest.ts new file mode 100644 index 0000000..0477289 --- /dev/null +++ b/packages/plugin-vectorizer/src/PluginManifest.ts @@ -0,0 +1,16 @@ +import Manifest from '../manifest.json'; +type ManifestType = typeof Manifest; + +// TODO: Extend this with the builtin commands from other packages + +// Contributions + +export type I18NKeys = keyof ManifestType["contributes"]["i18n"] +export type CommandContributions = keyof ManifestType["contributes"]["commands"] +export type ErrorKeys = keyof ManifestType["contributes"]["errors"] + +export type UIComponentKeys = keyof ManifestType["contributes"]["ui"] +export type ConfigKeys = keyof ManifestType["contributes"]["config"] + +export const PluginManifest = Manifest + diff --git a/packages/plugin-vectorizer/src/activate.ts b/packages/plugin-vectorizer/src/activate.ts new file mode 100644 index 0000000..4e9065a --- /dev/null +++ b/packages/plugin-vectorizer/src/activate.ts @@ -0,0 +1,90 @@ +import { PluginManifest, CommandContributions, UIComponentKeys } from './PluginManifest'; + +import { + fixDuplicateMetadata, + getPluginMetadata, + isDuplicate, +} from './utils/common'; + +import { PluginContext, CommandCallback } from './deps'; +import { update as handleUpdateEvent } from './handler'; + + +export async function activate(ctx: PluginContext) { + // const { engine, logger, i18n, ui, commands } = ctx + + + //@ts-ignore + ctx.logger?.trace("checking if engine has commands", cesdk.engine.commands ? "yes" : "no") + + { + ctx.logger?.trace("Registering commands") + type CommandsDef = Record + const commands: CommandsDef = await import('./commands') + + for (const command in commands) { + const callback = commands[command as CommandContributions] + const desc = PluginManifest.contributes.commands[command as CommandContributions]; + ctx.commands.registerCommand(desc.id, + async (params: any) => await callback(ctx as any, params), + desc + ); + } + } + + { + type UIComponentDefs = Record any> + const uiComponents: UIComponentDefs = await import('./ui') + + for (const key in uiComponents) { + const callback = uiComponents[key as UIComponentKeys] + + ctx.ui?.unstable_registerComponent( + PluginManifest.contributes.ui[key as UIComponentKeys].id, + (params: any) => callback(ctx, params)); + } + } + + + { + // FIXME: This should probablly be done automagically + ctx.logger?.trace("Registering I18N translations") + ctx.i18n.setTranslations({ en: PluginManifest.contributes.i18n }) + } + + + { + ctx.logger?.trace("Subscribing to events"); + const unsubscribe = ctx.engine.event.subscribe([], async (events) => { + events + .filter(e => ctx.engine.block.isValid(e.block) && ctx.engine.block.hasMetadata(e.block, PluginManifest.id)) + .filter(e => e.type === 'Updated') + .forEach(e => handleUpdateEvent(ctx.engine, e.block)) + }); + } + { + const unsubscribe = ctx.engine.event.subscribe([], async (events) => { + events + .filter(({ block: blockId }) => ctx.engine.block.isValid(blockId) && ctx.engine.block.hasMetadata(blockId, PluginManifest.id)) + .forEach(({ type, block: blockId }) => { + if (type === 'Created') { + const metadata = getPluginMetadata(ctx.engine, blockId); + if (isDuplicate(ctx.engine, blockId, metadata)) { + fixDuplicateMetadata(ctx.engine, blockId); + } + } + }); + }); + } + // { + // // THIS DOES not belong here maybe + // ctx.logger?.trace("Defaulting canvas menu order") + // const canvasMenuEntries = [ + // PluginManifest.contributes.ui.button.id, + // ...ctx.ui?.unstable_getCanvasMenuOrder() ?? [] + // ] + // ctx.ui?.unstable_setCanvasMenuOrder(canvasMenuEntries); + // } +} + +// maybe this should be just engine.event.onUpdate() diff --git a/packages/plugin-vectorizer/src/commands.ts b/packages/plugin-vectorizer/src/commands.ts new file mode 100644 index 0000000..6676f50 --- /dev/null +++ b/packages/plugin-vectorizer/src/commands.ts @@ -0,0 +1,197 @@ +import { + getPluginMetadata, + isBlockSupported, + isMetadataConsistent, + recoverInitialImageData, + setPluginMetadata + +} from './utils/common'; + +import { runInWorker } from './utils/worker.shared'; +import { createVectorPathBlocks } from './utils/cesdk+utils'; +import { PluginContext } from './deps'; + +export const vectorize = async (context: PluginContext, params: { blockIds?: number[] }) => { + const uploader: any = undefined; //cesdk.unstable_upload.bind(cesdk) + const {engine} = context; // the only function that needs the ui is the upload function + const blockApi = engine.block; + + + const blockIds = params.blockIds ?? engine.block.findAllSelected(); + + blockIds.forEach(async (blockId: number) => { + // this should happen before already and only be called if the feature is enabled for a certain block + if (!isBlockSupported(engine, blockId)) return; + + + if (!blockApi.hasFill(blockId)) + throw new Error('Block has no fill to vectorize'); + + const fillId = blockApi.getFill(blockId); + + // FIXME: Tis is only needed to tell the engin that we are processing something and it cannot export or save the scene file. + // Practicalle, we are not using the images directly but render the visible part of the block and then process this image + // Get the current image URI and source set as initial values. + const initialSourceSet = blockApi.getSourceSet(fillId, 'fill/image/sourceSet'); + const initialImageFileURI = blockApi.getString(fillId, 'fill/image/imageFileURI'); + const initialPreviewFileURI = blockApi.getString(fillId, 'fill/image/previewFileURI'); + + + const uriToProcess = + // Source sets have priority in the engine + initialSourceSet.length > 0 + ? // Choose the highest resolution image in the source set + initialSourceSet.sort( + (a: any, b: any) => b.width * b.height - a.height * a.width + )[0].uri + : initialImageFileURI; + + if (uriToProcess === undefined || uriToProcess === '') + return; // We shall return early if the uri is not defined or invalid + + try { + // Clear values in the engine to trigger the loading spinner + // @ts-ignore + const blob = await engine.block.export(blockId, "image/png"); + + // go into busy state + blockApi.setString(fillId, 'fill/image/imageFileURI', ''); + blockApi.setSourceSet(fillId, 'fill/image/sourceSet', []); + // ensure we show the last image while processsing. Some images don't have the preview set + if (initialPreviewFileURI === undefined || initialPreviewFileURI === '') { + blockApi.setString(fillId, 'fill/image/previewFileURI', uriToProcess); + } + + const metadata = getPluginMetadata(engine, blockId); + setPluginMetadata(engine, blockId, { + ...metadata, + version: PLUGIN_VERSION, + initialSourceSet, + initialImageFileURI, + blockId, + fillId, + status: 'PROCESSING' + }); + + const vectorized: Blob = await runInWorker(blob) + + if ( + getPluginMetadata(engine, blockId).status !== 'PROCESSING' || + !isMetadataConsistent(engine, blockId) + ) return; + if (engine.block.isValid(blockId)) { + setPluginMetadata(engine, blockId, { + version: PLUGIN_VERSION, + initialSourceSet, + initialImageFileURI, + blockId, + fillId, + status: 'PROCESSED', + }); + } + + + if (vectorized.type.length === 0 || vectorized.type === 'image/svg+xml') { + const pathname = new URL(uriToProcess).pathname; + const parts = pathname.split('/'); + const filename = parts[parts.length - 1]; + + const uploadedAssets = await uploader( + new File([vectorized], filename, { type: vectorized.type }), + () => { + // TODO Delegate process to UI component + } + ); + + // Check for externally changed state while we were uploading and + // do not proceed if the state was reset. + if ( + getPluginMetadata(engine, blockId).status !== 'PROCESSING' || + !isMetadataConsistent(engine, blockId) + ) + return; + + const url = uploadedAssets.meta?.uri;; + if (url == null) { + throw new Error('Could not upload vectorized image'); + } + + // Workaround Processing is done, restore state of the initial block + blockApi.setSourceSet(fillId, 'fill/image/sourceSet', initialSourceSet); + blockApi.setString(fillId, 'fill/image/imageFileURI', initialImageFileURI); + blockApi.setString(fillId, 'fill/image/previewFileURI', initialPreviewFileURI); + + setPluginMetadata(engine, blockId, { + version: PLUGIN_VERSION, + initialSourceSet, + initialImageFileURI, + blockId, + fillId, + status: 'PROCESSED', + }); + + blockApi.setString(fillId, 'fill/image/imageFileURI', url); + } else if (vectorized.type === 'application/json') { + + const json = await vectorized.text() + const blocks = JSON.parse(json) + const blockIds = createVectorPathBlocks(engine, blocks) + + const origRotation = engine.block.getRotation(blockId) + const origX = engine.block.getPositionX(blockId) + const origY = engine.block.getPositionY(blockId) + const origSelected = engine.block.isSelected(blockId) + + switch (engine.block.getType(blockId)) { + case "//ly.img.ubq/page": // this has been disabled + { + const parentId = blockId; + const containerId = engine.block.group(blockIds); + engine.block.appendChild(parentId, containerId); + const scale = engine.block.getFrameWidth(blockId) / engine.block.getFrameWidth(containerId) + engine.block.setPositionX(containerId, origX) + engine.block.setPositionY(containerId, origY) + engine.block.setRotation(containerId, origRotation) + engine.block.scale(containerId, scale) + engine.block.setFillEnabled(parentId, false) + engine.block.setSelected(containerId, origSelected) + break; + } + case "//ly.img.ubq/graphic": + default: { // replace the current block with the a new group of the vectors + const parentId = engine.block.getParent(blockId)! + const containerId = engine.block.group(blockIds); + engine.block.appendChild(parentId, containerId); + const scale = engine.block.getFrameWidth(blockId) / engine.block.getFrameWidth(containerId) + engine.block.setPositionX(containerId, origX) + engine.block.setPositionY(containerId, origY) + engine.block.setRotation(containerId, origRotation) + engine.block.scale(containerId, scale) + engine.block.destroy(blockId) + engine.block.setSelected(containerId, origSelected) + break; + } + } + } + // Finally, create an undo step + engine.editor.addUndoStep(); + + } catch (error) { + if (engine.block.isValid(blockId)) { + setPluginMetadata(engine, blockId, { + version: PLUGIN_VERSION, + initialSourceSet, + initialImageFileURI, + blockId, + fillId, + status: 'ERROR' + }); + + recoverInitialImageData(engine, blockId); + } + // eslint-disable-next-line no-console + console.error(error); + } + }) +} + diff --git a/packages/plugin-vectorizer/src/deps.ts b/packages/plugin-vectorizer/src/deps.ts new file mode 100644 index 0000000..e72ca92 --- /dev/null +++ b/packages/plugin-vectorizer/src/deps.ts @@ -0,0 +1,6 @@ +import { I18NKeys } from "./PluginManifest"; +import { type PluginContext as IPluginContext, CommandCallback } from '@imgly/plugin-core'; +type PluginContext = IPluginContext; + +export { CreativeEngine } from '@cesdk/cesdk-js'; +export { type PluginContext, type CommandCallback } diff --git a/packages/plugin-vectorizer/src/handler.ts b/packages/plugin-vectorizer/src/handler.ts new file mode 100644 index 0000000..16cdf6f --- /dev/null +++ b/packages/plugin-vectorizer/src/handler.ts @@ -0,0 +1,28 @@ +import {CreativeEngine} from "@cesdk/cesdk-js"; + +import { + clearPluginMetadata, + getPluginMetadata, + isMetadataConsistent +} from './utils/common'; +/** + * Handle every possible state of the vectorization state if the block was + * updated. + */ +export async function update(engine: CreativeEngine, blockId: number) { + const metadata = getPluginMetadata(engine, blockId); + switch (metadata.status) { + case 'PROCESSING': + case 'PROCESSED': { + if (!isMetadataConsistent(engine, blockId)) { + clearPluginMetadata(engine, blockId); + } + break; + } + + default: { + // We do not care about other states + } + } + } + \ No newline at end of file diff --git a/packages/plugin-vectorizer/src/index.ts b/packages/plugin-vectorizer/src/index.ts new file mode 100644 index 0000000..ffeba26 --- /dev/null +++ b/packages/plugin-vectorizer/src/index.ts @@ -0,0 +1,26 @@ +import Manifest from '../manifest.json'; +import { PluginContext } from "./deps"; + +import { activate } from './activate'; + + + +export interface PluginConfiguration { + +} + +export { Manifest }; + +export default (ctx: PluginContext, pluginConfiguration: PluginConfiguration) => { + + return { + ...Manifest, + async initializeUserInterface() { + return await activate(ctx) + }, + + // maybe this should be just engine.event.onUpdate() + + }; +}; +'' \ No newline at end of file diff --git a/packages/plugin-vectorizer/src/ui.ts b/packages/plugin-vectorizer/src/ui.ts new file mode 100644 index 0000000..abcf2b2 --- /dev/null +++ b/packages/plugin-vectorizer/src/ui.ts @@ -0,0 +1,29 @@ +import { PluginContext } from './deps'; + +import { PluginManifest } from './PluginManifest'; + +import { + getPluginMetadata, + isBlockSupported, +} from './utils/common'; + + +export const button = (ctx: PluginContext, params: any) => { + const builder = params.builder + + const selected = ctx.engine.block.findAllSelected(); + const candidates = selected.filter((id: number) => isBlockSupported(ctx.engine, id)) + if (candidates.length === 0) return; + const isLoading = candidates.some((id: number) => getPluginMetadata(ctx.engine, id).status === 'PROCESSING') + + // @maerch: Why do we need the Button ID here? + builder.Button(PluginManifest.contributes.ui.button.id, { + label: ctx.i18n.translate("vectorizer.commands.vectorize"), + icon: '@imgly/icons/Vectorize', + isActive: false, + isLoading: isLoading, + isDisabled: isLoading, + loadingProgress: undefined, // creates infinite spinner + onClick: () => ctx.commands.executeCommand(PluginManifest.contributes.commands.vectorize.id, { blockIds: candidates }) + }); +} diff --git a/packages/plugin-vectorizer/src/utils/cesdk+utils.ts b/packages/plugin-vectorizer/src/utils/cesdk+utils.ts new file mode 100644 index 0000000..82fcf7f --- /dev/null +++ b/packages/plugin-vectorizer/src/utils/cesdk+utils.ts @@ -0,0 +1,37 @@ +import { CreativeEngine } from "@cesdk/cesdk-js"; + +export const createVectorPathBlocks = (engine: CreativeEngine, blocks: any[]) => { + const blockIds = blocks.map((block: any) => { + const id = createVectorPathBlock( + engine, block + ) + + return id + }) + + return blockIds +} + + +export const createVectorPathBlock = (engine: CreativeEngine, block: any) => { + + const path = block.shape.path + const color = block.fill.color + const blockId = engine.block.create("//ly.img.ubq/graphic"); + engine.block.setKind(blockId, "shape"); + const shape = engine.block.createShape("//ly.img.ubq/shape/vector_path"); + engine.block.setShape(blockId, shape); + + engine.block.setString(shape, "vector_path/path", path); + engine.block.setFloat(shape, "vector_path/width", block.transform.width); + engine.block.setFloat(shape, "vector_path/height", block.transform.height); + + const fill = engine.block.createFill("color"); + engine.block.setColor(fill, "fill/color/value", { r: color[0], g: color[1], b: color[2], a: color[3] }); + engine.block.setFill(blockId, fill); + engine.block.setFloat(blockId, "width", block.transform.width); + engine.block.setFloat(blockId, "height", block.transform.height); + engine.block.setPositionX(blockId, block.transform.x) + engine.block.setPositionY(blockId, block.transform.y) + return blockId; +} diff --git a/packages/vectorizer/src/utils.ts b/packages/plugin-vectorizer/src/utils/common.ts similarity index 52% rename from packages/vectorizer/src/utils.ts rename to packages/plugin-vectorizer/src/utils/common.ts index 26f621e..9b2bd81 100644 --- a/packages/vectorizer/src/utils.ts +++ b/packages/plugin-vectorizer/src/utils/common.ts @@ -1,7 +1,6 @@ -import type CreativeEditorSDK from '@cesdk/cesdk-js'; +import { CreativeEngine } from '@cesdk/cesdk-js'; import isEqual from 'lodash/isEqual'; -import { PLUGIN_ID } from './constants'; import { PluginMetadata, PluginStatusError, @@ -9,15 +8,44 @@ import { PluginStatusProcessing } from './types'; +import { PluginManifest as manifest } from '../PluginManifest'; + + +export const areBlocksSupported = (engine: CreativeEngine, blockIds: number[]) => { + return blockIds.some(id => isBlockSupported(engine, id)) +} +/** + * Checks if a block is supported by the given CreativeEngine. + * @param engine - The CreativeEngine instance. + * @param blockId - The ID of the block to check. + * @returns A boolean indicating whether the block is supported or not. + */ +export const isBlockSupported = (engine: CreativeEngine, blockId: number) => { + if (!engine.block.isValid(blockId)) return false; + const blockType = engine.block.getType(blockId); + if (blockType === "//ly.img.ubq/page") return false; // There is some bug with the page block + + if (engine.block.hasFill(blockId)) { + const fillId = engine.block.getFill(blockId); + const fillType = engine.block.getType(fillId); + if (fillType !== '//ly.img.ubq/fill/image') { + return false; + } + return true + } + + return false; +} + /** * Sets the metadata for the plugin state. */ export function setPluginMetadata( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, id: number, metadata: PluginMetadata ) { - cesdk.engine.block.setMetadata(id, PLUGIN_ID, JSON.stringify(metadata)); + engine.block.setMetadata(id, manifest.id, JSON.stringify(metadata)); } /** @@ -25,11 +53,11 @@ export function setPluginMetadata( * is set on the given block, it will return an IDLE state. */ export function getPluginMetadata( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, id: number ): PluginMetadata { - if (cesdk.engine.block.hasMetadata(id, PLUGIN_ID)) { - return JSON.parse(cesdk.engine.block.getMetadata(id, PLUGIN_ID)); + if (engine.block.hasMetadata(id, manifest.id)) { + return JSON.parse(engine.block.getMetadata(id, manifest.id)); } else { return { status: 'IDLE' @@ -40,9 +68,9 @@ export function getPluginMetadata( /** * If plugin metadata is set, it will be cleared. */ -export function clearPluginMetadata(cesdk: CreativeEditorSDK, id: number) { - if (cesdk.engine.block.hasMetadata(id, PLUGIN_ID)) { - cesdk.engine.block.removeMetadata(id, PLUGIN_ID); +export function clearPluginMetadata(engine: CreativeEngine, id: number) { + if (engine.block.hasMetadata(id, manifest.id)) { + engine.block.removeMetadata(id, manifest.id); } } @@ -51,20 +79,16 @@ export function clearPluginMetadata(cesdk: CreativeEditorSDK, id: number) { * In that case the plugin state is still valid, but blockId and fillId have changed. */ export function isDuplicate( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, blockId: number, metadata: PluginMetadata ): boolean { - if (!cesdk.engine.block.isValid(blockId)) return false; - if ( - metadata.status === 'IDLE' || - metadata.status === 'PENDING' || - metadata.status === 'ERROR' - ) + if (!engine.block.isValid(blockId)) return false; + if (metadata.status === 'IDLE' || metadata.status === 'ERROR') return false; - if (!cesdk.engine.block.hasFill(blockId)) return false; - const fillId = cesdk.engine.block.getFill(blockId); + if (!engine.block.hasFill(blockId)) return false; + const fillId = engine.block.getFill(blockId); // It cannot be a duplicate if the blockId or fillId are the same if (metadata.blockId === blockId || metadata.fillId === fillId) return false; @@ -79,18 +103,18 @@ export function isDuplicate( * Please note: Call this method only on duplicates (see isDuplicate). */ export function fixDuplicateMetadata( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, blockId: number ) { - const fillId = cesdk.engine.block.getFill(blockId); - const metadata = getPluginMetadata(cesdk, blockId); + const fillId = engine.block.getFill(blockId); + const metadata = getPluginMetadata(engine, blockId); if ( metadata.status === 'IDLE' || - metadata.status === 'PENDING' || + // metadata.status === 'PENDING' || metadata.status === 'ERROR' ) return; - setPluginMetadata(cesdk, blockId, { + setPluginMetadata(engine, blockId, { ...metadata, blockId, fillId @@ -104,26 +128,28 @@ export function fixDuplicateMetadata( * @returns true if the metadata is consistent, false otherwise */ export function isMetadataConsistent( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, blockId: number ): boolean { // In case the block was removed, we just abort and mark it // as reset by returning true - if (!cesdk.engine.block.isValid(blockId)) return false; - const metadata = getPluginMetadata(cesdk, blockId); - if (metadata.status === 'IDLE' || metadata.status === 'PENDING') return true; + if (!engine.block.isValid(blockId)) return false; + const metadata = getPluginMetadata(engine, blockId); + if (metadata.status === 'IDLE' + // || metadata.status === 'PENDING' + ) return true; - if (!cesdk.engine.block.hasFill(blockId)) return false; - const fillId = cesdk.engine.block.getFill(blockId); + if (!engine.block.hasFill(blockId)) return false; + const fillId = engine.block.getFill(blockId); if (fillId == null) return false; if (blockId !== metadata.blockId || fillId !== metadata.fillId) return false; - const sourceSet = cesdk.engine.block.getSourceSet( + const sourceSet = engine.block.getSourceSet( fillId, 'fill/image/sourceSet' ); - const imageFileURI = cesdk.engine.block.getString( + const imageFileURI = engine.block.getString( fillId, 'fill/image/imageFileURI' ); @@ -145,12 +171,11 @@ export function isMetadataConsistent( // If we have already processed the image, we need to check if the source set // we need to check against both source sets, the removed and the initial if ( - metadata.status === 'PROCESSED_TOGGLE_OFF' || - metadata.status === 'PROCESSED_TOGGLE_ON' + metadata.status === 'PROCESSED' ) { - const processedAsset = metadata.processedAsset; + // const processedAsset = metadata.processedAsset; if ( - !isEqual(sourceSet, processedAsset) && + // !isEqual(sourceSet, processedAsset) && !isEqual(sourceSet, initialSourceSet) ) { return false; @@ -161,13 +186,10 @@ export function isMetadataConsistent( } } } else { - if ( - metadata.status === 'PROCESSED_TOGGLE_OFF' || - metadata.status === 'PROCESSED_TOGGLE_ON' - ) { + if (metadata.status === 'PROCESSED') { if ( - imageFileURI !== metadata.initialImageFileURI && - imageFileURI !== metadata.processedAsset + imageFileURI !== metadata.initialImageFileURI + // &&imageFileURI !== metadata.processedAsset ) { return false; } @@ -180,92 +202,41 @@ export function isMetadataConsistent( return true; } -/** - * Toggle between the vectorized image and the original image if either - * in the state "PROCESSED_TOGGLE_OFF" or "PROCESSED_TOGGLE_ON". Otherwise do - * nothing. - */ -export function toggleProcessedData(cesdk: CreativeEditorSDK, blockId: number) { - const blockApi = cesdk.engine.block; - if (!blockApi.hasFill(blockId)) return; // Nothing to recover (no fill anymore) - const fillId = blockApi.getFill(blockId); - - const metadata = getPluginMetadata(cesdk, blockId); - - if (metadata.status === 'PROCESSED_TOGGLE_OFF') { - setPluginMetadata(cesdk, blockId, { - ...metadata, - status: 'PROCESSED_TOGGLE_ON' - }); - - if (typeof metadata.processedAsset === 'string') { - blockApi.setString( - fillId, - 'fill/image/imageFileURI', - metadata.processedAsset - ); - blockApi.setSourceSet(fillId, 'fill/image/sourceSet', []); - } else { - blockApi.setSourceSet( - fillId, - 'fill/image/sourceSet', - metadata.processedAsset - ); - } - - cesdk.engine.editor.addUndoStep(); - } else if (metadata.status === 'PROCESSED_TOGGLE_ON') { - setPluginMetadata(cesdk, blockId, { - ...metadata, - status: 'PROCESSED_TOGGLE_OFF' - }); - - blockApi.setString( - fillId, - 'fill/image/imageFileURI', - metadata.initialImageFileURI - ); - blockApi.setSourceSet( - fillId, - 'fill/image/sourceSet', - metadata.initialSourceSet - ); - cesdk.engine.editor.addUndoStep(); - } -} /** * Recover the initial values to avoid the loading spinner and have the same * state as before the process was started. */ export function recoverInitialImageData( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, blockId: number ) { - const blockApi = cesdk.engine.block; + const blockApi = engine.block; if (!blockApi.hasFill(blockId)) return; // Nothing to recover (no fill anymore) - const metadata = getPluginMetadata(cesdk, blockId); + const metadata = getPluginMetadata(engine, blockId); - if (metadata.status === 'PENDING' || metadata.status === 'IDLE') { + if ( + // metadata.status === 'PENDING' || + metadata.status === 'IDLE') { return; } const initialSourceSet = metadata.initialSourceSet; const initialImageFileURI = metadata.initialImageFileURI; - const fillId = getValidFill(cesdk, blockId, metadata); + const fillId = getValidFill(engine, blockId, metadata); if (fillId == null) return; if (initialImageFileURI) { - cesdk.engine.block.setString( + engine.block.setString( fillId, 'fill/image/imageFileURI', initialImageFileURI ); } if (initialSourceSet.length > 0) { - cesdk.engine.block.setSourceSet( + engine.block.setSourceSet( fillId, 'fill/image/sourceSet', initialSourceSet @@ -278,21 +249,51 @@ export function recoverInitialImageData( * vectorizing. Returns undefined otherwise. */ function getValidFill( - cesdk: CreativeEditorSDK, + engine: CreativeEngine, blockId: number, metadata: PluginStatusProcessing | PluginStatusError | PluginStatusProcessed ): number | undefined { if ( - !cesdk.engine.block.isValid(blockId) || - !cesdk.engine.block.hasFill(blockId) || + !engine.block.isValid(blockId) || + !engine.block.hasFill(blockId) || blockId !== metadata.blockId ) { return undefined; } - const fillId = cesdk.engine.block.getFill(blockId); + const fillId = engine.block.getFill(blockId); if (fillId !== metadata.fillId) { return undefined; } return fillId; } + + + +// These don't belong here + +export class Scheduler { + #queue?: Promise = undefined + + async schedule(task: () => Promise): Promise { + if (this.#queue === undefined) { + this.#queue = task() + } else { + this.#queue = this.#queue.then(async () => { + return await task() + }) + } + return this.#queue + } +} + + +/** + * Generates a unique filename. + * @returns A string representing the unique filename. + */ +export function generateUniqueFilename(): string { + const timestamp = Date.now(); + const randomString = Math.random().toString(36).substring(2, 8); + return `${timestamp}_${randomString}`; +} \ No newline at end of file diff --git a/packages/vectorizer/src/types.ts b/packages/plugin-vectorizer/src/utils/types.ts similarity index 77% rename from packages/vectorizer/src/types.ts rename to packages/plugin-vectorizer/src/utils/types.ts index f286dcc..657f9de 100644 --- a/packages/vectorizer/src/types.ts +++ b/packages/plugin-vectorizer/src/utils/types.ts @@ -1,7 +1,7 @@ -import { type Source } from '@cesdk/cesdk-js'; +import { type Source, type CreativeEngine } from '@cesdk/cesdk-js'; export type PluginStatusIdle = { status: 'IDLE' }; -export type PluginStatusPending = { status: 'PENDING' }; +// export type PluginStatusPending = { status: 'PENDING' }; export type PluginStatusProcessing = { version: string; @@ -22,15 +22,13 @@ export type PluginStatusProcessing = { export type PluginStatusProcessed = { version: string; - status: 'PROCESSED_TOGGLE_ON' | 'PROCESSED_TOGGLE_OFF'; + status: 'PROCESSED' initialImageFileURI: string; initialSourceSet: Source[]; blockId: number; fillId: number; - - processedAsset: string | Source[]; }; export type PluginStatusError = { @@ -47,6 +45,8 @@ export type PluginStatusError = { export type PluginMetadata = | PluginStatusIdle | PluginStatusError - | PluginStatusPending | PluginStatusProcessing | PluginStatusProcessed; + + + diff --git a/packages/plugin-vectorizer/src/utils/worker.shared.ts b/packages/plugin-vectorizer/src/utils/worker.shared.ts new file mode 100644 index 0000000..c270865 --- /dev/null +++ b/packages/plugin-vectorizer/src/utils/worker.shared.ts @@ -0,0 +1,25 @@ +export interface MessageBody { + method: 'health' | 'imageToJson' | 'imageToSvg' + data?: any; + error?: Error +} + +const TIMEOUT_DEFAULT = 10000 +// we need a timeout +export const runInWorker = (blob: Blob) => new Promise((resolve, reject) => { + const worker = new Worker(new URL('./worker', import.meta.url), { type: 'module' }); + const msg: MessageBody = { method: "imageToJson", data: blob } + setTimeout(() => reject(new Error("Timeout")), TIMEOUT_DEFAULT); + worker.postMessage(msg) + worker.onmessage = (e: MessageEvent) => { + const msg = e.data + if (msg.error) { + reject(msg.error) + return; + } + resolve(msg.data) + // when done terminate + worker.terminate() + } + +}) diff --git a/packages/plugin-vectorizer/src/worker.ts b/packages/plugin-vectorizer/src/worker.ts new file mode 100644 index 0000000..8fd1eab --- /dev/null +++ b/packages/plugin-vectorizer/src/worker.ts @@ -0,0 +1,34 @@ + +import type { MessageBody } from "./utils/worker.shared"; +import * as vectorizer from '@imgly/vectorizer'; + +self.onmessage = async function (e: MessageEvent) { + try { + const msg = e.data; 0 + const data = msg.data ?? {} + const method = msg.method ?? '' // default to empty string + switch (method) { + case "health": { + postMessage({ data: 'ok' }) + break; + } + case "imageToJson": + { + const json = await vectorizer.imageToJson(data) + postMessage({ data: json }); + break; + } + case "imageToSvg": + { + const svg = await vectorizer.imageToSvg(data) + postMessage({ data: svg }); + + break; + } + default: + postMessage({ error: new Error("Unknown method") }); + } + } catch (err) { + postMessage({ error: err }); + } +} \ No newline at end of file diff --git a/packages/plugin-vectorizer/tsconfig.json b/packages/plugin-vectorizer/tsconfig.json new file mode 100644 index 0000000..445b415 --- /dev/null +++ b/packages/plugin-vectorizer/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es2017", + "module": "es2020", + "lib": ["es2018", "dom"], + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "declaration": true, + "declarationDir": "types/", + "skipLibCheck": true + }, + "include": ["src/**/*", "esbuild/global.d.ts", "manifest.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/plugin-vectorizer/types/PluginManifest.d.ts b/packages/plugin-vectorizer/types/PluginManifest.d.ts new file mode 100644 index 0000000..7913e0c --- /dev/null +++ b/packages/plugin-vectorizer/types/PluginManifest.d.ts @@ -0,0 +1,70 @@ +import Manifest from '../manifest.json'; +type ManifestType = typeof Manifest; +export type I18NKeys = keyof ManifestType["contributes"]["i18n"]; +export type CommandContributions = keyof ManifestType["contributes"]["commands"]; +export type ErrorKeys = keyof ManifestType["contributes"]["errors"]; +export type UIComponentKeys = keyof ManifestType["contributes"]["ui"]; +export type ConfigKeys = keyof ManifestType["contributes"]["config"]; +export declare const PluginManifest: { + id: string; + version: string; + publisher: string; + icon: string; + license: string; + pricing: string; + payment: { + oneTime: number; + subscription: number; + usage: number; + }; + engines: { + imgly: string; + }; + categories: string[]; + contributes: { + ui: { + button: { + id: string; + label: string; + }; + }; + commands: { + vectorize: { + id: string; + label: string; + group: string; + args: { + type: string; + format: string; + ui: string; + }[]; + returns: {}; + }; + }; + i18n: { + "vectorizer.commands.vectorize": string; + "vectorizer.commands.category": string; + "vectorizer.config.title": string; + "vectorizer.config.enabled": string; + "vectorizer.config.enabled.description": string; + }; + errors: { + validationError: { + id: string; + message: string; + }; + }; + config: { + type: string; + title: string; + settings: { + enabled: { + type: string; + default: boolean; + description: string; + }; + }; + }; + }; +}; +export {}; diff --git a/packages/plugin-vectorizer/types/activate.d.ts b/packages/plugin-vectorizer/types/activate.d.ts new file mode 100644 index 0000000..2c2cf5a --- /dev/null +++ b/packages/plugin-vectorizer/types/activate.d.ts @@ -0,0 +1,2 @@ +import { PluginContext } from './deps'; +export declare function activate(ctx: PluginContext): Promise; diff --git a/packages/plugin-vectorizer/types/commands.d.ts b/packages/plugin-vectorizer/types/commands.d.ts new file mode 100644 index 0000000..2932810 --- /dev/null +++ b/packages/plugin-vectorizer/types/commands.d.ts @@ -0,0 +1,4 @@ +import { PluginContext } from './deps'; +export declare const vectorize: (context: PluginContext, params: { + blockIds?: number[]; +}) => Promise; diff --git a/packages/plugin-vectorizer/types/deps.d.ts b/packages/plugin-vectorizer/types/deps.d.ts new file mode 100644 index 0000000..ab2960b --- /dev/null +++ b/packages/plugin-vectorizer/types/deps.d.ts @@ -0,0 +1,5 @@ +import { I18NKeys } from "./PluginManifest"; +import { type PluginContext as IPluginContext, CommandCallback } from '@imgly/plugin-core'; +type PluginContext = IPluginContext; +export { CreativeEngine } from '@cesdk/cesdk-js'; +export { type PluginContext, type CommandCallback }; diff --git a/packages/plugin-vectorizer/types/handler.d.ts b/packages/plugin-vectorizer/types/handler.d.ts new file mode 100644 index 0000000..35d222b --- /dev/null +++ b/packages/plugin-vectorizer/types/handler.d.ts @@ -0,0 +1,6 @@ +import { CreativeEngine } from "@cesdk/cesdk-js"; +/** + * Handle every possible state of the vectorization state if the block was + * updated. + */ +export declare function update(engine: CreativeEngine, blockId: number): Promise; diff --git a/packages/plugin-vectorizer/types/index.d.ts b/packages/plugin-vectorizer/types/index.d.ts new file mode 100644 index 0000000..0cc8e20 --- /dev/null +++ b/packages/plugin-vectorizer/types/index.d.ts @@ -0,0 +1,69 @@ +import Manifest from '../manifest.json'; +import { PluginContext } from "./deps"; +export interface PluginConfiguration { +} +export { Manifest }; +declare const _default: (ctx: PluginContext, pluginConfiguration: PluginConfiguration) => { + initializeUserInterface(): Promise; + id: string; + version: string; + publisher: string; + icon: string; + license: string; + pricing: string; + payment: { + oneTime: number; + subscription: number; + usage: number; + }; + engines: { + imgly: string; + }; + categories: string[]; + contributes: { + ui: { + button: { + id: string; + label: string; + }; + }; + commands: { + vectorize: { + id: string; + label: string; + group: string; + args: { + type: string; + format: string; + ui: string; + }[]; + returns: {}; + }; + }; + i18n: { + "vectorizer.commands.vectorize": string; + "vectorizer.commands.category": string; + "vectorizer.config.title": string; + "vectorizer.config.enabled": string; + "vectorizer.config.enabled.description": string; + }; + errors: { + validationError: { + id: string; + message: string; + }; + }; + config: { + type: string; + title: string; + settings: { + enabled: { + type: string; + default: boolean; + description: string; + }; + }; + }; + }; +}; +export default _default; diff --git a/packages/plugin-vectorizer/types/ui.d.ts b/packages/plugin-vectorizer/types/ui.d.ts new file mode 100644 index 0000000..943cb1c --- /dev/null +++ b/packages/plugin-vectorizer/types/ui.d.ts @@ -0,0 +1,2 @@ +import { PluginContext } from './deps'; +export declare const button: (ctx: PluginContext, params: any) => void; diff --git a/packages/plugin-vectorizer/types/utils/cesdk+utils.d.ts b/packages/plugin-vectorizer/types/utils/cesdk+utils.d.ts new file mode 100644 index 0000000..a5b5a9e --- /dev/null +++ b/packages/plugin-vectorizer/types/utils/cesdk+utils.d.ts @@ -0,0 +1,3 @@ +import { CreativeEngine } from "@cesdk/cesdk-js"; +export declare const createVectorPathBlocks: (engine: CreativeEngine, blocks: any[]) => number[]; +export declare const createVectorPathBlock: (engine: CreativeEngine, block: any) => number; diff --git a/packages/plugin-vectorizer/types/utils/common.d.ts b/packages/plugin-vectorizer/types/utils/common.d.ts new file mode 100644 index 0000000..2166d3f --- /dev/null +++ b/packages/plugin-vectorizer/types/utils/common.d.ts @@ -0,0 +1,56 @@ +import { CreativeEngine } from '@cesdk/cesdk-js'; +import { PluginMetadata } from './types'; +export declare const areBlocksSupported: (engine: CreativeEngine, blockIds: number[]) => boolean; +/** + * Checks if a block is supported by the given CreativeEngine. + * @param engine - The CreativeEngine instance. + * @param blockId - The ID of the block to check. + * @returns A boolean indicating whether the block is supported or not. + */ +export declare const isBlockSupported: (engine: CreativeEngine, blockId: number) => boolean; +/** + * Sets the metadata for the plugin state. + */ +export declare function setPluginMetadata(engine: CreativeEngine, id: number, metadata: PluginMetadata): void; +/** + * Returns the current metadata for the plugin state. If no metadata + * is set on the given block, it will return an IDLE state. + */ +export declare function getPluginMetadata(engine: CreativeEngine, id: number): PluginMetadata; +/** + * If plugin metadata is set, it will be cleared. + */ +export declare function clearPluginMetadata(engine: CreativeEngine, id: number): void; +/** + * Detect if the block has been duplicated with processed or processing state. + * In that case the plugin state is still valid, but blockId and fillId have changed. + */ +export declare function isDuplicate(engine: CreativeEngine, blockId: number, metadata: PluginMetadata): boolean; +/** + * Fixes the metadata if the block has been duplicated, i.e. the blockId and + * fillId will be updated to the current block/fill. + * + * Please note: Call this method only on duplicates (see isDuplicate). + */ +export declare function fixDuplicateMetadata(engine: CreativeEngine, blockId: number): void; +/** + * Check if the image has a consisten metadata state. A inconsistent state is + * caused by outside changes of the fill data. + * + * @returns true if the metadata is consistent, false otherwise + */ +export declare function isMetadataConsistent(engine: CreativeEngine, blockId: number): boolean; +/** + * Recover the initial values to avoid the loading spinner and have the same + * state as before the process was started. + */ +export declare function recoverInitialImageData(engine: CreativeEngine, blockId: number): void; +export declare class Scheduler { + #private; + schedule(task: () => Promise): Promise; +} +/** + * Generates a unique filename. + * @returns A string representing the unique filename. + */ +export declare function generateUniqueFilename(): string; diff --git a/packages/plugin-vectorizer/types/utils/types.d.ts b/packages/plugin-vectorizer/types/utils/types.d.ts new file mode 100644 index 0000000..c00d57f --- /dev/null +++ b/packages/plugin-vectorizer/types/utils/types.d.ts @@ -0,0 +1,34 @@ +import { type Source } from '@cesdk/cesdk-js'; +export type PluginStatusIdle = { + status: 'IDLE'; +}; +export type PluginStatusProcessing = { + version: string; + status: 'PROCESSING'; + initialImageFileURI: string; + initialSourceSet: Source[]; + blockId: number; + fillId: number; + progress?: { + key: string; + current: number; + total: number; + }; +}; +export type PluginStatusProcessed = { + version: string; + status: 'PROCESSED'; + initialImageFileURI: string; + initialSourceSet: Source[]; + blockId: number; + fillId: number; +}; +export type PluginStatusError = { + version: string; + status: 'ERROR'; + initialImageFileURI: string; + initialSourceSet: Source[]; + blockId: number; + fillId: number; +}; +export type PluginMetadata = PluginStatusIdle | PluginStatusError | PluginStatusProcessing | PluginStatusProcessed; diff --git a/packages/plugin-vectorizer/types/utils/worker.shared.d.ts b/packages/plugin-vectorizer/types/utils/worker.shared.d.ts new file mode 100644 index 0000000..bf21f38 --- /dev/null +++ b/packages/plugin-vectorizer/types/utils/worker.shared.d.ts @@ -0,0 +1,6 @@ +export interface MessageBody { + method: 'health' | 'imageToJson' | 'imageToSvg'; + data?: any; + error?: Error; +} +export declare const runInWorker: (blob: Blob) => Promise; diff --git a/packages/plugin-vectorizer/types/worker.d.ts b/packages/plugin-vectorizer/types/worker.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/plugin-vectorizer/types/worker.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/vectorizer/src/constants.ts b/packages/vectorizer/src/constants.ts deleted file mode 100644 index 241c544..0000000 --- a/packages/vectorizer/src/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const PLUGIN_ID = '@imgly/plugin-vectorizer-web'; -export const CANVAS_MENU_COMPONENT_ID = `${PLUGIN_ID}.canvasMenu`; -export const CANVAS_MENU_COMPONENT_BUTTON_ID = `${CANVAS_MENU_COMPONENT_ID}.button`; -export const FEATURE_ID = `${PLUGIN_ID}.feature`; -export const I18N_ID = "plugin.vectorizer.vectorize" -export const I18N_TRANSLATIONS = { - en: { [I18N_ID]: 'Vectorize' }, - de: { [I18N_ID]: 'Vektorisieren' } -} -export const ICON = '@imgly/icons/Vectorize' \ No newline at end of file diff --git a/packages/vectorizer/src/enableFeatures.ts b/packages/vectorizer/src/enableFeatures.ts deleted file mode 100644 index eaa865b..0000000 --- a/packages/vectorizer/src/enableFeatures.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type CreativeEditorSDK from '@cesdk/cesdk-js'; -import { FEATURE_ID } from './constants'; -/** - * Defines the feature that determines in which context (on which block) - * background removal is allowed/enabled. - */ -export function enableFeatures(cesdk: CreativeEditorSDK) { - cesdk.feature.unstable_enable(FEATURE_ID, ({ engine }) => { - const selectedIds = engine.block.findAllSelected(); - if (selectedIds.length !== 1) { - return false; - } - const [selectedId] = selectedIds; - - if (cesdk.engine.block.hasFill(selectedId)) { - const fillId = cesdk.engine.block.getFill(selectedId); - const fillType = cesdk.engine.block.getType(fillId); - - if (fillType !== '//ly.img.ubq/fill/image') { - return false; - } - return true - - } - - return false; - }); -} diff --git a/packages/vectorizer/src/index.ts b/packages/vectorizer/src/index.ts deleted file mode 100644 index a24e34c..0000000 --- a/packages/vectorizer/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import plugin, { type PluginConfiguration } from './plugin'; - -import { PLUGIN_ID } from './constants'; - -const Plugin = (pluginConfiguration?: PluginConfiguration) => ({ - name: PLUGIN_ID, - version: PLUGIN_VERSION, - ...plugin(pluginConfiguration) -}); - -export default Plugin; diff --git a/packages/vectorizer/src/plugin.ts b/packages/vectorizer/src/plugin.ts deleted file mode 100644 index 709041e..0000000 --- a/packages/vectorizer/src/plugin.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type CreativeEditorSDK from '@cesdk/cesdk-js'; - -import { FEATURE_ID, PLUGIN_ID } from './constants'; -import { enableFeatures } from './enableFeatures'; -import { processVectorization } from './processVectorization'; -import { registerComponents } from './registerComponents'; -import { - clearPluginMetadata, - fixDuplicateMetadata, - getPluginMetadata, - isDuplicate, - isMetadataConsistent -} from './utils'; - -export interface PluginConfiguration {} - -export default (pluginConfiguration: PluginConfiguration = {}) => { - return { - initialize() {}, - - update() {}, - - initializeUserInterface({ cesdk }: { cesdk: CreativeEditorSDK }) { - cesdk.engine.event.subscribe([], async (events) => { - events.forEach((e) => { - const id = e.block; - if ( - !cesdk.engine.block.isValid(id) || - !cesdk.engine.block.hasMetadata(id, PLUGIN_ID) - ) { - return; - } - - if (e.type === 'Created') { - const metadata = getPluginMetadata(cesdk, id); - if (isDuplicate(cesdk, id, metadata)) { - fixDuplicateMetadata(cesdk, id); - } - } else if (e.type === 'Updated') { - handleUpdateEvent(cesdk, id); - } - }); - }); - - registerComponents(cesdk); - enableFeatures(cesdk); - } - }; -}; - -/** - * Handle every possible state of the vectorization state if the block was - * updated. - */ -async function handleUpdateEvent(cesdk: CreativeEditorSDK, blockId: number) { - const metadata = getPluginMetadata(cesdk, blockId); - - switch (metadata.status) { - case 'PENDING': { - if ( - cesdk.feature.unstable_isEnabled(FEATURE_ID, { - engine: cesdk.engine - }) - ) { - processVectorization(cesdk, blockId); - } - break; - } - - case 'PROCESSING': - case 'PROCESSED_TOGGLE_OFF': - case 'PROCESSED_TOGGLE_ON': { - if (!isMetadataConsistent(cesdk, blockId)) { - clearPluginMetadata(cesdk, blockId); - } - break; - } - - default: { - // We do not care about other states - } - } -} diff --git a/packages/vectorizer/src/processVectorization.ts b/packages/vectorizer/src/processVectorization.ts deleted file mode 100644 index 3ea414a..0000000 --- a/packages/vectorizer/src/processVectorization.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type CreativeEditorSDK from '@cesdk/cesdk-js'; -import { imageToSvg } from '@imgly/vectorizer'; - -import { - getPluginMetadata, - isMetadataConsistent, - recoverInitialImageData, - setPluginMetadata -} from './utils'; - -class Scheduler { - #queue?: Promise = undefined - - async schedule(task: () => Promise): Promise { - if (this.#queue === undefined) { - this.#queue = task() - } else { - this.#queue = this.#queue.then(async () => { - return await task() - }) - } - return this.#queue - } -} - -let scheduler = new Scheduler() - - -/** - * Apply the vectorization process to the image. - */ -async function vectorize(uri: string): Promise { - const blob = await fetch(uri).then((res) => res.blob()) - const outBlob = await scheduler.schedule(async () => await imageToSvg(blob)); - return outBlob; -} - -/** - * Triggers the vectiorize process. - */ -export async function processVectorization( - cesdk: CreativeEditorSDK, - blockId: number -) { - const blockApi = cesdk.engine.block; - if (!blockApi.hasFill(blockId)) - throw new Error('Block has no fill to vectorize'); - - const fillId = blockApi.getFill(blockId); - - // Get the current image URI and source set as initial values. - const initialSourceSet = blockApi.getSourceSet( - fillId, - 'fill/image/sourceSet' - ); - const initialImageFileURI = blockApi.getString( - fillId, - 'fill/image/imageFileURI' - ); - const initialPreviewFileURI = blockApi.getString( - fillId, - 'fill/image/previewFileURI' - ); - - - const uriToProcess = - // Source sets have priority in the engine - initialSourceSet.length > 0 - ? // Choose the highest resolution image in the source set - initialSourceSet.sort( - (a, b) => b.width * b.height - a.height * a.width - )[0].uri - : initialImageFileURI; - - if (uriToProcess === undefined || uriToProcess === '') - return; // We shall return early if the uri is not defined or invalid - - - - try { - // Clear values in the engine to trigger the loading spinner - +9 - blockApi.setString(fillId, 'fill/image/imageFileURI', ''); - blockApi.setSourceSet(fillId, 'fill/image/sourceSet', []); - // ensure we show the last image while processsing. Some images don't have the preview set - if (initialPreviewFileURI === undefined || initialPreviewFileURI === '') { - blockApi.setString(fillId, 'fill/image/previewFileURI', uriToProcess); - } - const metadata = getPluginMetadata(cesdk, blockId); - setPluginMetadata(cesdk, blockId, { - ...metadata, - version: PLUGIN_VERSION, - initialSourceSet, - initialImageFileURI, - blockId, - fillId, - status: 'PROCESSING' - }); - - // Creating the mask from the highest resolution image - const vectorized = await vectorize(uriToProcess); - - // Check for externally changed state while we were uploading and - // do not proceed if the state was reset. - if ( - getPluginMetadata(cesdk, blockId).status !== 'PROCESSING' || - !isMetadataConsistent(cesdk, blockId) - ) - return; - - const pathname = new URL(uriToProcess).pathname; - const parts = pathname.split('/'); - const filename = parts[parts.length - 1]; - - const uploadedAssets = await cesdk.unstable_upload( - new File([vectorized], filename, { type: vectorized.type }), - () => { - // TODO Delegate process to UI component - } - ); - - // Check for externally changed state while we were uploading and - // do not proceed if the state was reset. - if ( - getPluginMetadata(cesdk, blockId).status !== 'PROCESSING' || - !isMetadataConsistent(cesdk, blockId) - ) - return; - - const url = uploadedAssets.meta?.uri; - if (url == null) { - throw new Error('Could not upload vectorized image'); - } - - setPluginMetadata(cesdk, blockId, { - version: PLUGIN_VERSION, - initialSourceSet, - initialImageFileURI, - blockId, - fillId, - status: 'PROCESSED_TOGGLE_ON', - processedAsset: url - }); - blockApi.setString(fillId, 'fill/image/imageFileURI', url); - // Finally, create an undo step - cesdk.engine.editor.addUndoStep(); - } catch (error) { - if (cesdk.engine.block.isValid(blockId)) { - setPluginMetadata(cesdk, blockId, { - version: PLUGIN_VERSION, - initialSourceSet, - initialImageFileURI, - blockId, - fillId, - status: 'ERROR' - }); - - recoverInitialImageData(cesdk, blockId); - } - // eslint-disable-next-line no-console - console.error(error); - } -} diff --git a/packages/vectorizer/src/registerComponents.ts b/packages/vectorizer/src/registerComponents.ts deleted file mode 100644 index e05622f..0000000 --- a/packages/vectorizer/src/registerComponents.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type CreativeEditorSDK from '@cesdk/cesdk-js'; - -import { - CANVAS_MENU_COMPONENT_BUTTON_ID, - CANVAS_MENU_COMPONENT_ID, - FEATURE_ID, - I18N_ID, - I18N_TRANSLATIONS, - ICON -} from './constants'; -import { - getPluginMetadata, - setPluginMetadata, - toggleProcessedData -} from './utils'; - -/** - * Registers the components that can be used to vectorize a block. - */ -export function registerComponents(cesdk: CreativeEditorSDK) { - cesdk.setTranslations(I18N_TRANSLATIONS); - // Always prepend the registered component to the canvas menu order. - cesdk.ui.unstable_setCanvasMenuOrder([ - CANVAS_MENU_COMPONENT_ID, - ...cesdk.ui.unstable_getCanvasMenuOrder() - ]); - cesdk.ui.unstable_registerComponent( - CANVAS_MENU_COMPONENT_ID, - ({ builder: { Button }, engine }) => { - if ( - !cesdk.feature.unstable_isEnabled(FEATURE_ID, { - engine - }) - ) { - return; - } - - const [id] = engine.block.findAllSelected(); - if (!cesdk.engine.block.hasFill(id)) return; - - const fillId = cesdk.engine.block.getFill(id); - const fileUri = engine.block.getString(fillId, 'fill/image/imageFileURI'); - const sourceSet = engine.block.getSourceSet( - fillId, - 'fill/image/sourceSet' - ); - - const hasNoValidFill = !(sourceSet.length > 0 || fileUri !== '') - - const metadata = getPluginMetadata(cesdk, id); - - const isActive = false // metadata.status === 'PROCESSED_TOGGLE_ON'; - const isLoading = metadata.status === 'PROCESSING'; - - const isPendingOrProcessing = metadata.status === 'PENDING' || metadata.status === 'PROCESSING'; - const isDisabled = hasNoValidFill || isPendingOrProcessing - - let loadingProgress: number | undefined; - if (isLoading && metadata.progress) { - const { current, total } = metadata.progress; - loadingProgress = (current / total) * 100; - } - - Button(CANVAS_MENU_COMPONENT_BUTTON_ID, { - label: I18N_ID, - icon: ICON, - isActive, - isLoading, - isDisabled, - loadingProgress, - onClick: () => { - setPluginMetadata(cesdk, id, { - status: 'PENDING' - }); - } - }); - } - ); -} diff --git a/turbo.json b/turbo.json index 55d300a..b126bc6 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,7 @@ "pipeline": { "build": { "dependsOn": ["^build"], - "outputs": ["dist/**"] + "outputs": ["dist/**", "types/**"] }, "dev": { "cache": false, @@ -12,6 +12,10 @@ "dev:types": { "cache": false, "persistent": true + }, + "check:pretty": { + "cache": false, + "persistent": true } } } diff --git a/yarn.lock b/yarn.lock index 03fef94..4859a50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -220,10 +220,10 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@cesdk/cesdk-js@^1.20.0", "@cesdk/cesdk-js@~1.20.0": - version "1.20.0" - resolved "https://registry.npmjs.org/@cesdk/cesdk-js/-/cesdk-js-1.20.0.tgz" - integrity sha512-vKDcnv5z5TZe1PcgvZagJ7QXVyijeTnkwPCJJFj/Uxcsef9GvLrzOVIYqPC0gqZuDlfHpADPsGAV+pZaZg8+eg== +"@cesdk/cesdk-js@^1.21.0": + version "1.21.1" + resolved "https://registry.yarnpkg.com/@cesdk/cesdk-js/-/cesdk-js-1.21.1.tgz#b3e4d4b584d0623bae584210098244d8d874c838" + integrity sha512-TQSDHjszpITGq1jj6985r59eZwJkoUjtxcmGj+scjX+gORfAN7jfzl8xGp8gvMl7/tX7i+Xhym74A0Qrq9g6Tw== "@colors/colors@1.5.0": version "1.5.0" @@ -377,6 +377,19 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz" integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@headlessui/react@^1.6.4": + version "1.7.18" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.18.tgz#30af4634d2215b2ca1aa29d07f33d02bea82d9d7" + integrity sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ== + dependencies: + "@tanstack/react-virtual" "^3.0.0-beta.60" + client-only "^0.0.1" + +"@heroicons/react@^2.0.13": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.1.tgz#422deb80c4d6caf3371aec6f4bee8361a354dc13" + integrity sha512-JyyN9Lo66kirbCMuMMRPtJxtKJoIsXKS569ebHGGRKbl8s4CtUfLnyKJxteA+vIKySocO4s1SkTkGS4xtG/yEA== + "@humanwhocodes/config-array@^0.11.13": version "0.11.14" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" @@ -409,10 +422,10 @@ onnxruntime-web "~1.17.0" zod "~3.21.0" -"@imgly/vectorizer@^0.1.0-rc4": - version "0.1.0-rc4" - resolved "https://registry.yarnpkg.com/@imgly/vectorizer/-/vectorizer-0.1.0-rc4.tgz#cd654fdffb834023426f6e07e4dce37b3b4cbcee" - integrity sha512-xR4wAZ6TkyEdY6v81SqD063WwR4JWlPAu1scqohmV3pUEJMs/2N5qNz7cTW9jf5NAZeoJO68kbm+Q0fN6riVTA== +"@imgly/vectorizer@~0.1.0-rc6": + version "0.1.0-rc6" + resolved "https://registry.yarnpkg.com/@imgly/vectorizer/-/vectorizer-0.1.0-rc6.tgz#509bec4e78d2654e4e3413c19e6e462d9bc62c7f" + integrity sha512-Q3QbRfB74wHZFB/TojJOJsSOAR8ZkV/mLXD8AztcTrwG1dVOPYfrcsrqbFLnHNz7XSxB8xCvSzdZOtylgXXugg== dependencies: "@types/lodash" "^4.14.195" "@types/node" "^20.3.1" @@ -460,6 +473,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" @@ -834,6 +855,18 @@ "@sigstore/core" "^0.2.0" "@sigstore/protobuf-specs" "^0.2.1" +"@tanstack/react-virtual@^3.0.0-beta.60": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.4.tgz#32a90aa6faa2eeebb5f4ca561d26bae9f3435e45" + integrity sha512-tiqKW/e2MJVCr7/pRUXulpkyxllaOclkHNfhKTo4pmHjJIqnhMfwIjc1Q1R0Un3PI3kQywywu/791c8z9u0qeA== + dependencies: + "@tanstack/virtual-core" "3.0.0" + +"@tanstack/virtual-core@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz#637bee36f0cabf96a1d436887c90f138a7e9378b" + integrity sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg== + "@tufjs/canonical-json@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz#a52f61a3d7374833fca945b2549bc30a2dd40d0a" @@ -885,6 +918,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + "@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1067,7 +1105,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +acorn@^8.8.2, acorn@^8.9.0: version "8.11.3" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -1293,6 +1331,11 @@ binary-extensions@^2.2.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -1325,6 +1368,11 @@ browserslist@^4.22.2: node-releases "^2.0.14" update-browserslist-db "^1.0.13" +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + builtins@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" @@ -1365,6 +1413,14 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + caniuse-lite@^1.0.30001580: version "1.0.30001584" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001584.tgz#5e3ea0625d048d5467670051687655b1f7bf7dfd" @@ -1409,6 +1465,13 @@ cidr-regex@4.0.3: dependencies: ip-regex "^5.0.0" +clean-css@^5.2.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== + dependencies: + source-map "~0.6.0" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -1431,6 +1494,11 @@ cli-table3@^0.6.3: optionalDependencies: "@colors/colors" "1.5.0" +client-only@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" @@ -1487,6 +1555,16 @@ columnify@^1.6.0: strip-ansi "^6.0.1" wcwidth "^1.0.0" +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + common-ancestor-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" @@ -1536,6 +1614,22 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -1634,6 +1728,51 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -1668,6 +1807,11 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -2401,6 +2545,11 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hosted-git-info@^7.0.0, hosted-git-info@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.1.tgz#9985fcb2700467fecf7f33a4d4874e30680b5322" @@ -2408,6 +2557,40 @@ hosted-git-info@^7.0.0, hosted-git-info@^7.0.1: dependencies: lru-cache "^10.0.1" +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-webpack-plugin@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" + integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" @@ -2746,6 +2929,11 @@ isexe@^3.1.1: resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== +isomorphic.js@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz#13eecf36f2dba53e85d355e11bf9d4208c6f7f88" + integrity sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw== + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -2872,6 +3060,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lib0@^0.2.86: + version "0.2.90" + resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.90.tgz#5187b921291dcbc26c7bcf6ac9de79a1437dc429" + integrity sha512-iQmk+fThPq1ZTD2cFUu8xN6JLp9gFWnjs8auR6hmI6QQXoy6sSEh85uKcdkqpuEnkhhwQm4GSlKHOYfSCVp0Mw== + dependencies: + isomorphic.js "^0.2.4" + libnpmaccess@^8.0.1: version "8.0.2" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-8.0.2.tgz#a13a72fd5b71a1063ea54973fa56d61ec38f718f" @@ -3004,7 +3199,7 @@ lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.21, lodash@~4.17.0: +lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.0: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3021,6 +3216,13 @@ loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0": version "10.2.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" @@ -3219,6 +3421,14 @@ negotiator@^0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-gyp@^10.0.0, node-gyp@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-10.0.1.tgz#205514fc19e5830fa991e4a689f9e81af377a966" @@ -3427,6 +3637,13 @@ npmlog@^7.0.1: gauge "^5.0.0" set-blocking "^2.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3605,6 +3822,14 @@ pacote@^17.0.0, pacote@^17.0.4, pacote@^17.0.6: ssri "^10.0.0" tar "^6.1.11" +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -3621,6 +3846,14 @@ parse-conflict-json@^3.0.0, parse-conflict-json@^3.0.1: just-diff "^6.0.0" just-diff-apply "^5.2.0" +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -3682,10 +3915,10 @@ postcss-selector-parser@^6.0.10: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.4.32: - version "8.4.34" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.34.tgz#563276e86b4ff20dfa5eed0d394d4c53853b2051" - integrity sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q== +postcss@^8.4.35: + version "8.4.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" + integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== dependencies: nanoid "^3.3.7" picocolors "^1.0.0" @@ -3706,6 +3939,14 @@ prettier@^2.8.5: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + pretty-quick@^3.1.3: version "3.3.1" resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-3.3.1.tgz#cfde97fec77a8d201a0e0c9c71d9990e12587ee2" @@ -3804,6 +4045,15 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-cmdk@^1.3.9: + version "1.3.9" + resolved "https://registry.yarnpkg.com/react-cmdk/-/react-cmdk-1.3.9.tgz#77123f5120a47e35a517a8176550e96731667654" + integrity sha512-MSVmAQZ9iqY7hO3r++XP6yWSHzGfMDGMvY3qlDT8k5RiWoRFwO1CGPlsWzhvcUbPilErzsMKK7uB4McEcX4B6g== + dependencies: + "@headlessui/react" "^1.6.4" + "@heroicons/react" "^2.0.13" + html-webpack-plugin "^5.5.0" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -3886,6 +4136,22 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" @@ -4117,6 +4383,19 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + spawn-command@0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz" @@ -4291,6 +4570,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +tapable@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + tar@^6.1.11, tar@^6.1.2, tar@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" @@ -4303,6 +4587,16 @@ tar@^6.1.11, tar@^6.1.2, tar@^6.2.0: mkdirp "^1.0.3" yallist "^4.0.0" +terser@^5.10.0: + version "5.27.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.27.0.tgz#70108689d9ab25fef61c4e93e808e9fd092bf20c" + integrity sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" @@ -4350,7 +4644,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0, tslib@^2.6.2: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -4516,6 +4810,11 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -4531,13 +4830,13 @@ validate-npm-package-name@^5.0.0: dependencies: builtins "^5.0.0" -vite@^5.0.8: - version "5.0.12" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47" - integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w== +vite@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.1.tgz#294e39b199d669981efc7e0261b14f78ec80819e" + integrity sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg== dependencies: esbuild "^0.19.3" - postcss "^8.4.32" + postcss "^8.4.35" rollup "^4.2.0" optionalDependencies: fsevents "~2.3.3" @@ -4698,6 +4997,13 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yjs@^13.6.14: + version "13.6.14" + resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.14.tgz#9326dfa03d1be3fb9af9ef7e41de4bfc78849a9f" + integrity sha512-D+7KcUr0j+vBCUSKXXEWfA+bG4UQBviAwP3gYBhkstkgwy5+8diOPMx0iqLIOxNo/HxaREUimZRxqHGAHCL2BQ== + dependencies: + lib0 "^0.2.86" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"