diff --git a/.ops/.gitlab-ci.yml b/.ops/.gitlab-ci.yml index 03ed78d7..b26ff71b 100644 --- a/.ops/.gitlab-ci.yml +++ b/.ops/.gitlab-ci.yml @@ -4,4 +4,4 @@ include: variables: APP_NAME: "xtreme1" - APP_VERSION: "0.8" + APP_VERSION: "0.8.1" diff --git a/README.md b/README.md index 81195446..52057037 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Xtreme1 logo -![](https://img.shields.io/badge/Release-v0.8-green) +![](https://img.shields.io/badge/Release-v0.8.1-green) ![](https://img.shields.io/badge/License-Apache%202.0-blueviolet) [![Twitter](https://img.shields.io/badge/Follow-Twitter-blue)](https://twitter.com/Xtreme1io) [![Online](https://img.shields.io/badge/Xtreme1_Online-App-yellow)](https://app.basic.ai/#/login) @@ -62,8 +62,8 @@ Image Data Curation (Visualizing & Debug) - [MobileNetV3](https://github.com/xi Download the latest release package and unzip it. ```bash -wget https://github.com/xtreme1-io/xtreme1/releases/download/v0.8/xtreme1-v0.8.zip -unzip -d xtreme1-v0.8 xtreme1-v0.8.zip +wget https://github.com/xtreme1-io/xtreme1/releases/download/v0.8.1/xtreme1-v0.8.1.zip +unzip -d xtreme1-v0.8.1 xtreme1-v0.8.1.zip ``` ## Start all services diff --git a/backend/Dockerfile b/backend/Dockerfile index 08c5222b..3c2b0883 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,7 +9,7 @@ RUN apt update && \ apt install -y iputils-ping curl wget netcat python3 python3-pip git RUN pip3 install --upgrade --force-reinstall git+https://github.com/xtreme1-io/xtreme1-sdk.git@d0cf4cc WORKDIR /app -COPY --from=build /build/target/xtreme1-backend-0.8-SNAPSHOT.jar ./app.jar +COPY --from=build /build/target/xtreme1-backend-0.8.1-SNAPSHOT.jar ./app.jar RUN mkdir -p config RUN wget 'https://github.com/xtreme1-io/asset/raw/main/datasets/xtreme1-lidar-fusion-trial.zip' -O xtreme1-lidar-fusion-trial.zip RUN wget 'https://github.com/xtreme1-io/asset/raw/main/datasets/xtreme1-image-trial.zip' -O xtreme1-image-trial.zip diff --git a/backend/README.md b/backend/README.md index 97159b53..b1843bf8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -92,7 +92,7 @@ cd backend mvn package # Using local configuration to start application. -java -Dspring.profiles.active=local -jar target/xtreme1-backend-0.8-SNAPSHOT.jar +java -Dspring.profiles.active=local -jar target/xtreme1-backend-0.8.1-SNAPSHOT.jar ``` Now you can access the backend service at `http://localhost:8080/`. diff --git a/backend/pom.xml b/backend/pom.xml index 0db8095b..a1dc1ab5 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -13,7 +13,7 @@ ai.basic xtreme1-backend - 0.8-SNAPSHOT + 0.8.1-SNAPSHOT Xtreme1 Backend diff --git a/backend/src/main/java/ai/basic/x1/usecase/UploadDataUseCase.java b/backend/src/main/java/ai/basic/x1/usecase/UploadDataUseCase.java index d27c3b35..b404960a 100644 --- a/backend/src/main/java/ai/basic/x1/usecase/UploadDataUseCase.java +++ b/backend/src/main/java/ai/basic/x1/usecase/UploadDataUseCase.java @@ -304,6 +304,7 @@ public void parseTextUploadFile(DataInfoUploadBO dataInfoUploadBO) { var rootPath = String.format("%s/%s", userId, datasetId); var errorBuilder = new StringBuilder(); var dataInfoBOBuilder = DataInfoBO.builder().datasetId(datasetId) + .parentId(Constants.DEFAULT_PARENT_ID) .type(ItemTypeEnum.SINGLE_DATA) .status(DataStatusEnum.VALID) .annotationStatus(DataAnnotationStatusEnum.NOT_ANNOTATED) @@ -349,7 +350,7 @@ public void parseTextUploadFile(DataInfoUploadBO dataInfoUploadBO) { dataInfoBOList.add(dataInfoBO); }); if (CollectionUtil.isNotEmpty(dataInfoBOList)) { - insertBatch(dataInfoBOList, datasetId, errorBuilder); + insertBatch(dataInfoBOList, datasetId, errorBuilder, Constants.DEFAULT_PARENT_ID); } } catch (Exception e) { log.error("Handle data error", e); @@ -463,7 +464,15 @@ public void commonParseUploadFile(DataInfoUploadBO dataInfoUploadBO, BiConsumer< var dataAnnotationObjectBOBuilder = DataAnnotationObjectBO.builder() .datasetId(datasetId).createdBy(userId).createdAt(OffsetDateTime.now()).sourceId(sourceId); sceneFileList.forEach(sceneFile -> { - var sceneId = this.saveScene(sceneFile, dataInfoUploadBO); + Long sceneId; + try { + sceneId = this.saveScene(sceneFile, dataInfoUploadBO); + } catch (DuplicateKeyException e) { + log.error("The scene already exists,scene name is {}", sceneFile.getName()); + errorBuilder.append("Duplicate scene names:").append(sceneFile.getName()).append(";"); + return; + } + var dataNameList = getDataNamesFunction.apply(sceneFile); if (CollectionUtil.isEmpty(dataNameList)) { log.error("The file in {} folder is empty", sceneFile); @@ -498,7 +507,7 @@ public void commonParseUploadFile(DataInfoUploadBO dataInfoUploadBO, BiConsumer< } }); if (CollectionUtil.isNotEmpty(dataInfoBOList)) { - var resDataInfoList = this.insertBatch(dataInfoBOList, datasetId, errorBuilder); + var resDataInfoList = this.insertBatch(dataInfoBOList, datasetId, errorBuilder, sceneId); this.saveBatchDataResult(resDataInfoList, dataAnnotationObjectBOList); } } catch (Exception e) { @@ -1045,9 +1054,9 @@ public String getFilename(File file) { * * @param dataInfoBOList Collection of data details */ - public List insertBatch(List dataInfoBOList, Long datasetId, StringBuilder errorBuilder) { + public List insertBatch(List dataInfoBOList, Long datasetId, StringBuilder errorBuilder, Long parentId) { var names = dataInfoBOList.stream().map(DataInfoBO::getName).collect(Collectors.toList()); - var existDataInfoList = this.findByNames(datasetId, names); + var existDataInfoList = this.findByNames(datasetId, parentId, names); if (CollUtil.isNotEmpty(existDataInfoList)) { var existNames = existDataInfoList.stream().map(DataInfoBO::getName).collect(Collectors.toList()); dataInfoBOList = dataInfoBOList.stream().filter(dataInfoBO -> !existNames.contains(dataInfoBO.getName())).collect(Collectors.toList()); @@ -1071,10 +1080,11 @@ public List insertBatch(List dataInfoBOList, Long datase } } - private List findByNames(Long datasetId, List names) { + private List findByNames(Long datasetId, Long parentId, List names) { var dataInfoLambdaQueryWrapper = Wrappers.lambdaQuery(DataInfo.class); dataInfoLambdaQueryWrapper.eq(DataInfo::getDatasetId, datasetId); dataInfoLambdaQueryWrapper.in(DataInfo::getName, names); + dataInfoLambdaQueryWrapper.eq(DataInfo::getParentId, parentId); return DefaultConverter.convert(dataInfoDAO.list(dataInfoLambdaQueryWrapper), DataInfoBO.class); } diff --git a/docker-compose.yml b/docker-compose.yml index 3bc7a534..d7506828 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: - 8191:3306 volumes: - mysql-data:/var/lib/mysql - - ./deploy/mysql/custom.cnf:/etc/mysql/conf.d + - ./deploy/mysql/custom.cnf:/etc/mysql/conf.d/custom.cnf - ./deploy/mysql/migration:/docker-entrypoint-initdb.d healthcheck: test: '/usr/bin/mysql --user=xtreme1 --password=Rc4K3L6f --execute "SHOW DATABASES;"' @@ -65,7 +65,7 @@ services: retries: 10 backend: # By default, Compose will pull image from Docker Hub when no local image found. - image: basicai/xtreme1-backend:v0.8 + image: basicai/xtreme1-backend:v0.8.1 pull_policy: always # Uncomment this line and comment previous line to build image locally, not pull from Docker Hub. # build: ./backend @@ -96,7 +96,7 @@ services: condition: service_healthy frontend: # By default, Compose will pull image from Docker Hub when no local image found. - image: basicai/xtreme1-frontend:v0.8 + image: basicai/xtreme1-frontend:v0.8.1 pull_policy: always # Uncomment this line and comment previous line to build image locally, not pull from Docker Hub. # build: ./frontend diff --git a/frontend/image-tool/index.html b/frontend/image-tool/index.html index b87ac144..81f239ed 100644 --- a/frontend/image-tool/index.html +++ b/frontend/image-tool/index.html @@ -3,7 +3,7 @@ - + Image Tool - Xtreme1 diff --git a/frontend/image-tool/package.json b/frontend/image-tool/package.json index b588abe1..27c24973 100644 --- a/frontend/image-tool/package.json +++ b/frontend/image-tool/package.json @@ -15,10 +15,11 @@ "@types/js-cookie": "^3.0.1", "@types/qs": "^6.9.7", "@types/three": "^0.136.1", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^5.16.0", "@vueuse/components": "^8.6.0", "@vueuse/core": "^8.6.0", - "ant-design-vue": "2.2.8", + "ant-design-vue": "3.2.19", "axios": "^0.26.1", "colord": "^2.9.2", "cross-env": "^7.0.3", @@ -34,7 +35,7 @@ "hotkeys-js": "^3.8.7", "interactjs": "^1.10.11", "js-cookie": "^3.0.1", - "konva": "^8.3.8", + "konva": "^9.2.3", "less": "^4.1.2", "less-loader": "^10.2.0", "lodash": "^4.17.21", @@ -50,7 +51,8 @@ "vue": "^3.2.25", "vue-clipboard2": "^0.3.3", "vue-i18n": "^9.1.9", - "vue-router": "^4.0.14" + "vue-router": "^4.0.14", + "vue3-colorpicker": "^2.2.3" }, "devDependencies": { "@commitlint/cli": "^16.2.3", diff --git a/frontend/image-tool/prettier.config.js b/frontend/image-tool/prettier.config.js index 1a56157c..01aaed56 100644 --- a/frontend/image-tool/prettier.config.js +++ b/frontend/image-tool/prettier.config.js @@ -6,6 +6,6 @@ module.exports = { trailingComma: 'all', proseWrap: 'never', htmlWhitespaceSensitivity: 'strict', - tabWidth: 4, + tabWidth: 2, endOfLine: 'auto', }; diff --git a/frontend/image-tool/public/iconfont/Read Me.txt b/frontend/image-tool/public/iconfont/Read Me.txt deleted file mode 100644 index 8491652f..00000000 --- a/frontend/image-tool/public/iconfont/Read Me.txt +++ /dev/null @@ -1,7 +0,0 @@ -Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. - -To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts - -You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. - -You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. diff --git a/frontend/image-tool/public/iconfont/demo-files/demo.css b/frontend/image-tool/public/iconfont/demo-files/demo.css deleted file mode 100644 index 39b8991d..00000000 --- a/frontend/image-tool/public/iconfont/demo-files/demo.css +++ /dev/null @@ -1,152 +0,0 @@ -body { - padding: 0; - margin: 0; - font-family: sans-serif; - font-size: 1em; - line-height: 1.5; - color: #555; - background: #fff; -} -h1 { - font-size: 1.5em; - font-weight: normal; -} -small { - font-size: .66666667em; -} -a { - color: #e74c3c; - text-decoration: none; -} -a:hover, a:focus { - box-shadow: 0 1px #e74c3c; -} -.bshadow0, input { - box-shadow: inset 0 -2px #e7e7e7; -} -input:hover { - box-shadow: inset 0 -2px #ccc; -} -input, fieldset { - font-family: sans-serif; - font-size: 1em; - margin: 0; - padding: 0; - border: 0; -} -input { - color: inherit; - line-height: 1.5; - height: 1.5em; - padding: .25em 0; -} -input:focus { - outline: none; - box-shadow: inset 0 -2px #449fdb; -} -.glyph { - font-size: 16px; - width: 15em; - padding-bottom: 1em; - margin-right: 4em; - margin-bottom: 1em; - float: left; - overflow: hidden; -} -.liga { - width: 80%; - width: calc(100% - 2.5em); -} -.talign-right { - text-align: right; -} -.talign-center { - text-align: center; -} -.bgc1 { - background: #f1f1f1; -} -.fgc1 { - color: #999; -} -.fgc0 { - color: #000; -} -p { - margin-top: 1em; - margin-bottom: 1em; -} -.mvm { - margin-top: .75em; - margin-bottom: .75em; -} -.mtn { - margin-top: 0; -} -.mtl, .mal { - margin-top: 1.5em; -} -.mbl, .mal { - margin-bottom: 1.5em; -} -.mal, .mhl { - margin-left: 1.5em; - margin-right: 1.5em; -} -.mhmm { - margin-left: 1em; - margin-right: 1em; -} -.mls { - margin-left: .25em; -} -.ptl { - padding-top: 1.5em; -} -.pbs, .pvs { - padding-bottom: .25em; -} -.pvs, .pts { - padding-top: .25em; -} -.unit { - float: left; -} -.unitRight { - float: right; -} -.size1of2 { - width: 50%; -} -.size1of1 { - width: 100%; -} -.clearfix:before, .clearfix:after { - content: " "; - display: table; -} -.clearfix:after { - clear: both; -} -.hidden-true { - display: none; -} -.textbox0 { - width: 3em; - background: #f1f1f1; - padding: .25em .5em; - line-height: 1.5; - height: 1.5em; -} -#testDrive { - display: block; - padding-top: 24px; - line-height: 1.5; -} -.fs0 { - font-size: 16px; -} -.fs1 { - font-size: 32px; -} - diff --git a/frontend/image-tool/public/iconfont/demo-files/demo.js b/frontend/image-tool/public/iconfont/demo-files/demo.js deleted file mode 100644 index 6f45f1c4..00000000 --- a/frontend/image-tool/public/iconfont/demo-files/demo.js +++ /dev/null @@ -1,30 +0,0 @@ -if (!('boxShadow' in document.body.style)) { - document.body.setAttribute('class', 'noBoxShadow'); -} - -document.body.addEventListener("click", function(e) { - var target = e.target; - if (target.tagName === "INPUT" && - target.getAttribute('class').indexOf('liga') === -1) { - target.select(); - } -}); - -(function() { - var fontSize = document.getElementById('fontSize'), - testDrive = document.getElementById('testDrive'), - testText = document.getElementById('testText'); - function updateTest() { - testDrive.innerHTML = testText.value || String.fromCharCode(160); - if (window.icomoonLiga) { - window.icomoonLiga(testDrive); - } - } - function updateSize() { - testDrive.style.fontSize = fontSize.value + 'px'; - } - fontSize.addEventListener('change', updateSize, false); - testText.addEventListener('input', updateTest, false); - testText.addEventListener('change', updateTest, false); - updateSize(); -}()); diff --git a/frontend/image-tool/public/iconfont/demo.css b/frontend/image-tool/public/iconfont/demo.css new file mode 100644 index 00000000..a67054a0 --- /dev/null +++ b/frontend/image-tool/public/iconfont/demo.css @@ -0,0 +1,539 @@ +/* Logo 字体 */ +@font-face { + font-family: "iconfont logo"; + src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834'); + src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'), + url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg'); +} + +.logo { + font-family: "iconfont logo"; + font-size: 160px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* tabs */ +.nav-tabs { + position: relative; +} + +.nav-tabs .nav-more { + position: absolute; + right: 0; + bottom: 0; + height: 42px; + line-height: 42px; + color: #666; +} + +#tabs { + border-bottom: 1px solid #eee; +} + +#tabs li { + cursor: pointer; + width: 100px; + height: 40px; + line-height: 40px; + text-align: center; + font-size: 16px; + border-bottom: 2px solid transparent; + position: relative; + z-index: 1; + margin-bottom: -1px; + color: #666; +} + + +#tabs .active { + border-bottom-color: #f00; + color: #222; +} + +.tab-container .content { + display: none; +} + +/* 页面布局 */ +.main { + padding: 30px 100px; + width: 960px; + margin: 0 auto; +} + +.main .logo { + color: #333; + text-align: left; + margin-bottom: 30px; + line-height: 1; + height: 110px; + margin-top: -50px; + overflow: hidden; + *zoom: 1; +} + +.main .logo a { + font-size: 160px; + color: #333; +} + +.helps { + margin-top: 40px; +} + +.helps pre { + padding: 20px; + margin: 10px 0; + border: solid 1px #e7e1cd; + background-color: #fffdef; + overflow: auto; +} + +.icon_lists { + width: 100% !important; + overflow: hidden; + *zoom: 1; +} + +.icon_lists li { + width: 100px; + margin-bottom: 10px; + margin-right: 20px; + text-align: center; + list-style: none !important; + cursor: default; +} + +.icon_lists li .code-name { + line-height: 1.2; +} + +.icon_lists .icon { + display: block; + height: 100px; + line-height: 100px; + font-size: 42px; + margin: 10px auto; + color: #333; + -webkit-transition: font-size 0.25s linear, width 0.25s linear; + -moz-transition: font-size 0.25s linear, width 0.25s linear; + transition: font-size 0.25s linear, width 0.25s linear; +} + +.icon_lists .icon:hover { + font-size: 100px; +} + +.icon_lists .svg-icon { + /* 通过设置 font-size 来改变图标大小 */ + width: 1em; + /* 图标和文字相邻时,垂直对齐 */ + vertical-align: -0.15em; + /* 通过设置 color 来改变 SVG 的颜色/fill */ + fill: currentColor; + /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示 + normalize.css 中也包含这行 */ + overflow: hidden; +} + +.icon_lists li .name, +.icon_lists li .code-name { + color: #666; +} + +/* markdown 样式 */ +.markdown { + color: #666; + font-size: 14px; + line-height: 1.8; +} + +.highlight { + line-height: 1.5; +} + +.markdown img { + vertical-align: middle; + max-width: 100%; +} + +.markdown h1 { + color: #404040; + font-weight: 500; + line-height: 40px; + margin-bottom: 24px; +} + +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + color: #404040; + margin: 1.6em 0 0.6em 0; + font-weight: 500; + clear: both; +} + +.markdown h1 { + font-size: 28px; +} + +.markdown h2 { + font-size: 22px; +} + +.markdown h3 { + font-size: 16px; +} + +.markdown h4 { + font-size: 14px; +} + +.markdown h5 { + font-size: 12px; +} + +.markdown h6 { + font-size: 12px; +} + +.markdown hr { + height: 1px; + border: 0; + background: #e9e9e9; + margin: 16px 0; + clear: both; +} + +.markdown p { + margin: 1em 0; +} + +.markdown>p, +.markdown>blockquote, +.markdown>.highlight, +.markdown>ol, +.markdown>ul { + width: 80%; +} + +.markdown ul>li { + list-style: circle; +} + +.markdown>ul li, +.markdown blockquote ul>li { + margin-left: 20px; + padding-left: 4px; +} + +.markdown>ul li p, +.markdown>ol li p { + margin: 0.6em 0; +} + +.markdown ol>li { + list-style: decimal; +} + +.markdown>ol li, +.markdown blockquote ol>li { + margin-left: 20px; + padding-left: 4px; +} + +.markdown code { + margin: 0 3px; + padding: 0 5px; + background: #eee; + border-radius: 3px; +} + +.markdown strong, +.markdown b { + font-weight: 600; +} + +.markdown>table { + border-collapse: collapse; + border-spacing: 0px; + empty-cells: show; + border: 1px solid #e9e9e9; + width: 95%; + margin-bottom: 24px; +} + +.markdown>table th { + white-space: nowrap; + color: #333; + font-weight: 600; +} + +.markdown>table th, +.markdown>table td { + border: 1px solid #e9e9e9; + padding: 8px 16px; + text-align: left; +} + +.markdown>table th { + background: #F7F7F7; +} + +.markdown blockquote { + font-size: 90%; + color: #999; + border-left: 4px solid #e9e9e9; + padding-left: 0.8em; + margin: 1em 0; +} + +.markdown blockquote p { + margin: 0; +} + +.markdown .anchor { + opacity: 0; + transition: opacity 0.3s ease; + margin-left: 8px; +} + +.markdown .waiting { + color: #ccc; +} + +.markdown h1:hover .anchor, +.markdown h2:hover .anchor, +.markdown h3:hover .anchor, +.markdown h4:hover .anchor, +.markdown h5:hover .anchor, +.markdown h6:hover .anchor { + opacity: 1; + display: inline-block; +} + +.markdown>br, +.markdown>p>br { + clear: both; +} + + +.hljs { + display: block; + background: white; + padding: 0.5em; + color: #333333; + overflow-x: auto; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} + +/* 代码高亮 */ +/* PrismJS 1.15.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre)>code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre)>code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} diff --git a/frontend/image-tool/public/iconfont/demo.html b/frontend/image-tool/public/iconfont/demo.html deleted file mode 100644 index 39d757d1..00000000 --- a/frontend/image-tool/public/iconfont/demo.html +++ /dev/null @@ -1,808 +0,0 @@ - - - - - IcoMoon Demo - - - - - -
-

Font Name: icomoon (Glyphs: 55)

-
-
-

Grid Size: 32

-
-
- - icon-polygon-hollow -
-
- - -
-
- liga: - -
-
-
-
- - icon-cancel-hollow -
-
- - -
-
- liga: - -
-
-
-
- - icon-polygon-clip -
-
- - -
-
- liga: - -
-
-
-
- - icon-keyboard -
-
- - -
-
- liga: - -
-
-
-
- - icon-command -
-
- - -
-
- liga: - -
-
-
-
- - icon-Job-information -
-
- - -
-
- liga: - -
-
-
-
- - icon-Expiration-time -
-
- - -
-
- liga: - -
-
-
-
- - icon-Work-flow -
-
- - -
-
- liga: - -
-
-
-
- - icon-save -
-
- - -
-
- liga: - -
-
-
-
- - icon-help -
-
- - -
-
- liga: - -
-
-
-
- - icon-enter-fullscreen -
-
- - -
-
- liga: - -
-
-
-
- - icon-exit-fullscreen -
-
- - -
-
- liga: - -
-
-
-
- - icon-hang-up -
-
- - -
-
- liga: - -
-
-
-
- - icon-submit -
-
- - -
-
- liga: - -
-
-
-
- - icon-setting -
-
- - -
-
- liga: - -
-
-
-
- - icon-information -
-
- - -
-
- liga: - -
-
-
-
- - icon-mapping -
-
- - -
-
- liga: - -
-
-
-
- - icon-rotating -
-
- - -
-
- liga: - -
-
-
-
- - icon-move -
-
- - -
-
- liga: - -
-
-
-
- - icon-tool -
-
- - -
-
- liga: - -
-
-
-
- - icon-Pack-up -
-
- - -
-
- liga: - -
-
-
-
- - icon-an-right -
-
- - -
-
- liga: - -
-
-
-
- - icon-down -
-
- - -
-
- liga: - -
-
-
-
- - icon-up -
-
- - -
-
- liga: - -
-
-
-
- - icon-Right-rotation -
-
- - -
-
- liga: - -
-
-
-
- - icon-Left-rotation -
-
- - -
-
- liga: - -
-
-
-
- - icon-remind -
-
- - -
-
- liga: - -
-
-
-
- - icon-Notes---list -
-
- - -
-
- liga: - -
-
-
-
- - icon-Have-been-added -
-
- - -
-
- liga: - -
-
-
-
- - icon-Add-a-notation -
-
- - -
-
- liga: - -
-
-
-
- - icon-notation-tool -
-
- - -
-
- liga: - -
-
-
-
- - icon-filter -
-
- - -
-
- liga: - -
-
-
-
- - icon-class-edit -
-
- - -
-
- liga: - -
-
-
-
- - icon-delete -
-
- - -
-
- liga: - -
-
-
-
- - icon-visible -
-
- - -
-
- liga: - -
-
-
-
- - icon-biaozhunkuang -
-
- - -
-
- liga: - -
-
-
-
- - icon-lifangti -
-
- - -
-
- liga: - -
-
-
-
- - icon-target -
-
- - -
-
- liga: - -
-
-
-
- - icon-auxiliaryline -
-
- - -
-
- liga: - -
-
-
-
- - icon-more -
-
- - -
-
- liga: - -
-
-
-
- - icon-unfold -
-
- - -
-
- liga: - -
-
-
-
- - icon-hidden -
-
- - -
-
- liga: - -
-
-
-
- - icon-left -
-
- - -
-
- liga: - -
-
-
-
- - icon-right -
-
- - -
-
- liga: - -
-
-
-
- - icon-dakai -
-
- - -
-
- liga: - -
-
-
-
- - icon-model -
-
- - -
-
- liga: - -
-
-
-
- - icon-interactive -
-
- - -
-
- liga: - -
-
-
-
- - icon-loading -
-
- - -
-
- liga: - -
-
-
-
- - icon-cube -
-
- - -
-
- liga: - -
-
-
-
- - icon-Frame -
-
- - -
-
- liga: - -
-
-
-
- - icon-polygon -
-
- - -
-
- liga: - -
-
-
-
- - icon-edit -
-
- - -
-
- liga: - -
-
-
-
- - icon-rectangle -
-
- - -
-
- liga: - -
-
-
-
- - icon-ai -
-
- - -
-
- liga: - -
-
-
-
- - icon-polyline -
-
- - -
-
- liga: - -
-
-
- - -
-

Font Test Drive

- - -
  -
-
- -
-

Generated by IcoMoon

-
- - - - diff --git a/frontend/image-tool/public/iconfont/demo_index.html b/frontend/image-tool/public/iconfont/demo_index.html new file mode 100644 index 00000000..869ef075 --- /dev/null +++ b/frontend/image-tool/public/iconfont/demo_index.html @@ -0,0 +1,1154 @@ + + + + + iconfont Demo + + + + + + + + + + + + + +
+

+ + +

+ +
+
+
    + +
  • + +
    cube
    +
    &#xe736;
    +
  • + +
  • + +
    play
    +
    &#xe741;
    +
  • + +
  • + +
    replay
    +
    &#xe743;
    +
  • + +
  • + +
    backup
    +
    &#xe745;
    +
  • + +
  • + +
    forward
    +
    &#xe746;
    +
  • + +
  • + +
    ongoing
    +
    &#xe750;
    +
  • + +
  • + +
    Crop non-first
    +
    &#xe769;
    +
  • + +
  • + +
    Crop the first
    +
    &#xe76a;
    +
  • + +
  • + +
    transform
    +
    &#xe621;
    +
  • + +
  • + +
    point
    +
    &#xe60d;
    +
  • + +
  • + +
    reset
    +
    &#xe61a;
    +
  • + +
  • + +
    rotate-right
    +
    &#xe6de;
    +
  • + +
  • + +
    rotate-left
    +
    &#xe6e0;
    +
  • + +
  • + +
    information
    +
    &#xe6fe;
    +
  • + +
  • + +
    hollow
    +
    &#xe620;
    +
  • + +
  • + +
    cancel-hollow
    +
    &#xe752;
    +
  • + +
  • + +
    hotkey
    +
    &#xe767;
    +
  • + +
  • + +
    rect
    +
    &#xe67c;
    +
  • + +
  • + +
    save
    +
    &#xe703;
    +
  • + +
  • + +
    help
    +
    &#xe704;
    +
  • + +
  • + +
    fullscreen
    +
    &#xe705;
    +
  • + +
  • + +
    exitfullscreen
    +
    &#xe706;
    +
  • + +
  • + +
    info
    +
    &#xe70b;
    +
  • + +
  • + +
    setting
    +
    &#xe70c;
    +
  • + +
  • + +
    toleft
    +
    &#xe712;
    +
  • + +
  • + +
    toright
    +
    &#xe713;
    +
  • + +
  • + +
    remind
    +
    &#xe71a;
    +
  • + +
  • + +
    view
    +
    &#xe71f;
    +
  • + +
  • + +
    delete
    +
    &#xe721;
    +
  • + +
  • + +
    editlabel
    +
    &#xe722;
    +
  • + +
  • + +
    filter
    +
    &#xe724;
    +
  • + +
  • + +
    more
    +
    &#xe72d;
    +
  • + +
  • + +
    hidden
    +
    &#xe72f;
    +
  • + +
  • + +
    left
    +
    &#xe730;
    +
  • + +
  • + +
    right
    +
    &#xe731;
    +
  • + +
  • + +
    loading
    +
    &#xe737;
    +
  • + +
  • + +
    model
    +
    &#xe738;
    +
  • + +
  • + +
    arrow
    +
    &#xe751;
    +
  • + +
  • + +
    ai
    +
    &#xe764;
    +
  • + +
  • + +
    smart
    +
    &#xe766;
    +
  • + +
  • + +
    polygon
    +
    &#xe601;
    +
  • + +
  • + +
    polyline
    +
    &#xe602;
    +
  • + +
+
+

Unicode 引用

+
+ +

Unicode 是字体在网页端最原始的应用方式,特点是:

+
    +
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • +
  • 默认情况下不支持多色,直接添加多色图标会自动去色。
  • +
+
+

注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)

+
+

Unicode 使用步骤如下:

+

第一步:拷贝项目下面生成的 @font-face

+
@font-face {
+  font-family: 'iconfont';
+  src: url('iconfont.woff2?t=1704191662778') format('woff2'),
+       url('iconfont.woff?t=1704191662778') format('woff'),
+       url('iconfont.ttf?t=1704191662778') format('truetype');
+}
+
+

第二步:定义使用 iconfont 的样式

+
.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+

第三步:挑选相应图标并获取字体编码,应用于页面

+
+<span class="iconfont">&#x33;</span>
+
+
+

"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

+
+
+
+
+
    + +
  • + +
    + cube +
    +
    .icon-cube +
    +
  • + +
  • + +
    + play +
    +
    .icon-play +
    +
  • + +
  • + +
    + replay +
    +
    .icon-replay +
    +
  • + +
  • + +
    + backup +
    +
    .icon-backup +
    +
  • + +
  • + +
    + forward +
    +
    .icon-forward +
    +
  • + +
  • + +
    + ongoing +
    +
    .icon-Frame23 +
    +
  • + +
  • + +
    + Crop non-first +
    +
    .icon-crop-non-first +
    +
  • + +
  • + +
    + Crop the first +
    +
    .icon-crop-first +
    +
  • + +
  • + +
    + transform +
    +
    .icon-transform +
    +
  • + +
  • + +
    + point +
    +
    .icon-point +
    +
  • + +
  • + +
    + reset +
    +
    .icon-reset +
    +
  • + +
  • + +
    + rotate-right +
    +
    .icon-rotate90 +
    +
  • + +
  • + +
    + rotate-left +
    +
    .icon-rotate270 +
    +
  • + +
  • + +
    + information +
    +
    .icon-information +
    +
  • + +
  • + +
    + hollow +
    +
    .icon-hollow +
    +
  • + +
  • + +
    + cancel-hollow +
    +
    .icon-cancel-hollow +
    +
  • + +
  • + +
    + hotkey +
    +
    .icon-hotkey +
    +
  • + +
  • + +
    + rect +
    +
    .icon-rect +
    +
  • + +
  • + +
    + save +
    +
    .icon-save +
    +
  • + +
  • + +
    + help +
    +
    .icon-help +
    +
  • + +
  • + +
    + fullscreen +
    +
    .icon-fullscreen +
    +
  • + +
  • + +
    + exitfullscreen +
    +
    .icon-exitfullscreen +
    +
  • + +
  • + +
    + info +
    +
    .icon-info +
    +
  • + +
  • + +
    + setting +
    +
    .icon-setting +
    +
  • + +
  • + +
    + toleft +
    +
    .icon-toleft +
    +
  • + +
  • + +
    + toright +
    +
    .icon-toright +
    +
  • + +
  • + +
    + remind +
    +
    .icon-remind +
    +
  • + +
  • + +
    + view +
    +
    .icon-view +
    +
  • + +
  • + +
    + delete +
    +
    .icon-delete +
    +
  • + +
  • + +
    + editlabel +
    +
    .icon-editlabel +
    +
  • + +
  • + +
    + filter +
    +
    .icon-filter +
    +
  • + +
  • + +
    + more +
    +
    .icon-more +
    +
  • + +
  • + +
    + hidden +
    +
    .icon-hidden +
    +
  • + +
  • + +
    + left +
    +
    .icon-left +
    +
  • + +
  • + +
    + right +
    +
    .icon-right +
    +
  • + +
  • + +
    + loading +
    +
    .icon-loading +
    +
  • + +
  • + +
    + model +
    +
    .icon-model +
    +
  • + +
  • + +
    + arrow +
    +
    .icon-arrow +
    +
  • + +
  • + +
    + ai +
    +
    .icon-ai +
    +
  • + +
  • + +
    + smart +
    +
    .icon-smart +
    +
  • + +
  • + +
    + polygon +
    +
    .icon-polygon +
    +
  • + +
  • + +
    + polyline +
    +
    .icon-polyline +
    +
  • + +
+
+

font-class 引用

+
+ +

font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。

+

与 Unicode 使用方式相比,具有如下特点:

+
    +
  • 相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • +
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。
  • +
+

使用步骤如下:

+

第一步:引入项目下面生成的 fontclass 代码:

+
<link rel="stylesheet" href="./iconfont.css">
+
+

第二步:挑选相应图标并获取类名,应用于页面:

+
<span class="iconfont icon-xxx"></span>
+
+
+

" + iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。

+
+
+
+
+
    + +
  • + +
    cube
    +
    #icon-cube
    +
  • + +
  • + +
    play
    +
    #icon-play
    +
  • + +
  • + +
    replay
    +
    #icon-replay
    +
  • + +
  • + +
    backup
    +
    #icon-backup
    +
  • + +
  • + +
    forward
    +
    #icon-forward
    +
  • + +
  • + +
    ongoing
    +
    #icon-Frame23
    +
  • + +
  • + +
    Crop non-first
    +
    #icon-crop-non-first
    +
  • + +
  • + +
    Crop the first
    +
    #icon-crop-first
    +
  • + +
  • + +
    transform
    +
    #icon-transform
    +
  • + +
  • + +
    point
    +
    #icon-point
    +
  • + +
  • + +
    reset
    +
    #icon-reset
    +
  • + +
  • + +
    rotate-right
    +
    #icon-rotate90
    +
  • + +
  • + +
    rotate-left
    +
    #icon-rotate270
    +
  • + +
  • + +
    information
    +
    #icon-information
    +
  • + +
  • + +
    hollow
    +
    #icon-hollow
    +
  • + +
  • + +
    cancel-hollow
    +
    #icon-cancel-hollow
    +
  • + +
  • + +
    hotkey
    +
    #icon-hotkey
    +
  • + +
  • + +
    rect
    +
    #icon-rect
    +
  • + +
  • + +
    save
    +
    #icon-save
    +
  • + +
  • + +
    help
    +
    #icon-help
    +
  • + +
  • + +
    fullscreen
    +
    #icon-fullscreen
    +
  • + +
  • + +
    exitfullscreen
    +
    #icon-exitfullscreen
    +
  • + +
  • + +
    info
    +
    #icon-info
    +
  • + +
  • + +
    setting
    +
    #icon-setting
    +
  • + +
  • + +
    toleft
    +
    #icon-toleft
    +
  • + +
  • + +
    toright
    +
    #icon-toright
    +
  • + +
  • + +
    remind
    +
    #icon-remind
    +
  • + +
  • + +
    view
    +
    #icon-view
    +
  • + +
  • + +
    delete
    +
    #icon-delete
    +
  • + +
  • + +
    editlabel
    +
    #icon-editlabel
    +
  • + +
  • + +
    filter
    +
    #icon-filter
    +
  • + +
  • + +
    more
    +
    #icon-more
    +
  • + +
  • + +
    hidden
    +
    #icon-hidden
    +
  • + +
  • + +
    left
    +
    #icon-left
    +
  • + +
  • + +
    right
    +
    #icon-right
    +
  • + +
  • + +
    loading
    +
    #icon-loading
    +
  • + +
  • + +
    model
    +
    #icon-model
    +
  • + +
  • + +
    arrow
    +
    #icon-arrow
    +
  • + +
  • + +
    ai
    +
    #icon-ai
    +
  • + +
  • + +
    smart
    +
    #icon-smart
    +
  • + +
  • + +
    polygon
    +
    #icon-polygon
    +
  • + +
  • + +
    polyline
    +
    #icon-polyline
    +
  • + +
+
+

Symbol 引用

+
+ +

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 + 这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:

+
    +
  • 支持多色图标了,不再受单色限制。
  • +
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • +
  • 兼容性较差,支持 IE9+,及现代浏览器。
  • +
  • 浏览器渲染 SVG 的性能一般,还不如 png。
  • +
+

使用步骤如下:

+

第一步:引入项目下面生成的 symbol 代码:

+
<script src="./iconfont.js"></script>
+
+

第二步:加入通用 CSS 代码(引入一次就行):

+
<style>
+.icon {
+  width: 1em;
+  height: 1em;
+  vertical-align: -0.15em;
+  fill: currentColor;
+  overflow: hidden;
+}
+</style>
+
+

第三步:挑选相应图标并获取类名,应用于页面:

+
<svg class="icon" aria-hidden="true">
+  <use xlink:href="#icon-xxx"></use>
+</svg>
+
+
+
+ +
+
+ + + diff --git a/frontend/image-tool/public/iconfont/fonts/icomoon.eot b/frontend/image-tool/public/iconfont/fonts/icomoon.eot deleted file mode 100644 index 2f97b8c2..00000000 Binary files a/frontend/image-tool/public/iconfont/fonts/icomoon.eot and /dev/null differ diff --git a/frontend/image-tool/public/iconfont/fonts/icomoon.svg b/frontend/image-tool/public/iconfont/fonts/icomoon.svg deleted file mode 100644 index 40b47ed1..00000000 --- a/frontend/image-tool/public/iconfont/fonts/icomoon.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - -Generated by IcoMoon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/image-tool/public/iconfont/fonts/icomoon.ttf b/frontend/image-tool/public/iconfont/fonts/icomoon.ttf deleted file mode 100644 index 5d00e908..00000000 Binary files a/frontend/image-tool/public/iconfont/fonts/icomoon.ttf and /dev/null differ diff --git a/frontend/image-tool/public/iconfont/fonts/icomoon.woff b/frontend/image-tool/public/iconfont/fonts/icomoon.woff deleted file mode 100644 index 4d5cdbca..00000000 Binary files a/frontend/image-tool/public/iconfont/fonts/icomoon.woff and /dev/null differ diff --git a/frontend/image-tool/public/iconfont/iconfont.css b/frontend/image-tool/public/iconfont/iconfont.css new file mode 100644 index 00000000..501ccba3 --- /dev/null +++ b/frontend/image-tool/public/iconfont/iconfont.css @@ -0,0 +1,183 @@ +@font-face { + font-family: "iconfont"; /* Project id 4374613 */ + src: url('iconfont.woff2?t=1704191662778') format('woff2'), + url('iconfont.woff?t=1704191662778') format('woff'), + url('iconfont.ttf?t=1704191662778') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-cube:before { + content: "\e736"; +} + +.icon-play:before { + content: "\e741"; +} + +.icon-replay:before { + content: "\e743"; +} + +.icon-backup:before { + content: "\e745"; +} + +.icon-forward:before { + content: "\e746"; +} + +.icon-Frame23:before { + content: "\e750"; +} + +.icon-crop-non-first:before { + content: "\e769"; +} + +.icon-crop-first:before { + content: "\e76a"; +} + +.icon-transform:before { + content: "\e621"; +} + +.icon-point:before { + content: "\e60d"; +} + +.icon-reset:before { + content: "\e61a"; +} + +.icon-rotate90:before { + content: "\e6de"; +} + +.icon-rotate270:before { + content: "\e6e0"; +} + +.icon-information:before { + content: "\e6fe"; +} + +.icon-hollow:before { + content: "\e620"; +} + +.icon-cancel-hollow:before { + content: "\e752"; +} + +.icon-hotkey:before { + content: "\e767"; +} + +.icon-rect:before { + content: "\e67c"; +} + +.icon-save:before { + content: "\e703"; +} + +.icon-help:before { + content: "\e704"; +} + +.icon-fullscreen:before { + content: "\e705"; +} + +.icon-exitfullscreen:before { + content: "\e706"; +} + +.icon-info:before { + content: "\e70b"; +} + +.icon-setting:before { + content: "\e70c"; +} + +.icon-toleft:before { + content: "\e712"; +} + +.icon-toright:before { + content: "\e713"; +} + +.icon-remind:before { + content: "\e71a"; +} + +.icon-view:before { + content: "\e71f"; +} + +.icon-delete:before { + content: "\e721"; +} + +.icon-editlabel:before { + content: "\e722"; +} + +.icon-filter:before { + content: "\e724"; +} + +.icon-more:before { + content: "\e72d"; +} + +.icon-hidden:before { + content: "\e72f"; +} + +.icon-left:before { + content: "\e730"; +} + +.icon-right:before { + content: "\e731"; +} + +.icon-loading:before { + content: "\e737"; +} + +.icon-model:before { + content: "\e738"; +} + +.icon-arrow:before { + content: "\e751"; +} + +.icon-ai:before { + content: "\e764"; +} + +.icon-smart:before { + content: "\e766"; +} + +.icon-polygon:before { + content: "\e601"; +} + +.icon-polyline:before { + content: "\e602"; +} + diff --git a/frontend/image-tool/public/iconfont/iconfont.js b/frontend/image-tool/public/iconfont/iconfont.js new file mode 100644 index 00000000..6769921b --- /dev/null +++ b/frontend/image-tool/public/iconfont/iconfont.js @@ -0,0 +1 @@ +window._iconfont_svg_string_4374613='',function(h){var a=(a=document.getElementsByTagName("script"))[a.length-1],l=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var c,t,o,v,z,m=function(a,l){l.parentNode.insertBefore(a,l)};if(l&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}c=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_4374613,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?m(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(c,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),c()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(o=c,v=h.document,z=!1,e(),v.onreadystatechange=function(){"complete"==v.readyState&&(v.onreadystatechange=null,i())})}function i(){z||(z=!0,o())}function e(){try{v.documentElement.doScroll("left")}catch(a){return void setTimeout(e,50)}i()}}(window); \ No newline at end of file diff --git a/frontend/image-tool/public/iconfont/iconfont.json b/frontend/image-tool/public/iconfont/iconfont.json new file mode 100644 index 00000000..aede06d0 --- /dev/null +++ b/frontend/image-tool/public/iconfont/iconfont.json @@ -0,0 +1,303 @@ +{ + "id": "4374613", + "name": "image-tool-open", + "font_family": "iconfont", + "css_prefix_text": "icon-", + "description": "图片工具开源", + "glyphs": [ + { + "icon_id": "29769749", + "name": "cube", + "font_class": "cube", + "unicode": "e736", + "unicode_decimal": 59190 + }, + { + "icon_id": "32271488", + "name": "play", + "font_class": "play", + "unicode": "e741", + "unicode_decimal": 59201 + }, + { + "icon_id": "32271490", + "name": "replay", + "font_class": "replay", + "unicode": "e743", + "unicode_decimal": 59203 + }, + { + "icon_id": "32271492", + "name": "backup", + "font_class": "backup", + "unicode": "e745", + "unicode_decimal": 59205 + }, + { + "icon_id": "32271493", + "name": "forward", + "font_class": "forward", + "unicode": "e746", + "unicode_decimal": 59206 + }, + { + "icon_id": "32608846", + "name": "ongoing", + "font_class": "Frame23", + "unicode": "e750", + "unicode_decimal": 59216 + }, + { + "icon_id": "34058418", + "name": "Crop non-first", + "font_class": "crop-non-first", + "unicode": "e769", + "unicode_decimal": 59241 + }, + { + "icon_id": "34058419", + "name": "Crop the first", + "font_class": "crop-first", + "unicode": "e76a", + "unicode_decimal": 59242 + }, + { + "icon_id": "553250", + "name": "transform", + "font_class": "transform", + "unicode": "e621", + "unicode_decimal": 58913 + }, + { + "icon_id": "16082215", + "name": "point", + "font_class": "point", + "unicode": "e60d", + "unicode_decimal": 58893 + }, + { + "icon_id": "23422758", + "name": "reset", + "font_class": "reset", + "unicode": "e61a", + "unicode_decimal": 58906 + }, + { + "icon_id": "26196680", + "name": "rotate-right", + "font_class": "rotate90", + "unicode": "e6de", + "unicode_decimal": 59102 + }, + { + "icon_id": "26196684", + "name": "rotate-left", + "font_class": "rotate270", + "unicode": "e6e0", + "unicode_decimal": 59104 + }, + { + "icon_id": "28765080", + "name": "information", + "font_class": "information", + "unicode": "e6fe", + "unicode_decimal": 59134 + }, + { + "icon_id": "32195920", + "name": "hollow", + "font_class": "hollow", + "unicode": "e620", + "unicode_decimal": 58912 + }, + { + "icon_id": "34047962", + "name": "cancel-hollow", + "font_class": "cancel-hollow", + "unicode": "e752", + "unicode_decimal": 59218 + }, + { + "icon_id": "34059152", + "name": "hotkey", + "font_class": "hotkey", + "unicode": "e767", + "unicode_decimal": 59239 + }, + { + "icon_id": "7405528", + "name": "rect", + "font_class": "rect", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "28765334", + "name": "save", + "font_class": "save", + "unicode": "e703", + "unicode_decimal": 59139 + }, + { + "icon_id": "28765336", + "name": "help", + "font_class": "help", + "unicode": "e704", + "unicode_decimal": 59140 + }, + { + "icon_id": "28765362", + "name": "fullscreen", + "font_class": "fullscreen", + "unicode": "e705", + "unicode_decimal": 59141 + }, + { + "icon_id": "28765402", + "name": "exitfullscreen", + "font_class": "exitfullscreen", + "unicode": "e706", + "unicode_decimal": 59142 + }, + { + "icon_id": "28766072", + "name": "info", + "font_class": "info", + "unicode": "e70b", + "unicode_decimal": 59147 + }, + { + "icon_id": "28766074", + "name": "setting", + "font_class": "setting", + "unicode": "e70c", + "unicode_decimal": 59148 + }, + { + "icon_id": "28766268", + "name": "toleft", + "font_class": "toleft", + "unicode": "e712", + "unicode_decimal": 59154 + }, + { + "icon_id": "28766291", + "name": "toright", + "font_class": "toright", + "unicode": "e713", + "unicode_decimal": 59155 + }, + { + "icon_id": "28766446", + "name": "remind", + "font_class": "remind", + "unicode": "e71a", + "unicode_decimal": 59162 + }, + { + "icon_id": "28766552", + "name": "view", + "font_class": "view", + "unicode": "e71f", + "unicode_decimal": 59167 + }, + { + "icon_id": "28766555", + "name": "delete", + "font_class": "delete", + "unicode": "e721", + "unicode_decimal": 59169 + }, + { + "icon_id": "28766569", + "name": "editlabel", + "font_class": "editlabel", + "unicode": "e722", + "unicode_decimal": 59170 + }, + { + "icon_id": "28766583", + "name": "filter", + "font_class": "filter", + "unicode": "e724", + "unicode_decimal": 59172 + }, + { + "icon_id": "28940596", + "name": "more", + "font_class": "more", + "unicode": "e72d", + "unicode_decimal": 59181 + }, + { + "icon_id": "28941931", + "name": "hidden", + "font_class": "hidden", + "unicode": "e72f", + "unicode_decimal": 59183 + }, + { + "icon_id": "28942286", + "name": "left", + "font_class": "left", + "unicode": "e730", + "unicode_decimal": 59184 + }, + { + "icon_id": "28942378", + "name": "right", + "font_class": "right", + "unicode": "e731", + "unicode_decimal": 59185 + }, + { + "icon_id": "29769750", + "name": "loading ", + "font_class": "loading", + "unicode": "e737", + "unicode_decimal": 59191 + }, + { + "icon_id": "29769751", + "name": "model", + "font_class": "model", + "unicode": "e738", + "unicode_decimal": 59192 + }, + { + "icon_id": "33261081", + "name": "arrow", + "font_class": "arrow", + "unicode": "e751", + "unicode_decimal": 59217 + }, + { + "icon_id": "34058413", + "name": "ai", + "font_class": "ai", + "unicode": "e764", + "unicode_decimal": 59236 + }, + { + "icon_id": "34058415", + "name": "smart", + "font_class": "smart", + "unicode": "e766", + "unicode_decimal": 59238 + }, + { + "icon_id": "36275522", + "name": "polygon", + "font_class": "polygon", + "unicode": "e601", + "unicode_decimal": 58881 + }, + { + "icon_id": "36275575", + "name": "polyline", + "font_class": "polyline", + "unicode": "e602", + "unicode_decimal": 58882 + } + ] +} diff --git a/frontend/image-tool/public/iconfont/iconfont.ttf b/frontend/image-tool/public/iconfont/iconfont.ttf new file mode 100644 index 00000000..18f61e1a Binary files /dev/null and b/frontend/image-tool/public/iconfont/iconfont.ttf differ diff --git a/frontend/image-tool/public/iconfont/iconfont.woff b/frontend/image-tool/public/iconfont/iconfont.woff new file mode 100644 index 00000000..706bbb19 Binary files /dev/null and b/frontend/image-tool/public/iconfont/iconfont.woff differ diff --git a/frontend/image-tool/public/iconfont/iconfont.woff2 b/frontend/image-tool/public/iconfont/iconfont.woff2 new file mode 100644 index 00000000..15b04429 Binary files /dev/null and b/frontend/image-tool/public/iconfont/iconfont.woff2 differ diff --git a/frontend/image-tool/public/iconfont/selection.json b/frontend/image-tool/public/iconfont/selection.json deleted file mode 100644 index 469320dc..00000000 --- a/frontend/image-tool/public/iconfont/selection.json +++ /dev/null @@ -1 +0,0 @@ -{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M150.991 102.401h531.21v102.401h106.24v-204.803h-743.692v716.806h212.483v-102.399h-106.241v-512.006z","M894.707 307.219v102.401h106.24v512.006h-531.206v-102.4h-106.241v204.8h743.687v-716.807h-212.48z","M788.467 307.219h-424.967v409.607h424.967v-409.607zM682.227 614.422h-212.486v-204.803h212.486v204.803z"],"attrs":[],"width":1152,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["polyon-hollow"],"colorPermutations":{}},"attrs":[],"properties":{"order":111,"id":54,"name":"polygon-hollow","prevSize":32,"code":59700},"setIdx":0,"setId":2,"iconIdx":0},{"icon":{"paths":["M872.376 200.458c0-12.55-4.759-24.992-14.173-34.405-18.932-18.933-49.875-18.933-68.807 0l-563.458 563.457c-9.412 9.415-14.173 21.962-14.173 34.406 0 12.439 4.653 24.883 14.173 34.4 18.933 18.938 49.876 18.938 68.809 0l563.455-563.454c9.415-9.412 14.173-21.962 14.173-34.405z","M536.986 487.92c13.416-14.497 28.887-25.642 46.197-33.431l-288.436-288.436c-18.933-18.933-49.876-18.933-68.809 0-9.412 9.412-14.173 21.855-14.173 34.405 0 12.442 4.761 24.992 14.173 34.405l285.082 285.083c6.925-11.685 15.58-22.288 25.966-32.025z","M612.412 882.839c-23.045 4-46.522 6.054-70.324 6.054-54.853 0-108.083-10.71-158.175-31.913-48.361-20.45-91.854-49.766-129.179-87.203-37.326-37.328-66.646-80.818-87.202-129.289-21.205-50.091-31.916-103.321-31.916-158.174s10.711-108.083 31.916-158.175c20.448-48.361 49.768-91.854 87.202-129.288 37.325-37.326 80.818-66.646 129.179-87.202 50.092-21.205 103.322-31.916 158.175-31.916s108.080 10.711 158.177 31.916c48.357 20.448 91.853 49.768 129.283 87.202 37.328 37.325 66.65 80.818 87.203 129.288 21.209 50.092 31.919 103.322 31.919 158.175 0 23.802-2.054 47.171-6.060 70.216h76.601c3.349-22.936 5.084-46.414 5.084-70.324 0-266.257-215.95-482.206-482.207-482.206s-482.206 215.948-482.206 482.206c0 266.26 215.949 482.203 482.206 482.203 23.91 0 47.388-1.729 70.324-5.084v-76.487zM978.745 732.124l-186.735 186.741c-16.878 16.878-44.357 16.878-61.235 0s-16.878-44.357 0-61.235l186.735-186.741c16.878-16.878 44.357-16.878 61.235 0 16.77 16.878 16.77 44.466 0 61.235z","M792.010 670.901l186.741 186.735c16.878 16.878 16.878 44.357 0 61.235s-44.363 16.878-61.235 0l-186.741-186.735c-16.878-16.878-16.878-44.357 0-61.235 16.878-16.77 44.466-16.77 61.235 0z"],"attrs":[],"width":1084,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["cancel-hollow"],"colorPermutations":{}},"attrs":[],"properties":{"order":112,"id":53,"name":"cancel-hollow","prevSize":32,"code":59701},"setIdx":0,"setId":2,"iconIdx":1},{"icon":{"paths":["M518.529 315.965c-3.389 0-5.081 0-8.466-1.692l-86.363-50.803c-5.081-3.389-8.466-8.466-8.466-15.242s3.389-11.853 8.466-15.242l196.437-115.151c40.643-23.707 91.442-25.399 133.782-5.080l33.866 16.934c5.080 3.389 8.466 8.466 8.466 15.242s-3.389 11.853-8.466 15.242l-262.466 152.407c-1.692 1.692-3.389 3.389-6.773 3.389h-0.010z","M196.505 442.965c0 9.351-7.581 16.934-16.934 16.934s-16.934-7.581-16.934-16.934c0-9.351 7.581-16.934 16.934-16.934s16.934 7.581 16.934 16.934z","M440.468 334.58l-86.363-50.802c-5.081-3.389-11.853-3.389-16.934 0l-77.898 45.722c-13.546 8.466-30.48 10.161-44.030 5.080-32.177-10.161-66.045-6.772-93.135 10.162-54.191 32.176-72.818 101.605-42.333 155.791 15.242 25.399 38.949 45.722 67.736 52.494 10.162 3.389 20.324 3.389 28.788 3.389 20.323 0 38.949-5.081 57.575-15.242 32.176-18.626 54.191-52.495 57.575-91.444l149.019-86.363c5.081-3.389 8.466-8.466 8.466-15.242s-3.389-10.161-8.466-13.546zM179.682 492.069c-27.096 0-49.11-22.015-49.11-49.111s22.015-49.11 49.11-49.11c27.096 0 49.11 22.015 49.11 49.11s-22.015 49.111-49.11 49.111z","M196.505 155.080c0 9.351-7.581 16.934-16.934 16.934s-16.934-7.581-16.934-16.934c0-9.351 7.581-16.934 16.934-16.934s16.934 7.581 16.934 16.934z","M787.627 436.187l-496.178-289.562c-3.389-37.258-23.706-72.818-57.575-91.444-25.399-15.242-55.883-18.626-86.363-11.853-28.787 8.466-54.191 27.096-67.736 52.494-30.48 54.191-11.853 125.312 42.333 155.791 27.096 16.934 60.964 20.323 91.444 10.161 15.242-5.080 30.479-3.389 44.029 5.081l362.391 211.685c22.015 13.546 45.722 18.626 71.124 18.626 22.020 0 42.336-5.080 62.658-15.242l33.866-16.934c5.080-3.389 8.466-8.466 8.466-15.242 0-3.389-3.389-10.161-8.466-13.546l0.013-0.020zM179.704 204.179c-27.096 0-49.111-22.015-49.111-49.11s22.015-49.111 49.111-49.111c27.096 0 49.11 22.015 49.11 49.111s-22.015 49.11-49.11 49.11z","M430.321 315.97c-1.692 0-5.080 0-6.772-1.692s-3.389-1.692-5.081-3.389c-1.692-1.692-3.389-3.389-3.389-5.080s-1.692-5.081-1.692-6.773c0-1.692 0-5.080 1.692-6.773s1.692-3.389 3.389-5.080c1.692-1.692 3.389-3.389 5.081-3.389 3.389-1.692 6.772-1.692 10.161-1.692 1.692 0 1.692 0 3.389 1.692 1.692 0 1.692 0 3.389 1.692 1.692 0 1.692 1.692 3.389 1.692 3.389 3.389 5.081 6.773 5.081 11.853s-1.692 8.466-5.081 11.853c-1.692 1.692-3.389 3.389-5.081 3.389-3.389 1.692-6.772 1.692-8.466 1.692z","M154.108 668.198c-17.143 0-28.576-6.772-28.576-16.933v-50.805c0-10.161 11.428-16.934 28.576-16.934s28.575 6.773 28.575 16.934v50.805c0 8.466-14.288 16.933-28.575 16.933z","M154.108 825.669c-17.143 0-28.576-6.772-28.576-16.933v-77.899c0-10.161 11.428-16.933 28.576-16.933s28.575 6.772 28.575 16.933v77.899c0 8.466-14.288 16.933-28.575 16.933z","M216.968 983.999h-68.579c-13.717 0-22.862-9.145-22.862-22.862v-68.577c0-13.717 9.144-22.862 22.862-22.862s22.861 9.145 22.861 22.862v45.719h45.718c13.717 0 22.862 9.145 22.862 22.859 0 13.717-11.428 22.862-22.862 22.862z","M951.706 977.653h-94.832c-10.161 0-16.933-11.427-16.933-28.575s6.772-28.575 16.933-28.575h94.832c10.161 0 16.939 11.427 16.939 28.575s-6.779 28.575-16.939 28.575zM762.046 977.653h-94.832c-10.161 0-16.932-11.427-16.932-28.575s6.772-28.575 16.932-28.575h94.832c10.161 0 16.933 11.427 16.933 28.575s-8.466 28.575-16.933 28.575zM572.383 977.653h-94.832c-10.161 0-16.934-11.427-16.934-28.575s6.773-28.575 16.934-28.575h94.832c10.161 0 16.934 11.427 16.934 28.575s-8.466 28.575-16.934 28.575zM382.724 977.653h-94.833c-10.161 0-16.934-11.427-16.934-28.575s6.773-28.575 16.934-28.575h94.833c10.161 0 16.934 11.427 16.934 28.575s-8.466 28.575-16.934 28.575z","M1095.98 983.999h-68.584c-13.714 0-22.859-9.145-22.859-22.862 0-13.714 9.145-22.859 22.859-22.859h45.719v-45.719c0-13.717 9.145-22.862 22.862-22.862 13.714 0 22.859 9.145 22.859 22.862v68.577c0 13.717-9.145 22.862-22.859 22.862z","M1090.27 782.927c-17.139 0-28.575-6.53-28.575-16.324v-102.87c0-9.794 11.433-16.324 28.575-16.324 17.143 0 28.582 6.53 28.582 16.324v102.87c0 8.161-11.433 16.324-28.582 16.324zM1090.27 575.568c-17.139 0-28.575-6.53-28.575-16.328v-102.867c0-9.797 11.433-16.328 28.575-16.328 17.143 0 28.582 6.53 28.582 16.328v102.867c0 8.166-11.433 16.328-28.582 16.328z","M1095.98 396.376c-13.717 0-22.862-9.144-22.862-22.862v-45.718h-45.719c-13.714 0-22.859-9.144-22.859-22.862s9.145-22.861 22.859-22.861h68.584c13.714 0 22.859 9.144 22.859 22.861v68.58c0 11.428-9.145 22.862-22.859 22.862z","M929.98 339.248h-116.846c-10.167 0-16.939-11.428-16.939-28.576s6.772-28.576 16.939-28.576h116.846c10.161 0 16.933 11.429 16.933 28.576s-6.772 28.576-16.933 28.576z","M697.993 339.248h-50.802c-10.162 0-16.934-11.428-16.934-28.576s6.772-28.576 16.934-28.576h50.802c10.161 0 16.933 11.429 16.933 28.576s-6.772 28.576-16.933 28.576z"],"attrs":[],"width":1184,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["polygon_clip"],"colorPermutations":{}},"attrs":[],"properties":{"order":55,"id":49,"name":"polygon-clip","prevSize":32,"code":59697},"setIdx":0,"setId":2,"iconIdx":2},{"icon":{"paths":["M965.569 130.968c67.618 0 122.432 54.815 122.432 122.432v0 517.202c0 67.618-54.815 122.432-122.432 122.432v0h-843.139c-67.618 0-122.432-54.815-122.432-122.432v-517.202c0-67.618 54.815-122.432 122.432-122.432v0h843.139zM965.569 212.589h-843.139c-22.538 0-40.811 18.273-40.811 40.811v0 517.202c0 22.527 18.284 40.811 40.811 40.811h843.139c22.538 0 40.811-18.273 40.811-40.811v0-517.202c0-22.538-18.273-40.811-40.811-40.811v0zM258.464 647.897h571.344c22.533 0.007 40.799 18.276 40.799 40.811 0 20.577-15.23 37.596-35.029 40.404l-0.218 0.027-5.552 0.381h-571.344c-22.533-0.007-40.799-18.276-40.799-40.811 0-20.577 15.23-37.596 35.029-40.404l0.218-0.027 5.552-0.381zM788.998 457.451c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM462.787 457.451c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM299.547 457.451c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM626.028 457.451c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM217.653 294.207c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM380.624 294.207c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM543.864 294.207c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM707.107 294.207c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0zM870.347 294.207c30.052 0 54.413 24.361 54.413 54.413s-24.361 54.413-54.413 54.413v0c-30.052 0-54.413-24.361-54.413-54.413s24.361-54.413 54.413-54.413v0z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["keyboard"],"colorPermutations":{},"width":1088},"attrs":[],"properties":{"order":56,"id":50,"name":"keyboard","prevSize":32,"code":59698},"setIdx":0,"setId":2,"iconIdx":3},{"icon":{"paths":["M749.022 594.044h-100.282v-164.094h100.282c85.593 0 154.978-69.385 154.978-154.978s-69.385-154.978-154.978-154.978c-85.593 0-154.978 69.385-154.978 154.978v0 100.282h-164.094v-100.282c0-85.593-69.385-154.978-154.978-154.978s-154.978 69.385-154.978 154.978c0 85.593 69.385 154.978 154.978 154.978v0h100.282v164.094h-100.282c-85.593 0-154.978 69.385-154.978 154.978s69.385 154.978 154.978 154.978c85.593 0 154.978-69.385 154.978-154.978v0-100.282h164.094v100.282c0 85.593 69.385 154.978 154.978 154.978s154.978-69.385 154.978-154.978c0-85.593-69.385-154.978-154.978-154.978v0zM648.746 274.978c0-55.381 44.895-100.282 100.282-100.282s100.282 44.895 100.282 100.282c0 55.381-44.895 100.282-100.282 100.282v0h-100.282zM174.696 274.978c0-55.381 44.895-100.282 100.282-100.282s100.282 44.895 100.282 100.282v0 100.282h-100.282c-55.278-0.256-100.020-44.998-100.282-100.254v-0.028zM375.254 749.022c0 55.381-44.895 100.282-100.282 100.282s-100.282-44.895-100.282-100.282c0-55.381 44.895-100.282 100.282-100.282v0h100.282zM429.956 429.956h164.094v164.094h-164.094zM749.022 849.304c-55.278-0.256-100.020-44.998-100.282-100.254v-100.304h100.282c55.381 0 100.282 44.895 100.282 100.282s-44.895 100.282-100.282 100.282v0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["command"]},"attrs":[{}],"properties":{"order":57,"id":51,"name":"command","prevSize":32,"code":59699},"setIdx":0,"setId":2,"iconIdx":4},{"icon":{"paths":["M358.39 1024c-11.024-0.165-20.77-5.496-26.945-13.673l-0.063-0.087c-7.097-8.543-11.403-19.624-11.403-31.71 0-0.372 0.004-0.743 0.012-1.113l-0.001 0.055v-93.12c-0.006-0.287-0.009-0.625-0.009-0.964 0-12.065 4.28-23.131 11.405-31.761l-0.068 0.085c6.189-8.365 15.994-13.749 27.060-13.824l0.012-0h38.4v-279.424h-38.4c-11.024-0.165-20.77-5.496-26.945-13.673l-0.063-0.087c-7.096-8.542-11.402-19.623-11.402-31.709 0-0.35 0.004-0.699 0.011-1.047l-0.001 0.052v-93.12c-0.006-0.279-0.009-0.608-0.009-0.937 0-12.095 4.279-23.19 11.406-31.854l-0.069 0.087c6.213-8.325 16.009-13.679 27.059-13.76l0.013-0h230.4c11.019 0.128 20.771 5.441 26.946 13.61l0.062 0.086c7.020 8.565 11.274 19.631 11.274 31.69 0 0.356-0.004 0.712-0.011 1.067l0.001-0.053v419.008h38.4c11.019 0.128 20.771 5.441 26.946 13.61l0.062 0.086c7.173 8.679 11.524 19.921 11.524 32.179 0 0.23-0.002 0.459-0.005 0.688l0-0.035v93.12c0.006 0.285 0.009 0.622 0.009 0.959 0 12.087-4.28 23.175-11.406 31.831l0.069-0.086c-6.246 8.298-16.016 13.653-27.045 13.824l-0.027 0zM435.19 232.576c-11.029-0.144-20.782-5.481-26.945-13.672l-0.063-0.088c-7.096-8.534-11.402-19.607-11.402-31.686 0-0.358 0.004-0.715 0.011-1.071l-0.001 0.053v-139.648c-0.008-0.325-0.012-0.709-0.012-1.093 0-12.044 4.282-23.089 11.407-31.694l-0.067 0.083c6.22-8.318 16.012-13.669 27.057-13.76l0.015-0h153.6c11.029 0.144 20.782 5.481 26.945 13.672l0.063 0.088c7.097 8.479 11.408 19.501 11.408 31.531 0 0.435-0.006 0.869-0.017 1.301l0.001-0.064v139.584c0.007 0.315 0.011 0.686 0.011 1.058 0 12.046-4.255 23.097-11.345 31.734l0.070-0.088c-6.226 8.279-15.979 13.616-26.985 13.76l-0.023 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Job information"]},"attrs":[{}],"properties":{"order":58,"id":48,"name":"Job-information","prevSize":32,"code":59648},"setIdx":0,"setId":2,"iconIdx":5},{"icon":{"paths":["M943.003 201.56c8.725 7.503 20.162 12.071 32.666 12.071 14.241 0 27.097-5.926 36.239-15.446l0.016-0.017c7.778-8.149 12.565-19.212 12.565-31.393 0-13.883-6.218-26.313-16.020-34.659l-0.064-0.053-126.322-109.492c-8.72-7.485-20.143-12.041-32.631-12.041-14.226 0-27.071 5.913-36.21 15.416l-0.016 0.016c-7.778 8.149-12.565 19.212-12.565 31.393 0 13.883 6.218 26.313 16.020 34.659l0.064 0.053 26.813 23.23-49.211 51.834c-54.445-39.050-118.927-68.012-188.687-82.614l-3.293-0.577c-1.994-17.969-8.906-34.028-19.342-47.159l0.144 0.188c-19.624-22.622-48.415-36.845-80.527-36.845-1.139 0-2.274 0.018-3.405 0.053l0.165-0.004h-1.984c-1.43-0.070-3.105-0.11-4.79-0.11-29.785 0-56.666 12.43-75.74 32.386l-0.038 0.040c-12.229 13.843-20.606 31.353-23.299 50.684l-0.058 0.511c-222.314 42.799-388.644 233.779-392.975 464.386l-0.007 0.46c12.023 265.362 230.080 475.948 497.323 475.948s485.3-210.586 497.284-474.869l0.039-1.079c-0.347-123.758-49.277-236.021-128.713-318.779l0.15 0.158 49.275-51.898 27.133 23.549zM497.995 930.122c-203.355-9.664-364.569-176.886-364.569-381.762 0-211.065 171.102-382.167 382.167-382.167s382.167 171.102 382.167 382.167c0 0.015-0 0.030-0 0.045l0-0.002c-5.421 212.011-178.576 381.809-391.39 381.809-2.945 0-5.882-0.033-8.812-0.097l0.437 0.008zM583.297 548.339c-1.519 44.752-38.163 80.454-83.146 80.454-0.556 0-1.11-0.005-1.664-0.016l0.083 0.001c-0.46 0.009-1.002 0.015-1.545 0.015-44.675 0-80.97-35.877-81.646-80.39l-0.001-0.064c-0.019-0.627-0.030-1.365-0.030-2.105 0-26.837 14.44-50.301 35.975-63.046l0.34-0.186v-188.78c1.367-25.869 22.675-46.328 48.763-46.328s47.396 20.459 48.758 46.206l0.005 0.121v188.78c20.678 12.756 34.253 35.285 34.253 60.985 0 1.599-0.053 3.185-0.156 4.758l0.011-0.214z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Expiration time"]},"attrs":[{}],"properties":{"order":59,"id":47,"name":"Expiration-time","prevSize":32,"code":59649},"setIdx":0,"setId":2,"iconIdx":6},{"icon":{"paths":["M926.663 374.901c-45.589-107.289-130.322-190.524-235.828-233.078l-2.885-1.028c-5.511-2.114-9.499-7.041-10.231-12.977l-0.008-0.077c-6.966-71.949-67.125-127.74-140.315-127.74-76.563 0-138.866 61.051-140.884 137.126l-0.004 0.187v7.525c-0 0.088-0 0.192-0 0.296 0 20.379 4.14 39.791 11.626 57.441l-0.363-0.965c21.142 51.962 71.249 87.927 129.756 87.927 53.453 0 99.895-30.020 123.385-74.12l0.366-0.753c2.694-5.011 7.9-8.36 13.888-8.36 2.213 0 4.32 0.458 6.23 1.283l-0.102-0.039c5.119 2.304 10.699 4.812 13.259 6.041 37.957 19.073 70.404 43.035 98.157 71.585l0.082 0.085c26.659 27.406 49.157 59.047 66.353 93.779l0.966 2.157c1.090 2.118 1.729 4.623 1.729 7.276 0 7.641-5.298 14.045-12.42 15.739l-0.11 0.022-27.644 5.836c-1.816 0.446-3.142 2.059-3.142 3.983 0 1.024 0.376 1.959 0.996 2.677l-0.004-0.005 108.017 124.348c0.717 0.826 1.769 1.345 2.942 1.345 1.733 0 3.202-1.134 3.705-2.7l0.008-0.028 20.989-65.066 30.101-92.967c0.114-0.355 0.18-0.763 0.18-1.187 0-2.205-1.788-3.993-3.993-3.993-0.245 0-0.484 0.022-0.717 0.064l0.024-0.004-36.654 7.73c-1.023 0.243-2.198 0.382-3.405 0.382-6.425 0-11.928-3.945-14.22-9.546l-0.037-0.102zM606.195 183.593c-13.199 24.914-38.963 41.598-68.624 41.598s-55.425-16.683-68.421-41.178l-0.203-0.42c-6.188-11.168-9.829-24.488-9.829-38.658 0-0.051 0-0.103 0-0.154l-0 0.008c0.022-9.153 1.5-17.953 4.214-26.192l-0.17 0.595c10.113-32.074 39.592-54.926 74.409-54.926s64.296 22.851 74.259 54.374l0.15 0.552c2.551 7.587 4.024 16.324 4.024 25.406 0 14.264-3.633 27.68-10.023 39.373l0.215-0.429zM955.69 616.072c-17.257-10.968-37.842-18.305-59.949-20.434l-0.561-0.044c-3.741-0.391-8.083-0.614-12.477-0.614-0.023 0-0.046 0-0.069 0l0.004-0c-78.203 1.034-141.201 64.671-141.201 143.022 0 0.616 0.004 1.231 0.012 1.846l-0.001-0.093c-0 0.128-0.001 0.28-0.001 0.431 0 28.414 8.1 54.937 22.116 77.384l-0.359-0.616c1.55 2.466 2.469 5.464 2.469 8.677 0 5.109-2.325 9.676-5.974 12.699l-0.027 0.022c-23.654 19.112-50.628 35.612-79.626 48.353l-2.283 0.894c-41.207 18.377-89.293 29.078-139.879 29.078-0.209 0-0.418-0-0.627-0.001l0.033 0c-8.447 0-16.843-0.307-25.187-0.921-8.27-0.696-14.719-7.58-14.719-15.97 0-2.101 0.404-4.108 1.139-5.946l-0.038 0.108 10.597-28.259c0.179-0.437 0.283-0.945 0.283-1.477 0-2.205-1.788-3.993-3.993-3.993-0.208 0-0.413 0.016-0.613 0.047l0.022-0.003-160.183 26.211c-1.91 0.372-3.332 2.032-3.332 4.024 0 0.958 0.329 1.84 0.881 2.538l-0.007-0.009 102.079 129.467c0.709 0.899 1.799 1.471 3.022 1.471 1.676 0 3.101-1.074 3.625-2.571l0.008-0.027 12.747-33.89c2.237-6.096 7.991-10.367 14.742-10.367 0.705 0 1.398 0.047 2.078 0.137l-0.080-0.009c17.118 2.494 36.882 3.918 56.979 3.918 105.985 0 202.719-39.611 276.204-104.835l-0.429 0.374c2.704-2.426 6.297-3.909 10.236-3.909 2.145 0 4.187 0.44 6.041 1.234l-0.1-0.038c14.35 6.178 31.012 10.034 48.5 10.642l0.235 0.006c0.762 0.015 1.66 0.023 2.56 0.023 79.165 0 143.34-64.176 143.34-143.34 0-0.008-0-0.016-0-0.024l0 0.001c0.008-0.567 0.012-1.236 0.012-1.906 0-51.774-26.987-97.245-67.661-123.114l-0.592-0.352zM882.637 820.332c-0.080 0-0.174 0-0.268 0-5.179 0-10.231-0.541-15.104-1.569l0.475 0.084c-22.147-4.577-40.512-18.012-51.559-36.353l-0.197-0.352c-7.394-12.030-11.775-26.602-11.775-42.197 0-0.049 0-0.098 0-0.147l-0 0.008c-0.003-0.255-0.005-0.556-0.005-0.857 0-43.102 34.256-78.204 77.027-79.564l0.125-0.003h1.28c22.429 0.039 42.614 9.611 56.726 24.879l0.047 0.052c13.42 14.456 21.655 33.888 21.655 55.244 0 0.088-0 0.175-0 0.262l0-0.013c0.004 0.292 0.006 0.636 0.006 0.981 0 43.543-34.982 78.915-78.374 79.545l-0.059 0.001zM193.733 595.543c-8.132-0.296-14.695-6.629-15.354-14.635l-0.004-0.057c-0.94-10.147-1.479-21.952-1.485-33.881l-0-0.009c-0.003-0.562-0.005-1.226-0.005-1.891 0-51.229 10.274-100.057 28.874-144.539l-0.918 2.475c10.481-25.285 22.302-46.972 36.091-67.211l-0.819 1.274c2.788-4.183 7.487-6.901 12.82-6.901 4.935 0 9.326 2.328 12.136 5.945l0.026 0.035 16.689 21.092c0.709 0.899 1.799 1.471 3.022 1.471 1.676 0 3.101-1.074 3.625-2.571l0.008-0.027 58.053-155.729c0.179-0.437 0.283-0.945 0.283-1.477 0-2.205-1.788-3.993-3.993-3.993-0.208 0-0.413 0.016-0.613 0.047l0.022-0.003-67.37 11.365-92.659 15.102c-1.91 0.372-3.332 2.032-3.332 4.024 0 0.958 0.329 1.84 0.881 2.538l-0.007-0.009 24.419 30.716c2.137 2.752 3.425 6.255 3.425 10.059s-1.289 7.307-3.453 10.097l0.028-0.037c-56.322 73.599-90.249 166.937-90.249 268.199 0 20.797 1.431 41.259 4.2 61.295l-0.263-2.32c0.096 0.666 0.151 1.435 0.151 2.216 0 5.362-2.576 10.123-6.559 13.111l-0.042 0.030c-36.604 26.711-60.118 69.462-60.118 117.705 0 0.788 0.006 1.574 0.019 2.359l-0.001-0.119c-0 0.007-0 0.015-0 0.023 0 79.165 64.176 143.34 143.34 143.34 0.9 0 1.798-0.008 2.694-0.025l-0.135 0.002c21.032-0.683 40.663-6.036 58.095-15.049l-0.759 0.357c19.77-10.104 36.255-24.003 49.044-40.859l0.255-0.351c18.532-24.279 29.693-55.049 29.693-88.426 0-0.157-0-0.313-0.001-0.469l0 0.024c0.006-0.484 0.009-1.056 0.009-1.629 0-72.87-54.376-133.040-124.765-142.148l-0.718-0.076c-3.843-0.297-8.322-0.467-12.841-0.467-0.507 0-1.013 0.002-1.519 0.006l0.077-0.001zM259.107 782.244c-11.29 18.686-29.626 32.146-51.238 36.766l-0.518 0.093c-4.398 0.944-9.45 1.485-14.629 1.485-0.094 0-0.188-0-0.283-0.001l0.015 0c-43.451-0.632-78.433-36.004-78.433-79.546 0-0.309 0.002-0.617 0.005-0.925l-0 0.047c-0-0.074-0-0.161-0-0.249 0-21.356 8.235-40.788 21.702-55.296l-0.047 0.051c14.149-15.335 34.341-24.912 56.77-24.931l1.283-0c42.896 1.365 77.151 36.466 77.151 79.567 0 0.247-0.001 0.494-0.003 0.741l0-0.038c0 0.020 0 0.043 0 0.066 0 15.585-4.38 30.147-11.977 42.523l0.202-0.354z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Work flow"]},"attrs":[{}],"properties":{"order":60,"id":46,"name":"Work-flow","prevSize":32,"code":59650},"setIdx":0,"setId":2,"iconIdx":7},{"icon":{"paths":["M565.453 274.995c20.517-0.029 37.142-16.654 37.171-37.168l0-0.003v-37.171c0-20.543-16.654-37.197-37.197-37.197s-37.197 16.654-37.197 37.197h-0v37.171c0.116 20.5 16.718 37.085 37.214 37.171l0.008 0z","M960.154 191.949l-147.61-147.456c-6.507-6.687-15.552-10.876-25.575-11.008l-0.025-0h-664.013c-61.008 0.174-110.418 49.584-110.592 110.575l-0 0.017v737.28c0.174 61.008 49.584 110.418 110.575 110.592l0.017 0h737.28c61.008-0.174 110.418-49.584 110.592-110.575l0-0.017v-663.808c-0.006-10.005-4.076-19.058-10.648-25.599l-0.001-0.001zM325.786 106.957h331.93v197.325c-0.226 13.17-10.843 23.787-23.991 24.013l-0.021 0h-283.904c-13.17-0.226-23.787-10.843-24.013-23.991l-0-0.021zM712.96 918.221h-442.419v-254.413c0.229-22.303 18.248-40.321 40.529-40.55l0.022-0h361.37c22.303 0.229 40.321 18.248 40.55 40.529l0 0.022v254.413zM897.28 881.357c-0.029 20.348-16.516 36.835-36.861 36.864l-0.003 0h-71.68v-254.413c-0.289-62.998-51.281-113.989-114.251-114.278l-0.028-0h-363.52c-62.998 0.289-113.989 51.281-114.278 114.251l-0 0.028v254.413h-73.728c-20.348-0.029-36.835-16.516-36.864-36.861l-0-0.003v-737.536c0.029-20.348 16.516-36.835 36.861-36.864l0.003-0h129.075v197.325c0.433 53.829 44.041 97.328 97.87 97.587l0.025 0h283.955c53.841-0.346 97.395-43.9 97.741-97.708l0-0.033v-197.171h40.55l125.44 125.44v648.96z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["save"]},"attrs":[{},{}],"properties":{"order":61,"id":45,"name":"save","prevSize":32,"code":59651},"setIdx":0,"setId":2,"iconIdx":8},{"icon":{"paths":["M775.398 5.024h-543.829c-125.552 1.999-226.563 104.246-226.563 230.085 0 0.999 0.006 1.996 0.019 2.992l-0.002-0.151v372.651c-0.011 0.845-0.018 1.842-0.018 2.841 0 125.839 101.011 228.086 226.375 230.083l0.188 0.002h138.369l133.546 158.516 133.546-158.516h138.369c125.552-1.999 226.563-104.246 226.563-230.085 0-0.999-0.006-1.996-0.019-2.992l0.002 0.151v-372.651c0.012-0.859 0.018-1.874 0.018-2.89 0-125.83-101.017-228.065-226.378-230.033l-0.185-0.002zM939.843 610.551c-10.218 87.415-77.644 156.416-163.402 168.942l-1.043 0.125h-179.819l-92.095 115.559-92.095-115.207h-179.819c-86.801-12.651-154.227-81.652-164.359-168.165l-0.086-0.903v-372.752c10.218-87.415 77.644-156.416 163.402-168.942l1.043-0.125h543.728c89.35 7.228 159.749 79.226 164.427 168.628l0.018 0.439v372.651z","M520.818 212.025c-7.22-1.282-15.533-2.015-24.016-2.015-73.902 0-134.81 55.633-143.129 127.304l-0.063 0.671 63.005 15.073q19.544-86.769 95.461-86.87c0.799-0.034 1.736-0.054 2.678-0.054 36.384 0 65.972 29.101 66.757 65.296l0.001 0.073q2.16 43.46-58.633 86.87c-34.041 19.907-56.545 56.282-56.545 97.914 0 1.417 0.026 2.828 0.078 4.232l-0.006-0.203v21.755h58.633v-17.384c-0-0.042-0-0.092-0-0.142 0-33.862 19.219-63.235 47.343-77.806l0.488-0.23c47.1-21.38 79.461-67.62 80.387-121.519l0.002-0.119c-4.912-63.565-57.699-113.283-122.099-113.283-3.64 0-7.243 0.159-10.803 0.47l0.462-0.033zM459.923 594.323h71.697v69.436h-71.697z"],"attrs":[{},{}],"width":1025,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["help"]},"attrs":[{},{}],"properties":{"order":62,"id":44,"name":"help","prevSize":32,"code":59652},"setIdx":0,"setId":2,"iconIdx":9},{"icon":{"paths":["M69.734 1003.827v-0.256h-5.12c-16.257-2.265-29.172-14.3-32.718-29.896l-0.050-0.261-0.41-7.629v-307.2c-0-21.547 17.467-39.014 39.014-39.014s39.014 17.467 39.014 39.014v0 213.197l227.891-227.635c7.057-7.076 16.815-11.454 27.597-11.454 21.524 0 38.974 17.449 38.974 38.974 0 10.743-4.347 20.471-11.378 27.521l0.001-0.001-227.942 227.482h220.16c18.507 3.204 32.404 19.139 32.404 38.321 0 20.992-16.645 38.097-37.456 38.836l-0.068 0.002zM614.81 965.222c-0.003-0.184-0.005-0.402-0.005-0.62 0-21.315 17.093-38.637 38.319-39.008l0.035-0h270.95v-267.878c0.342-21.377 17.624-38.606 38.99-38.861l0.024-0h3.584c20.033 1.857 35.624 18.479 35.84 38.788l0 0.022v306.79c-0.174 21.556-17.689 38.963-39.269 38.963-0 0-0.001-0-0.001-0l-309.555 0c-21.197-0.081-38.403-17.054-38.86-38.153l-0.001-0.043zM69.683 417.075c-21.473-0.087-38.854-17.486-38.912-38.958l-0-0.006v-307.2c0.317-21.467 17.766-38.754 39.265-38.81l0.005-0h307.2c0.522-0.025 1.133-0.039 1.748-0.039 21.519 0 38.963 17.444 38.963 38.963 0 21.179-16.898 38.412-37.948 38.95l-0.050 0.001h-271.36v268.288c-0.058 21.475-17.48 38.861-38.963 38.861-0 0-0-0-0-0l0 0zM924.006 378.163v-212.941l-227.635 227.533c-7.059 7.091-16.828 11.479-27.622 11.479-21.525 0-38.974-17.449-38.974-38.974 0-10.73 4.336-20.448 11.353-27.496l-0.001 0.001 227.789-227.533h-213.043c-0.404 0.015-0.88 0.024-1.357 0.024-21.588 0-39.089-17.501-39.089-39.089 0-21.111 16.736-38.314 37.664-39.064l0.069-0.002h314.931c16.446 2.348 29.474 14.616 32.925 30.461l0.047 0.259v3.174l2.97 7.066-1.997 305.050c-0.087 21.491-17.515 38.883-39.012 38.912l-0.003 0c-21.476-0.086-38.87-17.444-39.014-38.898l-0-0.014z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["enter-fullscreen"]},"attrs":[{}],"properties":{"order":63,"id":43,"name":"enter-fullscreen","prevSize":32,"code":59653},"setIdx":0,"setId":2,"iconIdx":10},{"icon":{"paths":["M367.014 619.016h5.678c17.137 2.353 30.758 15.033 34.484 31.469l0.052 0.274 0.419 8.006v323.997c-0.768 22.121-18.885 39.762-41.122 39.762s-40.355-17.641-41.12-39.692l-0.002-0.070v-224.484l-240.216 239.984c-7.493 7.808-18.015 12.66-29.671 12.66-22.698 0-41.099-18.401-41.099-41.099 0-11.621 4.823-22.115 12.576-29.59l0.013-0.012 240.216-240.030h-232.304c-19.499-3.406-34.132-20.207-34.132-40.426 0-22.145 17.554-40.189 39.506-40.979l0.072-0.002h326.696z","M657.731 0.020c22.594 0.131 40.874 18.412 41.006 40.993l0 0.013v224.438l240.077-239.844c7.495-7.821 18.026-12.682 29.692-12.682 22.698 0 41.099 18.401 41.099 41.099 0 11.608-4.813 22.092-12.551 29.566l-0.012 0.011-240.216 239.891h224.624c0.423-0.016 0.92-0.025 1.42-0.025 22.673 0 41.054 18.38 41.054 41.054 0 22.174-17.58 40.242-39.562 41.027l-0.072 0.002h-332.095c-17.4-2.48-31.169-15.511-34.721-32.312l-0.048-0.27v-3.118l-3.118-7.633 2.141-321.157c0.079-22.638 18.449-40.959 41.099-40.959 0 0 0 0 0 0l-0-0z","M656.148 1023.86c-22.194-0.646-39.939-18.792-39.939-41.082 0-21.815 16.997-39.661 38.47-41.016l0.119-0.006h285.039v-282.386c0.362-22.523 18.564-40.676 41.072-40.959l0.027-0h3.77c20.992 2.095 37.274 19.572 37.468 40.893l0 0.020v323.438c-0.158 22.7-18.57 41.047-41.28 41.099l-0.005 0zM40.923 405.563c-22.575-0.132-40.834-18.423-40.913-40.998l-0-0.008v-323.531c0.236-22.649 18.613-40.928 41.278-41.006l0.007-0h323.112c0.396-0.014 0.861-0.021 1.328-0.021 22.698 0 41.099 18.401 41.099 41.099 0 10.828-4.187 20.678-11.031 28.019l0.023-0.024c-7.179 7.711-17.271 12.626-28.514 12.984l-0.064 0.002h-285.225v282.525c-0.079 22.622-18.423 40.933-41.050 40.959l-0.003 0z"],"attrs":[{},{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["exit-fullscreen"]},"attrs":[{},{},{}],"properties":{"order":64,"id":42,"name":"exit-fullscreen","prevSize":32,"code":59654},"setIdx":0,"setId":2,"iconIdx":11},{"icon":{"paths":["M512 1024c-282.77 0-512-229.23-512-512s229.23-512 512-512c282.77 0 512 229.23 512 512v0c0 282.77-229.23 512-512 512v0zM512 930.889c231.346 0 418.889-187.543 418.889-418.889s-187.543-418.889-418.889-418.889c-231.346 0-418.889 187.543-418.889 418.889h-0c0 231.346 187.543 418.889 418.889 418.889v0zM418.889 325.851v372.297c0 25.712-20.844 46.555-46.555 46.555s-46.555-20.844-46.555-46.555v0-372.297c0-25.712 20.844-46.555 46.555-46.555s46.555 20.844 46.555 46.555v-0zM698.149 325.851v372.297c0 25.712-20.844 46.555-46.555 46.555s-46.555-20.844-46.555-46.555l0-0v-372.297c-0-25.712 20.844-46.555 46.555-46.555s46.555 20.844 46.555 46.555v-0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["hang up"]},"attrs":[{}],"properties":{"order":65,"id":41,"name":"hang-up","prevSize":32,"code":59655},"setIdx":0,"setId":2,"iconIdx":12},{"icon":{"paths":["M677.231 1023.845c-16.195-0.842-29.008-14.178-29.008-30.508s12.813-29.666 28.933-30.504l0.075-0.003h187.542v-901.582h-402.297v222.43c0 0.046 0 0.101 0 0.155 0 22.479-18.223 40.703-40.703 40.703-0.027 0-0.055-0-0.082-0l0.004 0h-261.582l-1.396 633.953h197.233c16.195 0.842 29.008 14.178 29.008 30.508s-12.813 29.666-28.933 30.504l-0.075 0.003h-218.631c-0.14 0.002-0.305 0.003-0.47 0.003-21.409 0-38.764-17.355-38.764-38.764 0-0.219 0.002-0.438 0.005-0.656l-0 0.033 1.396-696.208c0.068-10.992 4.593-20.914 11.857-28.060l0.005-0.005 249.875-244.758c6.987-6.855 16.57-11.087 27.141-11.087 0.052 0 0.105 0 0.157 0l-0.008-0h497.89c0.023-0 0.050-0 0.078-0 21.709 0 39.307 17.598 39.307 39.307 0 0.027-0 0.055-0 0.082l0-0.004v945.231c0 0.070 0.001 0.152 0.001 0.234 0 21.487-17.309 38.931-38.744 39.15l-0.021 0z","M799.028 441.216c0.004-0.185 0.006-0.403 0.006-0.622 0-16.678-13.434-30.218-30.071-30.39l-0.016-0h-486.416c-16.654 0.172-30.088 13.712-30.088 30.39 0 0.219 0.002 0.437 0.007 0.654l-0.001-0.032c-0.004 0.185-0.006 0.403-0.006 0.622 0 16.678 13.434 30.218 30.071 30.39l0.016 0h486.416c16.654-0.172 30.088-13.712 30.088-30.39 0-0.219-0.002-0.437-0.007-0.654l0.001 0.032zM799.028 545.182c0.004-0.185 0.006-0.403 0.006-0.622 0-16.678-13.434-30.218-30.071-30.39l-0.016-0h-486.416c-16.654 0.172-30.088 13.712-30.088 30.39 0 0.219 0.002 0.437 0.007 0.654l-0.001-0.032c-0.004 0.185-0.006 0.403-0.006 0.622 0 16.678 13.434 30.218 30.071 30.39l0.016 0h486.416c16.654-0.172 30.088-13.712 30.088-30.39 0-0.219-0.002-0.437-0.007-0.654l0.001 0.032zM799.028 650.544c0.004-0.185 0.006-0.403 0.006-0.622 0-16.678-13.434-30.218-30.071-30.39l-0.016-0h-486.416c-16.654 0.172-30.088 13.712-30.088 30.39 0 0.219 0.002 0.437 0.007 0.654l-0.001-0.032c-0.004 0.185-0.006 0.403-0.006 0.622 0 16.678 13.434 30.218 30.071 30.39l0.016 0h486.416c16.654-0.172 30.088-13.712 30.088-30.39 0-0.219-0.002-0.437-0.007-0.654l0.001 0.032zM519.149 761.177c-1.093-1.087-2.6-1.759-4.264-1.759s-3.171 0.672-4.264 1.76l0-0-84.894 91.949c-0.972 1.058-1.567 2.475-1.567 4.032 0 3.297 2.673 5.97 5.97 5.97 0.006 0 0.012-0 0.017-0l-0.001 0h54.968v137.381c-0.007 0.117-0.012 0.254-0.012 0.391 0 3.562 2.826 6.464 6.358 6.586l0.011 0h56.751c3.538-0.129 6.358-3.028 6.358-6.586 0-0.029-0-0.057-0.001-0.086l0 0.004v-137.536h54.968c0.003 0 0.008 0 0.012 0 3.297 0 5.97-2.673 5.97-5.97 0-1.709-0.718-3.251-1.87-4.339l-0.003-0.003z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["submit"]},"attrs":[{},{}],"properties":{"order":66,"id":40,"name":"submit","prevSize":32,"code":59656},"setIdx":0,"setId":2,"iconIdx":13},{"icon":{"paths":["M240.286 711.123c0.181-0.001 0.396-0.001 0.611-0.001 74.77 0 137.524 51.45 154.788 120.879l0.231 1.097-0.632-7.026h682.314c0.495-0.022 1.075-0.035 1.658-0.035 21.614 0 39.136 17.522 39.136 39.136s-17.522 39.136-39.136 39.136c-0.583 0-1.163-0.013-1.74-0.038l0.082 0.003h-682.244l-0.773 3.232c-19.748 67.758-81.294 116.428-154.208 116.428-76.758 0-140.919-53.937-156.648-125.978l-0.194-1.060 0.632 7.378h-43.914c-0.495 0.022-1.075 0.035-1.658 0.035-21.614 0-39.136-17.522-39.136-39.136s17.522-39.136 39.136-39.136c0.583 0 1.163 0.013 1.74 0.038l-0.082-0.003h43.914c0 11.734-4.005 27.402-4.005 39.066 0.040 3.142 0.243 6.189 0.602 9.188l-0.039-0.405v-6.464c1.147-86.625 71.638-156.407 158.428-156.407 0.4 0 0.799 0.001 1.199 0.004l-0.061-0zM240.286 789.325c-42.589 0.828-76.796 35.548-76.796 78.258 0 43.228 35.044 78.272 78.272 78.272s78.272-35.044 78.272-78.272c0-0.020-0-0.040-0-0.060l0 0.003c-0.594-43.317-35.847-78.203-79.249-78.203-0.175 0-0.351 0.001-0.526 0.002l0.027-0zM798.588 359.181c0.172-0.001 0.376-0.001 0.58-0.001 74.923 0 137.779 51.66 154.893 121.303l0.227 1.094-0.632-7.448h123.661c0.495-0.022 1.075-0.035 1.658-0.035 21.614 0 39.136 17.522 39.136 39.136s-17.522 39.136-39.136 39.136c-0.583 0-1.163-0.013-1.74-0.038l0.082 0.003h-123.45v-0.492c-18.397 69.637-80.828 120.141-155.054 120.141-76.764 0-140.912-54.016-156.507-126.113l-0.191-1.055 0.632 7.378h-602.497c-0.495 0.022-1.075 0.035-1.658 0.035-21.614 0-39.136-17.522-39.136-39.136s17.522-39.136 39.136-39.136c0.583 0 1.163 0.013 1.74 0.038l-0.082-0.003h602.427c0 11.734-3.935 27.402-3.935 39.066 0.035 3.581 0.263 7.067 0.676 10.497l-0.044-0.45v-7.659c1.227-86.563 71.687-156.265 158.426-156.265 0.277 0 0.554 0.001 0.831 0.002l-0.043-0zM798.588 437.382c-42.396 1.022-76.375 35.633-76.375 78.18 0 43.19 35.012 78.202 78.202 78.202 43.182 0 78.189-35 78.202-78.179l0-0.001c-0.592-43.318-35.846-78.205-79.249-78.205-0.274 0-0.548 0.001-0.822 0.004l0.042-0zM401.397 0.001c0.181-0.001 0.395-0.001 0.609-0.001 74.809 0 137.594 51.481 154.86 120.948l0.231 1.098h520.501c0.772-0.057 1.671-0.089 2.579-0.089 20.605 0 37.309 16.704 37.309 37.309 0 0.674-0.018 1.344-0.053 2.009l0.004-0.093c0.029 0.551 0.046 1.196 0.046 1.846 0 20.605-16.704 37.309-37.309 37.309-0.906 0-1.805-0.032-2.695-0.096l0.12 0.007h-522.679v-1.546c-20.351 66.508-81.196 114.041-153.143 114.041-86.998 0-157.763-69.501-159.801-156.008l-0.004-0.19c1.030-86.678 71.535-156.547 158.36-156.547 0.374 0 0.748 0.001 1.122 0.004l-0.057-0zM401.397 78.203c-42.558 0.867-76.726 35.572-76.726 78.257 0 43.228 35.044 78.272 78.272 78.272 43.223 0 78.263-35.035 78.272-78.256l0-0.001c-0.554-43.347-35.823-78.274-79.249-78.274-0.2 0-0.399 0.001-0.599 0.002l0.031-0zM243.518 122.117c0 11.734-3.935 27.402-3.935 39.066s3.935 27.402 3.935 39.066h-203.268c-0.77 0.057-1.669 0.089-2.575 0.089-20.605 0-37.309-16.704-37.309-37.309 0-0.649 0.017-1.294 0.049-1.935l-0.004 0.090c-0.029-0.551-0.046-1.196-0.046-1.846 0-20.605 16.704-37.309 37.309-37.309 0.906 0 1.805 0.032 2.695 0.096l-0.12-0.007z"],"attrs":[{}],"width":1118,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["setting"]},"attrs":[{}],"properties":{"order":67,"id":39,"name":"setting","prevSize":32,"code":59657},"setIdx":0,"setId":2,"iconIdx":14},{"icon":{"paths":["M512 0c282.77 0 512 229.23 512 512s-229.23 512-512 512c-282.77 0-512-229.23-512-512v0c0-282.77 229.23-512 512-512v0zM512 102.4c-226.216-0-409.6 183.384-409.6 409.6s183.384 409.6 409.6 409.6c226.216 0 409.6-183.384 409.6-409.6h0c-0-226.216-183.384-409.6-409.6-409.6l0 0zM512 435.2c28.277 0 51.2 22.923 51.2 51.2v-0 256c0 28.277-22.923 51.2-51.2 51.2s-51.2-22.923-51.2-51.2l0 0v-256c0-28.277 22.923-51.2 51.2-51.2v0zM512 230.4c35.346 0 64 28.654 64 64s-28.654 64-64 64c-35.346 0-64-28.654-64-64v0c0-35.346 28.654-64 64-64v0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["information"]},"attrs":[{}],"properties":{"order":68,"id":38,"name":"information","prevSize":32,"code":59658},"setIdx":0,"setId":2,"iconIdx":15},{"icon":{"paths":["M813.056 536.137l240.201 235.666c6.338 6.149 14.995 9.94 24.537 9.94 4.814 0 9.403-0.965 13.584-2.712l-0.233 0.086c12.616-5.164 21.356-17.314 21.431-31.515l0-0.009v-471.186c-0.087-14.226-8.858-26.383-21.276-31.442l-0.228-0.082c-3.948-1.661-8.537-2.626-13.351-2.626-9.542 0-18.199 3.791-24.546 9.949l0.009-0.009-239.909 235.666c-6.257 6.126-10.137 14.66-10.137 24.101s3.879 17.974 10.131 24.095l0.006 0.006zM1170.286 138.971v746.057c-0.087 14.226-8.858 26.383-21.276 31.442l-0.228 0.082c-3.948 1.661-8.537 2.626-13.351 2.626-9.542 0-18.199-3.791-24.546-9.949l0.009 0.009-379.465-373.102c-6.257-6.126-10.137-14.66-10.137-24.101s3.879-17.974 10.131-24.095l0.006-0.006 379.611-373.029c6.338-6.149 14.995-9.94 24.537-9.94 4.814 0 9.403 0.965 13.584 2.712l-0.233-0.086c12.541 5.167 21.233 17.242 21.358 31.363l0 0.016zM438.857 536.137l-379.685 373.029c-6.338 6.149-14.995 9.94-24.537 9.94-4.814 0-9.403-0.965-13.584-2.712l0.233 0.086c-12.533-5.198-21.204-17.302-21.285-31.441l-0-0.010v-746.057c0.087-14.226 8.858-26.383 21.276-31.442l0.228-0.082c3.948-1.661 8.537-2.626 13.351-2.626 9.542 0 18.199 3.791 24.546 9.949l-0.009-0.009 379.465 373.175c6.257 6.126 10.137 14.66 10.137 24.101s-3.879 17.974-10.131 24.095l-0.006 0.006zM621.714 0h-73.143v1024h73.143z"],"attrs":[{}],"width":1170,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["mapping"]},"attrs":[{}],"properties":{"order":69,"id":37,"name":"mapping","prevSize":32,"code":59659},"setIdx":0,"setId":2,"iconIdx":16},{"icon":{"paths":["M95.567 214.546c94-130.478 245.512-214.431 416.633-214.431 282.752 0 511.967 229.216 511.967 511.967s-229.216 511.967-511.967 511.967c-235.623 0-434.069-159.173-493.677-375.857l-0.843-3.594 98.366-26.486c48.295 176.319 207.099 303.751 395.667 303.751 226.201 0 409.574-183.372 409.574-409.574s-183.372-409.574-409.574-409.574c-135.036 0-254.808 65.349-329.418 166.148l-0.786 1.111 96.455 61.436-219.19 64.576-58.774-242.195z","M409.574 512.033c0.037 56.522 45.866 102.328 102.393 102.328 56.55 0 102.393-45.843 102.393-102.393s-45.843-102.393-102.393-102.393c-18.85 0-36.511 5.094-51.68 13.98l0.483-0.262c-30.822 18.034-51.197 50.975-51.197 88.675 0 0.023 0 0.046 0 0.069l-0-0.004z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["rotating"]},"attrs":[{},{}],"properties":{"order":70,"id":36,"name":"rotating","prevSize":32,"code":59660},"setIdx":0,"setId":2,"iconIdx":17},{"icon":{"paths":["M305.024 817.536l55.552-58.944 113.024 110.016-3.456-317.632-316.992 2.112 111.232 106.112-55.68 59.072-208.704-205.376 205.44-208.192 59.072 55.552-110.336 112.832 318.080-3.584-2.048-316.672-106.176 111.104-59.136-55.552 205.504-208.384 208.64 204.8-55.68 59.008-112.96-109.76 3.392 317.632 316.992-2.112-111.232-105.92 55.744-59.072 208.704 205.184-205.376 208.256-59.136-55.552 110.208-112.96-318.208 3.584 2.112 316.416 106.112-111.040 59.2 55.552-206.912 209.984z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["move"]},"attrs":[{}],"properties":{"order":71,"id":35,"name":"move","prevSize":32,"code":59661},"setIdx":0,"setId":2,"iconIdx":18},{"icon":{"paths":["M1024.147 236.212v472.79c0.001 0.119 0.001 0.26 0.001 0.4 0 14.624-4.138 28.282-11.308 39.866l0.187-0.325c-7.356 12.208-17.593 22-29.802 28.619l-0.41 0.204-433.214 236.431c-10.785 6.193-23.711 9.846-37.491 9.846s-26.706-3.653-37.864-10.043l0.373 0.197-433.214-236.431c-12.565-6.9-22.783-16.673-30.014-28.475l-0.198-0.348c-6.982-11.259-11.12-24.917-11.12-39.541 0-0.141 0-0.281 0.001-0.422l-0 0.022v-472.79c-0-0.047-0-0.103-0-0.159 0-33.649 21.233-62.339 51.032-73.403l0.542-0.176 433.214-157.572c8.086-3.045 17.43-4.836 27.185-4.901l0.028-0c9.707 0.067 18.976 1.859 27.543 5.083l-0.55-0.181 433.507 157.572c15.535 5.68 28.356 15.694 37.362 28.572l0.166 0.25c8.786 12.205 14.052 27.456 14.052 43.936 0 0.345-0.002 0.689-0.007 1.032l0.001-0.052zM512.074 391.37l429.629-156.329-429.629-156.548-429.629 156.475zM551.357 923.561l393.931-214.705v-391.59l-393.931 143.673z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["tool"]},"attrs":[{}],"properties":{"order":72,"id":34,"name":"tool","prevSize":32,"code":59662},"setIdx":0,"setId":2,"iconIdx":19},{"icon":{"paths":["M778.477 1023.996l-10.488-5.593-530.876-447.774c-3.767-3.080-7.054-6.525-9.879-10.334l-0.109-0.154c-7.406-9.712-11.866-22.017-11.866-35.365 0-2.639 0.174-5.237 0.512-7.784l-0.032 0.299c2.127-15.71 10.292-29.191 22.039-38.255l0.135-0.1 534.272-476.64 12.286-2.098 17.579 7.791 7.092 17.779v106.375c-0.182 11.406-5.49 21.537-13.715 28.213l-0.069 0.054-392.339 362.973 392.638 340.2c8.227 6.662 13.452 16.751 13.484 28.062l0 0.005v112.468l-5.593 10.488c-4.596 5.746-11.606 9.394-19.469 9.394-0.179 0-0.357-0.002-0.534-0.006l0.027 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Pack up"]},"attrs":[{}],"properties":{"order":73,"id":33,"name":"Pack-up","prevSize":32,"code":59663},"setIdx":0,"setId":2,"iconIdx":20},{"icon":{"paths":["M245.658 1023.996l10.488-5.593 530.876-447.774c3.767-3.080 7.054-6.525 9.879-10.334l0.109-0.154c7.406-9.712 11.866-22.017 11.866-35.365 0-2.639-0.174-5.237-0.512-7.784l0.032 0.299c-2.127-15.71-10.292-29.191-22.039-38.255l-0.135-0.1-534.272-476.64-12.286-2.098-17.579 7.791-7.092 17.779v106.375c0.182 11.406 5.49 21.537 13.715 28.213l0.069 0.054 392.339 362.973-392.638 340.2c-8.227 6.662-13.452 16.751-13.484 28.062l-0 0.005v112.468l5.593 10.488c4.596 5.746 11.606 9.394 19.469 9.394 0.179 0 0.357-0.002 0.534-0.006l-0.027 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["an-right"]},"attrs":[{}],"properties":{"order":74,"id":32,"name":"an-right","prevSize":32,"code":59664},"setIdx":0,"setId":2,"iconIdx":21},{"icon":{"paths":["M783.069 779.175l-225.798 226.077c-11.583 11.584-27.586 18.749-45.262 18.749s-33.679-7.165-45.262-18.749l0 0-225.798-226.17c-0.729-0.725-1.181-1.728-1.181-2.838s0.451-2.113 1.18-2.837l0-0 62.148-62.148c0.725-0.729 1.728-1.181 2.838-1.181s2.113 0.451 2.837 1.18l0 0 148.392 148.857c0.72 0.702 1.705 1.135 2.792 1.135 2.183 0 3.958-1.749 4-3.922l0-0.004v-853.324c0.241-2 1.928-3.535 3.973-3.535 0.010 0 0.019 0 0.029 0l-0.001-0h88.105c2.209-0 4.001 1.791 4.001 4.001v852.394c0.042 2.177 1.817 3.926 4 3.926 1.086 0 2.072-0.433 2.793-1.136l-0.001 0.001 148.299-148.392c0.725-0.729 1.728-1.181 2.838-1.181s2.113 0.451 2.837 1.18l0 0 62.148 62.148c0.729 0.725 1.181 1.728 1.181 2.838s-0.451 2.113-1.18 2.837l-0 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["down"]},"attrs":[{}],"properties":{"order":75,"id":31,"name":"down","prevSize":32,"code":59665},"setIdx":0,"setId":2,"iconIdx":22},{"icon":{"paths":["M783.116 244.825l-225.705-226.077c-11.583-11.584-27.586-18.749-45.262-18.749s-33.679 7.165-45.262 18.749l0-0-225.798 226.17c-0.729 0.725-1.181 1.728-1.181 2.838s0.451 2.113 1.18 2.837l0 0 62.148 62.148c0.725 0.729 1.728 1.181 2.838 1.181s2.113-0.451 2.837-1.18l0-0 148.857-148.857c0.72-0.702 1.705-1.135 2.792-1.135 2.183 0 3.958 1.749 4 3.922l0 0.004v853.324c0.204 1.834 1.621 3.283 3.421 3.533l0.021 0.002h88.105c2.209 0 4.001-1.791 4.001-4.001v0-852.394c0.042-2.177 1.817-3.926 4-3.926 1.086 0 2.072 0.433 2.793 1.136l-0.001-0.001 148.299 148.392c0.725 0.729 1.728 1.181 2.838 1.181s2.113-0.451 2.837-1.18l0-0 62.241-62.241c0.729-0.725 1.181-1.728 1.181-2.838s-0.451-2.113-1.18-2.837l-0-0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["up"]},"attrs":[{}],"properties":{"order":76,"id":30,"name":"up","prevSize":32,"code":59666},"setIdx":0,"setId":2,"iconIdx":23},{"icon":{"paths":["M832.992 440.855c35.967 2.461 64.21 32.239 64.21 68.615 0 1.524-0.050 3.036-0.147 4.536l0.011-0.204v365.159c0.068 1.142 0.106 2.477 0.106 3.822 0 36.365-28.227 66.137-63.966 68.601l-0.214 0.012h-509.345c-35.812-2.633-63.87-32.336-63.87-68.591 0-1.532 0.050-3.053 0.149-4.56l-0.011 0.205v-364.732c-0.085-1.277-0.133-2.768-0.133-4.27 0-36.254 28.055-65.955 63.639-68.578l0.226-0.013zM832.992 367.909h-509.345c-71.592 5.356-127.65 64.759-127.65 137.256 0 3.039 0.098 6.055 0.292 9.044l-0.021-0.407v365.159c-0.143 2.351-0.225 5.099-0.225 7.867 0 72.463 56.041 131.837 127.147 137.146l0.457 0.027h509.345c71.853-5.065 128.224-64.597 128.224-137.296 0-2.904-0.090-5.788-0.267-8.647l0.019 0.392v-364.732c0.171-2.567 0.268-5.564 0.268-8.584 0-72.672-56.399-132.171-127.815-137.114l-0.429-0.024zM568.508 83.716c-227.627-23.804-443.651 142.224-504.568 402.784-0.835 2.942-1.315 6.322-1.315 9.813 0 15.787 9.812 29.282 23.67 34.72l0.253 0.088c2.33 0.612 5.005 0.964 7.763 0.964 15.699 0 28.736-11.399 31.286-26.372l0.026-0.187c55.883-238.889 261.583-385.891 470.526-344.256l-47.778 54.603c-5.372 6.771-8.618 15.44-8.618 24.867 0 9.855 3.548 18.882 9.436 25.872l-0.050-0.061c5.376 6.499 13.441 10.61 22.467 10.61 8.671 0 16.456-3.793 21.786-9.811l0.027-0.031 90.437-103.234c5.729-6.895 9.206-15.839 9.206-25.595s-3.477-18.7-9.259-25.661l0.053 0.066-89.754-103.234c-5.34-5.84-12.992-9.489-21.496-9.489-9.059 0-17.152 4.141-22.487 10.633l-0.040 0.051c-5.807 6.941-9.333 15.964-9.333 25.81 0 9.269 3.125 17.808 8.379 24.621l-0.070-0.094 19.452 22.438z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Right rotation"]},"attrs":[{}],"properties":{"order":77,"id":29,"name":"Right-rotation","prevSize":32,"code":59667},"setIdx":0,"setId":2,"iconIdx":24},{"icon":{"paths":["M190.863 440.855c-35.812 2.633-63.87 32.336-63.87 68.591 0 1.532 0.050 3.053 0.149 4.56l-0.011-0.205v364.732c-0.081 1.251-0.127 2.713-0.127 4.185 0 36.252 28.052 65.952 63.633 68.577l0.226 0.013h509.345c35.812-2.633 63.87-32.336 63.87-68.591 0-1.532-0.050-3.053-0.149-4.56l0.011 0.205v-364.647c0.085-1.277 0.133-2.768 0.133-4.27 0-36.254-28.055-65.955-63.639-68.578l-0.226-0.013zM190.863 367.909h509.345c71.592 5.356 127.65 64.759 127.65 137.256 0 3.039-0.098 6.055-0.292 9.044l0.021-0.407v364.732c0.143 2.351 0.225 5.099 0.225 7.867 0 72.463-56.041 131.837-127.147 137.146l-0.457 0.027h-509.345c-71.842-5.078-128.199-64.604-128.199-137.294 0-2.755 0.081-5.49 0.241-8.205l-0.018 0.374v-364.732c-0.171-2.567-0.268-5.564-0.268-8.584 0-72.672 56.399-132.171 127.815-137.114l0.429-0.024zM455.347 83.716c227.627-23.804 443.651 142.395 504.568 402.784 0.835 2.942 1.315 6.322 1.315 9.813 0 15.787-9.812 29.282-23.67 34.72l-0.253 0.088c-2.33 0.612-5.005 0.964-7.763 0.964-15.699 0-28.736-11.399-31.286-26.372l-0.026-0.187c-55.883-238.889-261.583-385.891-470.526-344.256l47.778 54.603c5.372 6.771 8.618 15.44 8.618 24.867 0 9.855-3.548 18.882-9.436 25.872l0.050-0.061c-5.376 6.499-13.441 10.61-22.467 10.61-8.671 0-16.456-3.793-21.786-9.811l-0.027-0.031-90.437-103.234c-5.729-6.895-9.206-15.839-9.206-25.595s3.477-18.7 9.259-25.661l-0.053 0.066 89.754-103.234c5.34-5.84 12.992-9.489 21.496-9.489 9.059 0 17.152 4.141 22.487 10.633l0.040 0.051c5.807 6.941 9.333 15.964 9.333 25.81 0 9.269-3.125 17.808-8.379 24.621l0.070-0.094-19.452 22.438z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Left rotation"]},"attrs":[{}],"properties":{"order":78,"id":28,"name":"Left-rotation","prevSize":32,"code":59668},"setIdx":0,"setId":2,"iconIdx":25},{"icon":{"paths":["M1183.557 841.742c16.592 19.193 26.701 44.392 26.701 71.952 0 60.916-49.382 110.299-110.299 110.299-7.008 0-13.864-0.654-20.509-1.903l0.685 0.107h-949.293c-115.595 0-164.286-79.82-106.396-180.455l474.554-766.237c15.226-44.189 56.45-75.368 104.956-75.368s89.729 31.179 104.722 74.59l0.234 0.778zM566.182 267.204v392.596c-0 0.002-0 0.004-0 0.007 0 13.593 7.408 25.455 18.406 31.771l0.178 0.094c5.445 3.1 11.964 4.927 18.91 4.927s13.464-1.827 19.102-5.028l-0.192 0.1c11.177-6.41 18.584-18.273 18.584-31.866 0-0.002-0-0.005-0-0.007l0 0v-392.596c-0.003-20.884-16.934-37.813-37.819-37.813s-37.816 16.929-37.819 37.813l-0 0zM566.182 806.153v49.063c-0 0.002-0 0.004-0 0.007 0 13.593 7.408 25.455 18.406 31.771l0.178 0.094c5.445 3.1 11.964 4.927 18.91 4.927s13.464-1.827 19.102-5.028l-0.192 0.1c11.177-6.41 18.584-18.273 18.584-31.866 0-0.002-0-0.005-0-0.007l0 0v-48.691c-0.003-20.884-16.934-37.813-37.819-37.813s-37.816 16.929-37.819 37.813l-0 0z"],"attrs":[{}],"width":1212,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["remind"]},"attrs":[{}],"properties":{"order":79,"id":27,"name":"remind","prevSize":32,"code":59669},"setIdx":0,"setId":2,"iconIdx":26},{"icon":{"paths":["M924.521 970.278c-15.571-11.143-35.771-24.6-56.345-37.516l-5.608-3.285c-21.122-15.557-45.692-28.034-72.182-36.102l-1.719-0.45c-40.261 14.918-86.767 23.549-135.288 23.549-0.79 0-1.58-0.002-2.37-0.007l0.122 0.001c-0.943 0.006-2.057 0.009-3.173 0.009-28.053 0-55.601-2.159-82.488-6.321l2.997 0.382c-21.182-4.453-36.856-22.983-36.856-45.173 0-2.55 0.207-5.051 0.605-7.488l-0.036 0.266c1.908-10.353 6.582-19.391 13.221-26.588l-0.033 0.037c-24.893 4.765-53.561 7.531-82.861 7.611l-0.068 0c-1.054 0.008-2.3 0.012-3.548 0.012-55.192 0-108.341-8.745-158.138-24.925l3.617 1.016c-24.348 10.891-45.151 22.715-64.672 36.257l1.303-0.855c-29.295 17.701-55.227 35.933-61.953 40.358l-78.238 55.404 9.647-98.063c2.301-25.224 3.717-50.802 4.602-76.38 0.954-7.745 1.498-16.712 1.498-25.805 0-20.536-2.776-40.424-7.972-59.31l0.367 1.566c-67.417-69.825-108.957-165.018-108.957-269.914 0-60.050 13.614-116.92 37.923-167.693l-1.009 2.34c24.777-52.413 58.238-96.803 98.989-133.201l0.401-0.352c85.102-74.624 197.34-120.148 320.21-120.148s235.108 45.524 320.766 120.626l-0.556-0.478c41.234 36.752 74.779 81.139 98.586 131.062l1.070 2.492c23.361 48.625 37.009 105.714 37.009 165.991s-13.648 117.366-38.023 168.345l1.014-2.355c-24.836 52.432-58.322 96.845-99.081 133.292l-0.399 0.35c-51.415 45.736-113.45 80.743-181.864 100.842l-3.376 0.85c15.806 1.906 34.245 3.060 52.929 3.185l0.174 0.001c46.322-0.002 90.882-7.548 132.517-21.476l-2.946 0.855c5.873-1.328 12.644-2.13 19.588-2.212l0.060-0.001c0.117-0.001 0.256-0.001 0.395-0.001 18.123 0 35.046 5.139 49.391 14.039l-0.4-0.231c22.462 10.539 41.056 20.804 58.934 32.099l-2.202-1.299c-0.708-94.966 12.656-121.783 27.525-137.536 53.886-54.479 87.49-129.115 88.503-211.598l0.002-0.194c-0.16-47.376-11.52-92.062-31.569-131.598l0.77 1.673c-21.071-42.405-49.548-78.097-84.077-106.749l-0.534-0.43c-8.923-7.578-14.995-18.275-16.526-30.387l-0.024-0.235c-0.198-1.652-0.311-3.565-0.311-5.504 0-10.691 3.434-20.58 9.26-28.624l-0.098 0.142c7.816-10.766 20.357-17.689 34.515-17.701l0.002-0c10.151 0.107 19.418 3.802 26.614 9.875l-0.063-0.051c44.916 37.12 81.447 82.646 107.505 134.353l1.090 2.387c25.354 50.488 40.27 109.995 40.447 172.968l0 0.058c-0.755 107.912-43.96 205.589-113.728 277.287l0.088-0.091c-2.177 13.176-3.422 28.36-3.422 43.835 0 11.030 0.632 21.911 1.862 32.611l-0.122-1.306c0.974 36.641 3.806 69.122 4.514 76.291l9.647 99.037zM437.125 92.133c-0.005-0-0.011-0-0.017-0-185.645 0-337.662 143.974-350.488 326.356l-0.063 1.112c1.15 82.15 34.481 156.299 87.924 210.593l-0.039-0.040c15.488 16.55 29.472 44.783 27.702 139.129 28.9-24.049 64.831-40.611 104.285-46.245l1.125-0.132h0.797c0.519-0.015 1.13-0.024 1.743-0.024 6.156 0 12.107 0.882 17.732 2.525l-0.446-0.112c38.372 13.075 82.575 20.622 128.541 20.622 0.051 0 0.102-0 0.154-0l8.222 0c10.886 0.974 21.861 1.593 32.57 1.593 0.671 0.005 1.464 0.008 2.258 0.008 181.54 0 328.707-147.167 328.707-328.707 0-171.18-130.849-311.798-297.983-327.29l-1.297-0.097c-9.593-0.966-20.801-1.544-32.134-1.593l-0.081-0c-13.665 0.005-27.136 0.809-40.377 2.367l1.612-0.154c-7.611 0.266-14.072 0.089-20.445 0.089zM246.043 485.36c-35.866-2.502-64.175-31.609-65.402-67.589l-0.003-0.117c-0.464-2.973-0.729-6.401-0.729-9.891 0-37.246 30.194-67.441 67.441-67.441s67.441 30.194 67.441 67.441c0 1.612-0.057 3.211-0.168 4.794l0.012-0.213c0.010 0.451 0.016 0.983 0.016 1.516 0 19.164-7.338 36.613-19.358 49.692l0.048-0.053c-12.013 13.252-29.212 21.613-48.368 21.86l-0.044 0h-0.885z","M682.549 485.537c-37.106-0.097-67.208-29.95-67.706-66.951l-0-0.047c-0.009-0.42-0.015-0.915-0.015-1.411 0-18.222 7.265-34.748 19.057-46.838l-0.013 0.014c12.33-12.666 29.545-20.523 48.596-20.523 37.442 0 67.795 30.353 67.795 67.795 0 37.040-29.704 67.142-66.59 67.784l-0.060 0.001zM455.445 485.537c-37.241-0.201-67.352-30.437-67.352-67.705 0-37.393 30.313-67.706 67.706-67.706s67.706 30.313 67.706 67.706c0 37.269-30.112 67.504-67.333 67.705l-0.019 0z"],"attrs":[{},{}],"width":1102,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Notes - list"]},"attrs":[{},{}],"properties":{"order":80,"id":26,"name":"Notes---list","prevSize":32,"code":59670},"setIdx":0,"setId":2,"iconIdx":27},{"icon":{"paths":["M511.733 0c-282.623 0-511.733 229.111-511.733 511.733v0 461.093c-0 28.262 22.911 51.173 51.173 51.173h460.56c282.77 0 512-229.23 512-512s-229.23-512-512-512l0-0z","M750.756 562.907c-39.096 0-70.79-31.694-70.79-70.79s31.694-70.79 70.79-70.79c39.096 0 70.79 31.694 70.79 70.79v-0c-0.061 39.072-31.718 70.729-70.784 70.79l-0.006 0zM239.022 562.907c-39.096 0-70.79-31.694-70.79-70.79s31.694-70.79 70.79-70.79c39.096 0 70.79 31.694 70.79 70.79v-0c-0.060 38.997-31.597 70.608-70.559 70.79l-0.017 0zM506.51 562.907c-39.096 0-70.79-31.694-70.79-70.79s31.694-70.79 70.79-70.79c39.096 0 70.79 31.694 70.79 70.79l-0-0c-0.060 38.997-31.597 70.608-70.559 70.79l-0.017 0z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Have been added"]},"attrs":[{},{}],"properties":{"order":81,"id":25,"name":"Have-been-added","prevSize":32,"code":59671},"setIdx":0,"setId":2,"iconIdx":28},{"icon":{"paths":["M512 30.118c-266.136-0-481.882 215.746-481.882 481.882v0 433.694c-0 26.614 21.575 48.188 48.188 48.188v0h433.694c266.136 0 481.882-215.746 481.882-481.882s-215.746-481.882-481.882-481.882v0z","M489.653 240.941c27.479-0 49.754 22.276 49.754 49.754v149.203h149.203c0.516-0.019 1.122-0.030 1.731-0.030 27.479 0 49.754 22.276 49.754 49.754s-22.276 49.754-49.754 49.754c-0.609 0-1.215-0.011-1.818-0.033l0.087 0.002h-149.263v149.203c0.019 0.516 0.030 1.122 0.030 1.731 0 27.479-22.276 49.754-49.754 49.754s-49.754-22.276-49.754-49.754c0-0.609 0.011-1.215 0.033-1.818l-0.002 0.087v-149.203h-149.203c-0.516 0.019-1.122 0.030-1.731 0.030-27.479 0-49.754-22.276-49.754-49.754s22.276-49.754 49.754-49.754c0.609 0 1.215 0.011 1.818 0.033l-0.087-0.002h149.203v-149.203c0-27.479 22.276-49.754 49.754-49.754l-0 0z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Add a notation"]},"attrs":[{},{}],"properties":{"order":82,"id":24,"name":"Add-a-notation","prevSize":32,"code":59672},"setIdx":0,"setId":2,"iconIdx":29},{"icon":{"paths":["M977.665 277.433c-27.498-56.931-64.25-105.146-108.829-144.743l-0.467-0.407c-45.379-40.55-98.701-73.375-157.189-95.758l-3.502-1.177c-58.131-22.398-125.389-35.373-195.678-35.373s-137.547 12.975-199.51 36.66l3.833-1.287c-61.964 23.593-115.261 56.44-161.112 97.441l0.493-0.433c-45.046 40.003-81.799 88.219-108.11 142.441l-1.186 2.709c-25.501 52.443-40.407 114.084-40.407 179.207 0 75.804 20.198 146.89 55.505 208.171l-1.074-2.023c34.828 60.331 80.405 110.591 134.574 149.685l1.395 0.958c5.508 13.153 10.231 28.677 13.403 44.758l0.259 1.577c2.752 11.324 4.33 24.325 4.33 37.695 0 25.829-5.89 50.282-16.4 72.090l0.432-0.995c-2.704 4.946-4.295 10.835-4.295 17.096 0 19.978 16.195 36.173 36.173 36.173 13.717 0 25.651-7.635 31.783-18.888l0.095-0.19c31.688-49.367 80.287-85.517 137.457-100.478l1.62-0.36c33.251 6.987 71.459 10.987 110.602 10.987 0.074 0 0.148-0 0.222-0l-0.011 0c19.961 0 36.143-16.182 36.143-36.143s-16.182-36.143-36.143-36.143h0c-0.048 0-0.104 0-0.161 0-35.783 0-70.668-3.844-104.265-11.141l3.226 0.588c-2.805-0.782-6.026-1.231-9.352-1.231-2.549 0-5.037 0.264-7.437 0.766l0.235-0.041c-41.234 8.555-77.647 24.809-109.321 47.298l0.892-0.601c-3.688-38.016-13.699-72.879-28.996-104.721l0.804 1.858c-3.166-6.050-7.794-10.956-13.428-14.367l-0.161-0.091c-49.57-34.554-90.097-78.565-119.684-129.698l-1.033-1.934c-28.412-49.048-45.179-107.909-45.179-170.687 0-0.044 0-0.088 0-0.131l-0 0.007c1.076-107.969 49.395-204.441 125.24-269.884l0.465-0.392c38.971-34.807 84.762-62.981 134.986-82.19l3.007-1.010c50.523-19.43 108.973-30.684 170.052-30.684s119.53 11.255 173.391 31.803l-3.338-1.119c53.232 20.22 99.023 48.394 138.422 83.577l-0.428-0.376c76.256 65.857 124.523 162.328 125.559 270.096l0.001 0.18c0 0.058 0 0.125 0 0.193 0 64.773-17.838 125.379-48.869 177.175l0.872-1.569c-3.538 5.489-5.641 12.194-5.641 19.39 0 19.961 16.182 36.143 36.143 36.143 13.836 0 25.857-7.775 31.931-19.194l0.095-0.195c36.16-60.783 57.616-133.979 57.828-212.171l0-0.060c0-0.086 0-0.188 0-0.29 0-64.882-14.854-126.296-41.346-181.025l1.083 2.48z","M330.274 411.523h341.55c19.961 0 36.143-16.182 36.143-36.143s-16.182-36.143-36.143-36.143h-341.55c-19.961 0-36.143 16.182-36.143 36.143s16.182 36.143 36.143 36.143h0zM330.274 597.586h341.55c19.961 0 36.143-16.182 36.143-36.143s-16.182-36.143-36.143-36.143v0h-341.55c-19.961 0-36.143 16.182-36.143 36.143s16.182 36.143 36.143 36.143v0zM906.319 824.636h-90.791v-90.791c0-19.961-16.182-36.143-36.143-36.143s-36.143 16.182-36.143 36.143v-0 90.791h-90.863c-19.961 0-36.143 16.182-36.143 36.143s16.182 36.143 36.143 36.143v0h90.863v90.646c-0 19.961 16.182 36.143 36.143 36.143s36.143-16.182 36.143-36.143v0-90.863h90.791c19.961 0 36.143-16.182 36.143-36.143s-16.182-36.143-36.143-36.143l0 0z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["notation-tool"]},"attrs":[{},{}],"properties":{"order":83,"id":23,"name":"notation-tool","prevSize":32,"code":59673},"setIdx":0,"setId":2,"iconIdx":30},{"icon":{"paths":["M576.839 1024c-0.078 0-0.171 0-0.263 0-17.457 0-33.866-4.491-48.133-12.38l0.512 0.26-132.043-72.722c-31.168-17.418-51.896-50.209-51.896-87.842 0-0.086 0-0.171 0-0.257l-0 0.013v-419.944c-0.021-6.502-3.186-12.261-8.054-15.838l-0.055-0.038-304.972-243.346-4.609-4.012c-16.912-17.791-27.315-41.908-27.315-68.456 0-54.918 44.52-99.438 99.438-99.438 0.181 0 0.361 0 0.541 0.001l-0.028-0h821.623c55.207 0.24 99.869 45.050 99.869 100.291 0 26.083-9.957 49.84-26.277 67.678l0.069-0.076-4.012 4.012-307.276 247.101c-4.302 3.73-7.007 9.204-7.007 15.31 0 0.199 0.003 0.397 0.009 0.595l-0.001-0.029v489.081c0 0.025 0 0.055 0 0.085 0 55.154-44.711 99.865-99.865 99.865-0.060 0-0.12-0-0.18-0l0.009 0zM83.831 112.158l303.094 242.492c22.928 18.042 37.599 45.687 37.897 76.77l0 0.049v419.774c-0 0.036-0 0.079-0 0.122 0 7.585 4.071 14.219 10.148 17.835l0.095 0.053 132.043 71.954c2.926 1.718 6.444 2.733 10.2 2.733s7.274-1.015 10.296-2.785l-0.096 0.052c5.826-3.559 9.656-9.883 9.656-17.102 0-0.229-0.004-0.458-0.012-0.685l0.001 0.033v-488.228c-0.003-0.269-0.004-0.587-0.004-0.905 0-31.063 14.231-58.803 36.53-77.054l0.177-0.14 304.204-244.968c2.455-3.337 3.929-7.528 3.929-12.063 0-3.25-0.757-6.324-2.104-9.054l0.054 0.12c-3.524-6.162-10.057-10.248-17.545-10.248-0.163 0-0.326 0.002-0.488 0.006l0.024-0h-821.964c-0.077-0.001-0.167-0.002-0.258-0.002-11.031 0-19.973 8.942-19.973 19.973 0 4.58 1.541 8.8 4.134 12.169l-0.035-0.047zM983.724 512.044h-218.422c-21.53-1.020-38.6-18.724-38.6-40.415s17.070-39.395 38.509-40.412l0.091-0.003h218.337c21.53 1.020 38.6 18.724 38.6 40.415s-17.070 39.395-38.509 40.412l-0.091 0.003zM983.724 661.329h-218.422c-21.53-1.020-38.6-18.724-38.6-40.415s17.070-39.395 38.509-40.412l0.091-0.003h218.337c21.53 1.020 38.6 18.724 38.6 40.415s-17.070 39.395-38.509 40.412l-0.091 0.003zM983.724 809.504h-218.422c-21.53-1.020-38.6-18.724-38.6-40.415s17.070-39.395 38.509-40.412l0.091-0.003h218.337c21.53 1.020 38.6 18.724 38.6 40.415s-17.070 39.395-38.509 40.412l-0.091 0.003z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["filter"]},"attrs":[{}],"properties":{"order":84,"id":22,"name":"filter","prevSize":32,"code":59674},"setIdx":0,"setId":2,"iconIdx":31},{"icon":{"paths":["M287.649 444.927c0 44.901 36.4 81.301 81.301 81.301s81.301-36.4 81.301-81.301c0-44.901-36.4-81.301-81.301-81.301v0c-44.901 0-81.301 36.4-81.301 81.301v0zM865.27 773.226c0.982 0.047 2.133 0.074 3.291 0.074 40.464 0 73.267-32.803 73.267-73.267s-32.803-73.267-73.267-73.267c-40.464 0-73.267 32.803-73.267 73.267 0 0.008 0 0.016 0 0.024l-0-0.001c0.013 39.297 30.961 71.362 69.815 73.165l0.162 0.006zM865.27 842.138c-78.47-0-142.082-63.612-142.082-142.082s63.612-142.082 142.082-142.082c78.47 0 142.082 63.612 142.082 142.082v0c0 78.47-63.612 142.082-142.082 142.082v0zM831.298 445.895l-11.905 35.617-124.758 72.009-36.585-7.453-31.165 53.813 24.777 27.971v143.921l-24.777 27.971 31.165 53.813 36.585-7.549 124.758 72.106 11.905 35.617h61.943l11.808-35.521 125.822-72.106 36.585 7.549 31.165-53.813-22.551-27.971v-144.309l24.777-27.971-31.165-53.813-36.585 7.549-125.822-72.106-11.905-35.617h-64.073zM962.153 434.571l79.752 46.264 59.233-12.292 82.268 139.953-40.36 45.199v92.625l40.36 45.199-80.72 140.050-59.233-12.389-79.655 46.264-19.357 57.588h-161.44l-19.357-57.588-79.655-45.78-59.814 12.389-80.72-140.050 40.36-45.78v-92.141l-40.36-45.586 80.72-139.953 59.814 12.292 79.655-45.78 19.357-57.588h161.536l17.809 57.007z","M943.183 377.080h-161.44l-19.357 57.588-79.655 45.78-59.717-12.389-80.72 139.953 40.36 45.78v92.141l-40.36 45.683 80.72 140.050 59.717-12.389 79.655 45.78 19.357 57.588h161.536l19.357-57.588 79.752-46.264 59.233 12.292 81.784-139.469-40.36-45.199v-92.625l40.36-45.199-80.72-139.953-59.233 12.292-79.655-46.264zM694.539 553.618l124.758-72.106 11.905-35.617h61.943l11.808 35.617 125.822 72.009 36.585-7.453 31.165 53.813-24.681 27.971v145.18l24.681 27.971-31.165 53.813-36.585-7.549-125.822 72.106-11.905 35.617h-61.847l-11.905-35.617-124.758-72.106-36.585 7.549-31.165-53.813 24.681-27.971v-145.18l-24.681-27.971 31.165-53.813 36.585 7.549zM865.174 557.974c-78.47-0-142.082 63.612-142.082 142.082s63.612 142.082 142.082 142.082c78.47 0 142.082-63.612 142.082-142.082v0c0-0.029 0-0.063 0-0.097 0-78.47-63.612-142.082-142.082-142.082l-0 0zM865.174 773.226c-0.982 0.047-2.133 0.074-3.291 0.074-40.464 0-73.267-32.803-73.267-73.267s32.803-73.267 73.267-73.267c40.464 0 73.267 32.803 73.267 73.267 0 0.008-0 0.016-0 0.024l0-0.001c-0.013 39.297-30.961 71.362-69.815 73.165l-0.162 0.006z","M454.896 798.972h-84.978c-0.059 0-0.129 0-0.199 0-17.782 0-33.55-8.625-43.348-21.92l-0.104-0.148-219.608-301.974c-5.785-8.452-9.239-18.897-9.239-30.149s3.453-21.697 9.358-30.334l-0.119 0.185 219.608-301.974c9.902-13.443 25.67-22.068 43.452-22.068 0.070 0 0.14 0 0.209 0l-0.011-0h667.826c29.72 0 53.813 24.093 53.813 53.813h-0v235.772c0 25.284 20.496 45.78 45.78 45.78s45.78-20.496 45.78-45.78l-0 0v-272.357c0-0.029 0-0.063 0-0.097 0-59.426-48.119-107.613-107.519-107.723l-0.011-0h-728.898c-0.035-0-0.076-0-0.117-0-35.996 0-67.857 17.703-87.352 44.877l-0.22 0.322-241.192 340.398c-11.102 16.668-17.715 37.154-17.715 59.185s6.613 42.516 17.963 59.581l-0.248-0.396 241.095 340.204c19.824 26.888 51.375 44.137 86.955 44.137 0.258 0 0.516-0.001 0.773-0.003l-0.039 0h107.53c0.689 0.038 1.496 0.060 2.308 0.060 24.696 0 44.715-20.020 44.715-44.715s-20.020-44.715-44.715-44.715c-0.641 0-1.28 0.014-1.914 0.040l0.091-0.003z"],"attrs":[{},{},{}],"width":1184,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["class-edit"]},"attrs":[{},{},{}],"properties":{"order":85,"id":21,"name":"class-edit","prevSize":32,"code":59675},"setIdx":0,"setId":2,"iconIdx":32},{"icon":{"paths":["M519.4 0c-104.088 0.349-188.696 83.261-191.773 186.651l-0.007 0.284h-242.169c-27.197 0-49.244 22.047-49.244 49.244s22.047 49.244 49.244 49.244v0h54.001v550.673c-0.391 4.377-0.614 9.468-0.614 14.612 0 89.811 67.946 163.752 155.238 173.224l0.773 0.068h438.706c88.34-9.155 156.636-83.201 156.636-173.197 0-5.177-0.226-10.301-0.669-15.363l0.046 0.657v-550.409h48.892c27.197 0 49.244-22.047 49.244-49.244s-22.047-49.244-49.244-49.244v0h-227.546c-2.605-103.822-87.257-187.023-191.402-187.287l-0.026-0zM419.237 186.935c2.812-53.060 46.521-95.019 100.030-95.019s97.218 41.959 100.020 94.77l0.011 0.249zM294.849 931.942c-30.48 0-64.132-39.378-64.132-95.846v-550.409h566.882v550.937c0 56.292-33.564 95.846-64.132 95.846h-438.618zM294.849 931.942z","M361.8 811.871c22.994-2.767 40.642-22.159 40.642-45.673 0-1.537-0.075-3.056-0.223-4.553l0.015 0.19v-285.423c0.588-2.616 0.925-5.62 0.925-8.703 0-22.867-18.537-41.404-41.404-41.404s-41.404 18.537-41.404 41.404c0 3.083 0.337 6.087 0.976 8.978l-0.051-0.275v285.423c-0.142 1.352-0.222 2.921-0.222 4.509 0 23.516 17.719 42.893 40.534 45.508l0.211 0.020zM508.3 811.871c22.994-2.767 40.642-22.159 40.642-45.673 0-1.537-0.075-3.056-0.223-4.553l0.015 0.19v-285.423c0.588-2.616 0.925-5.62 0.925-8.703 0-22.867-18.537-41.404-41.404-41.404s-41.404 18.537-41.404 41.404c0 3.083 0.337 6.087 0.976 8.978l-0.051-0.275v285.423c-0.127 1.282-0.199 2.77-0.199 4.276 0 23.56 17.684 42.989 40.502 45.74l0.22 0.022zM662.376 811.871c22.994-2.767 40.642-22.159 40.642-45.673 0-1.537-0.075-3.056-0.223-4.553l0.015 0.19v-285.423c0.588-2.616 0.925-5.62 0.925-8.703 0-22.867-18.537-41.404-41.404-41.404s-41.404 18.537-41.404 41.404c0 3.083 0.337 6.087 0.976 8.978l-0.051-0.275v285.423c-0.152 1.396-0.238 3.016-0.238 4.656 0 23.517 17.79 42.879 40.647 45.363l0.203 0.018zM662.376 811.871z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["delete"]},"attrs":[{},{}],"properties":{"order":86,"id":20,"name":"delete","prevSize":32,"code":59676},"setIdx":0,"setId":2,"iconIdx":33},{"icon":{"paths":["M682.807 1024c-250.538 0-473.763-157.571-669.676-466.304-8.344-12.83-13.306-28.526-13.306-45.38s4.962-32.551 13.505-45.707l-0.199 0.326c195.913-308.524 419.138-466.935 669.676-466.935s473.763 157.571 669.676 466.304c8.344 12.83 13.306 28.526 13.306 45.38s-4.962 32.551-13.505 45.707l0.199-0.326c-195.703 309.259-418.928 466.935-669.676 466.935zM682.807 102.421c-207.573 0-399.179 134.46-576.184 409.684 176.794 275.119 368.611 409.684 576.184 409.684s399.179-134.46 576.184-409.684c-177.005-275.224-368.4-409.684-576.184-409.684zM682.807 750.982c-130.852-1.484-236.356-107.905-236.356-238.968 0-131.986 106.996-238.982 238.982-238.982 131.981 0 238.974 106.988 238.982 238.967l0 0.001c-0.594 132.116-107.832 238.988-240.031 238.988-0.555 0-1.109-0.002-1.663-0.006l0.085 0zM682.807 648.561c74.831-0.772 135.196-61.614 135.196-136.555 0-75.421-61.141-136.561-136.561-136.561-75.418 0-136.558 61.137-136.561 136.554l-0 0c0.654 75.56 62.059 136.562 137.712 136.562 0.076 0 0.151-0 0.227-0l-0.012 0zM682.807 648.561z"],"attrs":[{}],"width":1366,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["visible"]},"attrs":[{}],"properties":{"order":87,"id":19,"name":"visible","prevSize":32,"code":59677},"setIdx":0,"setId":2,"iconIdx":34},{"icon":{"paths":["M0 1023.744v-753.971l269.107-269.773h754.893v754.176l-269.107 269.824zM691.2 955.392v-197.171h-378.163l-196.813 197.171zM759.245 922.675l163.84-164.454h-163.84zM68.045 907.315l196.71-197.222v-376.474h-196.71zM956.006 690.176v-573.696l-196.813 197.274v376.474zM691.149 690.176v-356.506h-358.4v356.506zM711.219 265.216l196.813-197.222h-575.181v197.222zM264.755 265.216v-163.84l-163.84 163.84z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["biaozhunkuang"]},"attrs":[{}],"properties":{"order":88,"id":18,"name":"biaozhunkuang","prevSize":32,"code":59678},"setIdx":0,"setId":2,"iconIdx":35},{"icon":{"paths":["M29.191 581.779l32.952-22.57 37.090 57.779-35.059 23.698zM86.744 547.849l52.061-33.705 36.94 57.704-54.243 34.983zM163.106 502.709l52.137-33.78 37.090 57.779-54.243 34.983zM239.693 457.569l52.061-33.855 37.090 57.704-54.243 35.134zM316.281 412.429l10.307-9.028-1.204-9.178 68.613-2.407 2.483 51.46-45.14 28.137zM327.867 366.236l-2.483-62.444 68.613-2.483 2.483 65.002zM327.867 275.956l-2.483-62.444 68.613-2.483 2.483 65.002zM327.867 185.149l-2.483-62.444 68.613-2.483 2.483 65.002zM327.867 94.869l-2.483-40.099 68.613-2.483 2.483 42.582z","M1.354 623.383l-1.354-414.611 358.712-208.471 688.46 367.289h6.32l-1.128 10.232v435.074l-389.182 210.653zM70.193 248.796l1.279 335.014 595.321 357.734 315.98-171.005-1.354-359.314-620.298-330.349z","M630.53 998.045l-2.483-386.172-619.696-359.013 32.35-60.563 624.963 360.367 345.546-187.18 34.758 59.209-348.104 189.889 1.279 383.69z"],"attrs":[{},{},{}],"width":1054,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["lifangti"]},"attrs":[{},{},{}],"properties":{"order":89,"id":17,"name":"lifangti","prevSize":32,"code":59679},"setIdx":0,"setId":2,"iconIdx":36},{"icon":{"paths":["M804.16 1023.999h-584.159c-17.621-0-32.542-11.562-37.59-27.513l-0.076-0.279-180.506-584.159c-1.124-3.491-1.772-7.507-1.772-11.674 0-12.701 6.017-23.996 15.356-31.19l0.092-0.068 472.622-361.013c6.552-5.055 14.878-8.102 23.916-8.102s17.365 3.047 24.008 8.17l-0.092-0.068 472.622 361.013c9.431 7.263 15.449 18.558 15.449 31.259 0 4.167-0.648 8.183-1.848 11.952l0.077-0.279-180.506 584.232c-5.070 16.183-19.929 27.72-37.483 27.72-0.039 0-0.078-0-0.116-0l0.006 0zM249.037 945.229h526.233l163.611-530.329-426.911-325.979-426.764 325.979 163.831 530.183z","M341.558 839.909c-17.592-0.034-32.476-11.585-37.517-27.513l-0.076-0.279-105.393-340.9c-1.096-3.453-1.728-7.424-1.728-11.542 0-12.724 6.029-24.041 15.385-31.249l0.092-0.068 275.806-210.713c6.556-5.044 14.881-8.084 23.916-8.084s17.361 3.040 24.009 8.152l-0.093-0.069 275.806 210.713c9.389 7.278 15.373 18.559 15.373 31.237 0 7.359-2.016 14.246-5.526 20.141l0.1-0.181-162.514 272.369c-5.434 8.984-14.113 15.539-24.376 18.082l-0.271 0.057-283.778 68.458c-2.732 0.774-5.89 1.273-9.146 1.388l-0.069 0.002zM282.023 474.215l86.304 279.317 231.703-55.878 136.038-228.047-223.951-171.071z","M551.393 39.406h-78.697v511.532h78.697z","M34.595 366.702l-20.406 75.991 490.907 131.65 20.406-76.064z","M976.183 368.091l-485.641 130.187 20.406 75.991 485.641-130.187z","M486.738 527.607l-302.575 431.884 64.435 45.2 302.575-431.884z","M536.765 527.095l-63.85 45.639 303.453 442.27 63.777-45.565z"],"attrs":[{},{},{},{},{},{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["target"]},"attrs":[{},{},{},{},{},{},{}],"properties":{"order":90,"id":16,"name":"target","prevSize":32,"code":59680},"setIdx":0,"setId":2,"iconIdx":37},{"icon":{"paths":["M408.288 192.832h402.304v402.304h-402.304z","M257.888 0v225.216h-73.152v-225.216z","M257.888 266.24v225.216h-73.152v-225.216z","M257.888 532.544v225.216h-73.152v-225.216z","M257.888 798.784v225.216h-73.152v-225.216z","M1020.96 804.608h-225.216v-73.152h225.216z","M756.768 804.608h-225.216v-73.152h225.216z","M492.512 804.608h-225.216v-73.152h225.216z","M228.256 804.608h-225.216v-73.152h225.216z"],"attrs":[{},{},{},{},{},{},{},{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["auxiliaryline"]},"attrs":[{},{},{},{},{},{},{},{},{}],"properties":{"order":91,"id":15,"name":"auxiliaryline","prevSize":32,"code":59681},"setIdx":0,"setId":2,"iconIdx":38},{"icon":{"paths":["M853.333 512c0-47.128 38.205-85.333 85.333-85.333s85.333 38.205 85.333 85.333c0 47.128-38.205 85.333-85.333 85.333h0c-47.128 0-85.333-38.205-85.333-85.333v0zM426.667 512c0-47.128 38.205-85.333 85.333-85.333s85.333 38.205 85.333 85.333c0 47.128-38.205 85.333-85.333 85.333v0c-47.128 0-85.333-38.205-85.333-85.333v0zM0 512c0-47.128 38.205-85.333 85.333-85.333s85.333 38.205 85.333 85.333c0 47.128-38.205 85.333-85.333 85.333v0c-47.128 0-85.333-38.205-85.333-85.333v0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["more"]},"attrs":[{}],"properties":{"order":92,"id":14,"name":"more","prevSize":32,"code":59682},"setIdx":0,"setId":2,"iconIdx":39},{"icon":{"paths":["M183.723 355.755c7.665-8.863 18.925-14.437 31.488-14.437s23.823 5.574 31.443 14.384l0.045 0.053 265.301 293.376 265.301-293.376c7.665-8.863 18.925-14.437 31.488-14.437s23.823 5.574 31.443 14.384l0.045 0.053c8.101 9.258 13.042 21.46 13.042 34.816s-4.941 25.558-13.095 34.878l0.053-0.062-296.789 328.192c-7.665 8.863-18.925 14.437-31.488 14.437s-23.823-5.574-31.443-14.384l-0.045-0.053-296.789-328.192c-8.104-9.020-13.060-21.010-13.060-34.158 0-0.231 0.002-0.462 0.005-0.693l-0 0.035c-0.003-0.196-0.004-0.426-0.004-0.658 0-13.148 4.956-25.139 13.102-34.206l-0.042 0.047z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["unfold"]},"attrs":[{}],"properties":{"order":93,"id":13,"name":"unfold","prevSize":32,"code":59683},"setIdx":0,"setId":2,"iconIdx":40},{"icon":{"paths":["M992.335 347.294c4.027-7.335 6.396-16.072 6.396-25.363 0-13.735-5.177-26.261-13.687-35.732l0.044 0.049c-5.806-5.632-13.735-9.103-22.475-9.103-12.507 0-23.354 7.11-28.719 17.509l-0.085 0.181c-1.812 2.127-179.594 291.998-421.415 291.998-234.575 0-428.662-294.203-430.474-296.33-6.033-8.817-16.045-14.528-27.39-14.528-9.385 0-17.857 3.908-23.878 10.185l-0.011 0.011c-7.614 8.76-12.254 20.281-12.254 32.886 0 10.51 3.226 20.266 8.742 28.333l-0.111-0.172c24.783 53.571 58.084 99.096 98.673 136.897l0.261 0.24-93.499 115.554c-6.331 8.024-10.156 18.283-10.156 29.435 0 12.148 4.538 23.236 12.010 31.661l-0.043-0.050c3.947 7.863 11.948 13.165 21.187 13.165 0.25 0 0.499-0.004 0.747-0.012l-0.036 0.001c10.49-0.288 19.742-5.347 25.699-13.076l0.058-0.078 100.746-124.219c48.011 40.285 102.978 74.621 162.394 100.694l4.36 1.706-38.439 154.86c-1.198 3.869-1.887 8.317-1.887 12.927 0 18.627 11.264 34.622 27.351 41.548l0.294 0.112h11.028c18.095-1.059 32.651-14.91 34.798-32.589l0.018-0.179 38.439-154.86c27.353 6.615 58.868 10.611 91.247 11.025l0.283 0.003c32.599-0.276 64.127-4.28 94.356-11.606l-2.826 0.579 38.439 152.655c2.747 17.57 16.979 31.104 34.662 32.756l0.154 0.012c0.601 0.102 1.294 0.161 2 0.161 2.704 0 5.207-0.857 7.254-2.314l-0.038 0.026c16.375-7.066 27.628-23.071 27.628-41.706 0-4.592-0.683-9.025-1.954-13.202l0.084 0.321-38.991-152.734c63.776-27.782 118.742-62.118 167.809-103.26l-1.054 0.86 98.934 122.092c6.050 7.765 15.282 12.807 25.702 13.153l0.055 0.001c10.49-0.288 19.742-5.347 25.699-13.076l0.058-0.078c7.014-8.573 11.265-19.643 11.265-31.707 0-11.015-3.544-21.203-9.554-29.485l0.101 0.145-93.342-115.633c41.833-34.567 73.97-79.439 92.668-130.77l0.673-2.114zM992.335 347.294z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["hidden"]},"attrs":[{}],"properties":{"order":94,"id":12,"name":"hidden","prevSize":32,"code":59684},"setIdx":0,"setId":2,"iconIdx":41},{"icon":{"paths":["M309.248 258.475l-207.36 207.019c-10.625 10.624-17.196 25.302-17.196 41.515s6.572 30.89 17.196 41.515l-0-0 207.445 207.104c0.665 0.669 1.585 1.083 2.603 1.083s1.938-0.414 2.602-1.083l0-0 57.003-57.003c0.669-0.665 1.083-1.585 1.083-2.603s-0.414-1.938-1.083-2.602l-0-0-136.533-136.533c-0.644-0.66-1.041-1.564-1.041-2.561 0-2.003 1.604-3.63 3.598-3.669l0.004-0h782.677c1.682-0.187 3.011-1.487 3.24-3.138l0.002-0.019v-80.811c0-2.027-1.643-3.669-3.669-3.669v0h-781.824c-1.997-0.038-3.601-1.666-3.601-3.669 0-0.996 0.397-1.9 1.042-2.561l-0.001 0.001 136.107-136.021c0.669-0.665 1.083-1.585 1.083-2.603s-0.414-1.938-1.083-2.602l-0-0-57.088-57.088c-0.665-0.669-1.585-1.083-2.603-1.083s-1.938 0.414-2.602 1.083l-0 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["left"]},"attrs":[{}],"properties":{"order":95,"id":11,"name":"left","prevSize":32,"code":59685},"setIdx":0,"setId":2,"iconIdx":42},{"icon":{"paths":["M714.667 258.389l207.36 207.104c10.625 10.624 17.196 25.302 17.196 41.515s-6.572 30.89-17.196 41.515l0-0-207.445 207.189c-0.665 0.669-1.585 1.083-2.603 1.083s-1.938-0.414-2.602-1.083l-0-0-57.003-57.088c-0.669-0.665-1.083-1.585-1.083-2.603s0.414-1.938 1.083-2.602l0-0 136.533-136.533c0.644-0.66 1.041-1.564 1.041-2.561 0-2.003-1.604-3.63-3.598-3.669l-0.004-0h-782.677c-1.835-0.221-3.243-1.768-3.243-3.644 0-0.009 0-0.017 0-0.026l-0 0.001v-80.299c0-2.027 1.643-3.669 3.669-3.669v0h781.824c1.997-0.038 3.601-1.666 3.601-3.669 0-0.996-0.397-1.9-1.042-2.561l0.001 0.001-136.533-136.533c-0.669-0.665-1.083-1.585-1.083-2.603s0.414-1.938 1.083-2.602l0-0 57.003-57.088c0.665-0.669 1.585-1.083 2.603-1.083s1.938 0.414 2.602 1.083l0 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["right"]},"attrs":[{}],"properties":{"order":96,"id":10,"name":"right","prevSize":32,"code":59686},"setIdx":0,"setId":2,"iconIdx":43},{"icon":{"paths":["M313.088 882.944c-8.863-7.665-14.437-18.925-14.437-31.488s5.574-23.823 14.384-31.443l0.053-0.045 293.376-265.301-293.376-265.301c-8.863-7.665-14.437-18.925-14.437-31.488s5.574-23.823 14.384-31.443l0.053-0.045c9.258-8.101 21.46-13.042 34.816-13.042s25.558 4.941 34.878 13.095l-0.062-0.053 328.192 296.789c8.863 7.665 14.437 18.925 14.437 31.488s-5.574 23.823-14.384 31.443l-0.053 0.045-328.192 296.789c-9.020 8.104-21.010 13.060-34.158 13.060-0.231 0-0.462-0.002-0.693-0.005l0.035 0c-0.196 0.003-0.426 0.004-0.658 0.004-13.148 0-25.139-4.956-34.206-13.102l0.047 0.042z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["dakai"]},"attrs":[{}],"properties":{"order":97,"id":9,"name":"dakai","prevSize":32,"code":59687},"setIdx":0,"setId":2,"iconIdx":44},{"icon":{"paths":["M692.405 60.055c-79.209 0-147.697 59.151-159.744 134.927-1.601 3.95-2.53 8.532-2.53 13.33 0 0.036 0 0.072 0 0.108l-0-0.006 0.181 614.52c-16.023 64.934 8.915 133.783 65.837 174.682 18.372 10.24 38.129 17.709 58.669 22.227l14.878 2.53 5.542 0.602 10.842 0.723 6.325 0.12c0.38 0.003 0.829 0.005 1.278 0.005 44.45 0 84.683-18.052 113.768-47.226l0.004-0.004c29.469-30.165 47.65-71.469 47.65-117.019 0-11.192-1.098-22.128-3.191-32.706l0.176 1.064c79.39-10.421 141.553-79.752 141.553-160.949-0.272-50.341-23.374-95.232-59.47-124.883l-0.284-0.226c36.402-29.812 59.516-74.677 59.753-124.948l0-0.040c0-81.679-62.765-151.070-142.577-161.009l1.988-11.083c1.084-7.409 1.687-14.938 1.687-22.408-0.744-89.354-72.98-161.59-162.263-162.334l-0.071-0zM331.053 60.055c-89.354 0.744-161.59 72.98-162.334 162.263l-0 0.071c0 11.445 1.265 22.709 3.735 33.852-79.062 12.049-139.047 79.2-139.806 160.508l-0.001 0.079c-0 0.056-0 0.122-0 0.188 0 49.747 22.204 94.309 57.243 124.314l0.222 0.186c-37.527 30.6-60.235 76.499-60.235 125.53 0 79.511 59.753 147.878 137.156 160.226l-1.807 12.529c-3.795 38.972 6.024 78.065 27.708 110.652l6.927 10.12c5 6.746 10.541 13.131 16.504 19.034 27.365 24.91 63.103 41.053 102.534 43.762l0.528 0.029 11.686 0.422 10.24-0.301 9.999-0.904 16.866-2.891c29.515-6.686 56.621-21.685 78.005-43.128 23.191-23.010 38.49-52.887 43.61-85.173 1.546-3.205 2.669-6.926 3.173-10.84l0.019-0.183 0.422-4.939-0.060-279.733c0.723-7.529 0.783-15.059 0.12-22.588l-0.12-364.725c0.002-0.126 0.003-0.275 0.003-0.425 0-3.969-0.775-7.756-2.183-11.22l0.072 0.2c-13.34-77.663-79.81-136.131-160.083-136.914l-0.083-0.001zM692.465 134.566c47.586 0 87.823 40.177 87.823 87.823 0 37.768-22.648 70.234-58.007 82.944-18.854 7.529-29.395 29.756-22.287 47.164 4.262 15.259 17.905 26.32 34.179 26.623l0.034 0.001c0.288 0.012 0.627 0.020 0.967 0.020 1.922 0 3.79-0.227 5.581-0.655l-0.163 0.033c3.192-0.843 6.084-2.409 5.060-2.771 20.42-7.409 39.153-18.793 55.236-33.431l9.276-9.035 10.782-2.65c4.337-1.024 8.794-1.506 13.252-1.566 47.586 0 87.763 40.237 87.763 87.823-0.674 45.299-35.033 82.368-79.106 87.304l-0.404 0.037-8.433 0.482h-2.65c-0.478-0.023-1.039-0.036-1.602-0.036-19.694 0-35.659 15.965-35.659 35.659 0 0.563 0.013 1.124 0.039 1.681l-0.003-0.079c0 19.637 12.469 34.093 30.901 36.744l14.637 0.904c44.478 4.973 78.837 42.042 79.51 87.273l0.001 0.068c0 47.586-40.177 87.883-87.823 87.883-45.299-0.674-82.368-35.033-87.304-79.106l-0.037-0.404-0.904-14.697c-2.65-18.432-17.107-30.84-36.744-30.84-0.478-0.023-1.039-0.036-1.602-0.036-19.694 0-35.659 15.965-35.659 35.659 0 0.563 0.013 1.124 0.039 1.681l-0.003-0.079c0.541 66.246 40.315 123.071 97.202 148.427l1.042 0.415c17.769 29.094 16.143 65.596-4.939 95.955-4.382 6.337-9.305 11.834-14.833 16.655l-0.105 0.090c-35.539 28.311-85.353 25.48-117.82-6.987-16.279-15.963-26.369-38.186-26.369-62.765s10.090-46.802 26.355-62.751l0.014-0.014c6.851-6.615 11.105-15.882 11.105-26.142s-4.254-19.527-11.095-26.132l-0.011-0.010c-5.174-5.263-11.881-9.005-19.398-10.501l-0.239-0.040-5.421-0.602v-143.36c0-16.986 3.132-31.744 10.059-45.779 9.336-18.673 28.552-34.455 50.176-42.526 21.805-7.288 44.333-5.421 67.223 4.638 18.191 10.963 40.719 4.216 51.802-14.336 10.963-18.251 4.216-40.719-14.697-52.043-37.406-20.179-81.438-24.094-123.784-11.927-11.914 3.487-22.262 7.936-31.923 13.45l0.722-0.379-9.638 5.963v-223.895c0-47.586 40.237-87.823 87.883-87.823zM331.174 134.566c44.755 0 83.125 35.599 87.462 79.511l0.361 8.252v224.858c-10.912-7.139-23.517-12.996-36.948-16.917l-1-0.25c-14.186-4.426-30.496-6.975-47.402-6.975-28.095 0-54.545 7.040-77.683 19.453l0.88-0.431c-18.552 11.144-25.299 33.732-14.396 51.923 11.204 18.552 33.611 25.299 51.26 14.697 21.082-10.421 45.417-12.228 67.825-5.060 26.817 8.553 47.482 29.357 55.677 55.659l0.161 0.6 1.807 6.024-0.12 169.502c-9.846 0.375-18.651 4.544-25.052 11.077l-0.006 0.006c-6.883 6.591-11.161 15.855-11.161 26.118 0 7.717 2.419 14.869 6.539 20.74l-0.077-0.116 4.216 5.060 4.759 4.578 1.205 2.65 6.445 10.722c6.846 12.273 10.876 26.925 10.876 42.518 0 30.032-14.952 56.573-37.817 72.583l-0.285 0.189c-14.052 9.784-31.481 15.632-50.276 15.632-26.13 0-49.621-11.303-65.848-29.287l-0.070-0.078c-12.042-15.689-19.297-35.601-19.297-57.208 0-10.337 1.661-20.287 4.73-29.596l-0.191 0.668 4.337-11.384 0.843-3.012 10.421-4.819c54.071-27.161 90.637-81.986 91.136-145.401l0-0.067c0.023-0.477 0.036-1.037 0.036-1.599 0-19.694-15.965-35.659-35.659-35.659-0.542 0-1.082 0.012-1.618 0.036l0.076-0.003c-19.697 0-34.153 12.408-36.744 30.84l-1.024 15.119c-3.253 31.684-24.094 59.874-49.875 71.8-9.999-1.024-20.54 1.867-24.094 6.144-3.933 0.728-8.458 1.145-13.080 1.145-0.082 0-0.163-0-0.245-0l0.013 0c-47.586 0-87.823-40.237-87.823-87.823 0.67-45.281 34.998-82.34 79.044-87.304l0.406-0.037 14.697-0.904c18.372-2.711 30.901-17.167 30.901-36.804s-12.469-34.093-30.901-36.744l-14.637-0.904c-44.465-4.999-78.809-42.053-79.51-87.27l-0.001-0.071c0-47.586 40.177-87.883 87.823-87.883 6.987 0 14.697 1.265 18.191 0.723 19.034 21.444 43.43 37.527 70.716 46.562-0.602 0-0.723 0-0.542 0.12l3.494 1.626c1.514 0.65 3.277 1.027 5.128 1.027 0.103 0 0.206-0.001 0.309-0.004l-0.015 0c15.855-0.187 29.207-10.693 33.664-25.106l0.067-0.253c7.469-18.673-3.072-40.96-22.287-48.55-13.894-5.018-25.701-12.89-35.196-22.965l-0.042-0.045c-1.363-2.896-2.972-5.392-4.862-7.643l0.043 0.053-2.409-2.108-3.494-5.662c-7.349-12.679-11.686-27.896-11.686-44.125 0-0.115 0-0.231 0.001-0.346l-0 0.018c0-47.586 40.297-87.823 87.823-87.823z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["model"]},"attrs":[{}],"properties":{"order":98,"id":8,"name":"model","prevSize":32,"code":59688},"setIdx":0,"setId":2,"iconIdx":45},{"icon":{"paths":["M873.526 533.067c-23.202-107.298-99.606-213.771-163.547-195.279-22.88 6.602-40.46 18.692-51.794 35.374-20.615-9.56-41.719-10.483-62.369-1.375-15.136 6.499-27.787 17.675-36.119 31.87-23.78-12.028-51.114-11.771-79.761 1.671-6.093 2.815-11.876 6.263-17.254 10.282-69.833-98.686-126.887-143.222-178.309-131.564-72.823 16.482-72.823 79.994-17.575 163.172l106.063 164.004c-58.201-10.942-100.294-1.545-125.247 31.792-40.801 54.558 2.381 104.946 102.646 150.055l193.785 86.195c6.929 2.949 14.742 3.053 21.751 0.289s12.65-8.156 15.715-15.040c3.059-6.875 3.303-14.685 0.658-21.745s-7.952-12.789-14.786-15.967l-193.671-86.139c-39.081-17.575-64.888-34.056-76.778-47.95-6.896-8.041-7.097-10.285-3.329-15.311 14.183-18.954 58.256-18.954 135.098 9.711 25.896 9.673 49.156-19.262 34.136-42.498l-148.020-228.87c-36.781-55.387-36.781-71.383-17.775-75.687 25.187-5.689 78.153 44.332 148.532 153.045 5.289 12.461 13.529 25.438 24.584 38.559 22.851 27.166 64.982-3.185 46.46-33.478-6.771-11.092-13.662-22.103-20.669-33.051 0.49-3.354 3.48-5.515 8.354-7.807 20.162-9.447 32.73-5.859 46.83 14.070 15.677 22.108 50.448 12.198 52.089-14.875 1.143-19.007 6.398-28.076 15.738-32.19 8.041-3.53 18.030-0.49 33.626 14.359 3.91 3.731 8.802 6.272 14.086 7.349 5.293 1.074 10.791 0.641 15.841-1.269s9.472-5.194 12.748-9.5c3.266-4.307 5.261-9.434 5.738-14.819 1.319-14.842 8.103-22.65 24.958-27.536 17.688-5.113 74.596 74.198 91.48 152.266 12.148 56.166 12.148 122.921-0.289 200.213-1.207 7.525 0.619 15.213 5.088 21.39 4.463 6.168 11.196 10.314 18.719 11.52 7.525 1.207 15.213-0.619 21.381-5.088s10.314-11.196 11.52-18.719c13.467-83.903 13.467-157.718-0.314-221.434z","M292.381 856.543h-122.519c-17.857 0-32.529-14.899-32.529-33.503v-622.096c0-18.605 14.672-33.503 32.529-33.503h336.89c17.888 0 32.56 14.904 32.56 33.503 0 7.618 3.027 14.914 8.404 20.305s12.689 8.406 20.305 8.406c7.618 0 14.914-3.027 20.308-8.406 5.385-5.389 8.404-12.688 8.404-20.305 0-50.136-40.164-90.938-89.958-90.938h-336.922c-49.784 0-89.958 40.801-89.958 90.938v622.131c0 50.109 40.166 90.912 89.958 90.912h122.519c7.618 0 14.914-3.027 20.305-8.407s8.406-12.688 8.406-20.31c0-7.613-3.027-14.911-8.406-20.301s-12.688-8.407-20.305-8.407z"],"attrs":[],"width":964,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["interactive"],"colorPermutations":{}},"attrs":[],"properties":{"order":110,"id":52,"name":"interactive","prevSize":32,"code":59702},"setIdx":0,"setId":2,"iconIdx":46},{"icon":{"paths":["M507.725 981.092v-161.361c45.053 0 85.529-8.975 126.125-26.924l63.002 152.386c-56.059 22.089-120.961 35.2-188.838 35.896l-0.29 0.002zM863.514 828.706l-117.091-112.031c30.879-30.176 54.226-67.928 67.042-110.255l0.478-1.836 153.109 53.787c-24.446 65.953-59.62 122.593-103.924 170.693l0.325-0.357zM994.097 465.629l-162.144 8.975c-4.457-44.812-13.492-85.168-36.019-125.523l144.135-71.676c31.501 58.244 49.51 121.006 54.028 188.224zM818.461 124.958l-103.599 125.523c-31.846-26.741-69.271-48.102-110.135-62.039l-2.438-0.722 45.053-156.904c63.063 22.406 121.608 53.787 171.118 94.142zM440.206 17.384l22.527 161.421c-45.053 4.457-85.589 22.346-121.668 44.752l-85.469-143.352c58.545-31.381 117.091-53.787 184.671-62.761zM111.461 214.583l130.582 94.142c-25.020 32.881-43.765 71.982-53.626 114.49l-0.402 2.058-157.626-35.838c18.070-62.761 45.053-121.066 81.072-174.853zM30.389 595.609l157.626-31.321c9.035 44.752 26.984 80.65 49.511 116.488l-135.1 94.142c-31.501-53.847-58.545-112.031-72.037-179.31zM251.078 909.417l85.529-139.015c36.019 22.406 76.555 35.898 121.608 44.873l-22.527 161.361c-67.58-9.035-126.065-31.381-184.61-67.279z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["loading"]},"attrs":[{}],"properties":{"order":99,"id":7,"name":"loading","prevSize":32,"code":59689},"setIdx":0,"setId":2,"iconIdx":47},{"icon":{"paths":["M1108.66 572.841c0-0.001 0-0.002 0-0.003 0-26.984-15.41-50.368-37.91-61.834l-0.393-0.182-218.814-109.45c-11.424-5.843-19.109-17.53-19.109-31.013 0-0.014 0-0.028 0-0.042l-0 0.002v-226.066c0-28.834-17.915-54.768-45.042-64.834l-191.942-72.085c-12.035-4.663-25.962-7.364-40.521-7.364s-28.486 2.701-41.309 7.63l0.788-0.266-191.345 72.512c-26.379 10.221-44.75 35.378-44.787 64.829l-0 0.004v224.701c-0.018 13.465-7.698 25.131-18.913 30.876l-0.196 0.091-220.862 110.388c-22.893 11.647-38.304 35.032-38.304 62.016 0 0.091 0 0.182 0.001 0.273l-0-0.014 0.512 244.919c0 25.166 13.649 48.284 35.573 60.568l243.383 135.725c20.474 11.431 45.384 11.346 65.772-0.171l2.218-1.194 189.98-109.194c4.927-2.889 10.85-4.595 17.171-4.595 6.441 0 12.468 1.771 17.62 4.853l-0.157-0.087 191.26 111.071c9.557 5.558 21.032 8.838 33.274 8.838 11.998 0 23.259-3.151 33.002-8.671l-0.333 0.174 3.242-1.877 240.824-133.763c21.43-12.121 35.659-34.759 35.659-60.72 0-0.037-0-0.074-0-0.111l0 0.006-0.341-245.942zM763.164 368.871c0 0.015 0 0.032 0 0.049 0 13.501-7.725 25.198-18.997 30.912l-0.197 0.091-142.123 71.232c-1.12 0.563-2.441 0.893-3.839 0.893-4.79 0-8.675-3.87-8.701-8.653l-0-0.002v-176.587c0-31.564 19.109-59.886 48.284-71.659l124.891-49.905 0.597 203.63zM278.36 630.339l-0.256-0.171-161.232-81.298 179.402-89.744c4.498-2.319 9.817-3.679 15.453-3.679 5.897 0 11.446 1.488 16.293 4.11l-0.181-0.090 19.535 10.237 140.331 69.611c2.887 1.432 4.836 4.36 4.836 7.743 0 3.282-1.835 6.135-4.535 7.59l-0.046 0.023-140.587 75.753c-10.034 5.164-21.896 8.191-34.465 8.191-12.636 0-24.557-3.059-35.063-8.477l0.429 0.201zM500.843 851.286l-153.383 76.606-0.853-177.44c-0-0.065-0-0.142-0-0.219 0-29.958 17.044-55.935 41.965-68.762l0.434-0.203 117.725-59.118c1.119-0.569 2.44-0.902 3.839-0.902 4.746 0 8.596 3.837 8.616 8.578l0 0.002 0.853 190.407c-0.030 13.53-7.739 25.253-18.998 31.046l-0.196 0.092zM527.629 155.089l-111.412-41.801 138.454-52.038 138.454 52.038-111.412 41.801c-8.028 3.141-17.323 4.961-27.043 4.961s-19.015-1.82-27.562-5.139l0.52 0.177zM760.946 631.107l-139.734-76.35c-2.67-1.52-4.441-4.347-4.441-7.587 0-3.376 1.923-6.303 4.733-7.746l0.049-0.023 141.611-70.805 17.488-9.128c4.655-2.518 10.189-3.998 16.070-3.998 5.619 0 10.922 1.351 15.603 3.747l-0.194-0.090 20.218 10.066 159.184 79.763-158.075 83.004c-10.293 5.454-22.501 8.656-35.457 8.656-13.593 0-26.362-3.524-37.445-9.709l0.39 0.2zM1039.305 803.428c0 12.796-7.081 24.569-18.426 30.711l-188.53 99.895-0.597-183.326c-0-0.092-0.001-0.202-0.001-0.311 0-29.952 17.132-55.902 42.131-68.587l0.438-0.202 164.985-82.749v204.568z"],"attrs":[{}],"width":1109,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["cube"]},"attrs":[{}],"properties":{"order":100,"id":6,"name":"cube","prevSize":32,"code":59690},"setIdx":0,"setId":2,"iconIdx":48},{"icon":{"paths":["M761.6 6.4c64 0 128 25.6 179.2 76.8 102.4 102.4 102.4 262.4 0 364.8l-134.4 134.4-89.6-89.6 134.4-134.4c51.2-51.2 51.2-128 0-179.2s-128-51.2-179.2 0l-179.2 179.2c-25.6 25.6-38.4 57.6-38.4 89.6s12.8 64 32 89.6l70.4 64-89.6 89.6-70.4-70.4c-44.8-44.8-70.4-108.8-70.4-179.2s25.6-134.4 76.8-179.2l179.2-179.2c44.8-51.2 115.2-76.8 179.2-76.8z","M556.8 332.8l70.4 64c102.4 102.4 102.4 262.4 0 364.8l-179.2 179.2c-46.342 47.411-110.938 76.81-182.4 76.81s-136.059-29.399-182.353-76.762l-0.047-0.048c-47.411-46.342-76.81-110.938-76.81-182.4s29.399-136.059 76.762-182.353l0.048-0.047 134.4-134.4 89.6 89.6-134.4 134.4c-51.2 51.2-51.2 128 0 179.2s134.4 51.2 179.2 0l185.6-172.8c51.2-51.2 51.2-128 0-179.2l-70.4-70.4 89.6-89.6z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["Frame"]},"attrs":[{},{}],"properties":{"order":101,"id":5,"name":"Frame","prevSize":32,"code":59691},"setIdx":0,"setId":2,"iconIdx":49},{"icon":{"paths":["M942.592 821.299l-27.853-387.328c33.824-16.411 56.73-50.492 56.73-89.924 0-41.167-24.967-76.503-60.585-91.692l-0.651-0.247c-11.179-4.799-24.191-7.589-37.854-7.589-27.231 0-51.873 11.084-69.661 28.985l-0.005 0.005-245.76-122.317c0.135-2.112 0.211-4.58 0.211-7.066s-0.077-4.954-0.228-7.401l0.017 0.336c-6.602-48.799-47.991-86.018-98.074-86.018s-91.472 37.219-98.017 85.507l-0.057 0.511c0.051 9.062 1.485 18.022 4.147 26.675l-154.778 104.55c-16.345-13.49-37.447-21.745-60.471-21.964l-0.048-0c-54.475 0.973-98.262 45.362-98.262 99.979 0 43.605 27.911 80.691 66.842 94.367l0.701 0.214v229.478c-40.129 13.254-68.577 50.417-68.577 94.225 0 54.688 44.333 99.021 99.021 99.021 43.522 0 80.486-28.078 93.766-67.108l0.206-0.697h174.797c10.272 32.939 36.56 58.023 69.517 66.414l0.678 0.146c6.81 1.712 14.627 2.694 22.674 2.694 27.253 0 51.875-11.266 69.463-29.396l0.024-0.024 244.992 93.338c4.034 51.18 46.558 91.183 98.426 91.183 54.518 0 98.714-44.196 98.714-98.714 0-48.079-34.373-88.131-79.886-96.92l-0.62-0.1 0.461-3.123zM872.397 309.299c0.12-0.001 0.262-0.002 0.405-0.002 15.414 0 28.636 9.382 34.269 22.747l0.091 0.244c1.835 4.3 2.902 9.302 2.902 14.553 0 18.155-12.752 33.331-29.787 37.061l-0.251 0.046c-2.134 0.439-4.586 0.691-7.098 0.691-5.302 0-10.343-1.121-14.897-3.139l0.235 0.093c-13.461-5.932-22.687-19.157-22.687-34.537 0-0.224 0.002-0.448 0.006-0.671l-0 0.034c-0-0.046-0-0.1-0-0.155 0-20.308 16.376-36.791 36.643-36.965l0.016-0h0.154zM773.786 332.288c-0.512 4.71-0.512 9.421 0 14.131-0 0.103-0.001 0.226-0.001 0.348 0 47.965 33.754 88.046 78.804 97.785l0.659 0.119 27.853 387.328c-16.485 8.169-29.944 20.282-39.497 35.144l-0.234 0.389-233.318-88.883c0.12-2.111 0.188-4.58 0.188-7.066s-0.068-4.955-0.203-7.407l0.015 0.341c0-0.039 0-0.086 0-0.133 0-54.219-43.264-98.334-97.153-99.705l-0.127-0.003c-43.343 0.301-79.989 28.634-92.732 67.76l-0.196 0.695h-174.643c-9.98-29.672-32.847-52.623-61.77-62.514l-0.694-0.206v-229.632c39.937-13.628 68.147-50.821 68.147-94.603 0-0.005-0-0.011-0-0.016l0 0.001c-0.364-9.439-1.855-18.379-4.343-26.893l0.196 0.781 154.829-104.55c16.472 13.63 37.814 21.898 61.087 21.898 27.323 0 51.985-11.397 69.49-29.696l0.034-0.036 243.61 124.621zM459.469 99.994c0.185-0.003 0.404-0.005 0.623-0.005 17.996 0 33.014 12.754 36.507 29.716l0.041 0.241c0.506 2.317 0.795 4.978 0.795 7.707 0 15.563-9.422 28.926-22.873 34.695l-0.246 0.094c-4.143 1.711-8.954 2.704-13.997 2.704-20.755 0-37.581-16.825-37.581-37.581 0-20.456 16.344-37.095 36.686-37.57l0.044-0.001zM149.658 309.043c0.12-0.001 0.262-0.002 0.405-0.002 15.414 0 28.636 9.382 34.269 22.747l0.091 0.244c1.86 4.328 2.941 9.366 2.941 14.656 0 10.366-4.152 19.762-10.883 26.616l0.006-0.006c-6.689 6.836-16.010 11.075-26.32 11.075-5.294 0-10.327-1.117-14.875-3.129l0.235 0.093c-13.461-5.932-22.687-19.157-22.687-34.537 0-0.224 0.002-0.448 0.006-0.671l-0 0.034c-0.001-0.076-0.001-0.166-0.001-0.257 0-20.288 16.389-36.748 36.649-36.863l0.011-0h0.154zM149.658 801.946c-20.228-0.174-36.558-16.612-36.558-36.864 0-20.36 16.505-36.865 36.865-36.865 0.108 0 0.216 0 0.324 0.001l-0.017-0c20.228 0.174 36.558 16.612 36.558 36.864 0 20.36-16.505 36.865-36.865 36.865-0.108 0-0.216-0-0.324-0.001l0.017 0zM510.976 801.946c-20.762-0.029-37.581-16.866-37.581-37.632 0-20.784 16.848-37.632 37.632-37.632s37.632 16.848 37.632 37.632c0 20.766-16.819 37.603-37.578 37.632l-0.003 0h-0.102zM924.006 958.72c-20.228-0.174-36.558-16.612-36.558-36.864 0-20.36 16.505-36.865 36.865-36.865 0.108 0 0.216 0 0.324 0.001l-0.017-0c20.228 0.174 36.558 16.612 36.558 36.864 0 20.36-16.505 36.865-36.865 36.865-0.108 0-0.216-0-0.324-0.001l0.017 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["polygon"]},"attrs":[{}],"properties":{"order":102,"id":4,"name":"polygon","prevSize":32,"code":59692},"setIdx":0,"setId":2,"iconIdx":50},{"icon":{"paths":["M186.745 169.337l566.703 424.691-190.087 11.426c-78.050 8.234-145.198 49.017-188.481 108.324l-0.528 0.758-104.717 158.451-82.998-703.704 0.108 0.054zM143.091 55.134c-24.468-6.575-38.804 11.102-34.493 47.373l104.017 881.287c2.695 23.444 10.779 36.864 20.803 39.559s23.929-5.389 37.349-25.762l161.684-244.574c31.819-42.345 79.528-71.266 134.113-77.892l0.947-0.094 292.648-17.624c47.859-2.856 55.619-28.726 17.246-57.452l-709.47-531.672c-7.086-5.683-15.412-10.176-24.473-13.006l-0.535-0.144h0.108z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["edit"]},"attrs":[{}],"properties":{"order":103,"id":3,"name":"edit","prevSize":32,"code":59693},"setIdx":0,"setId":2,"iconIdx":51},{"icon":{"paths":["M905.168 756.72v-490.113c27.683-8.649 50.961-27.787 64.753-53.32 9.069-17.407 13.839-36.701 13.895-56.364-0.329-64.033-52.313-115.812-116.392-115.812-50.941 0-94.236 32.721-110.013 78.293l-0.247 0.816h-489.957c-15.697-46.969-59.279-80.22-110.631-80.22-64.281 0-116.393 52.112-116.393 116.393 0 51.352 33.251 94.935 79.399 110.392l0.821 0.239v490.113c-46.713 15.842-79.735 59.3-79.735 110.471 0 64.24 52.044 116.325 116.269 116.393h0.007c43.865-0.084 81.973-24.633 101.411-60.731l0.303-0.616c3.568-6.082 6.237-12.689 7.97-19.559h490.113c15.835 46.561 59.101 79.49 110.083 79.64h0.020c43.865-0.084 81.973-24.633 101.411-60.731l0.303-0.616c9.175-17.303 14-36.701 14.156-56.312-0.597-49.817-32.395-92.052-76.731-108.118l-0.809-0.259zM758.369 830.123h-490.533c-11.953-34.657-38.746-61.448-72.58-73.153l-0.822-0.248v-490.113c27.735-8.649 50.961-27.787 64.805-53.32 3.568-6.134 6.237-12.74 7.97-19.611h490.113c11.962 34.652 38.75 61.441 72.58 73.153l0.822 0.249v490.113c-34.44 11.813-61.066 38.437-72.634 72.065l-0.244 0.813h0.524zM868.469 114.664c0.019 0 0.036 0 0.057 0 23.657 0 42.835 19.178 42.835 42.835 0 0.24-0.002 0.479-0.006 0.719v-0.034c-0.077 7.641-1.865 14.839-5.004 21.267l0.13-0.293c-7.93 13.234-22.193 21.953-38.495 21.953-5.765 0-11.278-1.092-16.338-3.079l0.304 0.104c-15.963-6.52-27.008-21.922-27.008-39.913 0-12.004 4.919-22.856 12.847-30.651l0.006-0.006c7.867-7.864 18.727-12.727 30.72-12.74h0.002v-0.156zM159.62 114.664c0.001 0 0.003 0 0.005 0 23.688 0 42.887 19.204 42.887 42.887 0 0.22-0.002 0.443-0.005 0.664v-0.031c0 7.342-1.887 14.576-5.507 20.974-7.736 12.776-21.564 21.188-37.352 21.188-23.558 0-42.743-18.718-43.495-42.092l-0.002-0.070c0-24.035 19.484-43.515 43.515-43.515v0zM197 887.85c-7.559 13.211-21.567 21.973-37.624 21.973-23.862 0-43.203-19.342-43.203-43.203s19.342-43.203 43.203-43.203c6.071 0 11.848 1.253 17.090 3.511l-0.279-0.107c15.595 6.651 26.325 21.855 26.325 39.56 0 0.229-0.002 0.458-0.006 0.686v-0.033c0 7.342-1.887 14.576-5.507 20.974v-0.156zM906.428 887.85c-7.916 13.241-22.173 21.967-38.47 21.967-5.774 0-11.296-1.093-16.361-3.093l0.303 0.104c-15.928-6.502-26.948-21.87-26.948-39.815 0-0.012 0-0.023 0-0.030v0.002c0.044-23.999 19.509-43.439 43.515-43.439 5.977 0 11.67 1.206 16.852 3.386l-0.285-0.106c15.594 6.638 26.326 21.827 26.326 39.525 0 0.243-0.002 0.484-0.006 0.726v-0.034c-0.075 7.641-1.864 14.839-5.004 21.267l0.13-0.293v-0.156z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["rectangle"]},"attrs":[{}],"properties":{"order":104,"id":2,"name":"rectangle","prevSize":32,"code":59694},"setIdx":0,"setId":2,"iconIdx":52},{"icon":{"paths":["M90.675 635.392l69.786 2.355-2.253 86.118 372.326 218.778 232.755-134.963 34.97 62.72-2.355 2.355-267.725 151.245h-2.253l-437.606-260.71 2.355-127.898zM709.837 100.096l262.963 158.31-4.608 512-104.806 62.822-34.816-62.874 72.192-41.83 4.608-428.237-232.858-141.978 37.376-58.214zM514.304 302.541c18.586 0 25.6 11.725 34.918 23.347l2.355 2.253 174.592 335.206c4.382 5.761 7.021 13.055 7.021 20.966s-2.639 15.206-7.084 21.052l0.063-0.086c-6.113 8.564-16.018 14.081-27.21 14.081-14.003 0-25.99-8.635-30.924-20.872l-0.080-0.224-16.384-32.666-34.918-65.126c-4.086-7.36-7.967-13.481-12.152-19.372l0.427 0.633-37.171 72.192c-7.014 16.333-16.333 30.31-23.347 46.541-4.836 12.2-16.533 20.67-30.209 20.67-11.854 0-22.221-6.364-27.872-15.862l-0.082-0.149c-3.139-5.504-4.989-12.094-4.989-19.116 0-8.565 2.753-16.488 7.423-22.931l-0.078 0.114 48.845-90.726c6.963-16.333 16.384-30.31 23.245-46.592 3.289-4.521 5.261-10.184 5.261-16.307s-1.972-11.786-5.317-16.388l0.056 0.080c-16.333-27.853-27.955-55.808-44.186-83.763-5.846 4.571-10.040 11.017-11.685 18.423l-0.040 0.214-69.786 132.608-67.277 132.864c-5.807 13.113-16.569 23.141-29.85 27.845l-0.358 0.111c-2.052 0.473-4.409 0.745-6.829 0.745-17.503 0-31.693-14.189-31.693-31.693 0-3.073 0.437-6.044 1.253-8.853l-0.056 0.224c1.843-5.632 4.147-11.11 6.912-16.384 58.163-111.718 114.022-221.133 172.186-332.8 7.066-18.586 16.435-30.259 35.021-30.259zM537.446 0l104.755 65.126-34.97 60.57-72.090-44.186-374.784 209.408v267.725h-69.734l2.355-309.555 444.467-249.088zM707.43 332.8c31.414 2.369 56.005 28.44 56.005 60.255 0 1.715-0.071 3.413-0.211 5.091l0.015-0.22c-3.159 31.592-29.605 56.056-61.764 56.056-1.201 0-2.394-0.034-3.578-0.101l0.164 0.007c-32.278-0.205-58.365-26.419-58.365-58.725 0-1.478 0.055-2.943 0.162-4.393l-0.012 0.194c0.92-32.565 27.538-58.612 60.24-58.612 2.589 0 5.139 0.163 7.642 0.48l-0.298-0.031z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["ai"]},"attrs":[{}],"properties":{"order":105,"id":1,"name":"ai","prevSize":32,"code":59695},"setIdx":0,"setId":2,"iconIdx":53},{"icon":{"paths":["M424.842 314.4c-27.69-15.533-47.959-35.979-75.328-19.841-26.381 15.801-43.762 44.238-43.762 76.73 0 15.167 3.796 29.457 10.477 41.97l-0.237-0.475-163.763 159.739c-11.241-5.614-24.487-8.904-38.485-8.904h-0.001c-0.042 0-0.075 0-0.128 0-49.505 0-89.616 40.126-89.616 89.616s40.126 89.616 89.616 89.616c16.904 0 32.716-4.691 46.215-12.825l-0.401 0.22c13.777-8.288 25.092-20.129 32.668-34.326 7.627-13.258 11.7-28.314 11.739-43.647-0.107-15.381-4.284-29.767-11.488-42.171l0.215 0.411 163.763-159.507c12.020 6.302 25.381 9.51 38.972 9.473 48.553-0.45 87.732-39.925 87.732-88.527 0-0.042 0-0.103 0-0.157v0.016c0-12.825-33.576 39.953-39.009 28.41 75.752-67.184 4.074-49.386 2.647-74.848 0.417-12.12-17.518 24.623-10.462 4.736 3.738-6.015 26.651-58.094-11.353-15.714zM143.328 669.866c-6.015 10.315-16.952 17.186-29.509 17.379l-0.027 0.001c-16.718-1.081-29.854-14.903-29.854-31.799 0-17.592 14.271-31.867 31.867-31.867 16.897 0 30.719 13.146 31.788 29.767l0.010 0.103c-0.244 6.025-1.773 11.645-4.316 16.666l0.103-0.228h-0.042zM424.842 388.359c-6.015 10.315-16.952 17.186-29.509 17.379l-0.027 0.001c-16.718-1.081-29.854-14.903-29.854-31.799 0-17.592 14.271-31.867 31.867-31.867 16.897 0 30.719 13.146 31.788 29.767l0.010 0.103c-0.244 6.025-1.773 11.645-4.316 16.666l0.103-0.228h-0.042zM957.515 281.543c-49.399 0.215-89.385 40.213-89.579 89.609v0.016c-0.279 16.519 4.11 32.809 12.684 46.914l-161.396 156.24c-12.511-7.104-27.473-11.29-43.416-11.29-49.24 0-89.153 39.918-89.153 89.153 0 0.173 0.001 0.345 0.003 0.517v-0.024c0.941 14.209 5.495 27.978 13.165 40.011-5.355 4.736 13.827 0 9.473 7.766-11.353 11.841 13.445-3.591 11.319 9.473-10.315 25.704 53.116-2.369 17.938 20.548 11.501 6.768 24.521 10.554 37.876 11.039 49.1-0.33 88.894-39.693 89.957-88.576l0.004-0.103c-0.132-13.492-3.23-26.218-8.662-37.616l0.234 0.537 165.71-161.396c10.895 4.354 22.492 6.621 34.235 6.583 0.042 0 0.077 0 0.128 0 33.688 0 62.98-18.784 77.993-46.443l0.233-0.473c7.627-13.258 11.7-28.314 11.739-43.647-0.646-49.187-40.671-88.817-89.947-88.817-0.189 0-0.367 0.001-0.561 0.004h0.028zM705.594 669.111c-6.015 10.315-16.952 17.186-29.509 17.379l-0.027 0.001c-18.382-0.369-33.143-15.368-33.143-33.802 0-0.004 0-0.010 0-0.016v0.001c2.148-16.778 16.346-29.622 33.547-29.622s31.383 12.83 33.52 29.449l0.016 0.164c-0.302 6.047-1.897 11.654-4.518 16.652l0.103-0.215zM987.11 387.595c-6.015 10.315-16.952 17.186-29.509 17.379l-0.027 0.001c-18.382-0.369-33.143-15.368-33.143-33.802 0-0.004 0-0.010 0-0.016v0.001c2.148-16.778 16.346-29.622 33.547-29.622s31.383 12.83 33.52 29.449l0.016 0.164c-0.244 6.025-1.773 11.645-4.316 16.666l0.103-0.228h-0.189z","M746.878 688.474c9.622-12.738 15.397-28.846 15.397-46.303 0-12.972-3.187-25.19-8.842-35.925l0.204 0.417c-15.801-26.381-44.238-43.762-76.73-43.762-15.167 0-29.457 3.796-41.97 10.477l0.475-0.237-159.97-163.763c5.874-11.98 8.904-25.145 8.904-38.486 0-0.042 0-0.075 0-0.128 0-49.505-40.126-89.616-89.616-89.616s-89.616 40.126-89.616 89.616c0 16.904 4.691 32.716 12.825 46.215l-0.22-0.401c8.288 13.777 20.129 25.092 34.326 32.668 13.258 7.718 28.35 11.841 43.705 11.98 15.381-0.107 29.767-4.284 42.171-11.488l-0.411 0.215 159.549 163.521c-5.961 11.213-9.474 24.515-9.474 38.626 0 0.122 0 0.244 0.001 0.362v-0.016c0.33 49.1 39.693 88.894 88.576 89.957l0.103 0.004c12.886-0.475 25.565-3.738 37.068-9.473 1.513 1.707-6.101-14.768 0-11.179 15.195 14.069 8.525-36.025 22.578-26.185 46.776 34.326-39.111-66.418 10.981-7.105zM378.352 400.479c-10.315-6.015-17.186-16.952-17.379-29.509l-0.001-0.027c0.369-18.382 15.368-33.143 33.802-33.143 0.004 0 0.010 0 0.016 0h-0.001c16.778 2.148 29.622 16.346 29.622 33.547s-12.83 31.383-29.449 33.52l-0.164 0.016c-6.025-0.244-11.645-1.773-16.666-4.316l0.228 0.103v-0.189zM659.859 681.986c-10.315-6.015-17.186-16.952-17.379-29.509l-0.001-0.027c0.369-18.382 15.368-33.143 33.802-33.143 0.004 0 0.010 0 0.016 0h-0.001c16.778 2.148 29.622 16.346 29.622 33.547s-12.83 31.383-29.449 33.52l-0.164 0.016c-6.025-0.244-11.645-1.773-16.666-4.316l0.228 0.103v-0.189z"],"attrs":[{},{}],"width":1120,"isMulticolor":false,"isMulticolor2":false,"grid":32,"tags":["polyline"]},"attrs":[{},{}],"properties":{"order":106,"id":0,"name":"polyline","prevSize":32,"code":59696},"setIdx":0,"setId":2,"iconIdx":54}],"height":1024,"metadata":{"name":""},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"","majorVersion":1,"minorVersion":0},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon","name":"icomoon","autoHost":false,"height":32,"columns":16,"margin":16},"historySize":50,"showCodes":true,"gridSize":16,"quickUsageToken":{"UntitledProject":"NTg1MDYyOTQ5N2U5MTU4YzFjM2E2YjA2ZTYxOGU3ODcjMSMxNjU2NjU2MTQ0IyMjNGNlZGI4ODExOWQx"},"showGrid":true}} \ No newline at end of file diff --git a/frontend/image-tool/public/iconfont/style.css b/frontend/image-tool/public/iconfont/style.css deleted file mode 100644 index e3667fa5..00000000 --- a/frontend/image-tool/public/iconfont/style.css +++ /dev/null @@ -1,192 +0,0 @@ -@font-face { - font-family: 'icomoon'; - src: url('fonts/icomoon.eot?rxbdnw'); - src: url('fonts/icomoon.eot?rxbdnw#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?rxbdnw') format('truetype'), - url('fonts/icomoon.woff?rxbdnw') format('woff'), - url('fonts/icomoon.svg?rxbdnw#icomoon') format('svg'); - font-weight: normal; - font-style: normal; - font-display: block; -} - -[class^="icon-"], [class*=" icon-"] { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'icomoon' !important; - speak: never; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - - /* Better Font Rendering =========== */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.icon-polygon-hollow:before { - content: "\e934"; -} -.icon-cancel-hollow:before { - content: "\e935"; -} -.icon-polygon-clip:before { - content: "\e931"; -} -.icon-keyboard:before { - content: "\e932"; -} -.icon-command:before { - content: "\e933"; -} -.icon-Job-information:before { - content: "\e900"; -} -.icon-Expiration-time:before { - content: "\e901"; -} -.icon-Work-flow:before { - content: "\e902"; -} -.icon-save:before { - content: "\e903"; -} -.icon-help:before { - content: "\e904"; -} -.icon-enter-fullscreen:before { - content: "\e905"; -} -.icon-exit-fullscreen:before { - content: "\e906"; -} -.icon-hang-up:before { - content: "\e907"; -} -.icon-submit:before { - content: "\e908"; -} -.icon-setting:before { - content: "\e909"; -} -.icon-information:before { - content: "\e90a"; -} -.icon-mapping:before { - content: "\e90b"; -} -.icon-rotating:before { - content: "\e90c"; -} -.icon-move:before { - content: "\e90d"; -} -.icon-tool:before { - content: "\e90e"; -} -.icon-Pack-up:before { - content: "\e90f"; -} -.icon-an-right:before { - content: "\e910"; -} -.icon-down:before { - content: "\e911"; -} -.icon-up:before { - content: "\e912"; -} -.icon-Right-rotation:before { - content: "\e913"; -} -.icon-Left-rotation:before { - content: "\e914"; -} -.icon-remind:before { - content: "\e915"; -} -.icon-Notes---list:before { - content: "\e916"; -} -.icon-Have-been-added:before { - content: "\e917"; -} -.icon-Add-a-notation:before { - content: "\e918"; -} -.icon-notation-tool:before { - content: "\e919"; -} -.icon-filter:before { - content: "\e91a"; -} -.icon-class-edit:before { - content: "\e91b"; -} -.icon-delete:before { - content: "\e91c"; -} -.icon-visible:before { - content: "\e91d"; -} -.icon-biaozhunkuang:before { - content: "\e91e"; -} -.icon-lifangti:before { - content: "\e91f"; -} -.icon-target:before { - content: "\e920"; -} -.icon-auxiliaryline:before { - content: "\e921"; -} -.icon-more:before { - content: "\e922"; -} -.icon-unfold:before { - content: "\e923"; -} -.icon-hidden:before { - content: "\e924"; -} -.icon-left:before { - content: "\e925"; -} -.icon-right:before { - content: "\e926"; -} -.icon-dakai:before { - content: "\e927"; -} -.icon-model:before { - content: "\e928"; -} -.icon-interactive:before { - content: "\e936"; -} -.icon-loading:before { - content: "\e929"; -} -.icon-cube:before { - content: "\e92a"; -} -.icon-Frame:before { - content: "\e92b"; -} -.icon-polygon:before { - content: "\e92c"; -} -.icon-edit:before { - content: "\e92d"; -} -.icon-rectangle:before { - content: "\e92e"; -} -.icon-ai:before { - content: "\e92f"; -} -.icon-polyline:before { - content: "\e930"; -} diff --git a/frontend/image-tool/public/image/20736.jpg b/frontend/image-tool/public/image/20736.jpg new file mode 100644 index 00000000..8fbcbabd Binary files /dev/null and b/frontend/image-tool/public/image/20736.jpg differ diff --git a/frontend/image-tool/src/App.vue b/frontend/image-tool/src/App.vue index 6f2f9653..b622376e 100644 --- a/frontend/image-tool/src/App.vue +++ b/frontend/image-tool/src/App.vue @@ -9,7 +9,8 @@ import { ConfigProvider } from 'ant-design-vue'; import enUS from 'ant-design-vue/es/locale/en_US'; import zhCN from 'ant-design-vue/es/locale/zh_CN'; - import Editor from '/@/business/chengdu/Editor.vue'; + // import Editor from '/@/business/chengdu/Editor.vue'; + import Editor from '/@/businessNew/Editor.vue'; diff --git a/frontend/image-tool/src/businessNew/Editor.vue b/frontend/image-tool/src/businessNew/Editor.vue new file mode 100644 index 00000000..79748dee --- /dev/null +++ b/frontend/image-tool/src/businessNew/Editor.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/actions/flow.ts b/frontend/image-tool/src/businessNew/actions/flow.ts new file mode 100644 index 00000000..515b7097 --- /dev/null +++ b/frontend/image-tool/src/businessNew/actions/flow.ts @@ -0,0 +1,57 @@ +import { defineAction } from 'image-editor'; +import Editor from '../common/Editor'; +import { BsUIType } from '../configs/ui'; +import Event from '../configs/event'; +import { FlowAction } from '../types'; + +// save +export const flowSave = defineAction({ + valid(editor: Editor) { + const { state, bsState } = editor; + return state.modeConfig.ui[BsUIType.flowSave] && !bsState.doing.saving; + }, + execute(editor: Editor) { + editor.emit(Event.FLOW_ACTION, FlowAction.save); + }, +}); + +export const dataFrameNext = defineAction({ + valid(editor: Editor) { + return !editor.bsState.blocking; + }, + execute(editor: Editor) { + if (editor.state.isSeriesFrame) changeScene(editor, 1); + else changeFrame(editor, 1); + }, +}); +export const dataFrameLast = defineAction({ + valid(editor: Editor) { + return !editor.bsState.blocking; + }, + execute(editor: Editor) { + if (editor.state.isSeriesFrame) changeScene(editor, -1); + else changeFrame(editor, -1); + }, +}); +function changeFrame(editor: Editor, step: number) { + const { frames, frameIndex } = editor.state; + const index = frameIndex + step; + if (index < 0 || index >= frames.length) return; + editor.switchFrame(index); +} +async function changeScene(editor: Editor, step: number) { + const { state } = editor; + const { sceneIds, sceneId } = state; + const isShow = state.isSeriesFrame && sceneIds.length > 0; + if (!isShow) return; + const sceneIdx = sceneIds.indexOf(sceneId); + const nextIdx = sceneIdx + step; + if (nextIdx < 0 || nextIdx >= sceneIds.length) return; + const needSave = state.frames.findIndex((e) => e.needSave) !== -1; + if (needSave) { + const result = await editor.save(); + if (!result) return; + } + await editor.loadManager.loadSceneData(nextIdx); + editor.emit(Event.SCENE_CHANGE, { preScene: sceneIdx, newScene: nextIdx }); +} diff --git a/frontend/image-tool/src/businessNew/actions/index.ts b/frontend/image-tool/src/businessNew/actions/index.ts new file mode 100644 index 00000000..5851cb1e --- /dev/null +++ b/frontend/image-tool/src/businessNew/actions/index.ts @@ -0,0 +1 @@ +export * from './flow'; diff --git a/frontend/image-tool/src/businessNew/api/base.ts b/frontend/image-tool/src/businessNew/api/base.ts new file mode 100644 index 00000000..14e8bbbe --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/base.ts @@ -0,0 +1,70 @@ +import axios, { AxiosRequestHeaders, AxiosRequestConfig } from 'axios'; +import { BSError } from 'image-editor'; + +// token +export const requestConfig = { + token: + 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIzMDA4MCIsInRlYW1JZCI6IjUxIiwiaXNzIjoiYmFzaWMuYWkiLCJpYXQiOjE2NTI0MDc3MDcsImV4cCI6MTY1MjQ1MDkwN30.HC2tAjiDdYpjVvlnLXfNO_2_V7b0AAHrLVPm7Ox0tyuBA-c4-asQnDkbu6P4Nn1yvW2K9-8THpJEnOruA1Yf5g', +}; + +export function setToken(token: string) { + requestConfig.token = token; +} + +function isResource(headers: AxiosRequestHeaders) { + // 'x-request-type': 'resource' + return headers['x-request-type'] === 'resource'; +} + +// Service +const BaseURL = ''; +export const Service = axios.create({ + timeout: 1000 * 60 * 20, + baseURL: BaseURL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +Service.interceptors.request.use((config) => { + config.headers = config.headers || {}; + if (!isResource(config.headers)) { + config.headers['Authorization'] = requestConfig.token; + } + + return config; +}); + +Service.interceptors.response.use( + (response) => { + let data = response.data; + if (!isResource(response.config.headers || {}) && data.message) { + return Promise.reject(new BSError('', 'Network Error')); + } + return data; + }, + (error) => { + return Promise.reject(new BSError('', 'Network Error')); + }, +); + +export function get(url: string, data?: any, config?: AxiosRequestConfig) { + return Service.request({ + url, + method: 'get', + params: data, + ...config, + }); +} + +export function post(url: string, data?: any, config?: AxiosRequestConfig) { + let headers = {} as AxiosRequestHeaders; + + return Service.request({ + url, + data, + method: 'post', + headers, + ...config, + }); +} diff --git a/frontend/image-tool/src/businessNew/api/class.ts b/frontend/image-tool/src/businessNew/api/class.ts new file mode 100644 index 00000000..e97f2e17 --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/class.ts @@ -0,0 +1,24 @@ +import { parseClassesFromBackend, traverseClassification2Arr } from '../utils'; +import { get } from './base'; +import { Api } from './type'; + +/** all class */ +export async function getDataflowClass(datasetId: string) { + const url = `${Api.API}/datasetClass/findAll/${datasetId}`; + let data = await get(url); + data = data.data || []; + + const classTypes = parseClassesFromBackend(data); + + return classTypes; +} +/** all Classification */ +export async function getDataflowClassification(datasetId: string) { + const url = `${Api.API}/datasetClassification/findAll/${datasetId}`; + let data = await get(url); + data = data.data || []; + const classifications = traverseClassification2Arr(data); + console.log('classifications', classifications); + + return classifications; +} diff --git a/frontend/image-tool/src/businessNew/api/common.ts b/frontend/image-tool/src/businessNew/api/common.ts new file mode 100644 index 00000000..d6023445 --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/common.ts @@ -0,0 +1,82 @@ +import { Api, IModelResult } from './type'; +import { get, post } from './base'; +import { IFrame, LoadStatus, __UNSERIES__, IObjectSource, SourceType } from 'image-editor'; + +export async function unlockRecord(recordId: string) { + const url = `${Api.DATA}/unLock/${recordId}`; + return await post(url); +} + +export async function getInfoByRecordId(recordId: string) { + const url = `${Api.DATA}/findDataAnnotationRecord/${recordId}`; + let data = await get(url); + data = data.data; + if (!data) return { dataInfos: [], isSeriesFrame: false, seriesFrameId: '' }; + + const isSeriesFrame = data.itemType === 'SCENE'; + let model: IModelResult; + if (data.serialNo) { + model = { recordId: data.serialNo, id: '', version: '', state: LoadStatus.DEFAULT }; + } + const dataInfos: IFrame[] = []; + const seriesFrames: string[] = []; + (data.datas || []).forEach((config: any) => { + const sceneId = config.sceneId || __UNSERIES__; + dataInfos.push({ + id: config.dataId, + datasetId: config.datasetId, + needSave: false, + model, + sceneId, + } as IFrame); + if (!seriesFrames.includes(sceneId)) seriesFrames.push(sceneId); + }); + + return { dataInfos, isSeriesFrame, seriesFrames }; +} +export async function getResultSources(dataId: string) { + let url = `/api/data/getDataModelRunResult/${dataId}`; + let data = await get(url); + data = data.data || {}; + + let sources = [] as IObjectSource[]; + data.forEach((item: any) => { + let { modelId, modelName, runRecords = [] } = item; + runRecords.forEach((e: any) => { + sources.push({ + name: e.runNo, + sourceId: e.id, + modelId: modelId, + modelName: modelName, + sourceType: SourceType.MODEL, + frameId: dataId, + }); + }); + }); + return sources; +} +/** Data flow */ +export async function setInvalid(dataId: string) { + const url = `${Api.DATA}/flow/markAsInvalid/${dataId}`; + await post(url); +} +export async function setValid(dataId: string) { + const url = `${Api.DATA}/flow/markAsValid/${dataId}`; + await post(url); +} +export async function submit(dataId: string) { + const url = `${Api.DATA}/flow/submit/${dataId}`; + await post(url); +} +export async function getDataStatusByIds(dataId: string) { + let url = `${Api.DATA}/getDataStatusByIds`; + let data = await get(url, { dataIds: dataId }); + data = data.data || []; + return data[0]; +} + +export async function takeRecordByData(params: any) { + const url = `${Api.DATA}/annotate`; + const res = await post(url, params); + return res; +} diff --git a/frontend/image-tool/src/businessNew/api/data.ts b/frontend/image-tool/src/businessNew/api/data.ts new file mode 100644 index 00000000..3cd65d33 --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/data.ts @@ -0,0 +1,44 @@ +import { ISaveFormat, ISaveResp } from '../types'; +import { get, post } from './base'; +import { Api, DataflowAnnotationParamsReq, DataflowAnnotationParamsRsp, IFileConfig } from './type'; + +export async function getAnnotationByDataIds(params: DataflowAnnotationParamsReq) { + const url = `${Api.ANNOTATION}/data/listByDataIds`; + + const res = await get(url, { dataIds: params.dataIds.join(',') }); + const data = (res.data || []) as DataflowAnnotationParamsRsp[]; + console.log('frame data', data); + return data; +} + +export async function getDataFile(id: string) { + const url = `${Api.DATA}/listByIds`; + let data = await get(url, { dataIds: id }); + data = data.data || []; + + const name = data[0]?.name || ''; + let configs = [] as IFileConfig[]; + data[0].content.forEach((config: any) => { + let file = config.files?.[0].file || config.file; + configs.push({ + name, + size: +file.size, + url: file.url, + deviceName: config.name, + }); + }); + + return { + config: configs[0], + datasetId: data[0].datasetId, + annotationStatus: data[0].annotationStatus, + validStatus: data[0].status, + }; +} + +export async function saveData(datasetId: string, dataInfos: Array) { + const url = `${Api.ANNOTATION}/data/save`; + + let data = await post(url, { datasetId, dataInfos }); + return (data.data || []) as ISaveResp[]; +} diff --git a/frontend/image-tool/src/businessNew/api/index.ts b/frontend/image-tool/src/businessNew/api/index.ts new file mode 100644 index 00000000..b0047fc4 --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/index.ts @@ -0,0 +1,5 @@ +export * from './type'; +export * from './common'; +export * from './class'; +export * from './model'; +export * from './data'; diff --git a/frontend/image-tool/src/businessNew/api/model.ts b/frontend/image-tool/src/businessNew/api/model.ts new file mode 100644 index 00000000..1e94be3e --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/model.ts @@ -0,0 +1,49 @@ +import { IModel, IModelClass } from 'image-editor'; +import { get, post } from './base'; +import { Api } from './type'; +/** + * model + */ +export async function getModelList() { + const url = `${Api.API}/model/list`; + const res = await get(url); + const data = res.data || []; + + const models = [] as IModel[]; + data.forEach((e: any) => { + if (e.datasetType !== 'IMAGE') return; + let classes = (e.classes || []) as IModelClass[]; + // COCO subClass + classes = classes + .map((item) => { + return item.subClasses || item; + }) + .flat(1) + .map((e: any) => { + return { ...e, label: e.name, value: e.code }; + }); + models.push({ + id: e.id + '', + name: e.name, + version: e.version, + code: e.modelCode, + classes, + isInteractive: e.isInteractive, + type: e.type, + }); + }); + + return models; +} + +export async function getModelResult(dataIds: string[], recordId: string) { + const url = `${Api.DATA}/modelAnnotationResult`; + const args: string[] = []; + dataIds.forEach((e) => args.push(`dataIds=${e}`)); + args.push(`serialNo=${recordId}`); + return await get(`${url}?${args.join('&')}`); +} +export async function runModel(config: any) { + const url = `${Api.DATA}/modelAnnotate`; + return await post(url, config); +} diff --git a/frontend/image-tool/src/businessNew/api/type.ts b/frontend/image-tool/src/businessNew/api/type.ts new file mode 100644 index 00000000..40dcc0c1 --- /dev/null +++ b/frontend/image-tool/src/businessNew/api/type.ts @@ -0,0 +1,31 @@ +import { LoadStatus } from 'image-editor'; +import { IObject } from '../types'; + +export enum Api { + API = '/api', + DATA = '/api/data', + ANNOTATION = '/api/annotate', +} +/** Model Result */ +export interface IModelResult { + recordId: string; + id: string; + version: string; + state?: LoadStatus; +} +/** dataflow annotations request */ +export interface DataflowAnnotationParamsReq { + dataIds: Array; +} +/** dataflow annotations response */ +export interface DataflowAnnotationParamsRsp { + classificationValues: any[]; + dataId: string; + objects: IObject[]; +} +export interface IFileConfig { + deviceName: string; + name: string; + size: number; + url: string; +} diff --git a/frontend/image-tool/src/businessNew/common/DataManager.ts b/frontend/image-tool/src/businessNew/common/DataManager.ts new file mode 100644 index 00000000..00b8224e --- /dev/null +++ b/frontend/image-tool/src/businessNew/common/DataManager.ts @@ -0,0 +1,137 @@ +import { + DataManager as BaseDataManager, + IFrame, + IModelRunningState, + LoadStatus, + ModelCodeEnum, + MsgType, + utils as EditorUtils, +} from 'image-editor'; +import Editor from './Editor'; +import * as api from '../api'; +import { IObject } from '../types'; + +interface IModelData { + objects?: any[]; + modelCode: ModelCodeEnum; +} + +export default class DataManager extends BaseDataManager { + declare editor: Editor; + modelMap: Map = new Map(); + + constructor(editor: Editor) { + super(editor); + this.editor = editor; + } + clearModelData() { + this.modelMap.clear(); + } + removeModelResult(frameId: string, code?: ModelCodeEnum) { + if (code) { + const id = EditorUtils.formatId(frameId, code); + this.modelMap.delete(id); + } else { + const ids: string[] = Array.from(this.modelMap.keys()); + ids.forEach((id) => { + if (id.indexOf(frameId) === 0) this.modelMap.delete(id); + }); + } + } + setModelResult(frameId: any, data: IModelData) { + const id = EditorUtils.formatId(frameId, data.modelCode); + this.modelMap.set(id, data); + } + getModelResult(code: ModelCodeEnum, frame?: IFrame) { + frame = frame || this.editor.getCurrentFrame(); + const id = EditorUtils.formatId(frame.id, code); + return this.modelMap.get(id) as IModelData; + } + hasModelResult(modelCode: ModelCodeEnum, frame?: IFrame) { + frame = frame || this.editor.getCurrentFrame(); + if (!frame) return false; + const result = this.getModelResult(modelCode, frame); + if (result) return true; + return false; + } + + async pollDataModelResult() { + const _this = this; + const editor = this.editor; + const { state } = this.editor; + const confidence = state.modelConfig.confidence || [0.5, 1]; + const modelMap = {} as Record; + + state.frames.forEach((frame) => { + if (frame.model && frame.model.state !== LoadStatus.COMPLETE) { + const id = frame.model.recordId; + modelMap[id] = modelMap[id] || []; + modelMap[id].push(frame); + } + }); + if (Object.keys(modelMap).length === 0) return; + + const requests = [] as Promise[]; + Object.keys(modelMap).forEach((recordId) => { + requests.push(createRequest(recordId, modelMap[recordId])); + }); + + await Promise.all(requests); + + setTimeout(this.pollDataModelResult.bind(this), 1000); + + function createRequest(recordId: string, dataList: IFrame[]) { + const ids = dataList.map((e) => e.id); + const request = api + .getModelResult(ids, recordId) + .then((data) => { + const { frameIndex, frames } = state; + const curData = frames[frameIndex]; + data = data.data || {}; + const resultList = data.modelDataResults; + if (!resultList) return; + + const modelCode = data.modelCode; + const resultMap = {} as Record; + resultList.forEach((e: any) => { + resultMap[e.dataId] = e; + }); + + dataList.forEach((frame) => { + const info = resultMap[frame.id]; + const model = (frame.model ?? { code: modelCode }) as IModelRunningState; + + if (info) { + const modelResult = info.modelResult; + if (modelResult.code != 'OK') { + frame.model = undefined; + if (frame.id === curData.id) { + editor.showMsg(MsgType.error, modelResult.message || 'Model Run Error'); + } + return; + } + let objects = (modelResult.objects || []) as IObject[]; + if (objects.length > 0) { + model.state = LoadStatus.COMPLETE; + objects = objects.filter( + (e: any) => e.confidence >= confidence[0] && e.confidence <= confidence[1], + ); + + _this.setModelResult(frame.id, { objects, modelCode }); + frame.model = model; + } else { + frame.model = undefined; + if (frame.id === curData.id) editor.showMsg(MsgType.warning, 'No Model Results.'); + } + } else { + frame.model = undefined; + if (frame.id === curData.id) editor.showMsg(MsgType.warning, 'No Model Results.'); + } + }); + }) + .catch(() => {}); + + return request; + } + } +} diff --git a/frontend/image-tool/src/businessNew/common/Editor.ts b/frontend/image-tool/src/businessNew/common/Editor.ts new file mode 100644 index 00000000..80de562b --- /dev/null +++ b/frontend/image-tool/src/businessNew/common/Editor.ts @@ -0,0 +1,175 @@ +import { + Editor as BaseEditor, + __ALL__ as ALL, + IFrame, + Event, + MsgType, + BSError, + AnnotateObject, + IModel, + DataTypeEnum, + LoadStatus, +} from 'image-editor'; +import { getDefault } from '../state'; +import { IBSState, ISaveResp } from '../types'; +import { ILocale, languages } from '@/locales'; +import * as utils from '../utils'; +import * as api from '../api'; + +import LoadManager from './LoadManager'; +import DataManager from './DataManager'; + +export default class Editor extends BaseEditor { + bsState: IBSState = getDefault(); + + loadManager: LoadManager; + dataManager: DataManager; + + constructor() { + super(); + this.loadManager = new LoadManager(this); + this.dataManager = new DataManager(this); + } + // locale common + lang(name: T, args?: Record | any[]) { + const data = this.i18n.lang(this.state.lang, name, args); + if (!data) { + return this.i18n.lang(languages.default, name, args); + } + return data; + } + frameChangeToggle(change: boolean, frame?: IFrame | IFrame[]) { + let frames = frame || this.state.frames; + if (!Array.isArray(frames)) frames = [frames]; + frames.forEach((frame) => { + frame.needSave = !!change; + }); + // this.trackManager.clearChangedTrack(); + } + needSave(frames?: IFrame[]) { + frames = frames || this.state.frames; + const needSaveData = frames.filter((e) => e.needSave); + return needSaveData.length > 0; + } + async save() { + const { bsState } = this; + if (bsState.doing.saving) return; + const saveFrames = this.state.frames.filter((e) => e.needSave); + if (saveFrames.length == 0) return; + + const { saveDatas } = utils.getDataFlowSaveData(this, saveFrames); + console.log('========> saveDataFlow saveDatas: ', saveDatas); + if (saveDatas.length === 0) return; + + bsState.doing.saving = true; + let saveResult; + try { + const res = await api.saveData(bsState.datasetId, saveDatas); + this.updateBackId(res); + this.frameChangeToggle(false, saveFrames); + this.showMsg(MsgType.success, this.lang('save-ok')); + saveResult = true; + } catch (e: any) { + this.showMsg(MsgType.error, this.lang('save-error')); + } + bsState.doing.saving = false; + this.emit(Event.SAVE_SUCCESS); + return saveResult; + } + updateBackId(data: ISaveResp[]) { + data.forEach((e) => { + const frame = this.getFrame(e.dataId + ''); + const obj = this.dataManager.getObject(e.frontId, frame); + obj && (obj.userData.backId = e.id); + }); + } + handleErr(err: BSError | Error | any, message: string = '') { + if (err instanceof BSError) { + utils.handleError(this, err); + } else { + utils.handleError(this, new BSError('', message, err)); + } + } + getRenderFilter() { + const { bsState, state } = this; + + // source + const sourceMap: any = {}; + bsState.activeSource.forEach((s) => { + sourceMap[s] = true; + }); + + // class + const classMap: any = {}; + bsState.filterClasses.forEach((e) => { + classMap[e] = true; + }); + + return (e: AnnotateObject) => { + const userData = this.getUserData(e); + const sourceId = userData.sourceId || state.defaultSourceId; + const classId = userData.classId || ''; + + const validSource = sourceMap[ALL] || sourceMap[sourceId]; + const validClass = classMap[ALL] || classMap[classId]; + + return !!(validSource && validClass); + }; + } + async runModel() { + const modelConfig = this.state.modelConfig; + if (!modelConfig.model) { + this.showMsg(MsgType.warning, 'Please choose Model'); + return; + } + const frame = this.getCurrentFrame(); + const model = this.state.models.find((e) => e.name === modelConfig.model) as IModel; + const resultFilterParam: Record = { + classes: model?.classes.map((e) => e.value), + }; + if (modelConfig.confidence[0]) resultFilterParam.minConfidence = modelConfig.confidence[0]; + if (modelConfig.confidence[1]) resultFilterParam.maxConfidence = modelConfig.confidence[1]; + if (!modelConfig.predict) { + const selectedClasses = Object.values(modelConfig.classes[modelConfig.model]).reduce( + (classes, item) => { + if (item.selected) { + classes.push(item.value); + } + return classes; + }, + [] as string[], + ); + if (selectedClasses.length <= 0) { + this.showMsg(MsgType.warning, 'Select at least one Class!'); + return; + } + resultFilterParam.classes = selectedClasses; + } + const config = { + datasetId: this.bsState.datasetId, + dataIds: [+frame.id], + modelId: +model.id, + modelVersion: model?.version, + dataType: DataTypeEnum.SINGLE_DATA, + modelCode: model.code, + resultFilterParam, + }; + + modelConfig.loading = true; + try { + let result = await api.runModel(config); + if (!result.data) throw new Error('Model Run Error'); + frame.model = { + recordId: result.data, + id: model.id, + version: model.version, + state: LoadStatus.LOADING, + code: model.code, + }; + } catch (error: any) { + this.showMsg(MsgType.error, error.message || 'Model Run Error'); + } + modelConfig.loading = false; + await this.dataManager.pollDataModelResult(); + } +} diff --git a/frontend/image-tool/src/businessNew/common/LoadManager.ts b/frontend/image-tool/src/businessNew/common/LoadManager.ts new file mode 100644 index 00000000..9aa2e437 --- /dev/null +++ b/frontend/image-tool/src/businessNew/common/LoadManager.ts @@ -0,0 +1,89 @@ +import Editor from './Editor'; +import * as api from '../api'; +import { + AnnotateModeEnum, + LoadManager as BaseLoadManager, + Event, + IDataResource, + IFrame, + SourceType, +} from 'image-editor'; +import { ShapeRoot } from 'image-editor/ImageView'; +import * as utils from '../utils'; + +export default class LoadManager extends BaseLoadManager { + declare editor: Editor; + constructor(editor: Editor) { + super(editor); + } + /** + * load frames data + */ + async loadFramesData(frames: IFrame[]) { + console.log('loadFramesData'); + this.editor.clearResource({ resetBgRotation: true }); + + // Filter out data that has already been loaded + const loadFrames = frames.filter((frame) => { + return !this.editor.dataManager.getFrameObject(frame.id); + }); + if (loadFrames.length === 0) return; + + const dataIds = loadFrames.map((e) => e.id); + const frameDatas = await api.getAnnotationByDataIds({ dataIds }); + if (!frameDatas || frameDatas.length === 0) return; + + const dataMap: Record = {}; + frameDatas.forEach((e) => (dataMap[`${e.dataId}`] = e)); + + loadFrames.forEach(async (frame) => { + const frameData = dataMap[String(frame.id)]; + if (!frameData) return; + // set result objects + const root_ins = new ShapeRoot({ frame, type: AnnotateModeEnum.INSTANCE }); + this.editor.dataManager.setFrameRoot(frame.id, [root_ins]); + const instances = frameData.objects || []; + const annotates_ins = utils.convertObject2Annotate(this.editor, instances); + this.editor.mainView.updateObjectByUserData(annotates_ins); + root_ins.addObjects(annotates_ins); + // set classifications + const values = frameData.classificationValues || []; + frame.classifications = utils.classificationAssign( + this.editor.bsState.classifications, + values, + ); + }); + + this.editor.emit(Event.ANNOTATIONS_LOADED, dataIds); + } + /** + * load result source + */ + async loadResultSource(frames: IFrame[]) { + frames.forEach(async (frame) => { + const frameId = String(frame.id); + let sources = await api.getResultSources(frameId); + sources.unshift({ + name: 'Without Task', + sourceId: this.editor.state.defaultSourceId, + sourceType: SourceType.DATA_FLOW, + frameId, + }); + this.editor.dataManager.setSources(frame, sources); + }); + } + /** + * request data resource + * This is a virtual method that needs to be instantiated + */ + async requestResource(frame: IFrame): Promise { + const { config, annotationStatus, validStatus } = await api.getDataFile(frame.id); + frame.annotationStatus = annotationStatus; + frame.validStatus = validStatus; + const data: IDataResource = { + ...config, + time: Date.now(), + }; + return data; + } +} diff --git a/frontend/image-tool/src/businessNew/components/Collapse/index.vue b/frontend/image-tool/src/businessNew/components/Collapse/index.vue new file mode 100644 index 00000000..be64a3a4 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Collapse/index.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Header/components/ActionBtn.vue b/frontend/image-tool/src/businessNew/components/Header/components/ActionBtn.vue new file mode 100644 index 00000000..8ab12fb4 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/components/ActionBtn.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Header/components/Close.vue b/frontend/image-tool/src/businessNew/components/Header/components/Close.vue new file mode 100644 index 00000000..cd8b23ec --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/components/Close.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Header/components/Flowindex.vue b/frontend/image-tool/src/businessNew/components/Header/components/Flowindex.vue new file mode 100644 index 00000000..ee69229b --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/components/Flowindex.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Header/components/InfoData.vue b/frontend/image-tool/src/businessNew/components/Header/components/InfoData.vue new file mode 100644 index 00000000..b5b792d6 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/components/InfoData.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Header/index.vue b/frontend/image-tool/src/businessNew/components/Header/index.vue new file mode 100644 index 00000000..19007961 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/index.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Header/type.ts b/frontend/image-tool/src/businessNew/components/Header/type.ts new file mode 100644 index 00000000..4bc4eb02 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/type.ts @@ -0,0 +1,55 @@ +export interface IDataInfo { + name: string; + sceneName: string; + details: { label: string; value: IDataInfoItem[] }[]; +} +export interface IDataInfoItem { + label: string; + value: string | number; +} +export interface IStage { + id: string; + name: string; + type: string; +} +export interface IStageInfo { + stageName: string; + stageType: string; + stages: IStage[]; +} +export interface IFlowIndex { + total: number; + currentIndex: number; +} +export interface IFlowIndexCallback { + index: number; +} +export interface IStatus { + // visible + showTaskTimer?: boolean; + showWorkflow?: boolean; + showSave?: boolean; + showReject?: boolean; + showModify?: boolean; + showSubmit?: boolean; + showRevise?: boolean; + showSuspend?: boolean; + // doing + saving: boolean; + rejecting: boolean; + submitting: boolean; + suspending: boolean; + isModify: boolean; +} +export enum TimeStatus { + PAUSED = 'PAUSED', + WORKING = 'WORKING', + TIMEOUT = 'TIMEOUT', + DONE = 'DONE', +} +export interface ITimeInfo { + status?: TimeStatus; + updateTm: number; + remainTm: number; + pausedTm?: number; +} diff --git a/frontend/image-tool/src/businessNew/components/Header/useDataInfo.ts b/frontend/image-tool/src/businessNew/components/Header/useDataInfo.ts new file mode 100644 index 00000000..1847b7a8 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/useDataInfo.ts @@ -0,0 +1,43 @@ +import { reactive } from 'vue'; +import { useInjectBSEditor } from '../../context'; +import { divide, round } from 'lodash'; + +interface IDetails { + label: string; + value: any; +} +export default function useDataInfo() { + const dataInfo = reactive({ name: '', sceneName: '', details: [] as IDetails[] }); + const editor = useInjectBSEditor(); + const updateDataInfo = async () => { + let name: string = ''; + const details: IDetails[] = []; + const frame = editor.getCurrentFrame(); + if (frame) { + const dataName = frame.imageData?.name || ''; + if (!name) name = dataName; + details.push({ + label: editor.lang('titleFrame'), + value: [ + { label: editor.lang('Data ID'), value: frame.id }, + { label: editor.lang('Data Name'), value: dataName }, + { + label: editor.lang('Data Size'), + value: round(divide(frame.imageData?.size || 0, 1024 * 1024), 2) + 'MB', + }, + { + label: editor.lang('Width x height'), + value: `${editor.mainView.backgroundWidth} x ${editor.mainView.backgroundHeight}`, + }, + ], + }); + } + dataInfo.name = name || '...'; + dataInfo.sceneName = frame.imageData?.name || ''; + dataInfo.details = details; + }; + return { + dataInfo, + updateDataInfo, + }; +} diff --git a/frontend/image-tool/src/businessNew/components/Header/useFlowIndex.ts b/frontend/image-tool/src/businessNew/components/Header/useFlowIndex.ts new file mode 100644 index 00000000..9d12cee4 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/useFlowIndex.ts @@ -0,0 +1,47 @@ +import { computed } from 'vue'; +import { useInjectBSEditor } from '../../context'; +import Event from '../../configs/event'; + +export default function useFlowIndex() { + const editor = useInjectBSEditor(); + const { state } = editor; + + const indexInfo = computed(() => { + let total = 1; + let currentIndex = 1; + if (state.isSeriesFrame) { + total = state.sceneIds.length; + currentIndex = state.sceneIndex; + } else { + total = state.frames.length; + currentIndex = state.frameIndex; + } + return { + total, + currentIndex, + }; + }); + + async function onIndex(args: { index: number }) { + const { index } = args; + const { frames } = editor.state; + if (state.isSeriesFrame) { + const { sceneIds, sceneIndex } = state; + if (index < 0 || index >= sceneIds.length) return; + const needSave = frames.findIndex((e) => e.needSave) !== -1; + if (needSave) { + const result = await editor.save(); + if (!result) return; + } + await editor.loadManager.loadSceneData(index); + editor.emit(Event.SCENE_CHANGE, { preScene: sceneIndex, newScene: index }); + } else { + if (index < 0 || index >= frames.length) return; + editor.switchFrame(index); + } + } + return { + indexInfo, + onIndex, + }; +} diff --git a/frontend/image-tool/src/businessNew/components/Header/useHeader.ts b/frontend/image-tool/src/businessNew/components/Header/useHeader.ts new file mode 100644 index 00000000..0bd48409 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Header/useHeader.ts @@ -0,0 +1,172 @@ +import { useInjectBSEditor } from '../../context'; +import Event from '../../configs/event'; +import * as api from '../../api'; +import { AnnotateStatus, FlowAction, ValidStatus } from '../../types'; +import { computed } from 'vue'; +import { MsgType } from 'image-editor'; +import { ButtonOk } from '../../configs/ui'; +import { closeTab } from '../../utils'; + +export default function useHeader() { + // const { loadClaimRecord } = useLoadState(); + const editor = useInjectBSEditor(); + const { bsState, state } = editor; + + const validStatus = computed(() => { + const { frames, frameIndex } = state; + const frame = frames[frameIndex]; + return frame?.validStatus == ValidStatus.vaild; + }); + const annotateStatus = computed(() => { + const { frames, frameIndex } = state; + const frame = frames[frameIndex]; + return frame?.annotationStatus == AnnotateStatus.annotated; + }); + + function onHelp() { + editor + .showModal('HotkeyHelp', { title: editor.lang('titleHelp'), width: 1000 }) + .catch(() => {}); + } + async function updateDataStatus() { + const frame = editor.getCurrentFrame(); + const res = await api.getDataStatusByIds(frame.id); + frame.validStatus = res.status; + frame.annotationStatus = res.annotationStatus; + } + async function markVaild() { + if (bsState.doing.marking) return; + bsState.doing.marking = true; + const frame = editor.getCurrentFrame(); + try { + if (validStatus.value) { + await api.setInvalid(frame.id); + } else { + await api.setValid(frame.id); + } + await updateDataStatus(); + } catch (error) { + console.log(error); + } + bsState.doing.marking = false; + } + async function submitReq(id: string) { + try { + await api.submit(id); + await updateDataStatus(); + } catch (error) { + console.log(error); + } + } + async function onSubmit() { + if (bsState.doing.submitting) return; + bsState.doing.submitting = true; + const frame = editor.getCurrentFrame(); + const hasAnnotated = annotateStatus.value || frame.needSave; + await editor.save(); + if (validStatus.value && !hasAnnotated) { + await editor + .showConfirm({ + title: editor.lang('Reminder'), + subTitle: editor.lang('submitTips'), + okText: editor.lang('btnSubmit'), + }) + .then( + async () => { + await submitReq(frame.id); + await switchData(); + }, + () => {}, + ); + } else { + await submitReq(frame.id); + await switchData(); + } + bsState.doing.submitting = false; + + async function switchData() { + const { frameIndex, frames } = editor.state; + if (frameIndex < frames.length - 1) { + editor.loadFrame(frameIndex + 1); + } else { + const notAnnotateIndex = frames.findIndex((item) => { + return item.annotationStatus == AnnotateStatus.unannotated; + }); + if (notAnnotateIndex != -1) { + editor.loadFrame(notAnnotateIndex); + } else { + // All Annotated + await editor + .showModal('confirm', { + title: editor.lang('Reminder'), + data: { + content: editor.lang('Well Done!'), + subContent: editor.lang('You have finish all the annotation!'), + buttons: [{ ...ButtonOk, content: editor.lang('Close and release those data') }], + }, + }) + .then( + async () => { + await api.unlockRecord(bsState.recordId); + closeTab(); + }, + () => {}, + ); + } + } + } + } + async function onSkip() { + if (bsState.doing.skip) return; + bsState.doing.skip = true; + if (editor.needSave()) await editor.save(); + + const { frames, frameIndex } = state; + if (frameIndex < frames.length - 1) { + editor.loadFrame(frameIndex + 1); + } else { + editor.showMsg(MsgType.info, editor.lang('This is last data')); + } + bsState.doing.skip = false; + } + async function onViewToAnnotate() { + if (bsState.doing.modify) return; + bsState.doing.modify = true; + + const frame = editor.getCurrentFrame(); + try { + const res = await api.takeRecordByData({ + datasetId: bsState.datasetId, + dataIds: [frame.id], + dataType: 'SINGLE_DATA', + }); + console.log(res); + + const { origin, pathname } = window.location; + window.location.href = `${origin}${pathname}?recordId=${res.data}`; + } catch (error) { + // DATASET_DATA_EXIST_ANNOTATE + console.log(error); + editor.showMsg( + MsgType.warning, + `Fail to modify data because it is being annotated by others`, + ); + } + + bsState.doing.modify = false; + } + function onFlowAction(action: FlowAction) { + editor.emit(Event.FLOW_ACTION, action); + } + + return { + onHelp, + markVaild, + onSubmit, + onSkip, + onViewToAnnotate, + onFlowAction, + validStatus, + annotateStatus, + }; +} diff --git a/frontend/image-tool/src/businessNew/components/Layout/index.vue b/frontend/image-tool/src/businessNew/components/Layout/index.vue new file mode 100644 index 00000000..1b189c81 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Layout/index.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Mask/index.vue b/frontend/image-tool/src/businessNew/components/Mask/index.vue new file mode 100644 index 00000000..235a5e5b --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Mask/index.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Modal/Confirm.vue b/frontend/image-tool/src/businessNew/components/Modal/Confirm.vue new file mode 100644 index 00000000..8c43291f --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Modal/Confirm.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/HotkeyItem.vue b/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/HotkeyItem.vue new file mode 100644 index 00000000..57a7e286 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/HotkeyItem.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/index.ts b/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/index.ts new file mode 100644 index 00000000..4882df16 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/index.ts @@ -0,0 +1,148 @@ +import { AnnotateModeEnum } from 'image-editor'; + +const showCtrl = 'Ctrl/⌘'; +const Tags_Instance = [AnnotateModeEnum.INSTANCE]; +// const Tags_Segment = [AnnotateModeEnum.SEGMENTATION]; +const Tags_All = [AnnotateModeEnum.INSTANCE, AnnotateModeEnum.SEGMENTATION]; + +export interface IKeyboardConfig { + label: string; + tips: string; + keys: string[]; + otherKeys?: string[]; + tags: AnnotateModeEnum[]; + wrap?: boolean; +} + +export const dataConfig: IKeyboardConfig[] = [ + { + label: 'Page Up', + tips: '', + keys: ['Page Up'], + tags: Tags_All, + }, + { + label: 'Page Down', + tips: '', + keys: ['Page Down'], + tags: Tags_All, + }, +]; +export const instanceConfig: IKeyboardConfig[] = [ + { + label: 'Save', + tips: '', + keys: [showCtrl, 'S'], + tags: Tags_All, + }, + { + label: 'Delete instance/point', + tips: '', + keys: ['Delete'], + tags: Tags_Instance, + }, + { + label: 'Finish drawing', + tips: '', + keys: ['Space'], + otherKeys: ['Enter'], + tags: Tags_All, + }, + { + label: 'Show/hide tag pad', + tips: '', + keys: ['T'], + tags: Tags_All, + }, + { + label: 'Move the object 1px', + tips: '', + keys: [showCtrl, 'Shift', '↑↓←→'], + tags: Tags_Instance, + }, + { + label: 'Undo', + tips: '', + keys: [showCtrl, 'Z'], + tags: Tags_All, + }, + { + label: 'Redo', + tips: '', + keys: [showCtrl, 'Shift', 'Z'], + tags: Tags_All, + }, + { + label: 'Hollow', + tips: '', + keys: ['H'], + tags: Tags_Instance, + }, + { + label: 'Crop', + tips: '', + keys: ['C'], + tags: Tags_Instance, + }, + { + label: 'Horizontal Drawing Model', + tips: '', + keys: ['A'], + tags: Tags_Instance, + }, + { + label: 'Vertical Drawing Model', + tips: '', + keys: [showCtrl, 'A'], + tags: Tags_Instance, + }, +]; +export const resultConfig: IKeyboardConfig[] = [ + { + label: 'Show Class', + tips: '', + keys: ['M'], + tags: Tags_All, + }, + { + label: 'Show Size', + tips: '', + keys: ['B'], + tags: Tags_Instance, + }, + { + label: 'Show annotation sequence', + tips: '', + keys: ['D'], + tags: Tags_Instance, + }, +]; +export const imageConfig: IKeyboardConfig[] = [ + { + label: 'Zoom in、Zoom Out', + tips: '', + keys: ['wheel'], + tags: Tags_All, + }, + { + label: 'Drag', + tips: '', + keys: ['RightClick'], + tags: Tags_All, + }, +]; +// 其它 +export const elseConfig: IKeyboardConfig[] = [ + { + label: 'Cancel window', + tips: '', + keys: ['Esc'], + tags: Tags_All, + }, + { + label: 'Show Auxiliary Line', + tips: '', + keys: ['Y'], + tags: Tags_All, + }, +]; diff --git a/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/index.vue b/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/index.vue new file mode 100644 index 00000000..d32bbf4f --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Modal/HotkeyHelp/index.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Classification/AttrValue.vue b/frontend/image-tool/src/businessNew/components/Operation/Classification/AttrValue.vue new file mode 100644 index 00000000..d466ee83 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Classification/AttrValue.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Classification/index.vue b/frontend/image-tool/src/businessNew/components/Operation/Classification/index.vue new file mode 100644 index 00000000..02f34f6a --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Classification/index.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/Header.vue b/frontend/image-tool/src/businessNew/components/Operation/Results/Header.vue new file mode 100644 index 00000000..37ff691e --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/Header.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/ResultList.vue b/frontend/image-tool/src/businessNew/components/Operation/Results/ResultList.vue new file mode 100644 index 00000000..5f91a49d --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/ResultList.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/components/Filter.vue b/frontend/image-tool/src/businessNew/components/Operation/Results/components/Filter.vue new file mode 100644 index 00000000..a1669197 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/components/Filter.vue @@ -0,0 +1,106 @@ + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/components/Item.vue b/frontend/image-tool/src/businessNew/components/Operation/Results/components/Item.vue new file mode 100644 index 00000000..c437e0a7 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/components/Item.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/components/Setting.vue b/frontend/image-tool/src/businessNew/components/Operation/Results/components/Setting.vue new file mode 100644 index 00000000..c80d0cc7 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/components/Setting.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/context.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/context.ts new file mode 100644 index 00000000..1d75c32a --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/context.ts @@ -0,0 +1,12 @@ +import { inject, provide } from 'vue'; +import { IContext } from './type'; + +const context = Symbol('instance-context'); + +export function useResultsInject() { + return inject(context) as IContext; +} + +export function useResultsProvide(value: IContext) { + provide(context, value); +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/index.vue b/frontend/image-tool/src/businessNew/components/Operation/Results/index.vue new file mode 100644 index 00000000..4dcd2356 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/index.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/type.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/type.ts new file mode 100644 index 00000000..3447ebb8 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/type.ts @@ -0,0 +1,50 @@ +import { IFrame, ToolType } from 'image-editor'; + +export interface IItem { + key: string; + id: string; + name: string; + classType: string; + classId: string; + toolType: ToolType | ''; // todo + visible?: boolean; + isModel?: boolean; +} +export interface IObjectItem extends IItem { + trackId: string; + trackName: string; + frame?: IFrame; + attrLabel?: any; + sizeLabel?: string; + infoLabel?: string; + trueValue?: boolean; +} +export interface IClassItem extends IItem { + data: IObjectItem[]; + alias?: string; + color: string; +} +export interface IState { + objectN: number; + list: IClassItem[]; + updateListFlag: boolean; + updateSelectFlag: boolean; + selectMap: Record; + activeClass: string[]; +} +export enum IAction { + edit = 'edit', + click = 'click', + toggleVisible = 'toggleVisible', + delete = 'delete', +} + +export interface IHandler { + onAction: (action: IAction, ...args: any[]) => void; +} +export interface IContext { + resultState: IState; + onUpdateList: () => void; + itemHandler: IHandler; + classHandler: IHandler; +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/useClassItem.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/useClassItem.ts new file mode 100644 index 00000000..323d9eac --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/useClassItem.ts @@ -0,0 +1,73 @@ +import { useInjectEditor } from 'image-ui/context'; +import { IAction, IClassItem, IObjectItem } from './type'; +import { AnnotateObject, Event } from 'image-editor'; + +export default function useClassItem() { + const editor = useInjectEditor(); + + function onEdit(item: IClassItem) { + const objects = getObjects(item.data); + if (objects.length > 0) { + editor.selectObject(objects); + editor.emit(Event.SHOW_CLASS_INFO, objects); + } + } + function onDelete(item: IClassItem) { + const objects = getObjects(item.data); + if (objects.length > 0) { + editor + .showConfirm({ + title: editor.lang('delete-title'), + subTitle: editor.lang('delete-object'), + okText: editor.lang('delete'), + cancelText: editor.lang('cancel'), + okDanger: true, + }) + .then( + () => { + editor.cmdManager.withGroup(() => { + editor.cmdManager.execute('delete-object', objects); + }); + editor.emit(Event.ANNOTATE_HANDLE_DELETE, { objects, type: 2 }); + }, + () => {}, + ); + } + } + function onToggleVisible(item: IClassItem) { + const objects = getObjects(item.data); + if (objects.length > 0) { + const visible = !item.visible; + editor.dataManager.setAnnotatesVisible(objects, visible); + } + } + + function getObjects(items: IObjectItem[]) { + const frame = editor.getCurrentFrame(); + return items + .filter((e) => frame && e.frame?.id === frame.id) + .map((e) => editor.dataManager.getObject(e.id)) + .filter((e) => e) as AnnotateObject[]; + } + function onAction(action: IAction, item: IClassItem, args: any) { + console.log('item action', action); + switch (action) { + case IAction.edit: + onEdit(item); + break; + case IAction.delete: + onDelete(item); + break; + case IAction.toggleVisible: + onToggleVisible(item); + break; + case IAction.click: + // onClick(item); + break; + } + } + + return { + onAction, + }; +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/useList.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/useList.ts new file mode 100644 index 00000000..9047f646 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/useList.ts @@ -0,0 +1,181 @@ +import { Ref } from 'vue'; +import { IClassItem, IObjectItem, IState } from './type'; +import { useInjectBSEditor } from '../../../context'; +import { debounce } from 'lodash'; +import { IFrame, IUserData } from 'image-editor'; +import { getObjectInfo } from './utils'; + +export const animation = { + onEnter(node: any, done: any) {}, + onLeave(node: any, done: any) { + done(); + }, +}; + +export default function useList(state: IState, domRef: Ref) { + const editor = useInjectBSEditor(); + + /** list scroll */ + const scrollSelectToView = debounce(() => { + if (!domRef.value) return; + + const item = domRef.value.querySelector('.result-data-list .list > .item.active'); + if (item) { + if (!isSelectVisible(item as any, domRef.value)) { + item.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); + } + } + }, 100); + function isSelectVisible(dom: HTMLDivElement, parent: HTMLDivElement) { + const parentBox = parent.getBoundingClientRect(); + const domBox = dom.getBoundingClientRect(); + + if (domBox.y + domBox.height > parentBox.y + parentBox.height || domBox.y < parentBox.y) + return false; + return true; + } + function getClassInfo(userData: IUserData) { + let className = ''; + let classId = ''; + let isModel = false; + if (!userData.classId && userData.modelClass) { + isModel = true; + classId = '__Model__##' + userData.modelClass; + className = userData.modelClass; + } else { + classId = userData.classId || ''; + const classConfig = editor.getClassType(classId); + className = classConfig ? classConfig.name : ''; + } + return { className, isModel, classId }; + } + function getClassList() { + const classMap: Record = {}; + const classTypes = editor.state.classTypes; + classTypes.forEach((config) => { + const classItem: IClassItem = { + data: [], + alias: config.alias, + color: config.color || '#ffffff', + id: config.id, + name: config.name, + classType: config.name, + classId: config.id, + toolType: config.toolType, + key: config.id, + }; + classMap[config.id] = classItem; + }); + return classMap; + } + + const update = debounce(() => { + if (state.updateListFlag) { + state.updateSelectFlag = true; + updateList(); + } + if (state.updateSelectFlag) { + updateSelect(); + } + scrollSelectToView(); + }, 100); + function onUpdateList() { + state.updateListFlag = true; + update(); + } + function onSelect() { + state.updateSelectFlag = true; + update(); + } + + function updateList() { + const isShowAll = false; + let frames: IFrame[] = []; + if (isShowAll) frames = editor.state.frames; + else frames = [editor.getCurrentFrame()]; + const filter = editor.getRenderFilter(); + let objN = 0; + + // class items + const classMap = getClassList(); + // object items + frames.forEach((f) => { + let objs = editor.dataManager.getFrameObject(f.id) || []; + objs = objs.filter((e) => filter(e)); + objs.forEach((obj) => { + const userData = editor.getUserData(obj); + const trackName = userData.trackName || ''; + const trackId = userData.trackId || ''; + const { isModel, classId, className } = getClassInfo(userData); + const classConfig = editor.getClassType(classId); + const toolType = classConfig ? classConfig.toolType : obj.toolType; + let classItem = classMap[classId]; + if (!classItem) { + classItem = { + data: [], + alias: classConfig?.label, + color: 'rgb(252, 177, 122)', + id: classId, + name: classId ? className : editor.lang('Class Required'), + classType: className, + classId: classId, + toolType, + isModel: isModel, + visible: false, + key: classId, + }; + classMap[classId] = classItem; + } + const objItem: IObjectItem = { + id: obj.uuid, + trackId, + trackName, + name: trackName, + classType: classConfig?.name || '', + classId, + toolType, + visible: obj.showVisible, + isModel: false, + frame: obj.frame, + key: obj.uuid + Date.now(), + }; + objItem.attrLabel = classConfig ? editor.getValidAttrs(userData) : ''; + const infos = getObjectInfo(obj); + objItem.sizeLabel = infos.join(' | '); + classItem.data.push(objItem); + objN++; + if (obj.showVisible) classItem.visible = true; + }); + }); + const list: IClassItem[] = []; + Object.values(classMap).forEach((e) => { + if (e.data.length === 0) return; + if (e.id === '') list.unshift(e); + else list.push(e); + }); + state.list = list; + state.objectN = objN; + console.log('=======>>>>>>>>> result state', state); + } + function updateSelect() { + console.log('updateSelect'); + const selection = editor.selection; + state.selectMap = {}; + if (selection.length === 0) return; + selection.forEach((object) => { + state.selectMap[object.uuid] = true; + state.updateSelectFlag = false; + const userData = editor.getUserData(object); + const { classId } = getClassInfo(userData); + if (state.activeClass.indexOf(classId) < 0) { + state.activeClass = [...state.activeClass, classId]; + } + }); + } + + return { + update, + onUpdateList, + onSelect, + }; +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/useObjectItem.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/useObjectItem.ts new file mode 100644 index 00000000..9683d17c --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/useObjectItem.ts @@ -0,0 +1,76 @@ +import { useInjectEditor } from 'image-ui/context'; +import { IAction, IObjectItem } from './type'; +import { Event } from 'image-editor'; + +export default function useObjectItem() { + const editor = useInjectEditor(); + + function onEdit(item: IObjectItem) { + const find = editor.dataManager.getObject(item.id); + if (find) { + editor.selectObject(find); + editor.emit(Event.SHOW_CLASS_INFO, find); + } + } + function onDelete(item: IObjectItem) { + const find = editor.dataManager.getObject(item.id); + if (!find) return; + editor + .showConfirm({ + title: editor.lang('delete-title'), + subTitle: editor.lang('delete-object'), + okText: editor.lang('delete'), + cancelText: editor.lang('cancel'), + okDanger: true, + }) + .then( + () => { + find && editor.cmdManager.execute('delete-object', find); + editor.emit(Event.ANNOTATE_HANDLE_DELETE, { objects: [find], type: 2 }); + }, + () => {}, + ); + } + function onToggleVisible(item: IObjectItem) { + const find = editor.dataManager.getObject(item.id); + if (find) { + const visible = !find.showVisible; + editor.dataManager.setAnnotatesVisible(find, visible); + } + } + function onClick(item: IObjectItem, mult?: boolean) { + if (!item.id) return; + + const curFrame = editor.getCurrentFrame(); + const find = editor.dataManager.getObject(item.id, item.frame || curFrame); + if (!find) return; + if (mult) { + if (find.state?.select) { + editor.selectObject(editor.selection.filter((e) => e.uuid !== find.uuid)); + } else { + editor.selectObject([find, ...editor.selection]); + } + } else { + editor.selectObject(find); + } + } + function onAction(action: IAction, item: IObjectItem, args: any) { + console.log('item action', action); + switch (action) { + case IAction.edit: + onEdit(item); + break; + case IAction.delete: + onDelete(item); + break; + case IAction.toggleVisible: + onToggleVisible(item); + break; + case IAction.click: + onClick(item, args); + break; + } + } + + return { onAction }; +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/useResults.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/useResults.ts new file mode 100644 index 00000000..b2edb605 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/useResults.ts @@ -0,0 +1,33 @@ +import { reactive, ref } from 'vue'; +import { useInjectBSEditor } from '../../../context'; +import { IState } from './type'; +import vueEvent from 'image-ui/vueEvent'; +import { Event } from 'image-editor'; +import useList from './useList'; + +export default function useResults() { + const editor = useInjectBSEditor(); + + const domRef = ref(); + const resultState = reactive({ + list: [], + objectN: 0, + updateListFlag: true, + updateSelectFlag: true, + selectMap: {}, + activeClass: [], + }); + const { onSelect, onUpdateList, update } = useList(resultState, domRef); + + // *****hook****** + vueEvent(editor, Event.FRAME_CHANGE, onUpdateList); + vueEvent(editor, Event.ANNOTATE_ADD, onUpdateList); + // vueEvent(editor, Event.ANNOTATE_LOAD, onUpdateList); + vueEvent(editor, Event.ANNOTATE_REMOVE, onUpdateList); + // vueEvent(editor, Event.TRACK_OBJECT_CHANGE, onUpdateList); + vueEvent(editor, Event.ANNOTATE_CHANGE, onUpdateList); + vueEvent(editor, Event.ANNOTATE_VISIBLE, onUpdateList); + vueEvent(editor, Event.SELECT, onSelect); + + return { resultState, domRef, onUpdateList }; +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/Results/utils.ts b/frontend/image-tool/src/businessNew/components/Operation/Results/utils.ts new file mode 100644 index 00000000..160e4375 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/Results/utils.ts @@ -0,0 +1,30 @@ +import { AnnotateObject, Rect, Polygon, Line } from 'image-editor'; + +export function getObjectInfo(object: AnnotateObject) { + const infos = [] as string[]; + if (object instanceof Rect) { + const { width, height, rotation = 0 } = object.attrs; + infos.push(`width: ${format(width)}`); + infos.push(`height: ${format(height)}`); + infos.push(`area: ${format(width * height)}`); + infos.push(`W/H: ${format(width / height, 2)}`); + infos.push(`rotation: ${formatRotation(rotation)}`); + } else if (object instanceof Polygon) { + const area = object.getArea(); + infos.push(`area: ${format(area)}`); + } else if (object instanceof Line) { + const length = object.getLength(); + infos.push(`length: ${format(length)}`); + } + return infos; +} + +function format(v: number, precision: number = 0): string { + return Math.abs(v).toFixed(precision); +} + +function formatRotation(r: number, precision: number = 1): string { + let angle = r % 360; + angle = angle < 0 ? 360 + angle : angle; + return angle.toFixed(precision); +} diff --git a/frontend/image-tool/src/businessNew/components/Operation/SourceTab/index.vue b/frontend/image-tool/src/businessNew/components/Operation/SourceTab/index.vue new file mode 100644 index 00000000..a709e89c --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/SourceTab/index.vue @@ -0,0 +1,162 @@ + + + diff --git a/frontend/image-tool/src/businessNew/components/Operation/index.vue b/frontend/image-tool/src/businessNew/components/Operation/index.vue new file mode 100644 index 00000000..69ba85a0 --- /dev/null +++ b/frontend/image-tool/src/businessNew/components/Operation/index.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/image-tool/src/businessNew/configs/action.ts b/frontend/image-tool/src/businessNew/configs/action.ts new file mode 100644 index 00000000..7b62532c --- /dev/null +++ b/frontend/image-tool/src/businessNew/configs/action.ts @@ -0,0 +1,38 @@ +import * as BsActions from '../actions'; +import { IActionName, AllActions as baseActions } from 'image-editor'; + +export type IBsActionName = keyof typeof BsActions | IActionName; +export const allActions = [...baseActions, ...Object.keys(BsActions)] as IBsActionName[]; + +export const defaultActions: IBsActionName[] = [ + 'dataFrameNext', + 'dataFrameLast', + 'toggleClassView', + 'toggleClassTitle', + // 'toggleShowPolygonArrow', + 'toggleHelpLine', + 'selectTool', + 'changeTool', + 'toNextFrame', + 'previousFrame', +]; + +export const baseEditActions: IBsActionName[] = [ + ...defaultActions, + 'redo', + 'undo', + 'cutSelectionOther', + 'cutSelectionFirst', + 'deleteSelection', + 'adjustObject', + 'drawTool', + 'holeSelection', + 'removeHoleSelection', + 'stopCurrentDraw', + // 'copyToNext', + // 'copyToLast', + // 'toggleSizeInfo', + 'lineToolDrawMode', +]; + +export const baseViewActions: IBsActionName[] = [...defaultActions]; diff --git a/frontend/image-tool/src/businessNew/configs/event.ts b/frontend/image-tool/src/businessNew/configs/event.ts new file mode 100644 index 00000000..274d4472 --- /dev/null +++ b/frontend/image-tool/src/businessNew/configs/event.ts @@ -0,0 +1,10 @@ +const Event = { + BUSINESS_INIT: 'bussiness_init', + FLOW_ACTION: 'flow_action', + SCENE_CHANGE: 'scene_change', + SCENE_LOADED: 'scene_loaded', + + ERROR_CONSOLE: 'error_console', +}; + +export default Event; diff --git a/frontend/image-tool/src/businessNew/configs/hotkey.ts b/frontend/image-tool/src/businessNew/configs/hotkey.ts new file mode 100644 index 00000000..b4fe22ff --- /dev/null +++ b/frontend/image-tool/src/businessNew/configs/hotkey.ts @@ -0,0 +1,12 @@ +import { IHotkeyConfig, userAgent } from 'image-editor'; +import { IBsActionName } from './action'; + +const isMac = userAgent.isMac; +const Ctrl = isMac ? '⌘' : 'ctrl'; +const hotkeyConfig: IHotkeyConfig[] = [ + { key: `${Ctrl}+s`, action: 'flowSave' }, + { key: 'PageUp', action: 'dataFrameLast' }, + { key: 'PageDown', action: 'dataFrameNext' }, +]; + +export default hotkeyConfig; diff --git a/frontend/image-tool/src/businessNew/configs/mode.ts b/frontend/image-tool/src/businessNew/configs/mode.ts new file mode 100644 index 00000000..797f109d --- /dev/null +++ b/frontend/image-tool/src/businessNew/configs/mode.ts @@ -0,0 +1,51 @@ +import { IModeConfig, OPType } from 'image-editor'; +import { IBsUIType, BsUIType, baseEditUI, baseViewUI, allUI } from './ui'; +import { IBsActionName, baseEditActions, baseViewActions, allActions } from './action'; + +function toMap(arr: T[]) { + const map = {} as Record; + arr.forEach((e) => (map[e] = true)); + return map; +} + +/** + * all + */ +// all +const all: IModeConfig = { + name: 'all', + op: OPType.EDIT, + ui: toMap(allUI), // executeUI + actions: toMap(allActions), // +}; + +/** + * data flow + */ +// execute +const execute: IModeConfig = { + name: 'execute', + op: OPType.EDIT, + ui: toMap([...baseEditUI, BsUIType.flowSave, BsUIType.markValid, BsUIType.skip, BsUIType.submit]), + actions: toMap([...baseEditActions, 'flowSave']), // +}; + +// view +const view: IModeConfig = { + name: 'view', + op: OPType.VIEW, + ui: toMap([...baseViewUI, BsUIType.modify]), + actions: toMap(baseViewActions), +}; + +const pageModes = { + all, + execute, + view, +}; + +export type BsModeType = keyof typeof pageModes; + +(window as any).pageModes = pageModes; + +export default pageModes; diff --git a/frontend/image-tool/src/businessNew/configs/ui.ts b/frontend/image-tool/src/businessNew/configs/ui.ts new file mode 100644 index 00000000..e3b8ac5d --- /dev/null +++ b/frontend/image-tool/src/businessNew/configs/ui.ts @@ -0,0 +1,70 @@ +import { UIType } from 'image-editor'; +import { ButtonProps } from 'ant-design-vue'; + +export interface IConfirmBtn extends ButtonProps { + action: string; + content: string; + class?: string; +} +export interface IConfirmProps { + content?: string; + subContent?: string; + buttons: IConfirmBtn[]; +} + +export const BsUIType = { + ...UIType, + flowSave: 'flowSave', + markValid: 'markValid', + skip: 'skip', + submit: 'submit', + modify: 'modify', +}; + +export type IBsUIType = keyof typeof BsUIType; +export const allUI = Object.keys(BsUIType) as IBsUIType[]; + +export const baseViewUI = [BsUIType.setting, BsUIType.info] as IBsUIType[]; + +export const baseEditUI = [ + ...baseViewUI, + BsUIType.edit, + BsUIType.tool_rect, + BsUIType.tool_polygon, + BsUIType.tool_line, + BsUIType.tool_keyPoint, + BsUIType.model, +] as IBsUIType[]; + +export const ButtonCancel: IConfirmBtn = { + type: 'primary', + action: 'cancel', + content: 'Cancel', + class: 'ghost-white', + ghost: true, +}; + +export const ButtonDiscard: IConfirmBtn = { + type: 'primary', + action: 'discard', + content: 'Discard', + class: 'ghost-red', + ghost: true, +}; + +export const ButtonSave: IConfirmBtn = { + type: 'primary', + action: 'save', + content: 'Save', +}; + +export const ButtonOk: IConfirmBtn = { + type: 'primary', + action: 'ok', + content: 'Ok', +}; +export const ButtonRefresh: IConfirmBtn = { + type: 'primary', + action: 'refresh', + content: 'Refresh', +}; diff --git a/frontend/image-tool/src/businessNew/context.ts b/frontend/image-tool/src/businessNew/context.ts new file mode 100644 index 00000000..113993ae --- /dev/null +++ b/frontend/image-tool/src/businessNew/context.ts @@ -0,0 +1,23 @@ +import { provide, inject, reactive } from 'vue'; +import Editor from './common/Editor'; +import { context } from 'image-ui/context'; +import { initRegistry } from './registry'; + +export function useProvideBSEditor() { + const editor = new Editor(); + // @ts-ignore + window.editor = editor; + // @ts-ignore + editor.state = reactive(editor.state); + editor.bsState = reactive(editor.bsState); + + initRegistry(editor); + + provide(context, editor); + + return editor; +} + +export function useInjectBSEditor(): Editor { + return inject(context) as Editor; +} diff --git a/frontend/image-tool/src/businessNew/hook/useCommon.ts b/frontend/image-tool/src/businessNew/hook/useCommon.ts new file mode 100644 index 00000000..93deb3a2 --- /dev/null +++ b/frontend/image-tool/src/businessNew/hook/useCommon.ts @@ -0,0 +1,45 @@ +import { OPType } from 'image-editor'; +import { useInjectBSEditor } from '../context'; +import { ButtonCancel, ButtonDiscard, ButtonSave } from '../configs/ui'; +import pageModes from '../configs/mode'; +import * as api from '../api'; +import { closeTab } from '../utils'; + +export default function useCommon() { + const editor = useInjectBSEditor(); + const { state } = editor; + + async function onClose() { + if (state.modeConfig.op == OPType.VIEW) { + state.frames.forEach((frame) => { + frame.needSave = false; + }); + closeTab(); + return; + } + let action = ''; + if (editor.needSave()) { + action = await editor.showModal('confirm', { + title: editor.lang('Save Changes'), + data: { + subContent: editor.lang('Do you want to save changes?'), + buttons: [ButtonCancel, ButtonDiscard, ButtonSave], + }, + }); + } + if (action === ButtonSave.action) { + await editor.save(); + } else if (action === ButtonDiscard.action) { + editor.frameChangeToggle(false); + } else if (action === ButtonCancel.action) { + return; + } + + if (editor.state.modeConfig.name === pageModes.execute.name) { + await api.unlockRecord(editor.bsState.recordId); + } + closeTab(); + } + + return { onClose }; +} diff --git a/frontend/image-tool/src/businessNew/hook/useDataflow.ts b/frontend/image-tool/src/businessNew/hook/useDataflow.ts new file mode 100644 index 00000000..9e052cf0 --- /dev/null +++ b/frontend/image-tool/src/businessNew/hook/useDataflow.ts @@ -0,0 +1,58 @@ +import { useInjectBSEditor } from '../context'; +import * as api from '../api'; +import { BSError, Event } from 'image-editor'; + +export default function useDataFlow() { + const editor = useInjectBSEditor(); + const { bsState, state } = editor; + + async function loadRecord() { + try { + const { recordId } = bsState; + const { dataInfos, isSeriesFrame, seriesFrames } = await api.getInfoByRecordId(recordId); + + state.isSeriesFrame = isSeriesFrame; + state.sceneIds = seriesFrames || []; + bsState.datasetId = dataInfos[0].datasetId + ''; + + editor.dataManager.setSceneDataByFrames(dataInfos); + } catch (error) { + throw error instanceof BSError + ? error + : new BSError('', editor.lang('load-record-error'), error); + } + } + async function loadClasses() { + try { + const config = await api.getDataflowClass(bsState.datasetId); + editor.setClassTypes(config); + } catch (error) { + throw error instanceof BSError + ? error + : new BSError('', editor.lang('load-class-error'), error); + } + } + async function loadDateSetClassification() { + try { + const classifications = await api.getDataflowClassification(bsState.datasetId); + bsState.classifications = classifications; + } catch (error) { + throw error instanceof BSError + ? error + : new BSError('', editor.lang('load-dataset-classification-error'), error); + } + } + async function loadModels() { + try { + const models = await api.getModelList(); + state.models = models; + editor.emit(Event.MODEL_LOADED); + } catch (error) { + throw error instanceof BSError + ? error + : new BSError('', editor.lang('load-model-error'), error); + } + } + + return { loadModels, loadClasses, loadRecord, loadDateSetClassification }; +} diff --git a/frontend/image-tool/src/businessNew/hook/useFlow.ts b/frontend/image-tool/src/businessNew/hook/useFlow.ts new file mode 100644 index 00000000..1df97c6e --- /dev/null +++ b/frontend/image-tool/src/businessNew/hook/useFlow.ts @@ -0,0 +1,91 @@ +import * as pageHandler from '../pages'; +import { useInjectBSEditor } from '../context'; +import Event from '../configs/event'; +import useQuery from './useQuery'; +import useToken from './useToken'; +import { MsgType, StatusType } from 'image-editor'; +import { setToken } from '../api/base'; +import { enableEscOnFullScreen } from '../utils'; +import { computed, watch } from 'vue'; + +export type IHandlerType = keyof typeof pageHandler; + +export default function UseFlow() { + const editor = useInjectBSEditor(); + const { bsState, state } = editor; + const { query, iniQuery } = useQuery(); + const token = useToken(); + + iniQuery(); + const mode = getMode(query); + if (!mode || !pageHandler[mode]) { + editor.showMsg(MsgType.error, 'invalid-query'); + return; + } + const handler = pageHandler[mode](); + + // console.log('UseFlow ===> query', query); + // console.log('UseFlow ===> bsState', bsState.query); + // console.log('UseFlow ===> token', token); + // console.log('UseFlow ===> mode', mode); + + function handleUnload() { + window.addEventListener('beforeunload', (event: Event) => { + console.log('beforeunload'); + if (editor.needSave()) { + event.preventDefault(); + // @ts-ignore + event.returnValue = editor.t('msg-not-save'); + } + }); + } + function initFlowEvent() { + editor.on(Event.FLOW_ACTION, async (action, data) => { + if (bsState.blocking) return; + console.log('action', action); + handler.onAction(action, data); + }); + } + async function init() { + if (!token) { + editor.showMsg(MsgType.error, 'Not logged in'); + return; + } + setToken(token); + initFlowEvent(); + handleUnload(); + enableEscOnFullScreen(); + await handler.init(); + editor.emit(Event.BUSINESS_INIT); + } + const blocking = computed(() => { + return ( + bsState.doing.saving || + bsState.doing.submitting || + bsState.doing.marking || + bsState.doing.skip || + state.editorMuted || + state.status !== StatusType.Default + ); + }); + + watch( + () => blocking.value, + () => { + bsState.blocking = blocking.value; + }, + { + immediate: true, + }, + ); + + return { init }; +} + +function getMode(query: Record): IHandlerType { + let mode = 'execute' as IHandlerType; + if (query.type === 'readOnly') { + mode = 'view'; + } + return mode; +} diff --git a/frontend/image-tool/src/businessNew/hook/useQuery.ts b/frontend/image-tool/src/businessNew/hook/useQuery.ts new file mode 100644 index 00000000..38976b3c --- /dev/null +++ b/frontend/image-tool/src/businessNew/hook/useQuery.ts @@ -0,0 +1,33 @@ +import qs from 'qs'; +import { useInjectBSEditor } from '../context'; +import { parseUrlIds } from '../utils'; + +export default function useQuery() { + const queryStr = location.href.split('?').reverse(); + const query = qs.parse(queryStr[0] || ''); + + const editor = useInjectBSEditor(); + const { bsState } = editor; + function iniQuery() { + Object.assign(bsState.query, query || {}); + + bsState.recordId = (query.recordId as string) || ''; + bsState.datasetId = (query.datasetId as string) || ''; + if (bsState.query.itemIds) { + bsState.query.itemIds = parseUrlIds(bsState.query.itemIds || ''); + } + if (bsState.query.classIds) { + bsState.query.classIds = parseUrlIds(bsState.query.classIds || ''); + } + if (bsState.query.classificationIds) { + bsState.query.classificationIds = parseUrlIds(bsState.query.classificationIds || ''); + } + if (bsState.query.dataIds) { + bsState.query.dataIds = parseUrlIds(bsState.query.dataIds || ''); + } + if (bsState.query.dataType) { + bsState.query.dataType = bsState.query.dataType.toUpperCase(); + } + } + return { query, iniQuery }; +} diff --git a/frontend/image-tool/src/businessNew/hook/useToken.ts b/frontend/image-tool/src/businessNew/hook/useToken.ts new file mode 100644 index 00000000..bcf163e7 --- /dev/null +++ b/frontend/image-tool/src/businessNew/hook/useToken.ts @@ -0,0 +1,13 @@ +import Cookies from 'js-cookie'; + +window.Cookies = Cookies; + +let hostname = document.location.hostname || document.location.host; +// let dot = hostname.indexOf('.'); +// let parentHost = isIp(hostname) ? hostname : hostname.substring(dot + 1); + +export default function useToken() { + let token = Cookies.get(`${hostname} token`); + token = token ? `Bearer ${token}` : ''; + return token; +} diff --git a/frontend/image-tool/src/businessNew/hook/useUI.ts b/frontend/image-tool/src/businessNew/hook/useUI.ts new file mode 100644 index 00000000..2fa6e0b4 --- /dev/null +++ b/frontend/image-tool/src/businessNew/hook/useUI.ts @@ -0,0 +1,28 @@ +import { useInjectBSEditor } from '../context'; +import { IBsUIType } from '../configs/ui'; +import { OPType, StatusType } from 'image-editor'; + +export default function useUI() { + const editor = useInjectBSEditor(); + const state = editor.state; + function has(name: IBsUIType | string) { + return state.modeConfig.ui[name]; + } + function canEdit() { + return state.modeConfig.op === OPType.EDIT; + } + function canAnnotate() { + return true; + } + + function canOperate() { + return state.status === StatusType.Default; + } + + return { + has, + canEdit, + canAnnotate, + canOperate, + }; +} diff --git a/frontend/image-tool/src/businessNew/pages/execute.ts b/frontend/image-tool/src/businessNew/pages/execute.ts new file mode 100644 index 00000000..b510d541 --- /dev/null +++ b/frontend/image-tool/src/businessNew/pages/execute.ts @@ -0,0 +1,49 @@ +import { FlowAction, IPageHandler } from '../types'; +import { useInjectBSEditor } from '../context'; +import pageModes from '../configs/mode'; +import { MsgType } from 'image-editor'; +import useDataFlow from '../hook/useDataflow'; +import useCommon from '../hook/useCommon'; + +export function execute(): IPageHandler { + const editor = useInjectBSEditor(); + const { bsState } = editor; + + const { loadRecord, loadClasses, loadDateSetClassification, loadModels } = useDataFlow(); + const { onClose } = useCommon(); + + async function init() { + if (!bsState.query.recordId) { + editor.showMsg(MsgType.error, 'Invalid Query'); + return; + } + editor.setMode(pageModes.execute); + editor.showLoading(true); + try { + await loadRecord(); + await Promise.all([loadClasses(), loadDateSetClassification(), loadModels()]); + await editor.loadManager.loadSceneData(0); + } catch (error: any) { + editor.handleErr(error, 'Load Error'); + } + editor.showLoading(false); + + if (bsState.query.type === 'modelRun') { + editor.dataManager.pollDataModelResult(); + } + } + + function onAction(action: FlowAction) { + console.log(action); + switch (action) { + case FlowAction.save: + editor.save(); + break; + case FlowAction.close: + onClose(); + break; + } + } + + return { init, onAction }; +} diff --git a/frontend/image-tool/src/businessNew/pages/index.ts b/frontend/image-tool/src/businessNew/pages/index.ts new file mode 100644 index 00000000..3b77ad10 --- /dev/null +++ b/frontend/image-tool/src/businessNew/pages/index.ts @@ -0,0 +1,2 @@ +export * from './execute'; +export * from './view'; diff --git a/frontend/image-tool/src/businessNew/pages/view.ts b/frontend/image-tool/src/businessNew/pages/view.ts new file mode 100644 index 00000000..38c9a501 --- /dev/null +++ b/frontend/image-tool/src/businessNew/pages/view.ts @@ -0,0 +1,61 @@ +import { IPageHandler } from '../types'; +import { useInjectBSEditor } from '../context'; +import useDataFlow from '../hook/useDataflow'; +import pageModes from '../configs/mode'; +import { IFrame, MsgType } from 'image-editor'; + +export function view(): IPageHandler { + const editor = useInjectBSEditor(); + const { state, bsState } = editor; + const { loadClasses, loadDateSetClassification } = useDataFlow(); + + async function init() { + let { query } = bsState; + if (!query.datasetId || !query.dataId) { + editor.showMsg(MsgType.error, 'Invalid Query'); + return; + } + editor.setMode(pageModes.view); + + editor.showLoading(true); + try { + await Promise.all([loadClasses(), loadDateSetClassification(), loadDataInfo()]); + await editor.loadManager.loadSceneData(0); + } catch (error: any) { + editor.handleErr(error, 'Load Error'); + } + editor.showLoading(false); + } + + async function loadDataInfo() { + let { query } = bsState; + + let dataId = query.dataId; + if (query.dataType === 'frame') { + state.isSeriesFrame = true; + // await loadDataFromFrameSeries(dataId); + } else { + createSingleData(); + } + } + + function createSingleData() { + let { query } = bsState; + + let dataId = query.dataId; + let data: IFrame = { + id: dataId, + datasetId: query.datasetId, + teamId: '', + pointsUrl: '', + queryTime: '', + loadState: '', + needSave: false, + classifications: [], + }; + editor.dataManager.setSceneDataByFrames([data]); + } + function onAction() {} + + return { init, onAction }; +} diff --git a/frontend/image-tool/src/businessNew/registry.ts b/frontend/image-tool/src/businessNew/registry.ts new file mode 100644 index 00000000..9d8f1d05 --- /dev/null +++ b/frontend/image-tool/src/businessNew/registry.ts @@ -0,0 +1,10 @@ +import Editor from './common/Editor'; +import hotkeyConfig from './configs/hotkey'; +import * as Actions from './actions'; + +export function initRegistry(editor: Editor) { + editor.hotkeyManager.registryHotkey(hotkeyConfig); + Object.keys(Actions).forEach((name) => { + editor.actionManager.registryAction(name, (Actions as any)[name]); + }); +} diff --git a/frontend/image-tool/src/businessNew/state.ts b/frontend/image-tool/src/businessNew/state.ts new file mode 100644 index 00000000..64044976 --- /dev/null +++ b/frontend/image-tool/src/businessNew/state.ts @@ -0,0 +1,17 @@ +import { __ALL__ } from 'image-editor'; +import { IBSState } from './types'; + +export function getDefault(): IBSState { + return { + blocking: false, + user: {} as any, + team: {} as any, + query: {}, + recordId: '', + doing: { saving: false, marking: false, skip: false, submitting: false, modify: false }, + datasetId: '', + classifications: [], + activeSource: [__ALL__], + filterClasses: [__ALL__], + }; +} diff --git a/frontend/image-tool/src/businessNew/types/class.ts b/frontend/image-tool/src/businessNew/types/class.ts new file mode 100644 index 00000000..7973dfe8 --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/class.ts @@ -0,0 +1,34 @@ +import { AttrType } from 'image-editor'; + +export interface IClassificationAttr { + classificationId: string; + parent: string; + parentAttr: string; + parentValue: any; + parentLabel?: any; + id: string; + type: AttrType; + name: string; + label?: string; + alias?: string; + required: boolean; + attributeVersion?: number; + options: { + value: any; + name: string; + alias?: string; + [k: string]: any; + }[]; + value: any; + isLeaf?: boolean; + [k: string]: any; +} +export interface IClassification { + id: string; + uuid: string; + name: string; + label?: string; + alias?: string; + attrs: IClassificationAttr[]; + [k: string]: any; +} diff --git a/frontend/image-tool/src/businessNew/types/common.ts b/frontend/image-tool/src/businessNew/types/common.ts new file mode 100644 index 00000000..c77cb557 --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/common.ts @@ -0,0 +1,65 @@ +import { SourceType, ToolType, Vector2 } from 'image-editor'; + +export interface IDoing { + // flow + saving: boolean; + marking: boolean; + skip: boolean; + submitting: boolean; + modify: boolean; +} +export interface IObjectBasicInfo { + classAttributes: IObject; + classId: number; + frontId: string; + id: string | number; + sourceId: number; + sourceType?: SourceType; +} +export interface IObjectInfo extends IObjectBasicInfo { + dataId: number; + datasetId: number; + lockedBy: any; + objectCount: number; +} +export interface IObject { + backId: string | number; + classId: string; + classValues: any; + contour: IContour; // { points: [], ... } + id: string; + meta: any; // other + sourceId: string; + sourceType?: SourceType; + trackId?: string; + trackName?: string; + type: ToolType; + + modelConfidence?: number; + modelClass?: string; + version?: number; + createdAt?: string; + createdBy?: string | number; +} +export interface IDataAnnotations { + classificationId: string; + classificationAttributes: Record; +} +export interface ISaveFormat { + dataAnnotations: IDataAnnotations[]; + dataId: string | number; + objects: IObjectBasicInfo[]; + [key: string]: any; +} +export interface ISaveResp { + dataId: string | number; + frontId: string; + id: string | number; +} + +export interface IContour { + points?: Vector2[]; + interior?: { points: Vector2[] }[]; // only polygon + rotation?: number; // only rect; + area?: number; // closed figure: rect, polygon +} diff --git a/frontend/image-tool/src/businessNew/types/enum.ts b/frontend/image-tool/src/businessNew/types/enum.ts new file mode 100644 index 00000000..c7f3052a --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/enum.ts @@ -0,0 +1,21 @@ +export enum FlowAction { + close = 'close', + switch = 'switchSeriesFrame', + help = 'help', + save = 'save', + markvaild = 'markvaild', + markinvaild = 'markinvaild', + skip = 'skip', + submit = 'submit', + modify = 'modify', +} + +export enum ValidStatus { + vaild = 'VALID', + invaild = 'INVALID', +} +export enum AnnotateStatus { + annotated = 'ANNOTATED', + unannotated = 'NOT_ANNOTATED', + invalid = 'INVALID', +} diff --git a/frontend/image-tool/src/businessNew/types/index.ts b/frontend/image-tool/src/businessNew/types/index.ts new file mode 100644 index 00000000..f6366f5d --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/index.ts @@ -0,0 +1,6 @@ +export * from './class'; +export * from './common'; +export * from './enum'; +export * from './page'; +export * from './state'; +export * from './user'; diff --git a/frontend/image-tool/src/businessNew/types/page.ts b/frontend/image-tool/src/businessNew/types/page.ts new file mode 100644 index 00000000..52a956d0 --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/page.ts @@ -0,0 +1,6 @@ +import { FlowAction } from './enum'; + +export interface IPageHandler { + init: () => void; + onAction: (e: FlowAction, data?: any) => void; +} diff --git a/frontend/image-tool/src/businessNew/types/state.ts b/frontend/image-tool/src/businessNew/types/state.ts new file mode 100644 index 00000000..463820fc --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/state.ts @@ -0,0 +1,22 @@ +import { IClassification } from './class'; +import { IDoing } from './common'; +import { ITeam, IUser } from './user'; + +export interface IBSState { + user: IUser; + team: ITeam; + // config: IGlobalConfig; + + doing: IDoing; + blocking: boolean; + + query: Record; + + recordId: string; + datasetId: string; + classifications: IClassification[]; + + // fitler + activeSource: string[]; + filterClasses: string[]; +} diff --git a/frontend/image-tool/src/businessNew/types/user.ts b/frontend/image-tool/src/businessNew/types/user.ts new file mode 100644 index 00000000..770c4a75 --- /dev/null +++ b/frontend/image-tool/src/businessNew/types/user.ts @@ -0,0 +1,19 @@ +export interface IUser { + id: string; + nickname: string; + email?: string; + status?: string; + username?: string; + avatarUrl?: string; +} +export interface ITeam { + id: string; + name: string; + inviteCode: string; + isDefaultTeam: boolean; + isDeleted: boolean; + planId: number; + planType: string; + status: string; + expireAt: string; +} diff --git a/frontend/image-tool/src/businessNew/utils/class.ts b/frontend/image-tool/src/businessNew/utils/class.ts new file mode 100644 index 00000000..56a87f8c --- /dev/null +++ b/frontend/image-tool/src/businessNew/utils/class.ts @@ -0,0 +1,126 @@ +import { AttrType, IClassType } from 'image-editor'; +import { IClassification, IClassificationAttr } from '../types'; +import { cloneDeep } from 'lodash'; + +export function isAttrTypeMulti(type: string) { + switch (type) { + case AttrType.RANK: + case AttrType.MULTI_SELECTION: + return true; + case AttrType.DROPDOWN: + case AttrType.RADIO: + case AttrType.TEXT: + default: + return false; + } +} +export function parseClassesFromBackend(data: any[]) { + const classTypes = [] as IClassType[]; + + data.forEach((config: any) => { + const classType: IClassType = { + id: config.id + '', + name: config.name || '', + label: config.alias || '', + color: config.color || '#ff0000', + classVersion: config.version || 1, + // attrs: [], + attrs: config.attributes || [], + toolType: config.toolType, + toolTypeOptions: config.toolTypeOptions || {}, + }; + + classTypes.push(classType); + }); + + return classTypes; +} + +export function traverseClassification2Arr(data: any[]) { + let classifications = [] as IClassification[]; + + data.forEach((e: any) => { + let classificationId = e.id + ''; + let classification: IClassification = { + id: classificationId, + uuid: '', + name: e.name, + alias: e.alias, + attrs: [], + }; + const options = e.attribute?.options || []; + let classificationAttr: IClassificationAttr = { + id: e.attribute.id, + classificationId, + parent: '', + parentValue: '', + parentAttr: e.name, + type: e.inputType, + name: e.name, + alias: e.alias, + value: isAttrTypeMulti(e.inputType) ? [] : '', + required: e.isRequired, + options: options.map((e: any) => { + return { value: e.name, ...e }; + }), + }; + + classification.attrs.push(classificationAttr); + options.forEach((option: any) => { + traverseOption(classification, option, classificationAttr.id, e.name); + }); + classifications.push(classification); + }); + + return classifications; + + function traverseOption( + classification: IClassification, + option: any, + parent: string, + parentAttr: string, + ) { + if (!option.attributes || option.attributes.length === 0) return; + + option.attributes.forEach((attr: any) => { + let name = attr.name; + let classificationAttr: IClassificationAttr = { + id: attr.id, + key: `${parent}[${option.name}]-${name}`, + classificationId: classification.id, + parent, + parentAttr, + parentValue: option.name, + type: attr.type, + name, + alias: attr.alias, + value: isAttrTypeMulti(attr.type) ? [] : '', + required: attr.required, + options: attr.options.map((e: any) => { + return { value: e.name, ...e }; + }), + }; + classification.attrs.push(classificationAttr); + (attr.options || []).forEach((option: any) => { + traverseOption(classification, option, classificationAttr.id, name); + }); + }); + } +} + +export function classificationAssign(baseArr: IClassification[], values: any[]) { + const returnArr = cloneDeep(baseArr); + const valuesMap: Record = {}; + values.forEach((e: any) => { + const attrsValues = e.classificationAttributes.values; + attrsValues.forEach((attr: any) => { + valuesMap[attr.id] = attr.value; + }); + }); + returnArr.forEach((e) => { + e.attrs?.forEach((e) => { + if (valuesMap[e.id]) e.value = valuesMap[e.id]; + }); + }); + return returnArr; +} diff --git a/frontend/image-tool/src/businessNew/utils/common.ts b/frontend/image-tool/src/businessNew/utils/common.ts new file mode 100644 index 00000000..af78b618 --- /dev/null +++ b/frontend/image-tool/src/businessNew/utils/common.ts @@ -0,0 +1,53 @@ +import { Vector2 } from 'image-editor'; +import { isNaN } from 'lodash'; + +export function empty(value: any) { + return value == undefined || value == null || value === ''; +} +export function validNumber(val: any) { + return !empty(val) && typeof val === 'number' && !isNaN(val) && isFinite(val); +} +export function fixed(num: number, len: number = 4) { + return Number(num.toFixed(len)); +} +export function checkPoints(points?: Vector2[]) { + if (!Array.isArray(points)) return []; + const keys: String[] = []; + const pointKey = (p: Vector2) => fixed(p.x) + '##' + fixed(p.y); + return points.filter((point) => { + if (point && validNumber(point.x) && validNumber(point.y) && !keys.includes(pointKey(point))) { + keys.push(pointKey(point)); + point.x = fixed(point.x); + point.y = fixed(point.y); + return true; + } + return false; + }); +} +export function fillStr(v: string | number, fill = 2) { + let str = v + ''; + const len = Math.max(str.length, fill); + str = '0000000000' + str; + return str.substring(str.length - len); +} +export function parseUrlIds(str: string) { + str = decodeURIComponent(str); + const ids = str.split(','); + const set = new Set(ids); + return [...set]; +} + +export function enableEscOnFullScreen() { + // @ts-ignore + if (navigator.keyboard && navigator.keyboard.lock) { + // @ts-ignore + navigator.keyboard.lock(['Escape']); + } +} +export function closeTab() { + const win = window.open('about:blank', '_self'); + win && win.close(); +} +export function refreshTab() { + window.location.reload(); +} diff --git a/frontend/image-tool/src/businessNew/utils/error.ts b/frontend/image-tool/src/businessNew/utils/error.ts new file mode 100644 index 00000000..b689d9ae --- /dev/null +++ b/frontend/image-tool/src/businessNew/utils/error.ts @@ -0,0 +1,25 @@ +import { Editor, BSError, MsgType } from 'image-editor'; +// import Code from '../config/code'; + +function logErrorStack(err: BSError | Error) { + while (err) { + if (err instanceof BSError) { + console.log(err); + } else { + console.error(err); + } + + err = (err as any).oriError; + } +} + +export function handleError(editor: Editor, err: BSError | Error) { + const oriError = (err as any).oriError; + + if (err instanceof BSError) { + editor.showMsg(MsgType.error, err.message); + if (oriError) logErrorStack(oriError); + } else { + console.error(err); + } +} diff --git a/frontend/image-tool/src/businessNew/utils/index.ts b/frontend/image-tool/src/businessNew/utils/index.ts new file mode 100644 index 00000000..97dd3d8f --- /dev/null +++ b/frontend/image-tool/src/businessNew/utils/index.ts @@ -0,0 +1,5 @@ +export * from './result-save'; +export * from './result-request'; +export * from './common'; +export * from './error'; +export * from './class'; diff --git a/frontend/image-tool/src/businessNew/utils/result-request.ts b/frontend/image-tool/src/businessNew/utils/result-request.ts new file mode 100644 index 00000000..538db497 --- /dev/null +++ b/frontend/image-tool/src/businessNew/utils/result-request.ts @@ -0,0 +1,92 @@ +import { v4 as uuid } from 'uuid'; +import { + AnnotateObject, + utils as EditorUtils, + IPolygonInnerConfig, + IUserData, + KeyPoint, + Line, + Polygon, + Rect, + SourceType, + ToolType, +} from 'image-editor'; +import Editor from '../common/Editor'; +import { IContour, IObjectInfo } from '../types'; +import { checkPoints } from './common'; + +export function convertObject2Annotate(editor: Editor, objects: IObjectInfo[]) { + const annotates = [] as AnnotateObject[]; + + objects.forEach((e: IObjectInfo) => { + const obj = e.classAttributes; + const contour = (obj.contour || {}) as IContour; + const classConfig = editor.getClassType(obj.classId || ''); + + const userData: IUserData = { + ...obj?.meta, + backId: obj.backId, + classId: classConfig ? classConfig.id : '', + classType: classConfig ? classConfig.name : '', + attrs: arrayToObj(obj.classValues), + modelClass: obj.modelClass, + confidence: obj.modelConfidence, + trackId: obj.trackId, + trackName: obj.trackName, + sourceId: obj.sourceId || String(e.sourceId) || editor.state.defaultSourceId, + sourceType: obj.sourceType || e.sourceType || SourceType.DATA_FLOW, + createdAt: obj.createdAt, + createdBy: obj.createdBy, + version: obj.version, + }; + const points = checkPoints(contour.points || []); + const interior = contour.interior || []; + const pointsLen = points.length; + const type = obj.type.toLocaleUpperCase(); + let annotate; + switch (type) { + case ToolType.RECTANGLE: + case ToolType.BOUNDING_BOX: + if (pointsLen >= 2) { + const rectOption = EditorUtils.getRectFromPointsWithRotation(points); + annotate = new Rect({ ...rectOption, points }); + } + break; + case ToolType.POLYGON: + if (pointsLen >= 3) { + const pointsOrder = EditorUtils.countPointsOrder(points); + const innerPoints: IPolygonInnerConfig[] = []; + interior.forEach((e: any) => { + let filters = checkPoints(e.coordinate || e.points || []); + if (filters.length >= 3) { + if (EditorUtils.countPointsOrder(filters) === pointsOrder) filters.reverse(); + innerPoints.push({ points: filters }); + } + }); + annotate = new Polygon({ points, innerPoints }); + } + break; + case ToolType.POLYLINE: + if (pointsLen > 1) annotate = new Line({ points }); + break; + case ToolType.KEY_POINT: + if (pointsLen > 0) annotate = new KeyPoint({ ...points[0] }); + break; + } + if (!annotate) return; + annotate.uuid = obj.id || uuid(); + annotate.userData = userData; + annotates.push(annotate); + }); + return annotates; +} +function arrayToObj(data: any[] = []) { + const values = {} as Record; + if (!Array.isArray(data)) return values; + + data.forEach((e) => { + if (Array.isArray(e)) return; + values[e.id] = e; + }); + return values; +} diff --git a/frontend/image-tool/src/businessNew/utils/result-save.ts b/frontend/image-tool/src/businessNew/utils/result-save.ts new file mode 100644 index 00000000..aee5b307 --- /dev/null +++ b/frontend/image-tool/src/businessNew/utils/result-save.ts @@ -0,0 +1,147 @@ +/** + * + */ +import Editor from '../common/Editor'; +import { IContour, IObject, IObjectBasicInfo, ISaveFormat } from '../types'; +import { + AnnotateObject, + IFrame, + utils as EditorUtils, + IAttrOption, + ToolType, + Rect, + Vector2, + IPolygonInnerConfig, + Polygon, + SourceType, +} from 'image-editor'; +import { checkPoints, empty, fixed, validNumber } from './common'; + +function objToArray(obj: Record = {}, attrMap: Map) { + const data = [] as any[]; + Object.keys(obj).forEach((key) => { + const objAttr = obj[key]; + const attr = attrMap.get(key); + if (empty(objAttr.value) || !attr) return; + const option = attr?.options?.find((e: IAttrOption) => e.name == objAttr.value) as IAttrOption; + const hasChild = option?.attributes && option.attributes.length > 0; + data.push({ + id: key, + pid: attr.parent, + name: attr.name || '', + value: objAttr.value, + alias: attr.alias || '', + isLeaf: !hasChild, + type: attr?.type, + }); + }); + return data; +} +function updateObjectVersion(obj: AnnotateObject) { + if (!obj.updateTime || !obj.lastTime) return; + let version = (validNumber(obj.version) ? obj.version : 0) as number; + if (obj.updateTime > obj.lastTime) { + version++; + } + obj.lastTime = obj.updateTime; + obj.version = version; +} +function getContourData(object: AnnotateObject) { + const returnContour: IContour = {}; + const pos = object.position(); + switch (object.toolType) { + case ToolType.BOUNDING_BOX: + const rect = object as Rect; + returnContour.points = checkPoints(EditorUtils.getRotatedRectPoints(rect)); + returnContour.area = fixed(rect.getArea(), 0) || 0; + let r = object.rotation() || 0; + r = r < 0 ? 360 + r : r; + returnContour.rotation = fixed(r, 1); + break; + case ToolType.KEY_POINT: + returnContour.points = [{ ...pos }]; + break; + case ToolType.POLYGON: + case ToolType.POLYLINE: + const objectPoints = checkPoints(object.attrs.points); + returnContour.points = EditorUtils.getShapeRealPoint(object, objectPoints); + const objectInnerPoints = object.attrs.innerPoints as IPolygonInnerConfig[]; + if (objectInnerPoints) { + const innerPoints = [] as { points: Vector2[] }[]; + objectInnerPoints.forEach((inner) => { + let _points = checkPoints(inner.points); + _points = EditorUtils.getShapeRealPoint(object, _points); + innerPoints.push({ points: _points }); + }); + returnContour.interior = innerPoints; + } + if (object instanceof Polygon) returnContour.area = fixed(object.getArea(), 0) || 0; + break; + } + return returnContour; +} + +export function convertAnnotate2Object(editor: Editor, annotates: AnnotateObject[]) { + const objects = [] as IObject[]; + + annotates.forEach((obj: any) => { + const userData = editor.getUserData(obj); + const classConfig = editor.getClassType(userData.classId || ''); + + // updateVersion + updateObjectVersion(obj); + + const newInfo: IObject = { + backId: userData.backId, + classId: classConfig?.id || '', + classValues: objToArray(editor.getValidAttrs(userData) || {}, editor.attrMap), + contour: getContourData(obj), + id: obj.uuid, + meta: { + classType: userData.classType || '', + color: classConfig?.color || '', + }, + sourceId: userData.sourceId || editor.state.defaultSourceId, + sourceType: userData.sourceType || SourceType.DATA_FLOW, + trackId: userData.trackId, + trackName: userData.trackName, + type: obj.toolType, + }; + objects.push(newInfo); + }); + return objects; +} +export function getDataFlowSaveData(editor: Editor, frames?: IFrame[]) { + if (!frames) frames = editor.state.frames; + const dataMap: Record = {}; + frames.forEach((frame) => { + const id = String(frame.id); + const objectInfos: IObjectBasicInfo[] = []; + + // result object + const arr = editor.state.annotateModeList; + arr.forEach((type) => { + const annitates = editor.dataManager.getFrameObject(frame.id, type) || []; + const objects = convertAnnotate2Object(editor, annitates); + objects.forEach((e) => { + objectInfos.push({ + classAttributes: e, + classId: +e.classId, + frontId: e.id, + id: e.backId, + sourceId: +e.sourceId, + sourceType: e.sourceType, + }); + }); + }); + + dataMap[id] = { + dataAnnotations: [], + dataId: id, + objects: objectInfos, + }; + }); + const saveDatas = Object.values(dataMap); + + return { saveDatas }; +} diff --git a/frontend/image-tool/src/locales/helper.ts b/frontend/image-tool/src/locales/helper.ts deleted file mode 100644 index b9ba43b3..00000000 --- a/frontend/image-tool/src/locales/helper.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { LocaleType } from '/#/config'; - -import { set } from 'lodash-es'; - -export const loadLocalePool: LocaleType[] = []; - -export function setHtmlPageLang(locale: LocaleType) { - document.querySelector('html')?.setAttribute('lang', locale); -} - -export function setLoadLocalePool(cb: (loadLocalePool: LocaleType[]) => void) { - cb(loadLocalePool); -} - -export function genMessage(langs: Record>, prefix = 'lang') { - const obj: Recordable = {}; - - Object.keys(langs).forEach((key) => { - const langFileModule = langs[key].default; - let fileName = key.replace(`./${prefix}/`, '').replace(/^\.\//, ''); - const lastIndex = fileName.lastIndexOf('.'); - fileName = fileName.substring(0, lastIndex); - const keyList = fileName.split('/'); - const moduleName = keyList.shift(); - const objKey = keyList.join('.'); - - if (moduleName) { - if (objKey) { - set(obj, moduleName, obj[moduleName] || {}); - set(obj[moduleName], objKey, langFileModule); - } else { - set(obj, moduleName, langFileModule || {}); - } - } - }); - return obj; -} diff --git a/frontend/image-tool/src/locales/index.ts b/frontend/image-tool/src/locales/index.ts new file mode 100644 index 00000000..0dff8385 --- /dev/null +++ b/frontend/image-tool/src/locales/index.ts @@ -0,0 +1,13 @@ +import en from './lang/en-US'; +import zh from './lang/zh-CN'; +import ko from './lang/ko-KR'; +export * from './lang/type'; + +const languages = { + 'en-US': en, + 'zh-CN': zh, + 'ko-KR': ko, + default: en, +}; + +export { languages }; diff --git a/frontend/image-tool/src/locales/lang/en-US.ts b/frontend/image-tool/src/locales/lang/en-US.ts new file mode 100644 index 00000000..c1a19b50 --- /dev/null +++ b/frontend/image-tool/src/locales/lang/en-US.ts @@ -0,0 +1,505 @@ +export const text = { + 'load-point': 'Loading....', + 'load-class-error': 'Load class error', + 'load-model-error': 'Load model error', + 'load-dataset-classification-error': 'Load dataset classification error', + 'load-record-error': 'Load record error', + 'load-resource-error': 'Load resource error', + 'load-object-error': 'Load object error', + 'load-classification-error': 'Load classification error', + 'load-frame-series-error': 'Load frame series data error', + 'invalid-query': 'Invalid query', + 'load-error': 'Load error', + 'load-comment-error': 'Load comment error', + 'save-ok': 'Saved successfully', + 'save-error': 'Save failed', + 'success-submit': 'Successfully submitted', + 'success-accept': 'Successfully accepted', + 'success-reject': 'Successfully rejected', + 'model-run-error': 'Model run error', + 'model-run-no-data': 'No model results', + 'no-point-data': 'No point cloud data', + 'play-error': 'Play failed', + 'unknown-error': 'Error', + 'network-error': 'Network error', + 'login-invalid': 'Login invalid', + 'not-login': 'Not logged in', + 'copy-ok': 'Copy successfully', + 'no-valid-data': 'No valid data', + 'no-data': 'No data', + 'title-reject': 'Reject', + 'title-claiming': 'Claiming data...', + 'title-noData-claim': 'No data to claim', + 'confirm-title': 'Confirm', + 'delete-title': 'Delete', + 'delete-object': 'Are you sure to delete objects?', + 'frame-title': 'Frame', + 'result-incomplete': 'Results {results} is not completed. Please fix it.', + 'result-exceedLimit': 'Results {results} is exceed class limit. Please fix it.', + 'result-need-attributes': + 'Result {results} is required to have manual labels or necessary attributes. Please fix it.', + 'Frame need classifications': + 'Frame {results} is required to have classifications. Please fix it.', + 'data need classifications': 'It is required to have the classifications. Please fix it.', + confirm: 'Confirm', + cancel: 'Cancel', + Cancel: 'Cancel', + delete: 'Delete', + Delete: 'Delete', + hotKey: 'Hotkey', + save: 'Save', + Save: 'Save', + Close: 'Close', + Release: 'Release', + Discard: 'Discard', + groupInvalidTips: 'Multiple choice groups do not support only one result', + splitGroup: 'Disband and delete the group?', + polygonPlusDrawTips: '{pointsLimit} points must be drawn.', + hollowConditionTips: 'Not eligible for hollowing out.', + clipConditionTips: 'Not eligible for cropping.', + clipCannotTips: 'The selected result cannot be cropped.', + changeTips: + 'Are you sure to switch from {pre} to {cur}? If switched, it will force automatic adjustment of key points.', + 'track-object-title': 'Track object', + 'track-no-source': 'No tracking objects', + 'load-track': 'Tracking....', + 'track-no-data': "No Tracking object found. Please check your objects' location and direction", + 'track-error': 'Tracking error', + 'track-ok': 'Tracking successfully', + 'track-invalid': 'The current result type does not support model tracking.', + validity: 'Validity', + classification: 'Classification', + results: 'Results', + changeMode: 'Change {type} mode', + list: 'list', + layer: 'layer', + comments: 'Comments', + timeLine: 'Timeline', + autoLoad: 'Auto load', + copyLeft1: 'Copy one frame backward (Alt+←)', + copyRight1: 'Copy one frame forward (Alt+→)', + speedDown: 'Speed Down({n} times)', + speedUp: 'Speed Up({n} times)', + merge: 'Merge', + settingTrack: 'Setting', + objectTracking: 'Object Tracking', + allObjects: 'All Objects', + selectObjects: 'Selected Objects', + method: 'Method', + object: 'Object', + left: 'Backward', + right: 'Forward', + direction: 'Direction', + method_copy: 'Copy', + frameNumber: 'Frame number', + Frames: 'Frames', + mergeFrom: 'Merge from', + mergeTo: 'Merge to', + target: 'Target', + split: 'Split', + invisible: 'Disappeared', + invalid: 'Class invalid', + mark: 'Mark', + 'class-recent': 'Recent classes', + 'class-other': 'Other', + replay: 'Replay', + pre: 'Pre(←)', + next: 'Next(→)', + play: 'Play({n} times)', + pause: 'Pause', + switch_on: 'on', + switch_off: 'off', + selectObject: 'Select an object', + newBadge: 'Task Pool', + noPlayData: 'No data to play', + newTrackObject: 'New tracking object', + warnEmptyTarget: 'No data in the target frame', + warnTrackObjectEmpty: 'No tracking objects', + warnNoObject: 'Please select an object', + successTrack: 'Tracking successful', + successCopy: 'Copy successfully', + merge_error_object_not_exist: 'Error! Track object is not exist', + merge_error_object_repeat: 'Conflicts detected. Please check in the timeline.', + merge_error_class_none: 'Cannot merge objects without the class.', + merge_error_class_diff: 'Cannot merge objects with different classes.', + successMerge: 'Merge successfully', + errorMerge: 'Merge failed. Please try again', + warnEmptyObject: 'Cannot create an empty object', + successDelete: 'Successfully deleted', + successSplit: 'Split successful', + errorSplit: 'Split failed. Please try again', + errorDelete: 'Delete failed. Please try again', + btnCancelText: 'Cancel', + btnOkText: 'Ok', + btnDelete: 'Delete', + btnReview: 'Review', + btnCloseReview: 'Close review', + deleteTitle: 'You are going to delete this tracking object in all frames.', + class: 'Class', + 'Search Class': 'Search class', + titleHelp: 'Help', + Instruction: 'Instruction', + titleWorkFlow: 'Workflow', + titleFlow: 'Workflow Detail', + titleData: 'Data Detail', + titleBack: 'Back', + titleClose: 'Close', + Fullscreen: 'Fullscreen', + ExitFullscreen: 'Exit Fullscreen', + titleSaveChange: 'Save Changes', + titleHotKeys: 'Hotkeys', + msgSaveChange: 'Do you want to save changes?', + 'Save Changes': 'Save Changes', + btnReject: 'Reject', + btnModify: 'Modify', + btnQuitModify: 'Quit Modify', + btnAccept: 'Accept', + btnSubmit: 'Submit', + btnSave: 'Save', + btnPass: 'Pass', + btnClaim: 'Claim', + btnR_E: 'Reject & Exit', + btnR_C: 'Reject & Claim', + btnP_E: 'Pass & Exit', + btnP_C: 'Pass & Claim', + btnS_E: 'Submit & Exit', + btnS_C: 'Submit & Claim', + msgSample: + "You're now viewing a frame that is not sampled. Any comments in this frame will not be counted in Sampling Accuracy.", + titleAnnotate: 'Annotate', + titleReview: 'Review', + titleAcceptance: 'Acceptance', + titleQA: 'QA', + titleView: 'View', + titleModify: 'Modify', + titlePausedFor: 'Paused for', + titleDueIn: 'Due in', + titlePause: 'Pause', + titleRestart: 'Resume', + msgOutTime: + 'Your claimed data ({ n } data remaining) is expiring in { t }. After that, they will all be released to task pool.', + msgAgain: 'Please claim again.', + msgPaused: 'Claimed data is now paused.', + msgOver2M: "It seems you haven't operate in 2 minutes.", + msgClickStart: 'Click button {btn} to continue working.', + msgClaimMsg: 'Claimed data is now paused. Click button to continue working.', + msgExpire: 'Claimed data is about to expire', + msgExpired: 'Claimed data has expired', + 'Track Object': 'Track Object', + Status: 'Status', + 'Track ID': 'Track ID', + 'Mark All as Ground Truth': 'Mark All as Ground Truth', + Attribute: 'Attribute', + Objects: 'Objects', + copy_attrs_from: 'Copy attributes from other objects in this frame', + copy_attrs_from_frames: 'Copy these attributes to other frames of this tracking object', + copy_attrs_from_frames_tips: '(This will overwrite original attributes)', + Copy: 'Copy', + 'Copy To Frames': 'Copy to Frames', + Advanced: 'Advanced', + 'Split from Current Frame': 'Split from current frame', + 'New Tracking Object': 'New tracking object', + 'All Frames': 'All Frames', + 'Some Frames': 'Some Frames', + 'All Non-True Values': 'All Non-True Values', + deleteFromAllFrames: 'You are going to delete this tracking object in all frames.', + deleteFromSomeFrames: 'You are going to delete this tracking object in the following frames.', + deleteAllUntrue: 'You are going to delete all non-True Values of this tracking object.', + Undo: 'Undo', + Redo: 'Redo', + Hollow: 'Hollow', + Crop: 'Crop', + Drag: 'Drag', + Group: 'Group', + Ungroup: 'Ungroup', + 'Create group': 'Create group', + 'Dissolve group': 'Dissolve group', + editTips: 'Resize', + rectTips: 'Bounding box', + polygonTips: 'Polygon', + lineTips: 'Polyline', + pointTips: 'Key Point', + curveTips: 'Spline Curve', + skeletonTips: 'Skeleton', + groupTips: 'Create Group', + modelTips: 'Smart Editor', + Model: 'Model', + 'AI Annotation Setting': 'AI Annotation Setting', + 'Predict all in Model': 'Predict all in Model', + Confidence: 'Confidence', + interactiveTips: 'Interactive Editor', + 'Interactive Model Setting': 'Interactive Model Setting', + Smoothness: 'Smoothness', + commentTips: 'Comment Editor', + hollowTips: 'Hollow out', + cancelHollowTips: 'Cancel Hollow', + cutNotFirstTips: 'Do not crop the first object', + cutFirstTips: 'Crop the first object', + fill: 'fill', + Contour: 'Contour', + 'Page Up': 'Previous', + 'Page Down': 'Next', + 'Input keyword': 'Input keyword', + 'Delete instance/point': 'Delete instance/point', + 'Delete Mask': 'Delete mask', + 'Finish drawing': 'Finish drawing', + 'Show/hide tag pad': 'Show/hide tag pad', + 'Move the top edge 1px': 'Move the top edge 1px', + 'Move the bottom edge 1px': 'Move the bottom edge 1px', + 'Move the left edge 1px': 'Move the left edge 1px', + 'Movem the right edge 1px': 'Move the right edge 1px', + 'Move the object 1px': 'Move the object 1px', + 'Parallel result Edge': 'Parallel result Edge', + 'Show Single Result': 'Show Single Result', + 'Show Result': 'Show Result', + 'Switch Results': 'Switch Results', + 'Show Result Number': 'Show Result Number', + 'Show Comments': 'Show Comments', + 'Show Class': 'Show Class', + 'Show label and attributes': 'Show label and attributes', + 'Normal/Outline Select': 'Normal/outline Select', + 'Zoom in、Zoom Out': 'Zoom in/out', + 'Cancel window': 'Cancel/close popup', + Data: 'Data', + Instance: 'Instance', + Tool: 'Tool', + Result: 'Result', + Image: 'Image', + Else: 'Else', + 'Selection Tool': 'Selection Tool', + 'Rectangle Tool': 'B-box Tool', + 'Polygon Tool': 'Polygon Tool', + 'Polyline Tool': 'Polyline Tool', + 'KeyPoint Tool': 'Key-point Tool', + 'SplineCurve Tool': 'SplineCurve Tool', + 'Skeleton Tool': 'Skeleton Tool', + 'Group Tool': 'Group Tool', + 'Smart Tool': 'Smart Tool', + 'Interactive Tool': 'Interactive Tool', + 'Comment Tool': 'Comment Tool', + Info: 'Info', + 'Data info': 'Data info', + Name: 'Name', + Width: 'Width', + Height: 'Height', + Length: 'Length', + Area: 'Area', + Size: 'Size', + 'Map Classes': 'Map Classes', + 'Model Classes': 'Model Classes', + Reset: 'Reset', + 'Apply and Run': 'Apply and Run', + Apply: 'Apply', + 'Select all': 'Select all', + 'Unselect all': 'Unselect all', + Setting: 'Setting', + Brightness: 'Brightness', + Contrast: 'Contrast', + Opacity: 'Opacity', + 'Border Width': 'Border Width', + 'Show Size': 'Show Size', + 'Show Attr': 'Show Attr', + Comment: 'Comment', + Other: 'Other', + 'Auxiliary Line': 'Measure Line', + 'Auxiliary Shape': 'Measure Tools', + radius: 'Radius', + px: 'px', + BisectrixLine: 'Equisector', + horizontal: 'Horizontal', + vertical: 'Vertical', + 'Display Mode': 'Display Mode', + 'Do you want to save changes?': 'Do you want to save changes?', + 'Results Source': 'Results Source', + Valid: 'Valid', + Invalid: 'Invalid', + Unknown: 'Unknown', + Filter: 'Filter', + 'Please fill in as required': 'Please fill in as required', + Open: 'Open', + Resolved: 'Resolved', + 'Fix All': 'Fix All', + Fixed: 'Fixed', + 'Resolve All': 'Resolve All', + 'Are you sure to delete?': 'Are you sure to delete?', + 'View All': 'View All', + Type: 'Type', + 'Reply Comment': 'Reply Comment', + 'Task ID': 'Task ID', + 'Task Name': 'Task Name', + 'Data ID': 'Data ID', + 'Data Name': 'Data Name', + 'Data Size': 'Data Size', + Attachment: 'Attachment', + 'Open in New Tab': 'Open in New Tab', + 'the browser does not support copying': 'Your browser does not support copying.', + 'Target Stage': 'Target Stage', + Worker: 'Worker', + 'Who submitted this work': 'Who submitted this work', + New: 'Task Pool', + 'Assign to any other worker in the stage': 'Assign to any worker in the stage', + 'Keep results': 'Keep results', + 'Keep all results': 'Keep all results', + 'Clear results': 'Clear results', + 'Clear all ground truth results': 'Clear all ground truth results and performances', + 'Reject Reasons': 'Reject Reasons', + 'Delete {num} Objects?': 'Delete {num} Objects?', + 'No Attritube': 'No Attritube', + 'Class Invalid': 'Class Invalid', + language: 'language', + 'en-US': 'English', + 'zh-CN': 'Chinese', + 'ko-KR': 'Korean', + not_show_anymore: 'Do not show this any more', + class_change_warn: + 'Labeling this tracking object as {type} will rewrite all of its attributes. Are you sure to change?', + 'Width x height': 'Width x height', + Retry: 'Retry', + Ok: 'Ok', + Refresh: 'Refresh', + 'Polygon shared edge by points drawing mode': 'Shared edge by points drawing mode.', + 'Switch shared edge': 'Switch shared edge', + 'Not True-Value': 'Not True-Value', + 'Object of Frame': 'Object of Frame {index}', + 'No Object': 'No Object', + 'Show All Objects': 'Show All Objects', + '{num} selected': '{num} selected', + 'Class Required': 'Class Required', + Warning: 'Warning', + taskReviewCloseTips: + 'Do you want to save changes and keep these data to yourself? Or release these data to the pool which it belongs?', + Segmentation: 'Segmentation', + reload: 'reload', + 'Fail to load resource, please reload': 'Fail to load resource. Please reload', + 'Switch alias/name': 'Switch alias/name', + 'Polygon shared edge by edges drawing mode': 'Shared edge by edges drawing mode.', + 'Shared edge polygon by points': 'Shared edge polygon by points', + 'Shared edge polygon by edges': 'Shared edge polygon by edges', + 'Switch to view/edit Instance': 'Switch to view/edit Instance', + 'Switch to view/edit Segmentation': 'Switch to view/edit Segmentation', + cuboidTips: 'Cuboid', + 'Cuboid Tool': 'Cuboid Tool', + 'Segmentation Tool': 'Segmentation Tool', + 'Load Sources': 'Load Sources', + 'Editing Source': 'Editing Source', + 'No data available for now': 'No data available for now', + 'You have completed all claimed data': 'You have completed all claimed data', + 'There is no data in your current queue.': 'There is no data in your current queue.', + reClaimTips: + '{ pre } You can either click "ReClaim" to claim more data in the task pool, or close the current page.', + msgNormalCompleted: 'You have completed all the data in current task.', + msgSuspendCompleted: 'You have completed all the suspend data in current task.', + msgReReviewCompleted: 'You have completed all the re-review data in current task.', + msgRejectedCompleted: 'You have completed all the rejected in current task.', + 'Re-Claim': 'Re-Claim', + 'Claim Data': 'Claim Data', + continueClaimTips: "You currently don't have any data available. Continue to claim data?", + msgResolveAll: 'Are you sure to resolve all comments?', + msgFixAll: 'Are you sure to fix all comments?', + Resolve: 'Resolve', + unResolve: 'Cancel Resolve', + Fix: 'Fix', + unFix: 'Cancel Fix', + Restore: 'Restore', + 'You have already claimed {num} data.': 'You have already claimed {num} data.', + 'msg-dataflow-qaerror': + 'Your results have violated mandatory QA rules. Are you sure you want to save this result?', + 'Switch mark/mask': 'Switch mark/mask', + 'Panoramic Tool': 'Smart Tool', + panoramicTips: 'Panoramic', + 'Intellect Tool': 'Intellect Tool', + intellectTips: 'Intellect', + 'warn-addModel': + 'Adding model results may overwrite the annotated objects. Do you want to proceed?', + 'Add Results?': 'Do you want to proceed?', + 'Circle Tool': 'Circle Tool', + circleTips: 'Circle', + 'Ellipse Tool': 'Ellipse Tool', + ellipseTips: 'Ellipse', + 'brush tips': 'Brush', + 'Brush Tool': 'Brush Tool', + 'Brush width': 'Brush width', + 'Fill Tool': 'Fill Tool', + 'mask fill tips': 'Fill', + Boundary: 'Boundary', + Finer: 'Finer', + Coarser: 'Coarser', + titleFrame: 'Data', + titleTask: 'Task', + titleScene: 'Scene', + titleSceneId: 'Scene ID', + titleSceneName: 'Scene Name', + titleSceneIndex: 'Current Frame Index', + btnSaveQuit: 'Save and Quit', + 'Switch Tool': 'Switch Tool', + 'Add Region': 'Add Region', + 'Crop Region': 'Crop Region', + 'The model is running...': 'The model is running...', + cover: 'cover', + Actions: 'Actions', + 'Workflow Detail': 'Workflow Detail', + 'Reject Info': 'Reject Info', + 'No Info': 'No Info', + 'Show Auxiliary Line': 'Show Measure Line', + 'Show Auxiliary Shape': 'Show Measure Tools', + 'Show BisectrixLine': 'Show Bisectors', + 'show points': 'Show Key-points', + resultNotComplete: 'Results not complete', + 'Add a circle': 'Add a circle', + 'Add a rectangle': 'Add a rectangle', + 'Horizontal Drawing Model': 'Horizontal Drawing Model', + 'Vertical Drawing Model': 'Vertical Drawing Model', + 'Crop by selected area': 'Crop by selected area', + 'Show Group Result': 'Show Group Result', + titleAnnotator: 'Assignee Annotator', + noneText: 'None', + all: 'All', + Missed: 'Missed', + 'Wrong Object': 'Wrong Object', + 'Wrong Point': 'Wrong Point', + 'Wrong label': 'Wrong label', + 'Not Fit': 'Not Fit', + Duplicate: 'Duplicate', + Uncertain: 'Uncertain', + Discussion: 'Discussion', + AddReply: 'Add a new reply…', + msgChar: 'Input should be less than 500 characters', + filterByCreator: 'Filter By Creator', + filterByStage: 'Filter By Stage', + filterByType: 'Filter By Error Type', + you: 'You', + 'next point:': 'Next Point: ', + point: 'point', + shareFailed: 'Partial polygon({polygons}) sharing failed', + KeyPoints: 'KeyPoints', + 'Equidistant Skeleton': 'Equidistant Skeleton', + 'In order': 'In order', + Custom: 'Custom', + Curve: 'Curve', + Equal: 'Equal', + Creation: 'Creation', + 'Create new when complete': 'Create new when complete', + 'Canvas Skeleton Setting': 'Canvas Skeleton Setting', + 'Show Series Number': 'Show Series Number', + 'Show Attribute': 'Show Attribute', + 'Sample Skeleton Setting': 'Sample Skeleton Setting', + successResolved: 'Resolved', + successRestored: 'Restored', + successFixed: 'Fixed', + imageSmoothing: 'Image smoothing', + 'Shared Edge': 'Shared Edge', + 'By Edges': 'By Edges', + 'By Points': 'By Points', + 'Show annotation sequence': 'Show annotation sequence', + 'Mark as Invalid': 'Mark as Invalid', + 'Mark as valid': 'Mark as valid', + Skip: 'Skip', + 'This is last data': 'This is last data', + Update: 'Update', + Reminder: 'Reminder', + submitTips: `you don't have any annotation yet, are you sure you want to submit this data? If you can't annotate this data, you'd better mark this data as invaild.`, + 'Well Done!': 'Well Done!', + 'You have finish all the annotation!': 'You have finish all the annotation!', + 'Close and release those data': 'Close and release those data', +}; +export default text; diff --git a/frontend/image-tool/src/locales/lang/en.ts b/frontend/image-tool/src/locales/lang/en.ts deleted file mode 100644 index 6fc47748..00000000 --- a/frontend/image-tool/src/locales/lang/en.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { genMessage } from '../helper'; -import antdLocale from 'ant-design-vue/es/locale/en_US'; -// import momentLocale from 'moment/dist/locale/en-us'; - -const modules = import.meta.globEager('./en/**/*.ts'); -export default { - message: { - ...genMessage(modules, 'en'), - antdLocale, - }, - momentLocale: null, - momentLocaleName: 'en', -}; diff --git a/frontend/image-tool/src/locales/lang/en/action.ts b/frontend/image-tool/src/locales/lang/en/action.ts deleted file mode 100644 index 217f8816..00000000 --- a/frontend/image-tool/src/locales/lang/en/action.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - createSuccess: 'Create Success', - deleteSuccess: 'Delete Success', - renameSuccess: 'Rename Success', -}; diff --git a/frontend/image-tool/src/locales/lang/ko-KR.ts b/frontend/image-tool/src/locales/lang/ko-KR.ts new file mode 100644 index 00000000..15646fe0 --- /dev/null +++ b/frontend/image-tool/src/locales/lang/ko-KR.ts @@ -0,0 +1,504 @@ +import { ILocale } from './type'; + +const text: ILocale = { + 'load-point': '로드 중....', + 'load-class-error': '클래스 로드 오류', + 'load-model-error': '모델 로드 오류', + 'load-dataset-classification-error': '데이터셋 분류 로드 오류', + 'load-record-error': '기록 로드 오류', + 'load-resource-error': '리소스 로드 오류', + 'load-object-error': '객체 로드 오류', + 'load-classification-error': '분류 로드 오류', + 'load-frame-series-error': '씬 데이터 로드 오류', + 'invalid-query': '요청 무효', + 'load-error': '로드 오류', + 'load-comment-error': '코멘트 로드 오류', + 'save-ok': '저장 성공', + 'save-error': '저장 오류', + 'success-submit': '제출 성공', + 'success-accept': '검수 성공', + 'success-reject': '반려 성공', + 'model-run-error': '모델 실행 오류', + 'model-run-no-data': '모델 결과 없음', + 'no-point-data': 'PointCloud 데이터 없음', + 'play-error': '재생 오류', + 'unknown-error': '오류', + 'network-error': '네트워크 오류', + 'login-invalid': '로그인 무효', + 'not-login': '로그인되지 않음', + 'copy-ok': '복사 성공', + 'no-valid-data': '유효한 데이터 없음', + 'no-data': '데이터 없음', + 'title-reject': '반려', + 'title-claiming': '데이터 수령 중...', + 'title-noData-claim': '수령할 데이터가 없습니다.', + 'confirm-title': '확인', + 'delete-title': '삭제', + 'delete-object': '객체를 삭제하시겠습니까?', + 'frame-title': '프레임', + 'result-incomplete': '결과 {results}가 완료되지 않았습니다. 먼저 완료하세요.', + 'result-exceedLimit': '결과 {results}가 클래스 한도를 초과했습니다.', + 'result-need-attributes': + '결과 {results} 는 수동으로 라벨을 지정해야 하는 속성이므로, 먼저 수정 작업을 수행해야 합니다.', + 'Frame need classifications': '프레임 {results} 에는 분류가 필요합니다. 먼저 수정해야 합니다.', + 'data need classifications': '데이터에는 분류가 필요합니다. 먼저 수정해야 합니다.', + confirm: '확인', + cancel: '취소', + Cancel: '취소', + delete: '삭제', + Delete: '삭제', + hotKey: '단축키', + save: '저장', + Save: '저장', + Close: '닫기', + Release: '해제', + Discard: '취소', + groupInvalidTips: '한 결과는 다중 그룹에서 지원되지 않습니다.', + splitGroup: '객체를 분할 및 삭제 하시겠습니까?', + polygonPlusDrawTips: '{pointsLimit}포인트가 필요합니다.', + hollowConditionTips: '적합하지 않은 중앙 구멍 조건', + clipConditionTips: '선택한 결과가 중앙 구멍 조건에 맞지 않습니다.', + clipCannotTips: '선택한 결과는 중앙 구멍 내기 기능에 적용할 수 없습니다.', + changeTips: + '{pre} 라벨을 {cur} 라벨로 변경하시겠습니까? 이 변경으로 인해 포인트 수량 조정이 필요합니다.', + 'track-object-title': '객체 추적', + 'track-no-source': '추적 객체 없음', + 'load-track': '추적 중....', + 'track-no-data': '추적 가능한 객체를 찾을 수 없습니다. 객체의 위치와 방향을 확인하세요.', + 'track-error': '추적 오류', + 'track-ok': '추적 성공', + 'track-invalid': '현재 결과 유형은 모델 추적을 지원하지 않습니다.', + validity: '유효성', + classification: '분류', + results: '결과', + changeMode: '{type} 모드로 변경', + list: '리스트', + layer: '레이어', + comments: '코멘트', + timeLine: '타임라인', + autoLoad: '자동 로드', + copyLeft1: '한 프레임 앞으로 복사하기 (Alt+←)', + copyRight1: '한 프레임 뒤로 복사하기 (Alt+→)', + speedDown: '속도 감소({n}배)', + speedUp: '속도 증가({n}배)', + merge: '병합', + settingTrack: '객체 추적', + objectTracking: '객체 추적', + allObjects: '모든 객체', + selectObjects: '선택한 객체', + method: '방법', + object: '객체', + left: '앞으로', + right: '뒤로', + direction: '방향', + method_copy: '복사', + frameNumber: '프레임 번호', + Frames: '프레임', + mergeFrom: '병합 오기', + mergeTo: '병합 가기', + target: '대상', + split: '분할', + invisible: '안 보임', + invalid: '유효하지 않은 클래스', + mark: '표시', + 'class-recent': '최근 클래스', + 'class-other': '다른 클래스', + replay: '재생', + pre: '이전(←)', + next: '다음(→)', + play: '재생({n}배)', + pause: '일시정지', + switch_on: '켜기', + switch_off: '끄기', + selectObject: '객체 선택', + newBadge: 'New', + noPlayData: '재생 데이터 없음', + newTrackObject: '새로운 추적 객체', + warnEmptyTarget: '대상 프레임에 데이터가 없습니다', + warnTrackObjectEmpty: '추적 대상이 없습니다', + warnNoObject: '객체를 선택해주세요', + successTrack: '추적 성공', + successCopy: '복사 성공', + merge_error_object_not_exist: '오류! 추적 대상 객체가 존재하지 않습니다.', + merge_error_object_repeat: '충돌이 감지되었습니다. 타임라인에서 확인해주세요.', + merge_error_class_none: '클래스 없는 객체는 병합할 수 없습니다.', + merge_error_class_diff: '클래스 일치하지 않은 객체는 병합할 수 없습니다.', + successMerge: '병합 성공', + errorMerge: '병합 실패, 다시 시도해주세요.', + warnEmptyObject: '빈 객체를 생성할 수 없습니다.', + successDelete: '삭제 성공', + successSplit: '분할 성공', + errorSplit: '분할 실패, 다시 시도해주세요.', + errorDelete: '삭제 실패, 다시 시도해주세요.', + btnCancelText: '취소', + btnOkText: '확인', + btnDelete: '삭제', + btnReview: '리뷰', + btnCloseReview: '리뷰 닫기', + deleteTitle: '모든 프레임에서 추적 대상 객체를 삭제합니다.', + class: '클래스', + 'Search Class': '클래스 검색', + titleHelp: '도움말', + Instruction: '설명', + titleWorkFlow: '워크플로우', + titleFlow: '워크플로우 상세', + titleData: '데이터 상세', + titleBack: '뒤로', + titleClose: '닫기', + Fullscreen: '전체화면', + ExitFullscreen: '전체화면 종료', + titleSaveChange: '변경사항 저장', + titleHotKeys: '단축키', + msgSaveChange: '변경사항을 저장하시겠습니까?', + 'Save Changes': '변경 사항 저장', + btnReject: '반려', + btnModify: '수정', + btnQuitModify: '수정 종료', + btnAccept: '검수', + btnSubmit: '제출', + btnSave: '저장', + btnPass: '통과', + btnClaim: '수령', + btnR_E: '반려 및 종료', + btnR_C: '반려 및 수령', + btnP_E: '통과 및 종료', + btnP_C: '통과 및 수령', + btnS_E: '제출 및 종료', + btnS_C: '제출 및 수령', + msgSample: + '샘플링되지 않은 프레임입니다. 이 프레임에 대한 코멘트는 샘플링 정확도에 포함되지 않습니다.', + titleAnnotate: '작업', + titleReview: '리뷰', + titleAcceptance: '검수', + titleQA: '품질 검사', + titleView: '보기', + titleModify: '수정', + titlePausedFor: '일시정지', + titleDueIn: '만료 시간', + titlePause: '일시정지', + titleRestart: '다시 시작', + msgOutTime: + '수령한 ({ n } 데이터) { t } 후 만료됩니다. 만료 후 데이터는 모두 태스크 풀에 반환됩니다.', + msgAgain: '다시 수령하세요.', + msgPaused: '수령한 데이터가 일시정지되었습니다.', + msgOver2M: '2분 동안 작업이 없습니다.', + msgClickStart: '계속 작업하려면 버튼을 {btn} 클릭하십시오.', + msgClaimMsg: '수령한 데이터가 일시정지되었습니다. 버튼을 클릭하여 작업을 계속하십시오.', + msgExpire: '수령한 데이터의 만료일이 다가오고 있습니다.', + msgExpired: '수령한 데이터가 곧 만료됩니다.', + 'Track Object': '추적 객체', + Status: '상태', + 'Track ID': '추적 ID', + 'Mark All as Ground Truth': '모두 Ground Truth로 표시', + Attribute: '속성', + Objects: '객체', + copy_attrs_from: '이 프레임의 다른 객체에서 속성 복사하기', + copy_attrs_from_frames: '해당 속성을 이 추적 객체의 다른 프레임에 복사하기', + copy_attrs_from_frames_tips: '(원래 속성을 덮어씁니다)', + Copy: '복사', + 'Copy To Frames': '프레임에 복사', + Advanced: '고급', + 'Split from Current Frame': '현재 프레임에서 분할', + 'New Tracking Object': '새 추적 객체', + 'All Frames': '모든 프레임', + 'Some Frames': '일부 프레임', + 'All Non-True Values': '모든 모델 결과', + deleteFromAllFrames: '모든 프레임에서 이 추적 객체를 삭제합니다.', + deleteFromSomeFrames: '뒤로 모든 프레임에서 이 추적 대상 객체를 삭제합니다.', + deleteAllUntrue: '이 추적 대상 객체의 모든 모델 결과를 삭제합니다.', + Undo: '실행 취소', + Redo: '재실행', + Hollow: '중앙 구멍 내기', + Crop: '자르기', + Drag: '드래그', + Group: '그룹', + Ungroup: '그룹 해제', + 'Create group': '그룹 생성', + 'Dissolve group': '그룹 해체', + editTips: '크기 조정', + rectTips: '사각형', + polygonTips: '폴리곤', + lineTips: '폴리라인', + pointTips: '키포인트', + curveTips: '스플라인 곡선', + skeletonTips: '스켈레톤', + groupTips: '그룹 생성', + modelTips: '스마트 에디터', + Model: '모델', + 'AI Annotation Setting': 'AI 작업 설정', + 'Predict all in Model': '모두 모델로 예측하기', + Confidence: '신뢰도', + interactiveTips: '자동인식 에디터', + 'Interactive Model Setting': '자동인식 모델 설정', + Smoothness: '부드러움', + commentTips: '코멘트 에디터', + hollowTips: '중앙 구멍 내기 H ', + cancelHollowTips: '중앙 구멍 내기 취소', + cutNotFirstTips: '첫 번째 객체를 자르지 않음', + cutFirstTips: '첫 번째 객체 자르기', + fill: '채우기', + Contour: '실루엣', + 'Page Up': '이전', + 'Page Down': '다음', + 'Input keyword': '검색어 입력', + 'Delete instance/point': '인스턴스/포인트 삭제', + 'Delete Mask': '마스크 삭제', + 'Finish drawing': '완료', + 'Show/hide tag pad': '태그 패드 보이기/숨기기', + 'Move the top edge 1px': '위쪽 가장자리 1px 이동', + 'Move the bottom edge 1px': '아래쪽 가장자리 1px 이동', + 'Move the left edge 1px': '왼쪽 가장자리 1px 이동', + 'Movem the right edge 1px': '오른쪽 가장자리 1px 이동', + 'Move the object 1px': '객체 1px 이동', + 'Parallel result Edge': '평행 결과 엣지', + 'Show Single Result': '단일 결과 보이기', + 'Show Result': '결과 보기', + 'Switch Results': '결과 스위치', + 'Show Result Number': '결과 번호 보이기', + 'Show Comments': '코멘트 보이기', + 'Show Class': '클래스 보이기', + 'Show label and attributes': '클래스 및 속성 보이기', + 'Normal/Outline Select': '일반/실루엣 선택', + 'Zoom in、Zoom Out': '확대/축소', + 'Cancel window': '취소', + Data: '데이터', + Instance: '인스턴스', + Tool: '툴', + Result: '결과', + Image: '이미지', + Else: '기타', + 'Selection Tool': '선택 툴', + 'Rectangle Tool': '사각형 툴', + 'Polygon Tool': '폴리곤 툴', + 'Polyline Tool': '폴리라인 툴', + 'KeyPoint Tool': '키포인트 툴', + 'SplineCurve Tool': '스플라인 곡선 툴', + 'Skeleton Tool': '스켈레톤 툴', + 'Group Tool': '그룹 툴', + 'Smart Tool': '스마트 툴', + 'Interactive Tool': '자동인식 툴', + 'Comment Tool': '코멘트 툴', + Info: '정보', + 'Data info': '데이터 정보', + Name: '이름', + Width: '너비', + Height: '높이', + Length: '길이', + Area: '면적', + Size: '크기', + 'Map Classes': '클래스 매핑', + 'Model Classes': '모델 클래스', + Reset: '초기화', + 'Apply and Run': '적용 및 실행', + Apply: '적용', + 'Select all': '모두 선택', + 'Unselect all': '모두 선택 해제', + Setting: '설정', + Brightness: '밝기', + Contrast: '대조도', + Opacity: '불투명도', + 'Border Width': '테두리 너비', + 'Show Size': '크기 보이기', + 'Show Attr': '속성 보이기', + Comment: '코멘트', + Other: '기타', + 'Auxiliary Line': '보조선', + 'Auxiliary Shape': '보조원', + radius: '반지름', + px: '픽셀', + BisectrixLine: '이등분선', + horizontal: '수평', + vertical: '수직', + 'Display Mode': '디스플레이 모드', + 'Do you want to save changes?': '변경 내용을 저장하시겠습니까?', + 'Results Source': '결과 소스', + Valid: '유효', + Invalid: '유효하지 않음', + Unknown: '알 수 없음', + Filter: '필터', + 'Please fill in as required': '규칙 대로 작성해주세요.', + Open: '열기', + Resolved: '해결됨', + 'Fix All': 'Fix All', + Fixed: 'Fixed', + 'Resolve All': '모두 해결', + 'Are you sure to delete?': '삭제하시겠습니까?', + 'View All': '전체 보이기', + Type: '유형', + 'Reply Comment': '댓글 달기', + 'Task ID': '태스크 ID', + 'Task Name': '태스크 이름', + 'Data ID': '데이터 ID', + 'Data Name': '데이터 이름', + 'Data Size': '데이터 크기', + Attachment: '첨부파일', + 'Open in New Tab': '새 탭에서 열기', + 'the browser does not support copying': '해당 브라우저는 복사를 지원하지 않습니다', + 'Target Stage': '목표 단계', + Worker: '작업자', + 'Who submitted this work': '이 작업을 제출한 사람에게', + New: 'New', + 'Assign to any other worker in the stage': '단계 내 다른 작업자에게 할당', + 'Keep results': '결과 유지', + 'Keep all results': '모든 결과 유지', + 'Clear results': '결과 지우기', + 'Clear all ground truth results': '모든 Ground Truth 결과 및 성과 지우기', + 'Reject Reasons': '반려 이유', + 'Delete {num} Objects?': '객체 {num}번을 삭제하시겠습니까?', + 'No Attritube': '속성 없음', + 'Class Invalid': '무효 클래스', + language: '언어', + 'en-US': '영어', + 'zh-CN': '중국어', + 'ko-KR': '한국어', + not_show_anymore: '이 컨텐트는 더 이상 표시되지 않습니다.', + class_change_warn: + '이 추적 객체를 {type}으로 라벨링하면 모든 속성이 변경됩니다. 변경하시겠습니까?', + 'Width x height': '너비 x 높이', + Retry: '재시도', + Ok: '확인', + Refresh: '새로고침', + 'Polygon shared edge by points drawing mode': '공유 엣지 다각형/폴리라인 포인트 모드.', + 'Switch shared edge': '공유 엣지 전환', + 'Not True-Value': '모델 결과', + 'Object of Frame': '{index}번째 프레임의 객체', + 'No Object': '객체 없음', + 'Show All Objects': '모든 객체 보이기', + '{num} selected': '{num} 선택됨', + 'Class Required': '클래스 필수', + Warning: '알림', + taskReviewCloseTips: + '변경 사항을 저장하고 데이터를 유지하시겠습니까? 또는 작업 풀에 릴리스하시겠습니까?', + Segmentation: '세그멘테이션', + reload: '새로고침', + 'Fail to load resource, please reload': '리소스를 로드하지 못했습니다. 새로고침하세요.', + 'Switch alias/name': '별명/클래스 이름 보이기', + 'Polygon shared edge by edges drawing mode': '공유 엣지 다각형/폴리라인 엣지 모드.', + 'Shared edge polygon by points': '공유 엣지 다각형/폴리라인 포인트 모드', + 'Shared edge polygon by edges': '공유 엣지 다각형/폴리라인 엣지 모드', + 'Switch to view/edit Instance': '인스턴스 결과 보기/편집 모드로 전환되었습니다', + 'Switch to view/edit Segmentation': '세그멘테이션 결과 보기/편집 모드로 전환되었습니다', + cuboidTips: '큐보이드', + 'Cuboid Tool': '큐보이드 툴', + 'Segmentation Tool': '마스크 생성', + 'Load Sources': '로드 소스', + 'Editing Source': '편집 소스', + 'No data available for now': '해당 데이터 없습니다.', + 'You have completed all claimed data': '수령한 모든 데이터가 작업 완료했습니다. ', + 'There is no data in your current queue.': '현제 기다릴 데이터가 없습니다.', + reClaimTips: + '{ pre } You can either click "ReClaim" to claim more data in the task pool, or close the current page.', + msgNormalCompleted: 'You have completed all the data in current task.', + msgSuspendCompleted: 'You have completed all the suspend data in current task.', + msgReReviewCompleted: 'You have completed all the re-review data in current task.', + msgRejectedCompleted: 'You have completed all the rejected in current task.', + 'Re-Claim': '재수령', + 'Claim Data': '수령', + continueClaimTips: '작업 가능한 데이터가 없습니다.수령하시겠습니까? ', + msgResolveAll: '모든 코멘트를 처리하시겠습니까?', + msgFixAll: 'Are you sure to fix all comments?', + Resolve: '해결', + unResolve: 'Cancel Resolve', + Fix: 'Fix', + unFix: 'Cancel Fix', + Restore: '복구', + 'You have already claimed {num} data.': '{num} 개 데이터가 수령되었습니다.', + 'msg-dataflow-qaerror': '', + 'Switch mark/mask': 'mark/mask 전환', + 'Panoramic Tool': '자동 툴', + panoramicTips: '자동 툴', + 'Intellect Tool': '스마트 툴자동 툴', + intellectTips: '스마트 툴', + 'warn-addModel': '모델 결과를 추가하면 작업한 포인트가 커버 될 수 있습니다. 계속하시겠습니까?', + 'Add Results?': '계속하시겠습니까?', + 'Circle Tool': '원', + circleTips: '원', + 'Ellipse Tool': '타원', + ellipseTips: '타원', + 'brush tips': '브러시', + 'Brush Tool': '브러시 툴', + 'Brush width': '브러시 너비', + 'Fill Tool': '채우기 툴', + 'mask fill tips': '채우기', + Boundary: '초화소', + Finer: '정밀', + Coarser: '대략', + titleFrame: '데이터', + titleTask: '태스크', + titleScene: '씬', + titleSceneId: '씬 ID', + titleSceneName: '씬 이름', + titleSceneIndex: '현재 프레임 인덕스', + btnSaveQuit: '저장 및 종료', + 'Switch Tool': '툴 변경', + 'Add Region': '구역 추가', + 'Crop Region': '구역 감수', + 'The model is running...': '로딩 중...', + cover: '커버', + Actions: '작업', + 'Workflow Detail': '워크플로우 상세', + 'Reject Info': '반려 정보', + 'No Info': '정보 없음', + 'Show Auxiliary Line': '보조선 표시', + 'Show Auxiliary Shape': '보조 원 표시', + 'Show BisectrixLine': '이등분선 표시', + 'show points': '키포인트 보이기', + resultNotComplete: '미완성', + 'Add a circle': 'Add a circle', + 'Add a rectangle': 'Add a rectangle', + 'Horizontal Drawing Model': 'Horizontal Drawing Model', + 'Vertical Drawing Model': 'Vertical Drawing Model', + 'Crop by selected area': 'Crop by selected area', + 'Show Group Result': 'Show Group Result', + titleAnnotator: 'Assignee Annotator', + noneText: '없음', + all: '모두', + Missed: 'Missed', + 'Wrong Object': 'Wrong Object', + 'Wrong Point': 'Wrong Point', + 'Wrong label': 'Wrong label', + 'Not Fit': 'Not Fit', + Duplicate: 'Duplicate', + Uncertain: 'Uncertain', + Discussion: 'Discussion', + AddReply: 'Add a new reply…', + msgChar: '입력은 500자 미만이어야 합니다', + filterByCreator: '생성자별 필터링하기', + filterByStage: '단계별 필터링하기', + filterByType: '오류 유형별 필터링하기', + you: '나의 코멘트', + 'next point:': 'next point: ', + point: 'point', + shareFailed: 'Partial polygon({polygons}) sharing failed', + KeyPoints: 'KeyPoints', + 'Equidistant Skeleton': 'Equidistant Skeleton', + 'In order': 'In order', + Custom: 'Custom', + Curve: 'Curve', + Equal: 'Equal', + Creation: 'Creation', + 'Create new when complete': 'Create new when complete', + 'Canvas Skeleton Setting': 'Canvas Skeleton Setting', + 'Show Series Number': 'Show Series Number', + 'Show Attribute': 'Show Attribute', + 'Sample Skeleton Setting': 'Sample Skeleton Setting', + successResolved: 'Resolved', + successRestored: 'Restored', + successFixed: 'Fixed', + imageSmoothing: 'Image smoothing', + 'Shared Edge': 'Shared Edge', + 'By Edges': 'By Edges', + 'By Points': 'By Points', + 'Show annotation sequence': 'Show annotation sequence', + 'Mark as Invalid': 'Mark as Invalid', + 'Mark as valid': 'Mark as valid', + Skip: 'Skip', + 'This is last data': 'This is last data', + Update: 'Update', + Reminder: 'Reminder', + submitTips: `you don't have any annotation yet, are you sure you want to submit this data? If you can't annotate this data, you'd better mark this data as invaild.`, + 'Well Done!': 'Well Done!', + 'You have finish all the annotation!': 'You have finish all the annotation!', + 'Close and release those data': 'Close and release those data', +}; +export default text; diff --git a/frontend/image-tool/src/locales/lang/type.ts b/frontend/image-tool/src/locales/lang/type.ts new file mode 100644 index 00000000..9651c483 --- /dev/null +++ b/frontend/image-tool/src/locales/lang/type.ts @@ -0,0 +1,3 @@ +import en from './en-US'; + +export type ILocale = typeof en; diff --git a/frontend/image-tool/src/locales/lang/zh-CN.ts b/frontend/image-tool/src/locales/lang/zh-CN.ts new file mode 100644 index 00000000..694036f8 --- /dev/null +++ b/frontend/image-tool/src/locales/lang/zh-CN.ts @@ -0,0 +1,498 @@ +import { ILocale } from './type'; + +const text: ILocale = { + 'load-point': '加载中...', + 'load-class-error': '标签加载错误', + 'load-model-error': '模型加载错误', + 'load-dataset-classification-error': '数据集分类加载错误', + 'load-record-error': '记录加载错误', + 'load-resource-error': '资源加载错误', + 'load-object-error': '标注结果加载错误', + 'load-classification-error': '分类加载错误', + 'load-frame-series-error': '连续帧加载错误', + 'invalid-query': '查询无效', + 'load-error': '加载错误', + 'load-comment-error': '评论加载错误', + 'save-ok': '保存成功', + 'save-error': '保存失败', + 'success-submit': '提交成功', + 'success-accept': '验收通过', + 'success-reject': '驳回成功', + 'model-run-error': '模型运行出错', + 'model-run-no-data': '模型运行无结果', + 'no-point-data': '无数据', + 'play-error': '播放错误', + 'unknown-error': '未知错误', + 'network-error': '网络错误, 请检查您的网络', + 'login-invalid': '登录无效', + 'not-login': '未登陆', + 'copy-ok': '复制成功', + 'no-valid-data': '无有效数据', + 'no-data': '暂无数据', + 'title-reject': '驳回', + 'title-claiming': '领取', + 'title-noData-claim': '没有可领取的数据', + 'confirm-title': '确认', + 'delete-title': '删除', + 'delete-object': '删除结果', + 'frame-title': '当前帧', + 'result-incomplete': '以下结果未完成:{results}, 请先完成后再操作。', + 'result-exceedLimit': '以下结果超出标签限制:{results},请先规范结果后再操作。', + 'result-need-attributes': '结果{results},缺失人工标签或必须属性。请修改。', + 'Frame need classifications': '第 {results} 缺失分类标签。请修改。', + 'data need classifications': '当前数据缺失分类标签。请修改。', + confirm: '确认', + cancel: '取消', + Cancel: '取消', + delete: '删除', + Delete: '删除', + hotKey: '快捷键', + save: '保存', + Save: '保存', + Close: '关闭', + Release: '释放', + Discard: '放弃', + groupInvalidTips: '多选组不支持只有一个结果', + splitGroup: '解散并删除该组?', + polygonPlusDrawTips: '必须绘制{pointsLimit}个点', + hollowConditionTips: '不符合镂空条件', + clipConditionTips: '不符合裁剪条件', + clipCannotTips: '选择的结果不能进行裁剪', + changeTips: '确认切换{pre}为{cur}吗?如果切换,则会强制自动调整骨骼点。', + 'track-object-title': '追踪对象', + 'track-no-source': '无追踪对象', + 'load-track': '追踪加载中...', + 'track-no-data': '未找到追踪对象', + 'track-error': '追踪失败', + 'track-ok': '追踪成功', + 'track-invalid': '当前对象不支持进行模型追踪', + validity: '有效性', + classification: '分类', + results: '结果', + changeMode: '切换为{type}模式', + list: '列表', + layer: '图层', + comments: '评论', + timeLine: '时间轴', + autoLoad: '自动加载', + copyLeft1: '向左复制一帧(Alt+←)', + copyRight1: '向右复制一帧(Alt+→)', + speedDown: '{n} 倍速', + speedUp: '{n} 倍速', + merge: '合并', + settingTrack: '追踪设置', + objectTracking: '追踪/复制', + allObjects: '全部结果', + selectObjects: '选中结果', + method: '方式', + object: '目标', + left: '向左', + right: '向右', + direction: '方向', + method_copy: '复制', + frameNumber: '帧数', + Frames: '帧数', + mergeFrom: '合并从', + mergeTo: '合并到', + target: '目标', + split: '拆分', + invisible: '消失', + invalid: '标签限制', + mark: '批注', + 'class-recent': '最近使用标签', + 'class-other': '其他', + replay: '重新播放', + pre: '上一帧(←)', + next: '下一帧(→)', + play: '播放({n} 倍速)', + pause: '暂停', + switch_on: '开', + switch_off: '关', + selectObject: '选择一个对象', + newBadge: '新', + noPlayData: '无播放数据', + newTrackObject: '新追踪对象', + warnEmptyTarget: '无目标帧数据', + warnTrackObjectEmpty: '无追踪对象', + warnNoObject: '请选择一个对象', + successTrack: '追踪成功', + successCopy: '复制成功', + merge_error_object_not_exist: '合并对象不存在', + merge_error_object_repeat: '存在合并冲突,请在时间轴里查看', + merge_error_class_none: '不能合并没有标签的对象', + merge_error_class_diff: '不能合并不同标签', + successMerge: '合并成功', + errorMerge: '合并失败, 请重试', + warnEmptyObject: '不能创建空对象', + successDelete: '删除成功', + successSplit: '拆分成功', + errorSplit: '拆分失败, 请重试', + errorDelete: '删除失败, 请重试', + btnCancelText: '取消', + btnOkText: '确认', + btnDelete: '删除', + btnReview: '检查', + btnCloseReview: '关闭检查', + deleteTitle: '您将在所有帧中删除此追踪对象。', + class: '标签', + 'Search Class': '搜索标签', + titleHelp: '帮助', + Instruction: '说明', + titleWorkFlow: '工作流', + titleFlow: '工作流', + titleData: '数据详情', + titleBack: '返回', + titleClose: '关闭', + Fullscreen: '全屏', + ExitFullscreen: '退出全屏', + titleSaveChange: '保存修改', + titleHotKeys: '快捷键', + msgSaveChange: '是否保存修改?', + 'Save Changes': '保存修改', + btnReject: '驳回', + btnModify: '修改', + btnQuitModify: '退出修改', + btnAccept: '验收通过', + btnSubmit: '提交', + btnSave: '保存', + btnPass: '通过', + btnClaim: '领取', + btnR_E: '驳回 & 退出', + btnR_C: '驳回 & 领取', + btnP_E: '通过 & 退出', + btnP_C: '通过 & 领取', + btnS_E: '提交 & 退出', + btnS_C: '提交 & 领取', + msgSample: '你正在查看未被抽样的帧。这一帧的评论不会计入抽样准确率。', + titleAnnotate: '标注', + titleReview: '审核', + titleAcceptance: '验收', + titleQA: '质检', + titleView: '浏览', + titleModify: '修改', + titlePausedFor: '已挂起', + titleDueIn: '过期时间', + titlePause: '暂停', + titleRestart: '继续', + msgOutTime: '你领取的数据(剩余 {n} 条)将在 {t} 内过期。过期后,它们将全部被释放到任务池中。', + msgAgain: '请重新领取', + msgPaused: '领取的数据现已暂停。', + msgOver2M: '你已经有 2 分钟没有操作了。', + msgClickStart: '点击按钮{btn}继续工作。', + msgClaimMsg: '领取的数据现已暂停。点击按钮继续工作。', + msgExpire: '领取的作业即将过期', + msgExpired: '领取的作业已过期', + 'Track Object': '追踪对象', + Status: '状态', + 'Track ID': '追踪对象ID', + 'Mark All as Ground Truth': '全部帧标为人工标注', + Attribute: '属性', + Objects: '对象', + copy_attrs_from: '从当前帧的其他相同标签的对象复制属性', + copy_attrs_from_frames: '将该对象的标签属性复制到其他帧该结果上', + copy_attrs_from_frames_tips: '(将会覆盖目标对象的原有标签属性)', + Copy: '复制', + 'Copy To Frames': '选择需要复制的帧', + Advanced: '高级', + 'Split from Current Frame': '从当前帧开始拆分', + 'New Tracking Object': '新的追踪对象', + 'All Frames': '所有帧', + 'Some Frames': '某些帧', + 'All Non-True Values': '所有非真值的对象', + deleteFromAllFrames: '删除所有帧中的该追踪对象。', + deleteFromSomeFrames: '删除选择帧中的该追踪对象。', + deleteAllUntrue: '删除所有帧中该追踪对象非真值的对象。', + Undo: '撤销', + Redo: '执行', + Hollow: '镂空', + Crop: '裁剪', + Drag: '拖动', + Group: '组', + Ungroup: '取消编组', + 'Create group': '创建组', + 'Dissolve group': '解散组', + editTips: '编辑', + rectTips: '矩形', + polygonTips: '多边形', + lineTips: '折线', + pointTips: '关键点', + curveTips: '曲线', + skeletonTips: '骨骼点', + groupTips: '组', + modelTips: '模型', + Model: '模型', + 'AI Annotation Setting': '智能标注设置', + 'Predict all in Model': '使用模型所有标签进行识别', + Confidence: '置信度', + interactiveTips: '交互式识别', + 'Interactive Model Setting': '交互式模型设置', + Smoothness: '平滑度', + commentTips: '评论', + hollowTips: '镂空', + cancelHollowTips: '取消镂空', + cutNotFirstTips: '裁剪非第一个选中的结果', + cutFirstTips: '裁剪选中的第一个结果', + fill: '填充', + Contour: '轮廓', + 'Page Up': '上一页', + 'Page Down': '下一页', + 'Input keyword': '输入关键字', + 'Delete instance/point': '删除结果/点', + 'Delete Mask': '删除结果', + 'Finish drawing': '完成绘制', + 'Show/hide tag pad': '展示/隐藏标签卡片', + 'Move the top edge 1px': '移动上边框1像素', + 'Move the bottom edge 1px': '移动下边框1像素', + 'Move the left edge 1px': '移动左边框1像素', + 'Movem the right edge 1px': '移动右边框1像素', + 'Move the object 1px': '移动目标结果1像素', + 'Parallel result Edge': '平行边', + 'Show Single Result': '只显示选中的结果', + 'Show Result': '展示结果', + 'Switch Results': '切换选中结果', + 'Show Result Number': '显示结果序号', + 'Show Comments': '显示评论', + 'Show Class': '显示标签', + 'Show label and attributes': '显示结果属性值', + 'Normal/Outline Select': '选中模式: 正常/轮廓', + 'Zoom in、Zoom Out': '缩放', + 'Cancel window': '取消/关闭弹窗', + Data: '数据', + Instance: '实例', + Tool: '工具', + Result: '结果', + Image: '图片', + Else: '其他', + 'Selection Tool': '编辑工具', + 'Rectangle Tool': '矩形工具', + 'Polygon Tool': '多边形工具', + 'Polyline Tool': '折线工具', + 'KeyPoint Tool': '关键点工具', + 'SplineCurve Tool': '曲线工具', + 'Skeleton Tool': '骨骼工具', + 'Group Tool': '组工具', + 'Smart Tool': '模型工具', + 'Interactive Tool': '交互式识别工具', + 'Comment Tool': '评论工具', + Info: '信息', + 'Data info': '数据信息', + Name: '名称', + Width: '宽', + Height: '高', + Length: '长', + Area: '面积', + Size: '大小', + 'Map Classes': '映射标签', + 'Model Classes': '模型标签', + Reset: '重置', + 'Apply and Run': '应用并识别', + Apply: '应用', + 'Select all': '选中所有', + 'Unselect all': '取消选中所有', + Setting: '设置', + Brightness: '亮度', + Contrast: '对比度', + Opacity: '透明度', + 'Border Width': '边框宽度', + 'Show Size': '尺寸信息', + 'Show Attr': '属性信息', + Comment: '评论', + Other: '其他', + 'Auxiliary Line': '辅助线', + 'Auxiliary Shape': '辅助工具', + radius: '半径', + px: '像素', + BisectrixLine: '等分线', + horizontal: '水平', + vertical: '竖直', + 'Display Mode': '显示模式', + 'Do you want to save changes?': '是否保存修改?', + 'Results Source': '结果来源', + Valid: '有效的', + Invalid: '无效的', + Unknown: '未知的', + Filter: '筛选', + 'Please fill in as required': '请完善必填项', + Open: '未解决', + Resolved: '已解决', + Fixed: '已修改', + 'Fix All': '全部修改', + 'Resolve All': '全部解决', + 'Are you sure to delete?': '确定删除吗?', + 'View All': '显示全部', + Type: '类型', + 'Reply Comment': '回复评论', + 'Task ID': '任务ID', + 'Task Name': '任务名', + 'Data ID': '数据ID', + 'Data Name': '数据名', + 'Data Size': '数据大小', + Attachment: '附件', + 'Open in New Tab': '在新的标签页中打开', + 'the browser does not support copying': '您的浏览器不支持复制', + 'Target Stage': '目标阶段', + Worker: '人员', + 'Who submitted this work': '谁提交了这项工作', + New: '作业池', + 'Assign to any other worker in the stage': '分配给该阶段的所有人员', + 'Keep results': '保留结果', + 'Keep all results': '保留所有结果', + 'Clear results': '清除结果', + 'Clear all ground truth results': '清除所有真值结果', + 'Reject Reasons': '驳回原因', + 'Delete {num} Objects?': '是否删除这{num}个对象?', + 'No Attritube': '无属性', + 'Class Invalid': '标签无效', + language: '语言', + 'en-US': '英语', + 'zh-CN': '简体中文', + 'ko-KR': '韩语', + not_show_anymore: '不再提示', + class_change_warn: + '设置该追踪对象的标签为{type},将清空该追踪对象在全部帧中已标注的属性信息,是否确认?', + 'Width x height': '长 x 宽', + Retry: '重试', + Ok: '确定', + Refresh: '刷新', + 'Polygon shared edge by points drawing mode': '按点共享边模式。', + 'Switch shared edge': '切换共享边', + 'Not True-Value': '非真值', + 'Object of Frame': '第{index}帧的结果', + 'No Object': '当前帧无该对象', + 'Show All Objects': '展示全部帧结果', + '{num} selected': '已选择{num}个来源', + 'Class Required': '无标签', + Warning: '警告', + taskReviewCloseTips: '你想保存修改并保留数据吗?或不保存结果、将这些数据释放到所属任务池中?', + Segmentation: '分割', + reload: '重新加载', + 'Fail to load resource, please reload': '加载资源失败,请重新加载', + 'Switch alias/name': '切换别名/名称展示', + 'Polygon shared edge by edges drawing mode': '按边共享边绘制模式。', + 'Shared edge polygon by points': '按点共享边模式', + 'Shared edge polygon by edges': '按边共享边模式', + 'Switch to view/edit Instance': '切换到 查看/编辑 实例', + 'Switch to view/edit Segmentation': '切换到 查看/编辑 分割', + cuboidTips: '伪3D框', + 'Cuboid Tool': '伪3D框工具', + 'Segmentation Tool': '分割工具', + 'Load Sources': '加载来源', + 'Editing Source': '编辑来源', + 'No data available for now': '无有效数据', + 'You have completed all claimed data': '您已完成所有领取的数据', + 'There is no data in your current queue.': '当前队列无数据', + reClaimTips: '{ pre } 点击“重新领取”按钮领取作业池数据,或者关闭当前页面', + msgNormalCompleted: '你已经完成当前任务的所有数据', + msgSuspendCompleted: '你已经完成当前任务的所有挂起数据', + msgReReviewCompleted: '你已经完成当前任务的所有待复审数据', + msgRejectedCompleted: '你已经完成当前任务的所有被驳回数据', + 'Re-Claim': '重新领取', + 'Claim Data': '领取数据', + continueClaimTips: '无有效数据,是否继续领取?', + msgResolveAll: '是否确认解决全部评论?', + msgFixAll: '确定将所有评论标记为已修改吗?', + Resolve: '解决', + unResolve: '撤销已解决', + Fix: '修改', + unFix: '撤销已修改', + Restore: '恢复', + 'You have already claimed {num} data.': '成功领取{num}数据', + 'msg-dataflow-qaerror': '您的结果违反了质检强制性规则,是否确实要保存此结果?', + 'Switch mark/mask': '切换mark/mask', + 'Panoramic Tool': '全景分割工具', + panoramicTips: '全景分割', + 'Intellect Tool': '智能分割工具', + intellectTips: '智能分割', + 'warn-addModel': '添加模型结果可能会覆盖已标注的结果,是否确认添加?', + 'Add Results?': '是否确认添加?', + 'Circle Tool': '圆工具', + circleTips: '圆', + 'Ellipse Tool': '椭圆工具', + ellipseTips: '椭圆', + 'brush tips': '笔刷', + 'Brush Tool': '笔刷工具', + 'Brush width': '笔刷宽度', + 'Fill Tool': '填充工具', + 'mask fill tips': '填充', + Boundary: '超像素', + Finer: '精细', + Coarser: '粗略', + titleFrame: '数据', + titleTask: '任务', + titleScene: '连续帧', + titleSceneId: '连续帧ID', + titleSceneName: '连续帧名称', + titleSceneIndex: '当前帧', + btnSaveQuit: '保存并退出', + 'Switch Tool': '切换工具', + 'Add Region': '增加区域', + 'Crop Region': '减少区域', + 'The model is running...': '模型运行中, 请稍等...', + cover: '覆盖', + Actions: '操作', + 'Workflow Detail': '工作流', + 'Reject Info': '驳回信息', + 'No Info': '无信息', + 'Show Auxiliary Line': '展示辅助线', + 'Show Auxiliary Shape': '展示辅助工具', + 'Show BisectrixLine': '展示等分线', + 'show points': '展示结果关键点', + resultNotComplete: '未完成', + 'Add a circle': '添加辅助圆', + 'Add a rectangle': '添加辅助矩形', + 'Horizontal Drawing Model': '水平线模式', + 'Vertical Drawing Model': '垂直线模式', + 'Crop by selected area': '按所选区域裁剪', + 'Show Group Result': '展示组结果', + titleAnnotator: '标注员', + noneText: '空', + all: '全部', + Missed: '漏框', + 'Wrong Object': '错误结果', + 'Wrong Point': '错误点', + 'Wrong label': '错误标签', + 'Not Fit': '不贴合', + Duplicate: '重复', + Uncertain: '不确定', + Discussion: '待讨论', + AddReply: '请输入…', + msgChar: '输入应少于500个字符', + filterByCreator: '按创建者筛选', + filterByStage: '按工序筛选', + filterByType: '按错误类型筛选', + you: '你', + 'next point:': '下一个点:', + point: '点', + shareFailed: '部分多边形({polygons})共享失败', + KeyPoints: '关键点', + 'Equidistant Skeleton': '等分', + 'In order': '顺序等分', + Custom: '自定义等分', + Curve: '曲线', + Equal: '等分', + Creation: '创建', + 'Create new when complete': '连续创建新的骨骼点对象', + 'Canvas Skeleton Setting': '画布区骨骼点设置', + 'Show Series Number': '显示骨骼点序号', + 'Show Attribute': '显示骨骼点属性', + 'Sample Skeleton Setting': '模板区骨骼点设置', + successResolved: '已解决', + successRestored: '已撤销', + successFixed: '已修改', + imageSmoothing: '图像平滑处理', + 'Shared Edge': '共享边', + 'By Edges': '按边共享', + 'By Points': '按点共享', + 'Show annotation sequence': '展示标注顺序', + 'Mark as Invalid': 'Mark as Invalid', + 'Mark as valid': 'Mark as valid', + Skip: 'Skip', + 'This is last data': 'This is last data', + Update: 'Update', + Reminder: 'Reminder', + submitTips: `you don't have any annotation yet, are you sure you want to submit this data? If you can't annotate this data, you'd better mark this data as invaild.`, + 'Well Done!': 'Well Done!', + 'You have finish all the annotation!': 'You have finish all the annotation!', + 'Close and release those data': 'Close and release those data', +}; +export default text; diff --git a/frontend/image-tool/src/locales/lang/zh-CN/action.ts b/frontend/image-tool/src/locales/lang/zh-CN/action.ts deleted file mode 100644 index 1d7225e1..00000000 --- a/frontend/image-tool/src/locales/lang/zh-CN/action.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - createSuccess: '创建成功', - deleteSuccess: '删除成功', - renameSuccess: '重命名成功', -}; diff --git a/frontend/image-tool/src/locales/lang/zh_CN.ts b/frontend/image-tool/src/locales/lang/zh_CN.ts deleted file mode 100644 index 3d40cca4..00000000 --- a/frontend/image-tool/src/locales/lang/zh_CN.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { genMessage } from '../helper'; -import antdLocale from 'ant-design-vue/es/locale/zh_CN'; -// import momentLocale from 'moment/dist/locale/zh-cn'; - -const modules = import.meta.globEager('./zh-CN/**/*.ts'); -export default { - message: { - ...genMessage(modules, 'zh-CN'), - antdLocale, - }, - // momentLocale, - momentLocaleName: 'zh-cn', -}; diff --git a/frontend/image-tool/src/locales/setupI18n.ts b/frontend/image-tool/src/locales/setupI18n.ts deleted file mode 100644 index 97a5e569..00000000 --- a/frontend/image-tool/src/locales/setupI18n.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { App } from 'vue'; -import type { I18n, I18nOptions } from 'vue-i18n'; - -import { createI18n } from 'vue-i18n'; -import { setHtmlPageLang, setLoadLocalePool } from './helper'; -// import { localeSetting } from '/@/settings/localeSetting'; -// import { useLocaleStoreWithOut } from '/@/store/modules/locale'; - -// const { fallback, availableLocales } = localeSetting; -import en from './lang/en'; -import zh_CN from './lang/zh_CN'; - -export let i18n: ReturnType; - -async function createI18nOptions(): Promise { - // const localeStore = useLocaleStoreWithOut(); - // const locale = localeStore.getLocale; - let locale = 'zh-CN'; - // const defaultLocal = await import(`./lang/${locale}.ts`); - // const message = defaultLocal.default?.message ?? {}; - - let message = zh_CN; - - // setHtmlPageLang(locale); - // setLoadLocalePool((loadLocalePool) => { - // loadLocalePool.push(locale); - // }); - - return { - legacy: false, - locale, - // fallbackLocale: fallback, - messages: { - [locale]: message, - }, - // availableLocales: availableLocales, - sync: true, //If you don’t want to inherit locale from global scope, you need to set sync of i18n component option to false. - silentTranslationWarn: true, // true - warning off - missingWarn: false, - silentFallbackWarn: true, - }; -} - -// setup i18n instance with glob -export async function setupI18n(app: App) { - const options = await createI18nOptions(); - i18n = createI18n(options) as I18n; - app.use(i18n); -} diff --git a/frontend/image-tool/src/locales/useLocale.ts b/frontend/image-tool/src/locales/useLocale.ts index 994c9dd9..6a1b8954 100644 --- a/frontend/image-tool/src/locales/useLocale.ts +++ b/frontend/image-tool/src/locales/useLocale.ts @@ -1,72 +1,16 @@ -/** - * Multi-language related operations - */ -// import type { LocaleType } from '/#/config'; - -import moment from 'moment'; - -import { i18n } from './setupI18n'; -// import { useLocaleStoreWithOut } from '/@/store/modules/locale'; -import { unref, computed } from 'vue'; -import { loadLocalePool, setHtmlPageLang } from './helper'; - -interface LangModule { - message: Recordable; - momentLocale: Recordable; - momentLocaleName: string; -} - -function setI18nLanguage(locale: any) { - // const localeStore = useLocaleStoreWithOut(); - - if (i18n.mode === 'legacy') { - i18n.global.locale = locale; - } else { - (i18n.global.locale as any).value = locale; - } - // localeStore.setLocaleInfo({ locale }); - setHtmlPageLang(locale); -} - -export function useLocale() { - // const localeStore = useLocaleStoreWithOut(); - // const getLocale = computed(() => localeStore.getLocale); - // const getShowLocalePicker = computed(() => localeStore.getShowPicker); - - const getAntdLocale = computed((): any => { - return i18n.global.getLocaleMessage(unref(getLocale))?.antdLocale ?? {}; - }); - - // Switching the language will change the locale of useI18n - // And submit to configuration modification - async function changeLocale(locale: any) { - const globalI18n = i18n.global; - const currentLocale = unref(globalI18n.locale); - if (currentLocale === locale) { - return locale; - } - - if (loadLocalePool.includes(locale)) { - setI18nLanguage(locale); - return locale; - } - const langModule = ((await import(`./lang/${locale}.ts`)) as any).default as LangModule; - if (!langModule) return; - - const { message, momentLocale, momentLocaleName } = langModule; - - globalI18n.setLocaleMessage(locale, message); - moment.updateLocale(momentLocaleName, momentLocale); - loadLocalePool.push(locale); - - setI18nLanguage(locale); - return locale; +import { Editor } from 'image-editor'; +import { ILocale, languages } from './index'; + +export default function useLocale(editor: Editor) { + function lang(name: D, args?: Record | any[]) { + const data = editor.i18n.lang('common', name as any, args); + if (!data) { + return editor.i18n.lang(languages.default, String(name), args); } + return data; + } - return { - getLocale, - getShowLocalePicker, - changeLocale, - getAntdLocale, - }; + return { + lang, + }; } diff --git a/frontend/image-tool/src/package/image-editor/Editor.ts b/frontend/image-tool/src/package/image-editor/Editor.ts new file mode 100644 index 00000000..c557f997 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/Editor.ts @@ -0,0 +1,414 @@ +import EventEmitter from 'eventemitter3'; +import ImageView from './ImageView'; +import { Event } from './configs'; +import { IImageViewOption } from './ImageView'; +import { getDefaultState } from './state'; +import { + Const, + IState, + AnnotateModeEnum, + ToolType, + RegisterFn, + ModalFn, + MsgFn, + ConfirmFn, + LoadingFn, + I18n, + AnnotateObject, + IFrame, + IClassType, + IModeConfig, + OPType, + IAttr, + IUserData, + StatusType, + AttrType, +} from './types'; +import userAgent, { IUserAgent } from './lib/ua'; +import * as utils from './utils'; +import BSError from './common/BSError'; +import DataResource from './common/ResourceManager'; +import CmdManager from './common/CmdManager'; +import ActionManager from './common/ActionManager'; +import DataManager from './common/DataManager'; +import HotkeyManager from './common/HotkeyManager'; +import LoadManager from './common/LoadManager'; +import TrackManager from './common/TrackManager'; +import Stats from 'three/examples/jsm/libs/stats.module.js'; + +export default class Editor extends EventEmitter { + idCount: number = 1; + state: IState; + mainView!: ImageView; + userAgent: IUserAgent = userAgent; + editable: boolean = true; + eventSource = ''; + // frame + frameMap: Map = new Map(); + frameIndexMap: Map = new Map(); + // class + classMap: Map = new Map(); + classToolMap: Map = new Map(); + allAttrMap: Map = new Map(); + // select + selection: AnnotateObject[] = []; + selectionMap: Record = {}; + // manager + actionManager: ActionManager; + cmdManager: CmdManager; + loadManager: LoadManager; + dataManager: DataManager; + dataResource: DataResource; + hotkeyManager: HotkeyManager; + trackManager: TrackManager; + + i18n!: I18n; + registerModal: RegisterFn = () => {}; + showModal: ModalFn = () => Promise.resolve(); + showMsg: MsgFn = () => {}; + showConfirm: ConfirmFn = () => Promise.resolve(); + showLoading: LoadingFn = () => {}; + + constructor() { + super(); + this.state = getDefaultState(); + this.actionManager = new ActionManager(this); + this.cmdManager = new CmdManager(this); + this.loadManager = new LoadManager(this); + this.dataManager = new DataManager(this); + this.dataResource = new DataResource(this); + this.hotkeyManager = new HotkeyManager(this); + this.trackManager = new TrackManager(this); + + this.lang = this.lang.bind(this); + utils.initI18n(this); + + if (import.meta.env.DEV) { + this.initStats(); + } + } + // Stats + initStats() { + const stats = Stats(); + stats.dom.style.left = 'auto'; + stats.dom.style.right = '2px'; + stats.dom.style.top = '55px'; + document.body.appendChild(stats.dom); + const frame = () => { + stats.update(); + requestAnimationFrame(frame); + }; + frame(); + } + + init(container: HTMLDivElement, option?: IImageViewOption) { + this.mainView = new ImageView(this, container, option); + this.initEvent(); + this.emit(Event.INIT); + } + initEvent() { + this.on(Event.SELECT, () => { + if ( + this.editable && + this.selection.length === 1 && + this.selection[0].editable && + this.mainView.getShapeTool(this.selection[0].className as any) + ) { + this.mainView.enableEdit(this.selection[0] as any); + } else { + this.mainView.disableEdit(); + } + this.updateCurrentTrack(); + }); + this.cmdManager.on(Event.UNDO, () => { + this.updateCurrentTrack(); + }); + + this.cmdManager.on(Event.REDO, () => { + this.updateCurrentTrack(); + }); + } + createTrackObj(objects: AnnotateObject | AnnotateObject[]) { + if (!this.state.isSeriesFrame) return; + if (!Array.isArray(objects)) objects = [objects]; + const trackObjects = [] as IUserData[]; + objects.forEach((e) => { + const userData = e.userData as IUserData; + const classObj = this.getClassType(userData.classId || ''); + trackObjects.push({ + trackId: userData.trackId, + trackName: userData.trackName, + classId: userData.classId, + classType: userData.classType || classObj?.name || '', + annotationType: AnnotateModeEnum.INSTANCE, + }); + }); + return trackObjects; + } + initIDInfo(objects: AnnotateObject | AnnotateObject[]) { + if (!Array.isArray(objects)) objects = [objects]; + objects.forEach((object) => { + const userData = object.userData; + userData.id = object.uuid; + + if (!userData.trackId) { + userData.trackId = utils.createTrackId(); + } + if (!userData.trackName) { + userData.trackName = this.getId(); + } + this.initOtherInfo(object); + }); + } + initOtherInfo(object: AnnotateObject) { + object.userData.sourceId = '-1'; + !object.userData.resultStatus && (object.userData.resultStatus = Const.True_Value); + } + // trackName + getId() { + return this.idCount++ + ''; + } + updateTrack() { + const { frameIndex, frames, isSeriesFrame, defaultSourceId } = this.state; + let countFrames: IFrame[] = []; + if (isSeriesFrame) countFrames = frames; + else countFrames = [frames[frameIndex]]; + + const objects: AnnotateObject[] = []; + countFrames.forEach((f) => { + const objs_ins = this.dataManager.getFrameObject(f.id, AnnotateModeEnum.INSTANCE) || []; + const objs_seg = this.dataManager.getFrameObject(f.id, AnnotateModeEnum.SEGMENTATION) || []; + const objs = objs_ins.concat(objs_seg); + objects.push(...objs.filter((e) => e.userData.sourceId == defaultSourceId)); + }); + let maxId = 0; + objects.forEach((e) => { + if (!e.userData.trackName) return; + const id = parseInt(e.userData.trackName); + if (id > maxId) maxId = id; + }); + this.idCount = maxId + 1; + } + updateCurrentTrack() { + const selection = this.selection; + const userData = selection.length == 1 ? selection[0].userData : undefined; + this.setCurrentTrack(userData?.trackId); + } + setCurrentTrack(trackId: string = '') { + if (this.state.currentTrack !== trackId) { + this.state.currentTrack = trackId; + this.emit(Event.CURRENT_TRACK_CHANGE, this.state.currentTrack); + } + } + withEventSource(source: string, fn: () => void) { + this.eventSource = source; + try { + fn(); + } catch (e: any) {} + this.eventSource = ''; + } + showNameOrAlias(obj: { name: string; alias?: string; label?: string }, showAll: boolean = false) { + return obj.name; + } + // locale common + lang(name: string, args?: Record) { + return name; + } + selectObject(object?: AnnotateObject | AnnotateObject[], force?: boolean) { + const preSelection = this.selection; + let selection: AnnotateObject[] = []; + if (object) { + selection = Array.isArray(object) ? object : [object]; + } + if ( + !force && + selection.length === 1 && + this.selection.length === 1 && + this.selection[0] === selection[0] + ) { + return; + } + + preSelection.forEach((e) => { + this.mainView.setState(e, { select: false }); + }); + + this.selection = selection; + this.selectionMap = {}; + selection.forEach((e) => { + this.mainView.setState(e, { select: true }); + this.selectionMap[e.uuid] = e; + }); + this.emit(Event.SELECT, preSelection, this.selection); + } + updateSelect() { + const { selection, selectionMap } = this; + const filterSelection = selection.filter((e) => selectionMap[e.uuid]); + this.selectObject(filterSelection); + } + getUserData(object: AnnotateObject) { + const { isSeriesFrame } = this.state; + + const userData = object.userData as Required; + const trackId = userData.trackId as string; + if (isSeriesFrame) { + const globalTrack = this.trackManager.getTrackObject(trackId) || {}; + Object.assign(userData, globalTrack); + } + return userData; + } + setMode(modeConfig: IModeConfig) { + this.state.modeConfig = modeConfig; + + const editable = modeConfig.op === OPType.EDIT; + (this.mainView.stage as any).globalDisableDrag = !editable; + this.editable = editable; + + this.hotkeyManager.setHotKeyFromAction(this.state.modeConfig.actions); + this.actionManager.stopCurrentAction(); + this.mainView.disableEdit(); + this.mainView.disableDraw(); + this.selectObject(); + this.emit(Event.UPDATE_VIEW_MODE); + } + + // frame + setFrames(frames: IFrame[]) { + this.state.frames = frames; + this.updateFrameIndex(); + this.emit(Event.FRAMES); + } + updateFrameIndex() { + const frames = this.state.frames; + this.frameMap.clear(); + frames.forEach((e, index) => { + this.frameMap.set(e.id + '', e); + this.frameIndexMap.set(e.id + '', index); + }); + } + getFrameIndex(frameId: string) { + return this.frameIndexMap.get(frameId + '') as number; + } + getFrame(frameId: string) { + return this.frameMap.get(frameId + '') as IFrame; + } + getCurrentFrame() { + return this.state.frames[this.state.frameIndex]; + } + async switchFrame(index: number) { + if (index === this.state.frameIndex) return; + const beforeIndex = this.state.frameIndex; + await this.loadFrame(index); + this.emit(Event.FRAME_SWITCH, { from: beforeIndex, to: index }); + } + async loadFrame(index: number, showLoading: boolean = true, force: boolean = false) { + await this.loadManager.loadFrame(index, showLoading, force); + await this.mainView.renderFrame(); + // this.selectByTrackId(this.state.currentTrack); + this.emit(Event.FRAME_CHANGE, this.state.frameIndex); + } + + setClassTypes(classTypes: IClassType[]) { + this.classMap.clear(); + this.classToolMap.clear(); + this.allAttrMap.clear(); + + this.state.classTypes = classTypes; + classTypes.forEach((e) => { + this.classMap.set(e.name + '', e); + this.classMap.set(e.id + '', e); + + let classes = this.classToolMap.get(e.toolType); + if (!classes) { + classes = []; + this.classToolMap.set(e.toolType, classes); + } + classes.push(e); + }); + this.emit(Event.CLASS_INITDATA); + } + /** get class details by id or name */ + getClassType(name: string | number) { + return this.classMap.get(name + ''); + } + getClassTypesByToolType(tooltype: ToolType) { + let list = this.classToolMap.get(tooltype) || []; + return list; + } + getClassList(tooltype: ToolType) { + let list = this.classToolMap.get(tooltype) || ([] as IClassType[]); + return list; + } + // all classTypes [key: uuid]: [value: attr/attributes/options] map + get attrMap() { + if (this.allAttrMap.size === 0) { + this.state.classTypes.forEach((classType) => { + this.setAttrsMap(classType.attrs); + }); + } + return this.allAttrMap; + } + setAttrsMap(attrs: any[], parent?: any) { + if (!attrs || attrs.length === 0) return; + attrs.forEach((attr) => { + if (parent) { + attr.parent = parent.id; + attr.parentValue = parent.name; + } + this.allAttrMap.set(attr.id, attr); + if (attr.options) this.setAttrsMap(attr.options); + if (attr.attributes) this.setAttrsMap(attr.attributes, attr); + }); + } + getValidAttrs(userData: IUserData) { + const classId = userData.classId || ''; + const classConfig = this.getClassType(classId); + const rAttrs: Record = {}; + if (!classConfig) return rAttrs; + const data = userData.attrs || {}; + const validAttribute = (attr: IAttr) => { + const attrId = attr.id; + const item = data[attrId]; + if (!item) return; + const isMulti = [AttrType.MULTI_SELECTION, AttrType.RANK].includes(attr.type); + const hasValue = isMulti ? item.value.length > 0 : item.value; + if (hasValue) { + rAttrs[attrId] = item; + const options = attr.options.filter((e) => { + if (isMulti) { + return item.value.includes(e.name); + } else { + return item.value === e.name; + } + }); + options.forEach((o) => { + if (o.attributes) { + o.attributes.forEach(validAttribute); + } + }); + } + }; + classConfig.attrs.forEach(validAttribute); + return rAttrs; + } + + isDefaultStatus() { + return this.state.status === StatusType.Default; + } + clearResource(config?: { resetBgRotation?: boolean }) { + this.mainView.clearBackground(); + this.mainView.clearAllShape(); + if (config?.resetBgRotation) this.mainView.stage.rotation(0); + } + getRenderFilter() { + return (e: any) => Boolean(e); + } + + handleErr(err: BSError | Error | any, message: string = '') { + console.error(err); + } + // 跑模型函数 + async runModel() { + throw 'runModel implement error'; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/View.ts b/frontend/image-tool/src/package/image-editor/ImageView/View.ts new file mode 100644 index 00000000..acd78bfb --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/View.ts @@ -0,0 +1,44 @@ +import EventEmitter from 'eventemitter3'; +import Action from './actions/Action'; +import Actions, { ActionCtr } from './actions'; + +export default class View extends EventEmitter { + // plugin action + actions: string[] = []; + actionMap: { [key: string]: Action } = {}; + constructor() { + super(); + } + + getAction(name: string) { + return this.actionMap[name]; + } + addActions(actionNames: string[]) { + actionNames.forEach((name) => { + if (this.actionMap[name]) return; + let Ctr = Actions[name] as ActionCtr; + if (!Ctr) return; + let action: Action = new Ctr(this); + try { + action.init(); + } catch (error) { + console.error(error); + } + this.actionMap[name] = action; + this.actions.push(name); + }); + } + removeActions(actionNames: string[]) { + actionNames.forEach((name) => { + let action = this.actionMap[name]; + if (!action) return; + try { + action.destroy(); + } catch (error) { + console.error(error); + } + this.actions = this.actions.filter((e) => e !== name); + delete this.actionMap[name]; + }); + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/actions/Action.ts b/frontend/image-tool/src/package/image-editor/ImageView/actions/Action.ts new file mode 100644 index 00000000..f18b7dc2 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/actions/Action.ts @@ -0,0 +1,13 @@ +export default class Action { + actionName!: string; + enabled: boolean = true; + constructor() {} + init() {} + destroy() {} + toggle(enabled: boolean) { + this.enabled = enabled; + } + isEnable(): boolean { + return this.enabled; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/actions/SelectHoverAction.ts b/frontend/image-tool/src/package/image-editor/ImageView/actions/SelectHoverAction.ts new file mode 100644 index 00000000..6ebf474a --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/actions/SelectHoverAction.ts @@ -0,0 +1,123 @@ +import Action from './Action'; +import ImageView from '../index'; +import Konva from 'konva'; +import { Shape } from '../shape'; +import { Cursor } from '../../configs'; + +type IFilter = (e: any) => boolean; + +const actionName = 'select-hover'; +class SelectHoverAction extends Action { + // enabled = true; + selectFlag = true; + stageClickFlag = true; + cursorFlag = true; + view: ImageView; + filter?: IFilter; + + constructor(view: ImageView) { + super(); + this.view = view; + this.onMouseOver = this.onMouseOver.bind(this); + this.onMouseOut = this.onMouseOut.bind(this); + this.onMouseClick = this.onMouseClick.bind(this); + this.onClick = this.onClick.bind(this); + } + init() { + this.view.stage.on('mouseover', this.onMouseOver); + this.view.stage.on('mouseout', this.onMouseOut); + this.view.stage.on('click', this.onMouseClick); + // click + this.view.shapes.on('click', this.onClick); + } + destroy() { + this.view.stage.off('mouseover', this.onMouseOver); + this.view.stage.off('mouseout', this.onMouseOut); + this.view.stage.off('click', this.onMouseClick); + this.view.shapes.off('click', this.onClick); + } + + onMouseClick(e: Konva.KonvaEventObject) { + // if (!this.enabled) return; + if (e.evt.button !== 0 || !this.selectFlag || !this.stageClickFlag) return; + + if (this.filter && !this.filter(e.target)) return; + if (e.target === this.view.stage && !this.view.currentDrawTool) { + const editor = this.view.editor; + if (editor.selection.length > 0) editor.selectObject([]); + } + } + onMouseOver(e: Konva.KonvaEventObject) { + if (this.filter && !this.filter(e.target)) return; + + if (e.target === this.view.stage) { + this.view.setCursor(this.view.cursor || Cursor.auto); + return; + } + + let target = e.target as Shape; + const object = target.object; + + target = object || target; + if (!target.attrs.skipStateStyle) this.view.setState(target, { hover: true }); + + if (this.cursorFlag) { + this.view.setCursor(target.attrs.cursor || this.view.cursor || Cursor.pointer); + } + } + + onMouseOut(e: Konva.KonvaEventObject) { + if (this.filter && !this.filter(e.target)) return; + // if (!this.enabled) return; + // console.log('mouseout', e); + if (e.target === this.view.stage) { + return; + } + + let target = e.target as Shape; + const object = target.object; + + target = object || target; + if (!target.attrs.skipStateStyle) this.view.setState(target, { hover: false }); + + if (this.cursorFlag) { + this.view.setCursor(this.view.cursor || Cursor.auto); + } + } + + // click + onClick(e: Konva.KonvaEventObject) { + // if (!this.enabled) return; + if (e.evt.button !== 0 || !this.selectFlag) return; + + if (this.filter && !this.filter(e.target)) return; + + const editor = this.view.editor; + const event = e.evt; + let target = e.target as Shape; + + target = target.object || target; + + if (!target.attrs.selectable) return; + + console.log(target); + if (event.shiftKey) { + const { selection, selectionMap } = editor; + let newSelection = [...selection]; + if (selectionMap[target.uuid]) { + // remove + newSelection = newSelection.filter((e) => e !== target); + } else { + newSelection.push(target); + } + editor.selectObject(newSelection); + } else { + editor.selectObject(target); + } + // editor.mainView.focusView(); + } +} + +SelectHoverAction.prototype.actionName = actionName; + +export default SelectHoverAction; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/actions/ZoomMoveAction.ts b/frontend/image-tool/src/package/image-editor/ImageView/actions/ZoomMoveAction.ts new file mode 100644 index 00000000..003ff052 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/actions/ZoomMoveAction.ts @@ -0,0 +1,77 @@ +import Action from './Action'; +import ImageView from '../index'; +import Konva from 'konva'; + +const actionName = 'zoom-move'; +class ZoomMoveAction extends Action { + scaleRatio: number = 1.03; + view: ImageView; + mouseDown: number = -1; + startPos?: Konva.Vector2d; + startStagePos?: Konva.Vector2d; + startRotation?: number; + constructor(view: ImageView) { + super(); + this.view = view; + this.onMouseWheel = this.onMouseWheel.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + this.onMouseUp = this.onMouseUp.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + } + init() { + this.view.stage.on('wheel', this.onMouseWheel); + this.view.stage.on('mousedown', this.onMouseDown); + document.addEventListener('mouseup', this.onMouseUp); + document.addEventListener('mousemove', this.onMouseMove); + } + + destroy() { + this.view.stage.off('wheel', this.onMouseWheel); + this.view.stage.off('mousedown', this.onMouseDown); + document.removeEventListener('mouseup', this.onMouseUp); + document.removeEventListener('mousemove', this.onMouseMove); + } + + onMouseDown(e: Konva.KonvaEventObject) { + // 右键 + if (e.evt.button == 0) return; + + this.mouseDown = e.evt.button; + this.startPos = { x: e.evt.clientX, y: e.evt.clientY }; + this.startStagePos = this.view.stage.getAbsolutePosition(); + this.startRotation = this.view.stage.rotation(); + } + onMouseUp(e: MouseEvent) { + this.startPos = undefined; + this.startStagePos = undefined; + this.startRotation = undefined; + this.mouseDown = -1; + } + onMouseMove(e: MouseEvent) { + const stage = this.view.stage; + if (this.mouseDown < 1 || !this.startPos || !this.startStagePos) return; + + if (this.mouseDown === 1) { + // rotate + } else if (this.mouseDown === 2) { + // drag + stage.position({ + x: this.startStagePos.x + e.clientX - this.startPos.x, + y: this.startStagePos.y + e.clientY - this.startPos.y, + }); + } + } + onMouseWheel(e: Konva.KonvaEventObject) { + const oldScale = this.view.stage.scaleX(); + const direction = e.evt.deltaY < 0 ? 1 : -1; + let newScale = direction > 0 ? oldScale * this.scaleRatio : oldScale / this.scaleRatio; + + if (newScale < 0.01) newScale = 0.01; + else if (newScale > 50) newScale = 50; + + this.view.zoomTo(newScale); + } +} + +ZoomMoveAction.prototype.actionName = actionName; +export default ZoomMoveAction; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/actions/index.ts b/frontend/image-tool/src/package/image-editor/ImageView/actions/index.ts new file mode 100644 index 00000000..2d7a5ab4 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/actions/index.ts @@ -0,0 +1,16 @@ +import Action from './Action'; +import ZoomMoveAction from './ZoomMoveAction'; +import SelectHoverAction from './SelectHoverAction'; + +export type ActionCtr = new (args: any) => Action; +export type IActionMap = { [key: string]: ActionCtr }; + +const Actions: IActionMap = {}; + +[ZoomMoveAction, SelectHoverAction].forEach((actionCtr: ActionCtr) => { + Actions[actionCtr.prototype.actionName] = actionCtr; +}); + +export { SelectHoverAction, ZoomMoveAction }; + +export default Actions; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/components/BackgroundGroup.ts b/frontend/image-tool/src/package/image-editor/ImageView/components/BackgroundGroup.ts new file mode 100644 index 00000000..70242da3 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/components/BackgroundGroup.ts @@ -0,0 +1,98 @@ +import Konva from 'konva'; +import ImageView from '../index'; +import Editor from '../../Editor'; + +export default class BackgroundGroup { + private static _instance: BackgroundGroup; + public static getInstance() { + if (!this._instance) this._instance = new BackgroundGroup(); + return this._instance; + } + + view!: ImageView; + editor!: Editor; + group: Konva.Group; + bgImg: Konva.Image; + lines: Konva.Line[] = []; + + constructor() { + this.group = new Konva.Group({ listening: false, sign: 'renderLayer-bgGroup' }); + this.bgImg = new Konva.Image({ image: undefined }); + } + init(view: ImageView) { + this.view = view; + this.editor = this.view.editor; + this.view.renderLayer.add(this.group); + this.group.moveToBottom(); + this.initEvent(); + return this.group; + } + initEvent() {} + updateBgImage(image: HTMLImageElement) { + this.clearBgImage(); + this.bgImg = new Konva.Image({ + image, + x: 0, + y: 0, + width: image.naturalWidth, + height: image.naturalHeight, + }); + this.group.add(this.bgImg); + this.bgImg.moveToBottom(); + this.updateBackgroundStyle(); + this.updateEquisector(); + } + clearBgImage() { + this.bgImg?.remove?.(); + this.bgImg?.destroy?.(); + } + updateEquisector() { + if (!this.bgImg) return; + const { enable, vertical, horizontal, width, color } = + this.view.editor.state.config.bisectrixLine; + if (vertical < 2 || vertical > 10 || horizontal < 2 || vertical > 10) return; + this.clearEquisector(); + if (!enable) return; + const maxX = this.bgImg.width(); + const maxY = this.bgImg.height(); + const averageX = maxX / vertical; + const averageY = maxY / horizontal; + for (let i = 1; i < vertical; i++) { + const lineX = averageX * i; + const bline = new Konva.Line({ + points: [lineX, 0, lineX, maxY], + strokeWidth: width, + stroke: color, + hitStrokeWidth: 0, + }); + this.lines.push(bline); + } + for (let i = 1; i < horizontal; i++) { + const lineY = averageY * i; + const bline = new Konva.Line({ + points: [0, lineY, maxX, lineY], + strokeWidth: width, + stroke: color, + hitStrokeWidth: 0, + }); + this.lines.push(bline); + } + this.group.add(...this.lines); + } + clearEquisector() { + if (this.lines.length > 0) { + this.lines.forEach((line) => { + line.destroy(); + }); + } + this.lines.length = 0; + } + updateBackgroundStyle() { + if (!this.view) return; + const { brightness, contrast } = this.view.editor.state.config; + this.bgImg.cache(); + this.bgImg.filters([Konva.Filters.Brighten, Konva.Filters.Contrast]); + this.bgImg.brightness(brightness / 100); + this.bgImg.contrast(contrast); + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/components/ShapeRoot.ts b/frontend/image-tool/src/package/image-editor/ImageView/components/ShapeRoot.ts new file mode 100644 index 00000000..a78923a1 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/components/ShapeRoot.ts @@ -0,0 +1,113 @@ +import Konva from 'konva'; +import { v4 as uuid } from 'uuid'; +import * as utils from '../utils'; +import { removeShapeKey, addShapeKey } from '../utils'; +import { AnnotateObject } from '../shape'; +import { IFrame, AnnotateModeEnum } from '../../types'; + +interface IShapeRoot { + frame?: IFrame; + children?: AnnotateObject[]; + type?: AnnotateModeEnum; +} +const renderFilter = (e: AnnotateObject) => true; + +export default class ShapeRoot extends Konva.Container { + className: string = 'shape-root'; + uuid: string = uuid(); + children: AnnotateObject[] = []; + hasMap: Map = new Map(); + renderFilter = renderFilter; + frame!: IFrame; + type: AnnotateModeEnum = AnnotateModeEnum.INSTANCE; + + constructor(config: IShapeRoot) { + super(); + if (config.frame) this.frame = config.frame; + if (config.children) this.addObjects(config.children); + if (config.type) this.type = config.type; + } + + get allObjects() { + return Array.from(this.hasMap.values()); + } + + addObjects(objects: AnnotateObject[] | AnnotateObject) { + if (!Array.isArray(objects)) objects = [objects]; + if (objects.length === 0) return; + + utils.traverse(objects, (e) => { + e.frame = this.frame; + this.hasMap.set(e.uuid, e); + }); + + objects.forEach((e) => { + e._clearCaches(); + e.parent = this; + }); + + this.children.push(...objects); + this._setChildrenIndices(); + addShapeKey(objects); + this._requestDraw(); + } + + _addObjectIndex(index: number = Infinity, object: AnnotateObject) { + object.parent = this; + object._clearCaches(); + object.frame = this.frame; + + if (index === Infinity) { + this.children.push(object); + } else { + index = index < 0 ? 0 : index; + this.children.splice(index, 0, object); + } + addShapeKey(object); + this._requestDraw(); + } + + addObjectIndex(index: number = Infinity, object: AnnotateObject) { + this._addObjectIndex(index, object); + this._setChildrenIndices(); + this._requestDraw(); + } + + removeObjects(objects: AnnotateObject[] | AnnotateObject) { + if (!Array.isArray(objects)) objects = [objects]; + + utils.traverse(objects, (e) => { + this.hasMap.delete(e.uuid); + }); + + const delMap: any = {}; + objects.forEach((e) => { + e._clearCaches(); + delMap[e.uuid] = true; + e.parent = null; + e.remove(); + }); + + this.children = this.children.filter((e) => !delMap[e.uuid]); + this._setChildrenIndices(); + removeShapeKey(objects); + this._requestDraw(); + } + destroySelf() { + this.removeChildren(); + this.hasMap.clear(); + this.remove(); + this.destroy(); + } + + // override + _validateAdd(node: Konva.Node) {} + _drawChildren(drawMethod: string, canvas: any, top: any) { + // update bgRect + this.children?.forEach((child: AnnotateObject) => { + if (this.renderFilter(child)) { + child[drawMethod as keyof AnnotateObject](canvas, top); + } + }); + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/index.ts b/frontend/image-tool/src/package/image-editor/ImageView/index.ts new file mode 100644 index 00000000..2fa64d0b --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/index.ts @@ -0,0 +1,407 @@ +import Konva from 'konva'; +import View from './View'; +import Editor from '../Editor'; +import Actions from './actions'; +import * as utils from '../utils'; +import * as _utils from './utils'; +import { AnnotateObject, Polygon, Rect, Shape } from './shape'; +import { IShapeConfig, Vector2 } from './type'; +import { Cursor, Event } from '../configs'; +import { throttle } from 'lodash'; +import { AnnotateModeEnum, DisplayModeEnum, IStateMap, LoadStatus, ToolName } from '../types'; +import { allTools, IToolName, ShapeTool } from './shapeTool'; +import BackgroundGroup from './components/BackgroundGroup'; +import ShapeRoot from './components/ShapeRoot'; +import { colord } from 'colord'; + +_utils.hackOverwriteShape(); +export type IFilter = (e: AnnotateObject) => boolean; +export interface IImageViewOption { + actions?: string[]; +} +export * from './shapeTool'; +export { ShapeRoot, BackgroundGroup }; + +export default class ImageView extends View { + static Actions = Actions; + editor: Editor; + container: HTMLDivElement; + stage: Konva.Stage; + renderLayer: Konva.Layer; + helpLayer: Konva.Layer; + shapes: Konva.Group; + renderFilter: IFilter = () => true; + + currentDrawTool?: ShapeTool; + currentEditTool?: ShapeTool; + toolMap: Record = {}; + backgroundWidth = 1; + backgroundHeight = 1; + backgroundAsRatio = 1; + cursor: string = ''; + zoomAnimation?: Konva.Animation; + + constructor(editor: Editor, container: HTMLDivElement, option: IImageViewOption = {}) { + super(); + this.editor = editor; + this.container = container; + this.stage = new Konva.Stage({ + container: container, + width: container.clientWidth, + height: container.clientHeight, + }); + const imageSmoothingEnabled = this.editor.state.config.imageSmoothing; + this.renderLayer = new Konva.Layer({ imageSmoothingEnabled }); + this.helpLayer = new Konva.Layer({ imageSmoothingEnabled }); + this.shapes = new Konva.Group({ sign: 'renderLayer-shapes' }); + BackgroundGroup.getInstance().init(this); + const roots: any[] = [new ShapeRoot({ type: AnnotateModeEnum.INSTANCE })]; + this.editor.dataManager.setFrameRoot('test001', roots); + this.shapes.add(...roots); + + utils.disableContextMenu(this.renderLayer.canvas._canvas); + utils.disableContextMenu(this.helpLayer.canvas._canvas); + + _utils.hackContext(this, this.renderLayer); + _utils.hackContext(this, this.helpLayer); + + this.renderLayer.add(this.shapes); + this.stage.add(this.renderLayer, this.helpLayer); + const { actions } = option; + if (actions && actions.length > 0) this.addActions(actions); + this.resize = throttle(this.resize.bind(this), 50); + this.initEvent(); + } + draw() { + this.stage.batchDraw(); + } + initEvent() { + _utils.handleDragToCmd(this); + // draw + this.renderLayer.on('draw', () => { + this.editor.emit(Event.DRAW); + }); + this.editor.on(Event.FRAME_CHANGE, () => { + this.currentDrawTool?.clearDraw(); + BackgroundGroup.getInstance().updateEquisector(); + }); + window.addEventListener('resize', this.resize); + // object visible changed + this.editor.on(Event.ANNOTATE_VISIBLE, (objects) => { + const curTool = this.currentDrawTool || this.currentEditTool; + if (!curTool || !curTool.object) return; + const group = this.currentDrawTool?.drawGroup || this.currentEditTool?.editGroup || undefined; + if (!group) return; + const visible = curTool.object.showVisible; + if (visible) { + group.show(); + } else { + group.hide(); + } + }); + } + /** + * renderFrame: background image && annotations + */ + async renderFrame() { + try { + const frame = this.editor.getCurrentFrame(); + if (!frame) throw `render error: the frame does not exist`; + const resource = this.editor.dataResource.getResourceData(frame); + const root_ins = this.editor.dataManager.getFrameRoot(frame.id, AnnotateModeEnum.INSTANCE); + if (!resource || !root_ins) { + frame.loadState = LoadStatus.ERROR; + throw `render error: resource(${frame.id}) has not be loaded`; + } + this.setBackground(resource.image); + this.updateShapeRoot([root_ins]); + } catch (err) { + throw `render error: ${err}`; + } + } + resize() { + const bbox = this.container.getBoundingClientRect(); + this.stage.size(bbox); + this.editor.emit(Event.RESIZE); + } + imageSmoothing(val: boolean) { + this.renderLayer.setAttrs({ imageSmoothingEnabled: val }); + this.helpLayer.setAttrs({ imageSmoothingEnabled: val }); + } + zoomTo(newScale: number, pointer?: Konva.Vector2d) { + const stage = this.stage; + const oldScale = stage.scaleX(); + pointer = pointer || (stage.getPointerPosition() as Konva.Vector2d); + if (!pointer) return; + + const mousePointTo = { + x: (pointer.x - stage.x()) / oldScale, + y: (pointer.y - stage.y()) / oldScale, + }; + stage.scale({ x: newScale, y: newScale }); + const newPos = { + x: pointer.x - mousePointTo.x * newScale, + y: pointer.y - mousePointTo.y * newScale, + }; + stage.position(newPos); + this.emit(Event.ZOOM, newScale); + } + setCursor(cursor: string) { + this.container.style.cursor = cursor; + } + // shaperoot + getRoot(type?: AnnotateModeEnum) { + type = type || this.editor.state.annotateMode; + return this.shapes.getChildren((e: ShapeRoot) => { + return e.type === type; + })[0] as any as ShapeRoot; + } + updateShapeRoot(roots?: ShapeRoot[]) { + if (!roots) { + if (!this.shapes.children || this.shapes.children.length < 1) return; + roots = [this.shapes.children[0], this.shapes.children[1]] as any as ShapeRoot[]; + } + const { annotateMode } = this.editor.state; + roots.forEach((root) => { + root.index = root.type === annotateMode ? 1 : 0; + root.listening(root.type === annotateMode); + root.renderFilter = this.editor.getRenderFilter(); + // resultTypeFilter.includes(root.type) ? root.show() : root.hide(); + }); + roots.sort((root1, root2) => root1.index - root2.index); + this.shapes.removeChildren(); + this.shapes.add(...(roots as any)); + this.draw(); + } + setBackground(image?: HTMLImageElement) { + if (!image || !image.naturalWidth || !image.naturalHeight) return; + this.backgroundWidth = image.naturalWidth; + this.backgroundHeight = image.naturalHeight; + this.backgroundAsRatio = this.backgroundWidth / this.backgroundHeight; + BackgroundGroup.getInstance().updateBgImage(image); + this.fitBackgroundAsRatio(); + } + fitBackgroundAsRatio(resetRotation = true) { + if (resetRotation) this.stage.rotation(0); + const bgR = this.stage.rotation(); + let bgRect = { x: 0, y: 0, width: this.backgroundWidth, height: this.backgroundHeight }; + const bgPoints = utils.getRectPointsWithRotation(bgRect, bgR); + bgRect = utils.getPointsBoundRect(bgPoints); + + const width = this.stage.content.clientWidth; + const height = this.stage.content.clientHeight; + + const scaleX = bgRect.width / width; + const scaleY = bgRect.height / height; + + let scale = 1; + let offsetX = 0; + let offsetY = 0; + if (scaleX > scaleY) { + scale = 1 / scaleX; + offsetY = (height - bgRect.height * scale) / 2; + } else { + scale = 1 / scaleY; + offsetX = (width - bgRect.width * scale) / 2; + } + offsetX -= bgRect.x * scale; + offsetY -= bgRect.y * scale; + + this.stage.scale({ x: scale, y: scale }); + this.stage.position({ x: offsetX, y: offsetY }); + this.emit(Event.ZOOM, scale, false); + this.emit(Event.ROTATE, this.stage.rotation()); + } + rotateAroundCenter(r: number) { + function rotatePoint({ x, y }: Vector2, r: number) { + const rcos = Math.cos(r); + const rsin = Math.sin(r); + return { x: x * rcos - y * rsin, y: y * rcos + x * rsin }; + } + const stagePos = this.stage.position(); + const stageScale = this.stage.scaleX(); + const tl = { x: -this.backgroundWidth / 2, y: -this.backgroundHeight / 2 }; + const current = rotatePoint(tl, utils.deg2radian(this.stage.rotation())); + const rotated = rotatePoint(tl, utils.deg2radian(r)); + const dx = (rotated.x - current.x) * stageScale, + dy = (rotated.y - current.y) * stageScale; + this.stage.rotation(r); + this.stage.position({ x: stagePos.x + dx, y: stagePos.y + dy }); + this.emit(Event.ROTATE, r); + } + getStatePriority(object: AnnotateObject) { + return object.statePriority; + } + getStateStyles(object: AnnotateObject) { + return object.stateStyles; + } + getDefaultStyle(object: AnnotateObject) { + return object.stateStyles?.general || {}; + } + setState(object: AnnotateObject, config: Partial>) { + Object.assign(object.state || { hover: false, select: false }, config); + this.updateStateStyle(object); + } + updateStateStyle(objects: AnnotateObject | AnnotateObject[], viewType?: DisplayModeEnum) { + if (!Array.isArray(objects)) objects = [objects]; + viewType = viewType || this.editor.state.config.viewType; + const isMaskMode = DisplayModeEnum.MASK === viewType; + objects.forEach((object) => { + const { skipStateStyle } = object.attrs; + if (skipStateStyle) return; + + const statePriority = this.getStatePriority(object); + const stateStyles = this.getStateStyles(object); + let defaultStyle = this.getDefaultStyle(object); + if (object instanceof Rect || object instanceof Polygon) { + defaultStyle = { ...defaultStyle, fill: isMaskMode ? object.stroke() : '' }; + const rgba = colord(object.stroke()).toRgb(); + const colorRGBA = `rgba(${rgba.r},${rgba.g},${rgba.b},0)`; + stateStyles.hover.fill = colorRGBA; + stateStyles.select.fill = colorRGBA; + } + this.updateStyleByState(object, object.state, statePriority, stateStyles, defaultStyle); + }); + } + updateStyleByState( + object: AnnotateObject, + states: IStateMap, + statePriority: string[], + stateStyle: Record, + defaultStyle: IShapeConfig, + ) { + const config = { ...defaultStyle }; + statePriority?.forEach((state) => { + if (states && states[state] && stateStyle && stateStyle[state]) { + Object.assign(config, stateStyle[state]); + } + }); + object.setAttrs(config); + } + updateObjectByUserData(objects: AnnotateObject | AnnotateObject[]) { + if (!Array.isArray(objects)) objects = [objects]; + objects.forEach((object) => { + const userData = this.editor.getUserData(object); + const classConfig = this.editor.getClassType(userData.classId || ''); + let config: IShapeConfig = { stroke: classConfig ? classConfig.color : '#fff' }; + object.setAttrs(config); + this.setState(object, {}); + }); + } + updateToolStyleByClass() { + const tool = this.currentEditTool; + tool && tool.object && tool.edit(tool.object); + } + updateBGDisplayModel() { + const frame = this.editor.getCurrentFrame(); + const { + config: { viewType }, + annotateMode, + } = this.editor.state; + const objInstance = + this.editor.dataManager.getFrameObject(frame.id, AnnotateModeEnum.INSTANCE) || []; + const objSegmentation = + this.editor.dataManager.getFrameObject(frame.id, AnnotateModeEnum.SEGMENTATION) || []; + const objectsMap: Record = { + [AnnotateModeEnum.INSTANCE]: objInstance, + [AnnotateModeEnum.SEGMENTATION]: objSegmentation, + }; + Object.keys(objectsMap).forEach((type: AnnotateModeEnum) => { + const objs = objectsMap[type]; + const objArr: any[] = objs.filter((e) => e instanceof Rect || e instanceof Polygon); + const inCurToolModel = annotateMode === type; + this.updateStateStyle(objArr, inCurToolModel ? viewType : DisplayModeEnum.MARK); + }); + } + clearBackground() { + BackgroundGroup.getInstance().clearBgImage(); + } + clearAllShape() { + this.shapes.removeChildren(); + } + + /** + * tool + */ + setShapeTool(name: string, tool: ShapeTool) { + this.toolMap[name] = tool; + } + getShapeTool(name: IToolName) { + if (!this.toolMap[name]) { + const Ctr = allTools[name]; + if (!Ctr) return; + const tool = new Ctr(this); + this.toolMap[name] = tool; + } + return this.toolMap[name]; + } + enableDraw(name: IToolName | string) { + const tool = this.getShapeTool(name as any); + if (!tool) return; + + const curTool = this.currentDrawTool || this.currentEditTool; + if (curTool?.doing()) { + // return this.editor.showMsg('warning', 'Please finish drawing first'); + } + if (curTool) { + this.disableDraw(); + this.disableEdit(); + } + + this.currentDrawTool = tool; + try { + tool.draw(); + } catch (error) { + console.error(error); + } + + this.cursor = tool.cursor; + this.setCursor(this.cursor); + if (tool.config.disableRenderLayer) { + this.renderLayer.listening(false); + } + + this.editor.state.activeTool = name as any; + _utils.handleDrawToCmd(this, this.currentDrawTool); + } + + disableDraw() { + const previousTool = this.currentDrawTool; + this.cursor = ''; + this.editor.state.activeTool = ToolName.default; + this.setCursor(Cursor.auto); + this.renderLayer.listening(true); + this.currentDrawTool = undefined; + if (previousTool) { + previousTool.stopDraw(); + _utils.handleDrawToCmdClear(previousTool); + } + this.editor.emit(Event.ANNOTATE_DISABLED_DRAW); + } + + enableEdit(shape: Shape) { + const name = shape.className; + const tool = this.getShapeTool(name as any); + if (!tool) return; + + this.disableEdit(); + this.disableDraw(); + + this.currentEditTool = tool; + try { + tool.edit(shape); + } catch (error) { + console.error(error); + } + _utils.handleEditToCmd(this, this.currentEditTool); + } + disableEdit() { + if (!this.currentEditTool) return; + _utils.handleEditToCmdClear(this.currentEditTool); + this.currentEditTool.stopEdit(); + this.cursor = ''; + this.setCursor(Cursor.auto); + this.currentEditTool = undefined; + this.editor.state.activeTool = ToolName.default; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Anchor.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Anchor.ts new file mode 100644 index 00000000..e6149dc0 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Anchor.ts @@ -0,0 +1,18 @@ +import { isNumber } from 'lodash'; +import { AnnotateClassName, ICircleConfig } from '../type'; +import Circle from './Circle'; + +/** + * Class Anchor + */ +export default class Anchor extends Circle { + className = 'anchor' as AnnotateClassName; + anchorIndex: number = 0; + anchorType: number = -1; + + constructor(config?: ICircleConfig) { + super(config); + this.anchorIndex = isNumber(config?.pointIndex) ? config?.pointIndex : 0; + this.anchorType = isNumber(config?.pointType) ? config?.pointType : -1; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Circle.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Circle.ts new file mode 100644 index 00000000..ea35505b --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Circle.ts @@ -0,0 +1,53 @@ +import { cloneDeep } from 'lodash'; +import Konva from 'konva'; +import { AnnotateClassName, ICircleConfig } from '../type'; +import { CircleStateStyle, defaultCircleConfig } from '../../configs'; +import Shape from './Shape'; + +// Circle Point +export default class Circle extends Shape { + className = 'circle' as AnnotateClassName; + declare attrs: Required; + _stateStyles = cloneDeep(CircleStateStyle); + + constructor(config?: ICircleConfig) { + const _cfg = Object.assign({ sizeAttenuation: false }, defaultCircleConfig, config); + super(_cfg); + // this.stateStyles = CircleStateStyle; + } + _sceneFunc(context: Konva.Context, shape: Konva.Shape) { + const { radius, sizeAttenuation } = this.attrs; + let scale = 1; + if (!sizeAttenuation) { + const stage = this.getStage(); + if (stage) { + scale = 1 / stage.scaleX(); + } + } + context.beginPath(); + context.arc(0, 0, radius * scale, 0, 2 * Math.PI, true); + context.closePath(); + context.fillStrokeShape(shape); + } + getSelfRect(onlySelf?: boolean) { + const { radius, strokeWidth } = this.attrs; + const size = onlySelf ? 0 : radius + (strokeWidth || 0); + return { x: -size, y: -size, width: size * 2, height: size * 2 }; + } + get stateStyles() { + const { general, hover, select } = this._stateStyles; + return { + general, + hover: { + ...hover, + radius: general?.radius + hover.radius, + strokeWidth: (general?.strokeWidth ?? 0) + (hover.strokeWidth ?? 0), + }, + select: { + ...select, + radius: general.radius + select.radius, + strokeWidth: (general.strokeWidth ?? 0) + (select.strokeWidth ?? 0), + }, + }; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Ellipse.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Ellipse.ts new file mode 100644 index 00000000..e69de29b diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Keypoint.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Keypoint.ts new file mode 100644 index 00000000..875364a6 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Keypoint.ts @@ -0,0 +1,17 @@ +import { AnnotateClassName, ICircleConfig } from '../type'; +import { ToolType } from '../../types'; +import Circle from './Circle'; + +export default class KeyPoint extends Circle { + className = 'key-point' as AnnotateClassName; + + constructor(config?: ICircleConfig) { + super(config); + } + newShape() { + return new KeyPoint(); + } + get toolType() { + return ToolType.KEY_POINT; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Line.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Line.ts new file mode 100644 index 00000000..c4fad05c --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Line.ts @@ -0,0 +1,70 @@ +import Konva from 'konva'; +import { AnnotateClassName, CacheName, IShapeConfig } from '../type'; +import { ToolType } from '../../types'; +import Shape from './Shape'; +import * as utils from '../../utils'; +import { cloneDeep } from 'lodash'; + +export default class Line extends Shape { + className = 'polyline' as AnnotateClassName; + + constructor(config?: IShapeConfig) { + super(Object.assign({ points: [] }, config)); + + this.on('pointsChange', function () { + this.onPointChange(); + }); + } + _sceneFunc(context: Konva.Context, shape: Konva.Shape) { + const { points } = this.attrs; + + if (!points || points.length === 0) return; + + context.beginPath(); + context.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) { + const p = points[i]; + context.lineTo(p.x, p.y); + } + + context.strokeShape(this); + } + onPointChange() { + this._clearCache('length' as CacheName); + super.onPointChange(); + } + getLength() { + return this._getCache('length' as CacheName, this._getLength) as number; + } + _getLength() { + const { points } = this.attrs; + const len = utils.getLineLength(points); + return len; + } + getSelfRect() { + const { points } = this.attrs; + return utils.getPointsBoundRect(points); + } + _getTextPosition() { + this.updateTextPosition(); + const { points, textPosIndex } = this.attrs; + return points[textPosIndex || 0]; + } + + updateTextPosition() { + const { points } = this.attrs; + const rect = this.getBoundRect(); + const index = utils.getMinVectorIndex({ x: rect.x, y: rect.y }, points); + if (index < 0) return; + this.attrs.textPosIndex = index; + } + clonePointsData() { + return { points: cloneDeep(this.attrs.points) }; + } + newShape() { + return new Line(); + } + get toolType() { + return ToolType.POLYLINE; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Polygon.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Polygon.ts new file mode 100644 index 00000000..32416d21 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Polygon.ts @@ -0,0 +1,91 @@ +import Konva from 'konva'; +import Shape from './Shape'; +import { IPolygonConfig, CacheName, AnnotateClassName } from '../type'; +import * as utils from '../../utils'; +import { ToolType } from '../../types'; +import { cloneDeep } from 'lodash'; + +export default class Polygon extends Shape { + className = 'polygon' as AnnotateClassName; + declare attrs: Required; + + constructor(config?: IPolygonConfig) { + super(Object.assign({ points: [], innerPoints: [] }, config)); + + this.on('pointsChange innerPointsChange', function () { + this.onPointChange(); + }); + } + + onPointChange() { + this._clearCache('area' as CacheName); + super.onPointChange(); + } + + _sceneFunc(context: Konva.Context, shape: Konva.Shape) { + const { points, innerPoints } = this.attrs; + if (!points || points.length < 3) return; + + context.beginPath(); + context.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) { + const p = points[i]; + context.lineTo(p.x, p.y); + } + context.closePath(); + if (innerPoints && innerPoints.length > 0) { + innerPoints.forEach((config) => { + const points = config.points; + context.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) { + const p = points[i]; + context.lineTo(p.x, p.y); + } + context.closePath(); + }); + } + context.fillStrokeShape(this); + } + + getArea() { + return this._getCache('area' as CacheName, this._getArea) as number; + } + _getArea() { + const { points, innerPoints } = this.attrs; + const area = utils.getArea(points, innerPoints); + return area; + } + + getSelfRect() { + const { points } = this.attrs; + return utils.getPointsBoundRect(points); + } + + _getTextPosition() { + this.updateTextPosition(); + const { points, textPosIndex } = this.attrs; + return points[textPosIndex || 0]; + } + + updateTextPosition() { + const { points } = this.attrs; + const rect = this.getBoundRect(); + const index = utils.getMinVectorIndex({ x: rect.x, y: rect.y }, points); + if (index < 0) return; + this.attrs.textPosIndex = index; + } + + clonePointsData() { + const { points, innerPoints } = this.attrs; + return { + points: cloneDeep(points), + innerPoints: cloneDeep(innerPoints), + }; + } + newShape() { + return new Polygon(); + } + get toolType() { + return ToolType.POLYGON; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Rect.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Rect.ts new file mode 100644 index 00000000..da49a5c7 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Rect.ts @@ -0,0 +1,56 @@ +import Konva from 'konva'; +import { AnnotateClassName, IShapeConfig } from '../type'; +import { ToolType } from '../../types'; +import Shape from './Shape'; + +export default class Rect extends Shape { + className = 'rect' as AnnotateClassName; + + constructor(config?: IShapeConfig) { + const cfg = Object.assign({ width: 1, height: 1, points: [] }, config); + super(cfg); + + this.on('xChange yChange widthChange heightChange', () => { + this.onPointChange(); + }); + } + _sceneFunc(context: Konva.Context, shape: Konva.Shape) { + const { width, height } = this.attrs; + + context.beginPath(); + context.rect(0, 0, width, height); + context.closePath(); + + context.fillStrokeShape(this); + } + get rotationCenter() { + const { x, y } = this.attrs; + return { x, y }; + } + getSelfRect(onlySelf?: boolean) { + const { width, height } = this.attrs; + if (onlySelf) return { x: 0, y: 0, width, height }; + const w = width - 2; + const h = height - 2; + return { + x: 1, + y: 1, + width: Math.abs(w) < 1 ? 1 : w, + height: Math.abs(h) < 1 ? 1 : h, + }; + } + getArea() { + const { width, height } = this.attrs; + return width * height; + } + clonePointsData() { + const { width, height, x, y } = this.attrs; + return { width, height, x, y }; + } + newShape() { + return new Rect(); + } + get toolType() { + return ToolType.BOUNDING_BOX; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/Shape.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/Shape.ts new file mode 100644 index 00000000..a2723062 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/Shape.ts @@ -0,0 +1,129 @@ +import Konva from 'konva'; +import { v4 as uuid } from 'uuid'; +import { cloneDeep } from 'lodash'; +import { IFrame, IUserData, ToolType } from '../../types'; +import { defaultConfig, Cursor, defaultStateStyle } from '../../configs'; +import { IShapeConfig, Vector2, IStateMap, ITransform, IRectOption, CacheName } from '../type'; +import { AnnotateClassName } from './index'; + +export interface IAnnotateObject { + className: AnnotateClassName; + uuid: string; + userData: IUserData; + frame: IFrame; + boundRect: IRectOption; + object?: any; + // state + state: IStateMap; + // style + stateStyles?: Record; + statePriority?: string[]; + showVisible: boolean; + toolType: ToolType | ''; + + clonePointsData(): ITransform; + cloneThisShape(children?: boolean): Shape; + cloneProps(prop: string): any; + newShape(): any; + // updateBoundRect(): void; + onPointChange(): void; + // updateGroup(): void; + remove(): this; + // isObject(): boolean; + getBoundRect(): IRectOption; + // _getBoundRect(): IRectOption; + getTextPosition(): Vector2; + _getTextPosition(): Vector2; +} + +export default class Shape extends Konva.Shape implements IAnnotateObject { + className = '' as AnnotateClassName; + uuid = uuid(); + userData: IUserData = {}; + frame!: IFrame; + boundRect!: IRectOption; + object!: any; + state: IStateMap = {}; + statePriority = ['hover', 'select']; + _stateStyles: Record = cloneDeep(defaultStateStyle); + _editable = true; + + // 统计信息 + lastTime?: number; + updateTime?: number; + createdAt?: any; + createdBy?: any; + version?: number; + + declare attrs: Required; + constructor(config: IShapeConfig = {}) { + const _config: IShapeConfig = { + draggable: true, + x: 0, + y: 0, + stroke: '#fff', + cursor: Cursor.pointer, + skipStageScale: true, + selectable: true, + }; + super(Object.assign(_config, defaultConfig, config)); + } + get stateStyles() { + return this._stateStyles; + } + get defaultStyle(): IShapeConfig { + return this._stateStyles.general; + } + get showVisible() { + return this.visible(); + } + set showVisible(val: boolean) { + this.visible(val); + } + get editable() { + return this._editable && this.showVisible; + } + set editable(val: boolean) { + this._editable = val; + } + get toolType() { + return '' as ToolType; + } + updateStateStyles(styles: Record) { + const { general = {}, hover = {}, select = {} } = styles; + Object.assign(this._stateStyles.general, general); + Object.assign(this._stateStyles.hover, hover); + Object.assign(this._stateStyles.select, select); + } + clonePointsData(): ITransform { + return {}; + } + cloneThisShape(): Shape { + const newShape = this.newShape(); + newShape.setAttrs(this.cloneProps('attrs')); + newShape.userData = this.cloneProps('userData'); + return newShape; + } + cloneProps(prop: string) { + return this[prop as keyof Shape] ? cloneDeep(this[prop as keyof Shape]) : {}; + } + newShape() { + return new Shape(); + } + onPointChange() { + this._clearCache('boundRect' as CacheName); + this._clearCache('textPosition' as CacheName); + } + getSelfRect(onlySelf?: boolean) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + getBoundRect() { + return this._getCache('boundRect' as CacheName, this.getSelfRect) as IRectOption; + } + getTextPosition() { + return this._getCache('textPosition' as CacheName, this._getTextPosition) as Vector2; + } + _getTextPosition(): Vector2 { + return { x: 0, y: 0 }; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shape/index.ts b/frontend/image-tool/src/package/image-editor/ImageView/shape/index.ts new file mode 100644 index 00000000..6daafb7b --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shape/index.ts @@ -0,0 +1,11 @@ +import Shape from './Shape'; +import Circle from './Circle'; +import Anchor from './Anchor'; +import KeyPoint from './Keypoint'; +import Rect from './Rect'; +import Line from './Line'; +import Polygon from './Polygon'; + +export { Shape, Circle, Anchor, KeyPoint, Rect, Line, Polygon }; +export type AnnotateObject = Shape; +export type AnnotateClassName = 'rect' | 'polyline' | 'polygon' | 'key-point'; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/KeyPointTool.ts b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/KeyPointTool.ts new file mode 100644 index 00000000..c516e33b --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/KeyPointTool.ts @@ -0,0 +1,53 @@ +import { ToolName, Vector2 } from '../../types'; +import ShapeTool from './ShapeTool'; +import ImageView from '../index'; +import { KeyPoint } from '../shape'; +import Konva from 'konva'; + +export default class KeyPointTool extends ShapeTool { + name = ToolName['key-point']; + points: Vector2[] = []; + declare object?: KeyPoint; + + constructor(view: ImageView) { + super(view); + } + + // draw + draw() { + this.clearDraw(); + this.clearEvent(); + this.initEvent(); + this.onDrawStart(); + } + stopDraw() { + this.clearDraw(); + this.clearEvent(); + this.onDrawEnd(); + } + stopCurrentDraw() { + let keyPoint = undefined; + if (this.points.length === 1) keyPoint = new KeyPoint({ ...this.points[0] }); + this.onDraw(keyPoint); + this.clearDraw(); + } + clearDraw() { + this.mouseDown = false; + this.points = []; + this.onDrawClear(); + } + onMouseDown(e: Konva.KonvaEventObject, point: Vector2) { + this.addPoint(point); + this.stopCurrentDraw(); + } + addPoint(point: Vector2) { + this.points.push(point); + } + edit(object: KeyPoint) { + this.object = object; + } + stopEdit() { + this.removeChangeEvent(); + this.object = undefined; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/PolygonTool.ts b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/PolygonTool.ts new file mode 100644 index 00000000..1583d962 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/PolygonTool.ts @@ -0,0 +1,296 @@ +import { IPolygonInnerConfig, MsgType, ToolName, Vector2 } from '../../types'; +import { Event } from '../../configs'; +import ImageView from '../index'; +import { Anchor, Line, Polygon, Shape } from '../shape'; +import PolylineTool from './PolylineTool'; +import * as utils from '../../utils'; +import Konva from 'konva'; + +export default class PolygonTool extends PolylineTool { + name = ToolName.polygon; + _minPointNum = 3; + _innerPoints?: IPolygonInnerConfig[]; + + holderFill: Polygon; + + constructor(view: ImageView) { + super(view); + + this.holderFill = new Polygon({ fill: 'rgba(255,255,255,0.2)', stroke: '' }); + this.drawGroup.add(this.holderFill); + this.holderFill.moveToBottom(); + this.changeEvent = 'absoluteTransformChange pointsChange innerPointsChange'; + } + + updateLastHolderLine() { + const endPos = this.currentAnchor.position(); + this.holderFill.show(); + this.holderFill.setAttrs({ points: [...this.points, endPos] }); + super.updateLastHolderLine(); + } + stopCurrentDraw() { + if (this.points.length < this._minPointNum) return; + const poly = new Polygon({ points: this.points.slice(0) }); + this.onDraw(poly); + this.onDrawEnd(); + this.clearDraw(); + } + clearDraw() { + this.holderFill.hide(); + super.clearDraw(); + } + // edit + edit(object: Shape) { + this.removeChangeEvent(); + this.object = object; + this.initEditObject(); + this.updateEditObject(); + this.editGroup.show(); + this.addChangEvent(); + } + stopEdit() { + this.removeChangeEvent(); + this.object = undefined; + this.editGroup.hide(); + } + initEditObject() { + if (!this.object) return; + const object = this.object as Polygon; + const { points, innerPoints } = object.attrs; + // clear + const children = [...(this.editGroup.children || [])]; + children.forEach((e) => e.destroy()); + + this._points = points; + this._innerPoints = innerPoints; + const realPoints = utils.getShapeRealPoint(object, points); + + // anchors + this.addEditLines(realPoints, -1); + if (innerPoints) { + innerPoints.forEach((e, index) => { + this.addEditLines(utils.getShapeRealPoint(object, e.points), index); + }); + } + + this.addEditPoints(realPoints, -1); + if (innerPoints) { + innerPoints.forEach((e, index) => { + this.addEditPoints(utils.getShapeRealPoint(object, e.points), index); + }); + } + this.selectAnchorIndex(-1, -1); + } + + // points: -1, innerPoints: 0 1 2 3 + addEditPoints(points: Vector2[], typeIndex: number) { + points.forEach((p, index) => { + const anchor = new Anchor({ + pointIndex: index, + pointType: typeIndex, + x: p.x, + y: p.y, + }); + this.editGroup.add(anchor); + }); + } + // points: -1, innerPoints: 0 1 2 3 + addEditLines(points: Vector2[], typeIndex: number) { + const len = points.length; + points.forEach((p, index) => { + const nextP = points[(index + 1) % len]; + const line = new Line({ + stroke: '#ff0000', + perfectDrawEnabled: false, + typeIndex, + lineIndex: index, + draggable: false, + opacity: 0, + points: [p, nextP], + }); + this.editGroup.add(line); + }); + } + initEditEvent() { + this.editGroup.on(Event.DRAG_START, () => { + this.onEditStart(); + }); + + this.editGroup.on(Event.DRAG_MOVE, (e: Konva.KonvaEventObject) => { + if (!this.object) return; + const target = e.target; + const object = this.object as Polygon; + if (target instanceof Anchor) { + const { x, y, points, innerPoints } = object.attrs; + const pointIndex = target.anchorIndex; + const typeIndex = target.anchorType; + const anchorX = target.attrs.x as number; + const anchorY = target.attrs.y as number; + + if (typeIndex === -1) { + const newPoints = points; + newPoints[pointIndex].x = anchorX - x; + newPoints[pointIndex].y = anchorY - y; + this.object.setAttrs({ points: newPoints }); + } else { + const newPoints = innerPoints[typeIndex].points; + newPoints[pointIndex].x = anchorX - x; + newPoints[pointIndex].y = anchorY - y; + this.object.setAttrs({ innerPoints }); + } + } + this.onEditChange(); + }); + + this.editGroup.on(Event.DRAG_END, () => { + // console.log('dragstend'); + this.onEditEnd(); + if (!this.checkPositionWithInterior()) { + this.view.editor.showMsg(MsgType.warning, 'Unqualified hollow condition'); + this.view.editor.actionManager.execute('undo'); + } + }); + + // line + this.editGroup.on(Event.CLICK, (e: Konva.KonvaEventObject) => { + if (!this.object) return; + if (e.target instanceof Anchor) { + // select anchor + this.selectAnchor(e.target); + } else if (e.target instanceof Line) { + this.onPolygonEdgeEdit(e.target); + } + }); + } + selectAnchor(anchor: Anchor) { + const { anchorIndex, anchorType } = anchor; + const anchors = this.editGroup.children?.filter((e) => e instanceof Anchor) as Anchor[]; + if (!anchors || anchors.length === 0) return; + anchors.forEach((e) => { + e.state.select = anchorIndex === e.anchorIndex && anchorType === e.anchorType; + }); + this.view.updateStateStyle(anchors); + this.selectAnchorIndex(anchorIndex, anchorType); + this.updateAttrHtml(); + } + onPolygonEdgeEdit(target: Line) { + const object = this.object as Polygon; + const { x, y, points, innerPoints } = object.attrs; + const lineIndex = target.attrs.lineIndex as number; + const typeIndex = target.attrs.typeIndex as number; + this._points = undefined; + this.onEditStart(); + let relPos = this.editGroup.getRelativePointerPosition() || { x: 0, y: 0 }; + relPos = utils.getPointOnLine(relPos, target.attrs.points[0], target.attrs.points[1]); + if (typeIndex === -1) { + const newPoints = points; + newPoints.splice(lineIndex + 1, 0, { x: relPos.x - x, y: relPos.y - y }); + this.updateAttrsInDataManager({ points: newPoints }); + } else { + const newPoints = innerPoints[typeIndex].points; + newPoints.splice(lineIndex + 1, 0, { x: relPos.x - x, y: relPos.y - y }); + this.updateAttrsInDataManager({ innerPoints }); + } + this.onEditEnd(); + } + onToolDelete() { + if (!this.object || this.object.userData.pointsLimit > 0) return; + const idx = this.selectAnchorIndex(); + const type = this.anchorType(); + const anchor = this.editGroup.children?.find( + (e) => e.attrs.pointIndex === idx && e.attrs.typeIndex === type, + ); + if (!anchor) return; + + const { points, innerPoints } = this.object.attrs; + const arr = type > -1 ? innerPoints[type].points : points; + if (arr.length <= this._minPointNum) { + this.view.editor.showMsg( + MsgType.warning, + `The polygon has at least ${this._minPointNum} points`, + ); + return; + } + + this.onEditStart(); + arr.splice(idx, 1); + this._points = undefined; + this.updateAttrsInDataManager({ points, innerPoints }); + this.onEditEnd(); + this.view.editor.once(Event.ACTION_END, () => { + if (!this.checkPositionWithInterior()) { + this.view.editor.showMsg(MsgType.warning, 'Unqualified hollow condition'); + this.view.editor.actionManager.execute('undo'); + } + }); + } + updateEditObject() { + if (!this.object) return; + + const object = this.object as Polygon; + const { points, innerPoints } = object.attrs; + const children = this.editGroup.children || []; + this.editGroup.setAttrs({ x: 0, y: 0 }); + const realPoints = utils.getShapeRealPoint(object, points); + const realInners = innerPoints?.map((e) => utils.getShapeRealPoint(object, e.points)); + + children.forEach((e) => { + if (e instanceof Anchor) { + const anchor = e as Anchor; + const pointIndex = anchor.anchorIndex; + const typeIndex = anchor.anchorType; + + const p = typeIndex === -1 ? realPoints[pointIndex] : realInners[typeIndex][pointIndex]; + const fill = object.attrs.stroke as string; + anchor.updateStateStyles({ general: { fill } }); + anchor.setAttrs({ x: p.x, y: p.y, fill }); + } else { + const line = e as Line; + const lineIndex = line.attrs.lineIndex as number; + const typeIndex = line.attrs.typeIndex as number; + + const nexIndex = + typeIndex === -1 + ? (lineIndex + 1) % realPoints.length + : (lineIndex + 1) % realInners[typeIndex].length; + const p1 = typeIndex === -1 ? realPoints[lineIndex] : realInners[typeIndex][lineIndex]; + const p2 = typeIndex === -1 ? realPoints[nexIndex] : realInners[typeIndex][nexIndex]; + line.setAttrs({ points: [p1, p2], x: 0, y: 0 }); + } + }); + } + onObjectChange() { + if (!this.object) return; + const object = this.object as Polygon; + const { points, innerPoints } = object.attrs; + if (this._points !== points || this._innerPoints !== innerPoints) { + this.initEditObject(); + } + this.updateEditObject(); + } + checkPositionWithInterior() { + if (!this.object) { + this.view.editor.showMsg(MsgType.error, 'this Object is not exist'); + return false; + } + if (!this.object.attrs.innerPoints || this.object.attrs.innerPoints.length === 0) { + return true; + } + return utils.checkPolygonInnerPoints(this.object as Polygon); + } + drawInfo() { + let nowPoints: Vector2[] = []; + let nowInner: IPolygonInnerConfig[] = []; + if (this.object) { + nowPoints = this.object.attrs.points; + nowInner = this.object.attrs.innerPoints; + } else if (this.holder.visible()) { + nowPoints = [...this.points, this.currentAnchor.position()]; + } + if (nowPoints.length > 0) { + const area = utils.getArea(nowPoints, nowInner); + return `area:${area.toFixed(0)}px`; + } + return ''; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/PolylineTool.ts b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/PolylineTool.ts new file mode 100644 index 00000000..475c1d9c --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/PolylineTool.ts @@ -0,0 +1,304 @@ +import { ITransform, LineDrawMode, MsgType, ToolAction, ToolName, Vector2 } from '../../types'; +import { Event } from '../../configs'; +import ShapeTool from './ShapeTool'; +import ImageView from '../index'; +import { Anchor, Line, Shape } from '../shape'; +import Konva from 'konva'; +import * as utils from '../../utils'; + +export default class PolylineTool extends ShapeTool { + name = ToolName.polyline; + _points?: Vector2[]; + points: Vector2[] = []; + _minPointNum = 2; + intervalTm: number = 0; + + holder: Line; + holderLastLine: Line; + anchors: Konva.Group; + currentAnchor: Anchor; + + constructor(view: ImageView) { + super(view); + + this.holder = new Line(); + this.holderLastLine = new Line({ dash: [5, 5], strokeWidth: 2 }); + this.anchors = new Konva.Group(); + this.currentAnchor = new Anchor(); + this.drawGroup.add(this.holder, this.holderLastLine, this.anchors, this.currentAnchor); + + this.initEditEvent(); + this.changeEvent = 'absoluteTransformChange pointsChange'; + } + // draw + draw() { + console.log('draw'); + this.clearDraw(); + this.clearEvent(); + this.initEvent(); + this.drawGroup.show(); + this.onDrawStart(); + } + stopDraw() { + this.drawGroup.hide(); + this.clearDraw(); + this.clearEvent(); + this.onDrawEnd(); + } + stopCurrentDraw() { + if (this.points.length < this._minPointNum) return; + const line = new Line({ points: this.points.slice(0) }); + this.onDraw(line); + this.onDrawEnd(); + this.clearDraw(); + } + clearDraw() { + this.mouseDown = false; + this.points = []; + this.holder.hide(); + this.holderLastLine.hide(); + this.anchors.removeChildren(); + this.currentAnchor.hide(); + } + undoDraw() { + if (this.points.length === 0) return; + + this.points.pop(); + const children = this.anchors.children || []; + const anchor = children[children.length - 1] as Anchor; + anchor?.remove(); + anchor?.destroy(); + if (this.points.length > 0) { + this.updateHolder(); + } else { + this.clearDraw(); + } + this.onDrawChange(); + } + drawInfo() { + if (this.object) { + const len = utils.getLineLength(this.object.attrs.points); + return `length:${len.toFixed(0)}px`; + } else if (this.holder.visible()) { + const points = [...this.points, this.currentAnchor.position()]; + const len = utils.getLineLength(points); + return `length:${len.toFixed(0)}px`; + } + return ''; + } + onMouseDown(e: Konva.KonvaEventObject, point: Vector2) { + point = this.updateHolderAnchor(point); + this.addPoint(point); + this.updateHolder(); + } + onMouseMove(e: Konva.KonvaEventObject, point: Vector2) { + this.updateHolderAnchor(point); + this.updateLastHolderLine(); + this.autoAddPoint(point); + this.onDrawChange(); + } + autoAddPoint(point: Vector2) { + if (!this.mouseDowning) return; + const { polyAuto, polyAutoTm } = this.view.editor.state.toolConfig; + if (!polyAuto) return; + if (Date.now() - this.intervalTm < polyAutoTm) return; + this.intervalTm = Date.now(); + this.addPoint(point); + this.updateHolder(); + } + addPoint(point: Vector2) { + this.points.push(point); + this.anchors.add(new Anchor({ ...point })); + } + updateHolder() { + this.holder.show(); + this.holder.setAttrs({ points: this.points }); + this.updateHolderAnchor(this.currentAnchor.attrs.originPos); + this.updateLastHolderLine(); + } + updateLastHolderLine() { + const endPos = this.currentAnchor.position(); + this.holderLastLine.setAttrs({ + points: [this.points[this.points.length - 1], endPos], + }); + this.holderLastLine.show(); + this.currentAnchor.show(); + } + updateHolderAnchor(pos: Vector2) { + if (!pos) return { x: 0, y: 0 }; + const mode = this.view.editor.state.toolConfig.lineMode; + this.currentAnchor.setAttrs({ originPos: pos }); + const lastPoint = this.points[this.points.length - 1]; + if (mode === LineDrawMode.horizontal && lastPoint) { + pos = { x: pos.x, y: lastPoint.y }; + } else if (mode === LineDrawMode.vertical && lastPoint) { + pos = { x: lastPoint.x, y: pos.y }; + } + this.currentAnchor.position(pos); + return pos; + } + doing(): boolean { + return this.points.length > 0; + } + + // edit + edit(object: Shape) { + this.removeChangeEvent(); + this.object = object; + this.initEditObject(); + this.updateEditObject(); + this.editGroup.show(); + this.addChangEvent(); + } + stopEdit() { + this.removeChangeEvent(); + this.object = undefined; + this.editGroup.hide(); + } + initEditObject() { + if (!this.object) return; + const object = this.object as Line; + const { points } = object.attrs; + // clear + const children = [...(this.editGroup.children || [])]; + children.forEach((e) => e.destroy()); + this._points = points; + const realPoints = utils.getShapeRealPoint(object, points); + realPoints.forEach((p, index) => { + if (index >= realPoints.length - 1) return; + const nextP = realPoints[index + 1]; + const line = new Line({ + lineIndex: index, + draggable: false, + opacity: 0, + points: [p, nextP], + }); + this.editGroup.add(line); + }); + + realPoints.forEach((p, index) => { + const anchor = new Anchor({ pointIndex: index, x: p.x, y: p.y }); + this.editGroup.add(anchor); + }); + this.selectAnchorIndex(-1); + } + initEditEvent() { + this.editGroup.on(Event.DRAG_START, (e: Konva.KonvaEventObject) => { + this.onEditStart(); + }); + + this.editGroup.on(Event.DRAG_MOVE, (e: Konva.KonvaEventObject) => { + if (!this.object) return; + const target = e.target; + if (target instanceof Anchor) { + const { x, y, points } = this.object.attrs; + const pointIndex = target.anchorIndex as number; + const anchorX = target.attrs.x as number; + const anchorY = target.attrs.y as number; + points[pointIndex].x = anchorX - x; + points[pointIndex].y = anchorY - y; + this.object.setAttrs({ points }); + } + this.onEditChange(); + }); + + this.editGroup.on(Event.DRAG_END, () => { + this.onEditEnd(); + }); + + // line + this.editGroup.on(Event.CLICK, (e: Konva.KonvaEventObject) => { + if (!this.object) return; + const target = e.target; + const object = this.object as Line; + if (target instanceof Line) { + const { x, y, points } = object.attrs; + const lineIndex = target.attrs.lineIndex as number; + this.onEditStart(); + let relPos = this.editGroup.getRelativePointerPosition() || { x: 0, y: 0 }; + relPos = utils.getPointOnLine(relPos, target.attrs.points[0], target.attrs.points[1]); + points.splice(lineIndex + 1, 0, { x: relPos.x - x, y: relPos.y - y }); + this._points = undefined; + this.updateAttrsInDataManager({ points }); + this.onEditEnd(); + } else if (target instanceof Anchor) { + this.updateAnchors(target.attrs.pointIndex); + } + }); + } + updateEditObject() { + if (!this.object) return; + const { points } = this.object.attrs; + const children = this.editGroup.children || []; + this.editGroup.setAttrs({ x: 0, y: 0 }); + const realPoints = utils.getShapeRealPoint(this.object, points); + + children.forEach((e) => { + if (e instanceof Anchor) { + const anchor = e as Anchor; + const pointIndex = anchor.anchorIndex as number; + const p = realPoints[pointIndex]; + const fill = this.object?.attrs.stroke as string; + anchor.updateStateStyles({ general: { fill } }); + anchor.setAttrs({ x: p.x, y: p.y, fill }); + } else { + const line = e as Line; + const lineIndex = line.attrs.lineIndex as number; + const p1 = realPoints[lineIndex]; + const p2 = realPoints[lineIndex + 1]; + line.setAttrs({ points: [p1, p2], x: 0, y: 0 }); + } + }); + } + updateAnchors(idx: number = -1) { + const anchors = this.editGroup.children?.filter((e) => e instanceof Anchor) as Anchor[]; + if (!anchors || anchors.length === 0) return; + anchors.forEach((e) => { + e.state.select = idx === e.anchorIndex; + }); + this.view.updateStateStyle(anchors); + this.selectAnchorIndex(idx); + } + onObjectChange() { + if (!this.object) return; + + const object = this.object as Line; + const { points } = object.attrs; + + if (this._points !== points) { + this.initEditObject(); + } + this.updateEditObject(); + } + checkEditAction(action: ToolAction) { + return [ToolAction.del, ToolAction.esc].includes(action) && this.selectAnchorIndex() !== -1; + } + onToolDelete() { + if (!this.object) return; + const idx = this.selectAnchorIndex(); + const anchor = this.editGroup.children?.find((e) => (e as Anchor).anchorIndex === idx); + if (!anchor) return; + + const { points } = this.object.attrs; + if (points.length <= this._minPointNum) { + this.view.editor.showMsg( + MsgType.warning, + `The line has at least ${this._minPointNum} points`, + ); + return; + } + + this.onEditStart(); + points.splice(idx, 1); + this._points = undefined; + this.updateAttrsInDataManager({ points }); + this.onEditEnd(); + } + updateAttrsInDataManager(attrs: ITransform) { + if (!this.object) return; + this.view.editor.dataManager.setAnnotatesTransform(this.object, attrs); + } + clearEdit() { + this.updateAnchors(-1); + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/RectTool.ts b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/RectTool.ts new file mode 100644 index 00000000..703afe4a --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/RectTool.ts @@ -0,0 +1,453 @@ +import Konva from 'konva'; +import { ToolAction, ToolName, Vector2 } from '../../types'; +import { Cursor, Event, defaultCircleConfig } from '../../configs'; +import ShapeTool from './ShapeTool'; +import ImageView from '../index'; +import { Anchor, Line, Rect, Shape } from '../shape'; +import * as utils from '../../utils'; +import { handleRotateToCmd } from '../utils'; + +// rect lines +enum Lines { + TOP = 'line-top', + BOTTOM = 'line-bottom', + LEFT = 'line-left', + RIGHT = 'line-right', + TRANS = 'line-trans', +} +// rect anchors +enum Anchors { + TOPLEFT = 'top-left', + TOPRIGHTT = 'top-right', + BOTTOMLEFT = 'bottom-left', + BOTTOMRIGHT = 'bottom-right', + TRANS = 'rotater', +} +// clockwise +const AnchorsOrder = [Anchors.TOPLEFT, Anchors.TOPRIGHTT, Anchors.BOTTOMRIGHT, Anchors.BOTTOMLEFT]; + +export default class RectTool extends ShapeTool { + name = ToolName.rect; + points: Vector2[] = []; + // draw + holder: Rect; + currentAnchor: Anchor; + anchors: Konva.Group; + // edit + declare object?: Rect; + editObjectMap = {} as Record; + transform: Konva.Transformer = new Konva.Transformer({ + resizeEnabled: false, + // rotationSnaps: [0, 90, 180, 270], + // rotationSnapTolerance: 5, + borderEnabled: true, + enabledAnchors: [ + 'top-left', + 'top-right', + 'bottom-left', + 'bottom-right', + 'middle-right', + 'middle-left', + 'top-center', + 'bottom-center', + ], + }); + // drag + dragging = false; + dragObject!: any; + dragLastPos: Vector2 | undefined; + + constructor(view: ImageView) { + super(view); + this.holder = new Rect({ dash: [5, 5], strokeWidth: 2 }); + this.currentAnchor = new Anchor(); + this.anchors = new Konva.Group(); + this.drawGroup.add(this.holder, this.anchors, this.currentAnchor); + + // edit + this.initEditObject(); + this.initEditEvent(); + this.changeEvent = 'absoluteTransformChange widthChange heightChange transform'; + handleRotateToCmd(this.view, this.transform); + } + doing(): boolean { + return this.points.length > 0; + } + validPoint(p: Vector2, referPoint?: Vector2) { + if (!referPoint) return true; + const w = p.x - referPoint.x; + const h = p.y - referPoint.y; + if (this.invalidSide(w) || this.invalidSide(h)) return false; + return true; + } + invalidSide(val: number) { + if (!val || Math.abs(val) < 0.01) return true; + return false; + } + // draw + draw() { + console.log('draw'); + this.clearDraw(); + this.clearEvent(); + this.initEvent(); + this.drawGroup.show(); + this.onDrawStart(); + } + stopDraw() { + this.clearDraw(); + this.clearEvent(); + this.drawGroup.hide(); + this.onDrawEnd(); + } + clearDraw() { + this.mouseDown = false; + this.points = []; + this.holder.hide(); + this.currentAnchor.hide(); + this.anchors.removeChildren(); + this.onDrawClear(); + } + stopCurrentDraw() { + let rect = undefined; + if (this.points.length === 2) { + const rectOption = utils.getRectFromPoints(this.points as any); + rect = new Rect(rectOption); + } + this.onDraw(rect); + this.clearDraw(); + } + undoDraw() { + this.clearDraw(); + } + drawInfo() { + if (!this.holder.visible()) return ''; + const { width, height } = this.holder.attrs; + return `width:${Math.abs(width).toFixed(0)}px; + height:${Math.abs(height).toFixed(0)}px; + area:${Math.abs(width * height).toFixed(0)}; + W/H:${Math.abs(width / height).toFixed(2)};`; + } + onMouseDown(e: Konva.KonvaEventObject, point: Vector2) { + this.addPoint(point); + if (this.points.length >= 2) { + this.stopCurrentDraw(); + } + } + onMouseMove(e: Konva.KonvaEventObject, point: Vector2) { + this.currentAnchor.position(point); + this.updateHolder(); + this.onDrawChange(); + } + addPoint(point: Vector2) { + if (this.validPoint(point, this.points[0])) { + this.points.push(point); + this.anchors.add(new Anchor({ ...point })); + } + } + updateHolder() { + const startPos = this.points[0]; + const endPos = this.currentAnchor.position(); + + const rectOption = utils.getRectFromPoints([startPos, endPos]); + this.holder.setAttrs(rectOption); + + this.holder.show(); + this.currentAnchor.show(); + } + + // edit + edit(object: Rect) { + this.removeChangeEvent(); + this._hoverIndex = -1; + this.object = object; + this.updateAnchors(-1); + this.updateEditObject(); + this.updateTransformer(); + this.editGroup.show(); + + this.addChangEvent(); + } + stopEdit() { + this.removeChangeEvent(); + this.transform.detach(); + this.object = undefined; + this.editGroup.hide(); + } + initEditObject() { + this.editGroup.add(this.transform); + const lines = [Lines.BOTTOM, Lines.LEFT, Lines.RIGHT, Lines.TOP]; + const cursor = Cursor.move; + lines.forEach((id) => { + const line = new Line({ + id, + cursor, + points: [ + { x: 0, y: 0 }, + { x: 0, y: 0 }, + ], + opacity: 0, + }); + this.editGroup.add(line); + this.editObjectMap[id] = line; + }); + + const anchors = [Anchors.BOTTOMLEFT, Anchors.BOTTOMRIGHT, Anchors.TOPLEFT, Anchors.TOPRIGHTT]; + anchors.forEach((id) => { + const anchor = new Anchor({ id, cursor }); + this.editGroup.add(anchor); + this.editObjectMap[id] = anchor; + }); + } + initEditEvent() { + let endRect: { x: number; y: number; width: number; height: number } | undefined; + this.editGroup.on(Event.DRAG_START, (e: Konva.KonvaEventObject) => { + endRect = undefined; + this.onEditStart(); + // drag + this.dragging = true; + this.dragObject = e.target; + this.dragLastPos = { x: this.dragObject.x(), y: this.dragObject.y() }; + }); + + this.editGroup.on(Event.DRAG_MOVE, (e: Konva.KonvaEventObject) => { + if (!this.dragging) return; + + const dragNode = e.target; + this.validLineDrag(dragNode); + const id = dragNode.attrs.id; + const rect = this.object as Rect; + const { x, y } = dragNode.attrs; + + const r = rect.rotation(); + let rectInfo = { x: 0, y: 0, width: 0, height: 0 }; + let compareAnchor: Shape, transPos: Vector2; + let topLeft!: Vector2, topRight!: Vector2, bottomLeft!: Vector2, bottomRight!: Vector2; + let newWidth: number = 0, + newHeight: number = 0, + newPos: Vector2 = { x: 0, y: 0 }; + + if (id === Lines.TOP) { + topLeft = { x, y }; + const bottomLeftAnchor = this.editObjectMap[Anchors.BOTTOMLEFT]; + bottomLeft = { x: bottomLeftAnchor.x(), y: bottomLeftAnchor.y() }; + const bottomTrans = utils.countTransformPoint(topLeft, bottomLeft, -r); + newWidth = rect.attrs.width; + newHeight = bottomTrans.y - topLeft.y; + } else if (id === Lines.BOTTOM) { + const topLeftAnchor = this.editObjectMap[Anchors.TOPLEFT]; + topLeft = { x: topLeftAnchor.x(), y: topLeftAnchor.y() }; + transPos = utils.countTransformPoint(topLeft, { x, y }, -r); + + newWidth = rect.attrs.width; + newHeight = transPos.y - topLeft.y + dragNode.attrs.points[0].y; + bottomLeft = { x: topLeft.x, y: topLeft.y + newHeight }; + bottomLeft = utils.countTransformPoint(topLeft, bottomLeft, r); + } else if (id === Lines.LEFT) { + topLeft = { x, y }; + const topRightAnchor = this.editObjectMap[Anchors.TOPRIGHTT]; + topRight = { x: topRightAnchor.x(), y: topRightAnchor.y() }; + const rightTrans = utils.countTransformPoint(topLeft, topRight, -r); + newWidth = rightTrans.x - topLeft.x; + newHeight = rect.attrs.height; + } else if (id === Lines.RIGHT) { + const topLeftAnchor = this.editObjectMap[Anchors.TOPLEFT]; + topLeft = { x: topLeftAnchor.x(), y: topLeftAnchor.y() }; + transPos = utils.countTransformPoint(topLeft, { x, y }, -r); + + newWidth = transPos.x - topLeft.x + dragNode.attrs.points[0].x; + newHeight = rect.attrs.height; + topRight = { x: topLeft.x + newWidth, y: topLeft.y }; + topRight = utils.countTransformPoint(topLeft, topRight, r); + } else if (id === Anchors.TOPLEFT) { + topLeft = { x, y }; + compareAnchor = this.editObjectMap[Anchors.BOTTOMRIGHT]; + bottomRight = { x: compareAnchor.x(), y: compareAnchor.y() }; + transPos = utils.countTransformPoint(bottomRight, topLeft, -r); + topRight = utils.countTransformPoint(bottomRight, { x: bottomRight.x, y: transPos.y }, r); + bottomLeft = utils.countTransformPoint(bottomRight, { x: transPos.x, y: bottomRight.y }, r); + newWidth = bottomRight.x - transPos.x; + newHeight = bottomRight.y - transPos.y; + } else if (id === Anchors.TOPRIGHTT) { + topRight = { x, y }; + compareAnchor = this.editObjectMap[Anchors.BOTTOMLEFT]; + bottomLeft = { x: compareAnchor.x(), y: compareAnchor.y() }; + transPos = utils.countTransformPoint(bottomLeft, topRight, -r); + topLeft = utils.countTransformPoint(bottomLeft, { x: bottomLeft.x, y: transPos.y }, r); + bottomRight = utils.countTransformPoint(bottomLeft, { x: transPos.x, y: bottomLeft.y }, r); + newWidth = transPos.x - bottomLeft.x; + newHeight = bottomLeft.y - transPos.y; + } else if (id === Anchors.BOTTOMLEFT) { + bottomLeft = { x, y }; + compareAnchor = this.editObjectMap[Anchors.TOPRIGHTT]; + topRight = { x: compareAnchor.x(), y: compareAnchor.y() }; + transPos = utils.countTransformPoint(topRight, bottomLeft, -r); + topLeft = utils.countTransformPoint(topRight, { x: transPos.x, y: topRight.y }, r); + bottomRight = utils.countTransformPoint(topRight, { x: topRight.x, y: transPos.y }, r); + newWidth = topRight.x - transPos.x; + newHeight = transPos.y - topRight.y; + } else if (id === Anchors.BOTTOMRIGHT) { + bottomRight = { x, y }; + compareAnchor = this.editObjectMap[Anchors.TOPLEFT]; + topLeft = { x: compareAnchor.x(), y: compareAnchor.y() }; + transPos = utils.countTransformPoint(topLeft, bottomRight, -r); + topRight = utils.countTransformPoint(topLeft, { x: transPos.x, y: topLeft.y }, r); + bottomLeft = utils.countTransformPoint(topLeft, { x: topLeft.x, y: transPos.y }, r); + newWidth = transPos.x - topLeft.x; + newHeight = transPos.y - topLeft.y; + } + rectInfo = { ...topLeft, width: newWidth, height: newHeight }; + if (newWidth < 0 && newHeight < 0) newPos = bottomRight; + else if (newWidth < 0) newPos = topRight; + else if (newHeight < 0) newPos = bottomLeft; + else newPos = topLeft; + endRect = { ...newPos, width: Math.abs(newWidth), height: Math.abs(newHeight) }; + this.dragLastPos = { x: this.dragObject.x(), y: this.dragObject.y() }; + rect.setAttrs(rectInfo); + this.updateEditObject(); + this.onEditChange(); + }); + + this.editGroup.on(Event.DRAG_END, (e: Konva.KonvaEventObject) => { + // console.log('dragstend'); + this.dragging = false; + this.dragObject = undefined; + this.dragLastPos = undefined; + endRect && this.object?.setAttrs(endRect); + endRect = undefined; + this.updateEditObject(); + this.onEditEnd(); + if (this.object) { + if (this.invalidSide(this.object.width()) || this.invalidSide(this.object.height())) { + this.view.editor.actionManager.execute('undo'); + } + } + }); + + this.editGroup.on(Event.CLICK, (e: Konva.KonvaEventObject) => { + const isAnchors = Object.values(Anchors).includes(e.target.attrs?.id); + if (e.target instanceof Anchor && isAnchors) { + const idx = AnchorsOrder.indexOf(e.target.attrs?.id as any); + this.updateAnchors(idx); + } + }); + } + updateTransformer() { + if (!this.object) return; + const anchorStyle = defaultCircleConfig; + this.transform.setAttrs({ + anchorFill: anchorStyle.fill, + anchorSize: ((anchorStyle.radius || 3) + (anchorStyle.strokeWidth || 2)) * 2, + anchorStrokeWidth: anchorStyle.strokeWidth, + anchorStroke: this.object.stroke(), + borderStroke: this.object.stroke(), + }); + this.transform.nodes([this.object]); + const rotaterAnchor = this.transform.getChildren((child: any) => { + return child.attrs.name?.indexOf('rotater') != -1; + })[0]; + if (rotaterAnchor) { + rotaterAnchor.on('mouseenter', () => { + const stage = rotaterAnchor.getStage(); + if (stage && stage.content) { + stage.content.style.cursor = Cursor.rotating; + } + }); + } + } + updateEditObject() { + if (!this.object) return; + const object = this.object; + const { x, y, width, height, rotation = 0 } = object.attrs; + this.editGroup.setAttrs({ x: 0, y: 0 }); + + const children = this.editGroup.children || []; + const left = x; + const top = y; + const right = x + width; + const bottom = y + height; + children.forEach((e) => { + if (e === this.dragObject || e instanceof Konva.Transformer) return; + + const id = e.attrs.id as any; + const isAnchors = Object.values(Anchors).includes(id); + const isLines = Object.values(Lines).includes(id); + const center = object.rotationCenter; + if (isAnchors) { + const anchor = e as Anchor; + anchor.updateStateStyles({ general: { fill: object.attrs.stroke as string } }); + anchor.fill(object.attrs.stroke as string); + if (id === Anchors.TOPLEFT) { + anchor.position(utils.countTransformPoint(center, { x: left, y: top }, rotation)); + } else if (id === Anchors.TOPRIGHTT) { + anchor.position(utils.countTransformPoint(center, { x: right, y: top }, rotation)); + } else if (id === Anchors.BOTTOMLEFT) { + anchor.position(utils.countTransformPoint(center, { x: left, y: bottom }, rotation)); + } else if (id === Anchors.BOTTOMRIGHT) { + anchor.position(utils.countTransformPoint(center, { x: right, y: bottom }, rotation)); + } + } else if (isLines) { + const line = e as Line; + line.position(center); + line.rotation(rotation); + line.setAttrs({ center }); + const point0 = line.attrs.points[0]; + const point1 = line.attrs.points[1]; + if (id === Lines.TOP) { + point1.x = width; + point1.y = 0; + } else if (id === Lines.BOTTOM) { + point0.x = 0; + point0.y = height; + point1.x = width; + point1.y = height; + } else if (id === Lines.LEFT) { + point1.x = 0; + point1.y = height; + } else if (id === Lines.RIGHT) { + point0.x = width; + point0.y = 0; + point1.x = width; + point1.y = height; + } + } + }); + } + updateAnchors(idx: number = -1) { + AnchorsOrder.forEach((id, i) => { + const anchor = this.editObjectMap[id]; + anchor.state.select = i === idx; + }); + this.view.updateStateStyle(AnchorsOrder.map((e) => this.editObjectMap[e])); + this.selectAnchorIndex(idx); + this.updateAttrHtml(); + } + validLineDrag(dragNode: any) { + const { x, y, rotation = 0, id } = dragNode.attrs; + const isLines = Object.values(Lines).includes(id); + if (!this.object || !isLines) return; + + const topLeftAnchor = this.editObjectMap[Anchors.TOPLEFT]; + const topleft = { x: topLeftAnchor.x(), y: topLeftAnchor.y() }; + const transPos = utils.countTransformPoint(topleft, { x, y }, -rotation); + + let validPos = { x: 0, y: 0 }; + if (id === Lines.TOP || id === Lines.BOTTOM) { + transPos.x = topleft.x; + } else if (id === Lines.LEFT || id === Lines.RIGHT) { + transPos.y = topleft.y; + } + + validPos = utils.countTransformPoint(topleft, transPos, rotation); + dragNode.position(validPos); + } + onObjectChange() { + if (this.dragging) return; + this.updateEditObject(); + } + checkEditAction(action: ToolAction) { + return [ToolAction.esc].includes(action) && this.selectAnchorIndex() !== -1; + } + clearEdit() { + this.updateAnchors(-1); + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/ShapeTool.ts b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/ShapeTool.ts new file mode 100644 index 00000000..406ac158 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/ShapeTool.ts @@ -0,0 +1,313 @@ +import { AnnotateObject, Anchor } from '../shape'; +import Konva from 'konva'; +import ImageView from '../index'; +import EventEmitter from 'eventemitter3'; +import { Cursor, Event } from '../../configs'; +import { Vector2, ToolAction, AnnotateModeEnum, StatusType, ToolName } from '../../types'; +import { isBoolean, isNumber } from 'lodash'; + +export type ToolEvent = + | 'object' + | 'draw-start' + | 'draw-clear' + | 'draw-change' + | 'draw-end' + | 'edit-start' + | 'edit-change' + | 'edit-end'; + +interface IConfig { + disableRenderLayer: boolean; +} + +export default class ShapeTool extends EventEmitter { + name: ToolName = ToolName.default; + toolMode: AnnotateModeEnum = AnnotateModeEnum.INSTANCE; + view: ImageView; + enable: boolean = true; + cursor: string = Cursor.crosshair; + // config + config: IConfig = { + disableRenderLayer: true, + }; + // draw + drawGroup: Konva.Group; + mouseDown: boolean = false; + mouseDowning: boolean = false; + holder!: AnnotateObject; + currentAnchor!: Anchor; + // edit + editGroup: Konva.Group; + _anchorIndex = -1; + _anchorType = -1; + _hoverIndex = -1; + _hoverType = -1; + // help + helpGroup: Konva.Group; + object?: AnnotateObject; + changeEvent: string = ''; + roundMousePoint = false; + constructor(view: ImageView) { + super(); + this.view = view; + this.drawGroup = new Konva.Group({ visible: false, sign: 'tool-draw-group' }); + this.editGroup = new Konva.Group({ visible: false, sign: 'tool-edit-group' }); + this.helpGroup = new Konva.Group({ visible: false, sign: 'tool-help-group' }); + this.drawGroup.listening(false); + this.view.helpLayer.add(this.drawGroup); + this.view.helpLayer.add(this.editGroup); + this.view.helpLayer.add(this.helpGroup); + + this._onMouseDown = this._onMouseDown.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseUp = this._onMouseUp.bind(this); + this._onDocMouseUp = this._onDocMouseUp.bind(this); + + this._onObjectChange = this._onObjectChange.bind(this); + } + // hooks + /** + * start + */ + draw() {} + /** + * stop + */ + stopDraw() {} + /** + * clear + */ + clearDraw() {} + /** + * complete + */ + stopCurrentDraw() {} + /** + * undo + */ + undoDraw() {} + /** + * delete + */ + onToolDelete() {} + /** + * update + */ + updateTool() {} + updateHolder() {} + /** + * complete handler + */ + onDraw(object: AnnotateObject | AnnotateObject[] | undefined) { + this.emit('object' as ToolEvent, object); + } + onDrawStart() { + this.emit('draw-start' as ToolEvent); + } + onDrawChange() { + this.emit('draw-change' as ToolEvent); + this.updateStatus(); + } + onDrawClear() { + this.emit('draw-clear' as ToolEvent); + this.updateStatus(false); + } + onDrawEnd() { + this.emit('draw-end' as ToolEvent); + this.updateStatus(false); + } + drawInfo() { + return ''; + } + + doing(): boolean { + return false; + } + + updateStatus(doing?: boolean) { + const state = this.view.editor.state; + doing = isBoolean(doing) ? doing : this.doing(); + const toStatus = doing ? StatusType.Create : StatusType.Default; + const curStatus = state.status; + if (toStatus === curStatus) return; + state.status = toStatus; + } + + // edit + edit(object: AnnotateObject) {} + updateEditObject() {} + stopEdit() {} + clearEdit() {} + + onEditStart() { + this.emit('edit-start' as ToolEvent); + } + onEditChange() { + this.object?.onPointChange(); + this.emit('edit-change' as ToolEvent); + } + onEditEnd() { + this.emit('edit-end' as ToolEvent); + } + selectAnchorIndex(val?: number, type?: number) { + if (isNumber(val)) { + this.anchorType(isNumber(type) ? type : -1); + this._anchorIndex = val; + this.view.editor.emit(Event.ANNOTATE_OBJECT_POINT); + } + return this._anchorIndex; + } + anchorType(val?: number) { + if (isNumber(val)) this._anchorType = val; + return this._anchorType; + } + hoverIndex(index?: number, type?: number) { + if (isNumber(index)) { + this.hoverType(isNumber(type) ? type : -1); + this._hoverIndex = index; + } + return this._hoverIndex; + } + hoverType(val?: number) { + if (isNumber(val)) this._hoverType = val; + return this._hoverType; + } + + onAction(action: ToolAction) { + switch (action) { + case ToolAction.undo: + this.undoDraw(); + this.updateStatus(); + break; + case ToolAction.esc: + if (this.drawGroup.visible()) { + this.clearDraw(); + this.onDrawClear(); + } else if (this.editGroup.visible()) { + this.clearEdit(); + } + break; + case ToolAction.stop: + this.stopCurrentDraw(); + this.updateStatus(); + break; + case ToolAction.del: + this.onToolDelete(); + break; + } + } + + checkAction(action: ToolAction): boolean { + // draw + if (this.drawGroup.visible()) { + return this.checkDrawAction(action); + } else if (this.editGroup.visible()) { + // edit + return this.checkEditAction(action); + } + return false; + } + checkDrawAction(action: ToolAction) { + return [ToolAction.esc, ToolAction.stop, ToolAction.undo].includes(action); + } + checkEditAction(action: ToolAction) { + return false; + } + + // default draw event handle + clearEvent() { + this.mouseDown = false; + this.view.stage.off('mousedown', this._onMouseDown); + this.view.stage.off('mousemove', this._onMouseMove); + document.removeEventListener('mouseup', this._onDocMouseUp); + } + initEvent() { + this.view.stage.on('mousedown', this._onMouseDown); + this.view.stage.on('mousemove', this._onMouseMove); + document.addEventListener('mouseup', this._onDocMouseUp); + } + _onDocMouseUp(e: MouseEvent) { + this.mouseDowning = false; + this._onMouseUp({ evt: e } as Konva.KonvaEventObject); + } + _onMouseUp(e: Konva.KonvaEventObject) { + const point = this.intPoint(this.view.stage.getRelativePointerPosition()); + this.mouseUpInevitable(e, point); + if (e.evt.button !== 0) return; + this.onMouseUp(e, point); + } + _onMouseDown(e: Konva.KonvaEventObject) { + let point = this.intPoint(this.view.stage.getRelativePointerPosition()); + this.mouseDownInevitable(e, point); + if (e.evt.button !== 0) return; + + // valid position + if (!this.isValid(point)) { + point = this.validPosition(point); + } + + this.mouseDown = true; + this.onMouseDown(e, point); + this.mouseDowning = true; + } + _onMouseMove(e: Konva.KonvaEventObject) { + let point = this.intPoint(this.view.stage.getRelativePointerPosition()); + this.mouseMoveInevitable(e, point); + if (!this.mouseDown) return; + + // valid position + if (!this.isValid(point)) { + point = this.validPosition(point); + } + + this.onMouseMove(e, point); + } + _onMouseOut() { + this.onMouseOut(); + } + + updateAttrHtml() { + this.view.emit(Event.UPDATE_ATTR); + } + + onMouseDown(e: Konva.KonvaEventObject, point: Vector2) {} + onMouseUp(e: Konva.KonvaEventObject, point: Vector2) {} + onMouseMove(e: Konva.KonvaEventObject, point: Vector2) {} + onMouseOut() {} + mouseMoveInevitable(e: Konva.KonvaEventObject, point: Vector2) {} + mouseDownInevitable(e: Konva.KonvaEventObject, point: Vector2) {} + mouseUpInevitable(e: Konva.KonvaEventObject, point: Vector2) {} + + // edit + addChangEvent() { + if (!this.object || !this.changeEvent) return; + this.object.on(this.changeEvent, this._onObjectChange); + } + removeChangeEvent() { + if (!this.object || !this.changeEvent) return; + this.object.off(this.changeEvent, this._onObjectChange); + } + + _onObjectChange(e: any) { + this.onObjectChange(); + } + + onObjectChange() {} + + // valid + validPosition(position: Vector2) { + return position; + } + + isValid(position: Vector2) { + return true; + } + + intPoint(point?: Vector2 | null) { + if (this.roundMousePoint && point) { + point.x = Math.floor(point.x); + point.y = Math.floor(point.y); + } + return point || { x: 0, y: 0 }; + } +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/index.ts b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/index.ts new file mode 100644 index 00000000..3de92095 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/shapeTool/index.ts @@ -0,0 +1,21 @@ +import ShapeTool, { ToolEvent } from './ShapeTool'; +import RectTool from './RectTool'; +import PolygonTool from './PolygonTool'; +import PolylineTool from './PolylineTool'; +import KeyPointTool from './KeyPointTool'; + +export { ShapeTool, RectTool, PolygonTool, PolylineTool, KeyPointTool }; + +export type ShapeToolCtr = new (...args: any) => ShapeTool; + +export const toolMap = { + rect: RectTool, + polyline: PolylineTool, + polygon: PolygonTool, + 'key-point': KeyPointTool, +}; + +export const allTools = toolMap as Record; + +export type IToolName = keyof typeof toolMap; +export type { ToolEvent }; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/type.ts b/frontend/image-tool/src/package/image-editor/ImageView/type.ts new file mode 100644 index 00000000..2098c9de --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/type.ts @@ -0,0 +1,89 @@ +import Konva from 'konva'; +import { IUserData } from '../types'; + +export type { AnnotateObject, AnnotateClassName } from './shape'; + +export interface IObjectStatus { + select?: boolean; + hover?: boolean; +} + +export type CacheName = + | 'boundRect' + | 'textPosition' + | 'expandData' + | 'curveLength' + | 'area' + | 'length'; + +export interface Vector2 { + x: number; + y: number; + attr?: IUserData; +} + +export interface ITransform { + x?: number; + y?: number; + scaleX?: number; + scaleY?: number; + rotation?: number; + width?: number; + height?: number; + points?: Vector2[]; + innerPoints?: IPolygonInnerConfig[]; +} + +export interface IRectOption { + x: number; + y: number; + width: number; + height: number; +} + +export interface ISideRect { + left: number; + right: number; + top: number; + bottom: number; +} +export interface IPolygonInnerConfig { + points: Vector2[]; +} + +export interface IShapeConfig extends Konva.ShapeConfig { + points?: Vector2[]; + innerPoints?: IPolygonInnerConfig[]; + cursor?: string; + skipStageScale?: boolean; + skipStateStyle?: boolean; + textPosIndex?: number; + selectable?: boolean; + // Skeleton point + valid?: boolean; + // fill + fillColorRgba?: { r: number; g: number; b: number; a: number }; +} +export interface ICircleConfig extends IShapeConfig { + radius?: number; + sizeAttenuation?: boolean; + pointIndex?: number; + pointType?: number; +} + +export interface IStateMap { + select?: boolean; + hover?: boolean; + edit?: boolean; + [k: string]: any; +} + +export interface IPolygonInnerConfig { + points: Vector2[]; +} + +export interface IPolygonConfig extends IShapeConfig { + innerPoints?: IPolygonInnerConfig[]; +} + +export type ICubicControl = [undefined, Vector2] | [Vector2, Vector2] | [Vector2, undefined]; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/common.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/common.ts new file mode 100644 index 00000000..3e3f8d20 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/common.ts @@ -0,0 +1,18 @@ +import Konva from 'konva'; +import { AnnotateObject } from '../type'; + +export function traverse(objects: AnnotateObject[], fn: (e: AnnotateObject) => void) { + objects.forEach((e) => { + fn(e); + }); +} +export function traverseShape(objects: AnnotateObject[], fn: (e: Konva.Shape) => void) { + objects.forEach((e) => { + if (e instanceof Konva.Shape) { + fn(e); + } + if (e instanceof Konva.Container && e.children) { + traverseShape(e.children, fn); + } + }); +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/context.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/context.ts new file mode 100644 index 00000000..2a6c4c51 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/context.ts @@ -0,0 +1,75 @@ +import ImageView from '../index'; +import Konva from 'konva'; +import { Shape } from '../shape'; + +export function hackContext(view: ImageView, layer: Konva.Layer) { + const context = layer.canvas.context; + + // copy from Konva.Context#_stroke + context._stroke = function (shape: Shape) { + const dash = shape.dash(), + // ignore strokeScaleEnabled for Text + strokeScaleEnabled = (shape as any).getStrokeScaleEnabled(); + + if (shape.hasStroke()) { + if (!strokeScaleEnabled) { + this.save(); + const pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + } + + this._applyLineCap(shape); + if (dash && shape.dashEnabled()) { + this.setLineDash(dash); + this.setAttr('lineDashOffset', shape.dashOffset()); + } + + let scale = 1; + if (shape.attrs.skipStageScale) { + scale = 1 / view.stage.scaleX(); + } + if (shape.defaultStyle?.hitStrokeWidth) { + shape.setAttrs({ + hitStrokeWidth: Number(shape.defaultStyle.hitStrokeWidth) * scale, + }); + } + const baseLineWidth = view.editor.state.config.baseLineWidth || 0; + this.setAttr('lineWidth', (shape.strokeWidth() + baseLineWidth) * scale); + + if (!(shape as any).getShadowForStrokeEnabled()) { + this.setAttr('shadowColor', 'rgba(0,0,0,0)'); + } + + const hasLinearGradient = (shape as any).getStrokeLinearGradientColorStops(); + if (hasLinearGradient) { + (this as any)._strokeLinearGradient(shape); + } else { + this.setAttr('strokeStyle', shape.stroke()); + } + + shape._strokeFunc(this); + + if (!strokeScaleEnabled) { + this.restore(); + } + } + }; + + (context as any)._fillColor = function (shape: Shape) { + const state = shape.state; + const { fillColorRgba } = shape.attrs; + const { r, g, b, a } = fillColorRgba; + + let addOpacity = 0; + if (state && (state.hover || state.select)) { + addOpacity = view.editor.state.config.baseFillOpacity || 0; + } + this.setAttr('fillStyle', `rgba(${r},${g},${b},${a + addOpacity})`); + shape._fillFunc(this); + }; + + layer.hitCanvas.context.fillStrokeShape = function fillStrokeShape(shape: Shape) { + this.fillShape(shape); + this.strokeShape(shape); + }; +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/drag.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/drag.ts new file mode 100644 index 00000000..01844a4d --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/drag.ts @@ -0,0 +1,57 @@ +import Konva from 'konva'; +import ImageView from '../index'; +import { AnnotateObject } from '../shape'; +import { ITransform } from '../type'; +import UpdateTransform from '../../common/CmdManager/cmd/UpdateTransform'; + +enum EVENT { + START = 'dragstart', + DRAG = 'dragmove', + END = 'dragend', +} +/** + * drag + */ +export function handleDragToCmd(view: ImageView) { + let startTransform: ITransform; + let object: AnnotateObject; + + view.renderLayer.on(EVENT.START, (e: Konva.KonvaEventObject) => { + object = e.target as AnnotateObject; + + startTransform = { + x: object.x(), + y: object.y(), + }; + }); + + view.renderLayer.on(EVENT.DRAG, (e: Konva.KonvaEventObject) => { + object = e.target as AnnotateObject; + view.editor.dataManager.onAnnotatesChange([object], 'transform'); + }); + + view.renderLayer.on(EVENT.END, (e: Konva.KonvaEventObject) => { + if (!startTransform || object !== e.target) return; + + const endTransform: ITransform = { + x: object.x(), + y: object.y(), + }; + addDragCmd(view, object, startTransform, endTransform); + }); +} + +export function addDragCmd( + view: ImageView, + object: AnnotateObject, + startData: ITransform, + endData: ITransform, +) { + const editor = view.editor; + const cmd = new UpdateTransform(editor, { objects: object, transforms: endData }); + cmd.undoData = { + objects: [object], + transforms: [startData], + }; + editor.cmdManager.addExecuteManually(cmd as any); +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/handleDrawCmd.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/handleDrawCmd.ts new file mode 100644 index 00000000..d1485044 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/handleDrawCmd.ts @@ -0,0 +1,116 @@ +import ImageView from '../index'; +import { AnnotateObject } from '../shape'; +import { ShapeTool, ToolEvent } from '../shapeTool'; +import { ITransform } from '../type'; +import UpdateTransform from '../../common/CmdManager/cmd/UpdateTransform'; +import { Event } from '../../configs'; + +/** + * shapeTool draw + */ +let viewDraw = {} as ImageView; +let shapeToolDraw = {} as ShapeTool; +export function handleDrawToCmd(_view: ImageView, _shapeTool: ShapeTool) { + viewDraw = _view; + shapeToolDraw = _shapeTool; + handleDrawToCmdClear(shapeToolDraw); + + shapeToolDraw.on('object' as ToolEvent, onObject); + shapeToolDraw.on('draw-change' as ToolEvent, onDrawChange); + shapeToolDraw.on('draw-clear' as ToolEvent, onDrawClear); + shapeToolDraw.on('draw-end' as ToolEvent, onDrawEnd); +} + +function onObject(shapes: AnnotateObject | AnnotateObject[]) { + const et = viewDraw.editor; + console.log(shapes); + if (!shapes) return; + if (!Array.isArray(shapes)) shapes = [shapes]; + console.log('created==>>', shapes); + + et.initIDInfo(shapes); + et.cmdManager.withGroup(() => { + if (et.state.isSeriesFrame) { + et.cmdManager.execute('add-track', et.createTrackObj(shapes)); + } + et.cmdManager.execute('add-object', shapes); + }); + if (shapes.length > 0) { + et.setCurrentTrack(shapes[0].userData.trackId); + et.emit(Event.SHOW_CLASS_INFO, shapes); + } + et.emit(Event.TOOL_DRAW, 'end', shapeToolDraw.holder, shapeToolDraw); + et.emit(Event.ANNOTATE_CREATE, shapes); +} +function onDrawChange() { + viewDraw.editor.emit(Event.TOOL_DRAW, 'change', shapeToolDraw.holder, shapeToolDraw); +} +function onDrawEnd() { + viewDraw.editor.emit(Event.TOOL_DRAW, 'end', shapeToolDraw.holder, shapeToolDraw); +} +function onDrawClear() { + viewDraw.editor.emit(Event.TOOL_DRAW, 'end', shapeToolDraw.holder, shapeToolDraw); +} + +export function handleDrawToCmdClear(shapeTool: ShapeTool) { + shapeTool.off('object' as ToolEvent, onObject); + shapeTool.off('draw-change' as ToolEvent, onDrawChange); + shapeTool.off('draw-clear' as ToolEvent, onDrawClear); + shapeTool.off('draw-end' as ToolEvent, onDrawEnd); +} + +/** + * shapeTool edit + */ +let viewEdit = {} as ImageView; +let shapeToolEdit = {} as ShapeTool; +export function handleEditToCmd(_view: ImageView, _shapeTool: ShapeTool) { + viewEdit = _view; + shapeToolEdit = _shapeTool; + handleEditToCmdClear(shapeToolEdit); + + // console.log('handleEditToCmd'); + shapeToolEdit.on('edit-start' as ToolEvent, onEditStart); + shapeToolEdit.on('edit-change' as ToolEvent, onEditChange); + shapeToolEdit.on('edit-end' as ToolEvent, onEditEnd); +} + +let startPointsInfo: ITransform; +let object: AnnotateObject | undefined; +function onEditStart() { + object = shapeToolEdit.object; + if (object) { + startPointsInfo = object.clonePointsData(); + } +} +function onEditChange() { + // console.log('onEditChange'); + viewEdit.editor.emit(Event.TOOL_Edit, 'change', shapeToolEdit.object, shapeToolEdit); + viewEdit.editor.dataManager.onAnnotatesChange([shapeToolEdit.object as any], 'transform'); +} +function onEditEnd() { + if (!startPointsInfo || object !== shapeToolEdit.object) return; + if (object) { + const endPointsInfo = object.clonePointsData(); + addEditCmd(viewEdit, object, startPointsInfo, endPointsInfo); + } + viewEdit.editor.emit(Event.TOOL_Edit, 'end', shapeToolEdit.object, shapeToolEdit); +} + +export function handleEditToCmdClear(shapeTool: ShapeTool) { + shapeTool.off('edit-start' as ToolEvent, onEditStart); + shapeTool.off('edit-end' as ToolEvent, onEditEnd); + shapeTool.off('edit-change' as ToolEvent, onEditChange); +} + +export function addEditCmd( + view: ImageView, + object: AnnotateObject, + startData: ITransform, + endData: ITransform, +) { + const editor = view.editor; + const cmd = new UpdateTransform(editor, { objects: object, transforms: endData }); + cmd.undoData = { objects: object, transforms: startData }; + editor.cmdManager.addExecuteManually(cmd as any); +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/index.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/index.ts new file mode 100644 index 00000000..c06918b1 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/index.ts @@ -0,0 +1,6 @@ +export * from './konvaProto'; +export * from './common'; +export * from './drag'; +export * from './rotate'; +export * from './context'; +export * from './handleDrawCmd'; diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/konvaProto.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/konvaProto.ts new file mode 100644 index 00000000..b4ab1cc9 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/konvaProto.ts @@ -0,0 +1,104 @@ +import Konva from 'konva'; +import { colord } from 'colord'; +import { Shape } from '../shape'; +import { traverseShape } from './common'; + +const DD = Konva.DD; +export function hackOverwriteShape() { + Konva.Node.prototype._listenDrag = function () { + this._dragCleanup(); + + this.on('mousedown.konva touchstart.konva', function (evt: Konva.KonvaEventObject) { + // TODO + const stage = this.getStage(); + if (stage && (stage as any).globalDisableDrag) { + return; + } + + const shouldCheckButton = evt.evt['button'] !== undefined; + const canDrag = !shouldCheckButton || Konva.dragButtons.indexOf(evt.evt['button']) >= 0; + if (!canDrag) { + return; + } + if (this.isDragging()) { + return; + } + + let hasDraggingChild = false; + DD._dragElements.forEach((elem) => { + if (this.isAncestorOf(elem.node)) { + hasDraggingChild = true; + } + }); + // nested drag can be started + // in that case we don't need to start new drag + if (!hasDraggingChild) { + this._createDragElement(evt); + } + }); + }; + // shape._dragChange(); + + const _oldSetAttr = Konva.Node.prototype._setAttr; + Konva.Node.prototype._setAttr = function (key: any, val: any) { + if (key === 'fill' && val) { + const rgba = colord(val); + this.attrs.fillColorRgba = rgba.toRgb(); + } + _oldSetAttr.call(this, key, val); + }; + + Konva.Group.prototype._validateAdd = function () {}; + + // colorKey + const _fire = Konva.Container.prototype._fire; + Konva.Container.prototype._fire = function (eventType: string, evt: any) { + _fire.call(this, eventType, evt); + if (eventType === 'add' && evt.child) { + if (!evt.currentTarget.getStage()) return; + addShapeKey(evt.child as Shape); + } + }; + + const _remove = Konva.Node.prototype._remove; + Konva.Node.prototype._remove = function () { + if (this.getStage()) { + removeShapeKey(this); + } + _remove.call(this); + }; +} + +export function removeShapeKey(object: any) { + object = Array.isArray(object) ? object : [object]; + traverseShape(object, (child: Konva.Shape) => { + // console.log('remove', child, child.colorKey); + if (child.colorKey) delete Konva.shapes[child.colorKey]; + }); +} + +export function addShapeKey(object: any) { + object = Array.isArray(object) ? object : [object]; + traverseShape(object, (child: Konva.Shape) => { + // console.log('add', child, child.colorKey); + const colorKey = child.colorKey; + const existShape = Konva.shapes[colorKey]; + if (existShape && existShape !== child) { + child.colorKey = getColorKey(); + } + Konva.shapes[child.colorKey] = child; + }); +} + +export function getColorKey() { + let key = ''; + // eslint-disable-next-line no-constant-condition + while (true) { + key = Konva.Util.getRandomColor(); + if (key && !(key in Konva.shapes)) { + break; + } + } + + return key; +} diff --git a/frontend/image-tool/src/package/image-editor/ImageView/utils/rotate.ts b/frontend/image-tool/src/package/image-editor/ImageView/utils/rotate.ts new file mode 100644 index 00000000..2d3594b7 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/ImageView/utils/rotate.ts @@ -0,0 +1,49 @@ +import Konva from 'konva'; +import ImageView from '../index'; +import { AnnotateObject, Rect } from '../shape'; +import { ITransform } from '../type'; +import UpdateTransform from '../../common/CmdManager/cmd/UpdateTransform'; + +enum EVENT { + START = 'transformstart', + ROTATE = 'transform', + END = 'transformend', +} +/** + * rotate + */ +export function handleRotateToCmd(view: ImageView, transform: Konva.Transformer) { + let startTransform: ITransform; + let object: Rect; + + transform.on(EVENT.START, (e: Konva.KonvaEventObject) => { + object = e.target as Rect; + + startTransform = { x: object.x(), y: object.y(), rotation: object.rotation() || 0 }; + }); + transform.on(EVENT.ROTATE, (e: Konva.KonvaEventObject) => { + object = e.target as Rect; + view.editor.dataManager.onAnnotatesChange([object], 'transform'); + }); + transform.on(EVENT.END, (e: Konva.KonvaEventObject) => { + if (!startTransform || object !== e.target) return; + + const endTransform = { x: object.x(), y: object.y(), rotation: object.rotation() || 0 }; + addRotateCmd(view, object, startTransform, endTransform); + }); +} + +export function addRotateCmd( + view: ImageView, + object: AnnotateObject, + startData: ITransform, + endData: ITransform, +) { + const editor = view.editor; + const cmd = new UpdateTransform(editor, { objects: object, transforms: endData }); + cmd.undoData = { + objects: [object], + transforms: [startData], + }; + editor.cmdManager.addExecuteManually(cmd as any); +} diff --git a/frontend/image-tool/src/package/image-editor/assets/command.svg b/frontend/image-tool/src/package/image-editor/assets/command.svg new file mode 100644 index 00000000..f0e9d42c --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/command.svg @@ -0,0 +1,17 @@ + diff --git a/frontend/image-tool/src/package/image-editor/assets/comment.svg b/frontend/image-tool/src/package/image-editor/assets/comment.svg new file mode 100644 index 00000000..1961312d --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/comment.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/commentPointer.svg b/frontend/image-tool/src/package/image-editor/assets/commentPointer.svg new file mode 100644 index 00000000..7d98d27a --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/commentPointer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/default-avatar.svg b/frontend/image-tool/src/package/image-editor/assets/default-avatar.svg new file mode 100644 index 00000000..6896c1c8 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/default-avatar.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/empty_help.svg b/frontend/image-tool/src/package/image-editor/assets/empty_help.svg new file mode 100644 index 00000000..65eb1eab --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/empty_help.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/keyboard.svg b/frontend/image-tool/src/package/image-editor/assets/keyboard.svg new file mode 100644 index 00000000..a91a3708 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/keyboard.svg @@ -0,0 +1,16 @@ + diff --git a/frontend/image-tool/src/package/image-editor/assets/loading.png b/frontend/image-tool/src/package/image-editor/assets/loading.png new file mode 100644 index 00000000..a46f1f91 Binary files /dev/null and b/frontend/image-tool/src/package/image-editor/assets/loading.png differ diff --git a/frontend/image-tool/src/package/image-editor/assets/logo.png b/frontend/image-tool/src/package/image-editor/assets/logo.png new file mode 100644 index 00000000..f3d2503f Binary files /dev/null and b/frontend/image-tool/src/package/image-editor/assets/logo.png differ diff --git a/frontend/image-tool/src/package/image-editor/assets/maskfill.svg b/frontend/image-tool/src/package/image-editor/assets/maskfill.svg new file mode 100644 index 00000000..082a3082 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/maskfill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/minus.svg b/frontend/image-tool/src/package/image-editor/assets/minus.svg new file mode 100644 index 00000000..46b6a6a0 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/minus.svg @@ -0,0 +1,17 @@ + diff --git a/frontend/image-tool/src/package/image-editor/assets/negative.svg b/frontend/image-tool/src/package/image-editor/assets/negative.svg new file mode 100644 index 00000000..03a82aab --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/negative.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/positive.svg b/frontend/image-tool/src/package/image-editor/assets/positive.svg new file mode 100644 index 00000000..764f1dbe --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/positive.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/image-tool/src/package/image-editor/assets/rotating.svg b/frontend/image-tool/src/package/image-editor/assets/rotating.svg new file mode 100644 index 00000000..7d5ded6e --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/rotating.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/image-tool/src/package/image-editor/assets/tools/interactive.svg b/frontend/image-tool/src/package/image-editor/assets/tools/interactive.svg new file mode 100644 index 00000000..bf1808bc --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/assets/tools/interactive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/clip.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/clip.ts new file mode 100644 index 00000000..fb7605d1 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/clip.ts @@ -0,0 +1,253 @@ +import Editor from '../../../Editor'; +import { Polygon, Shape } from '../../../ImageView/shape'; +import { define } from '../define'; +import { + getArea, + polygonContains, + shapePositionRelation, + POSTYPE, + polygon2PointsVector, + polygonsHollow, + getShapeRealPoint, + polygonToClip, + polygonToClips, +} from '../../../utils'; +import { MsgType, OPType } from '../../../types/enum'; + +function isAllPolygons(shapes: Shape[] | Shape) { + if (!Array.isArray(shapes)) shapes = [shapes]; + const invalid = shapes.find((shape) => !(shape instanceof Polygon)); + return !invalid; +} + +export const holeSelection = define({ + valid() { + return true; + }, + execute(editor: Editor) { + const selection = editor.selection as Polygon[]; + if (selection.length < 2) return; + const hollowTips: string = editor.lang('hollowConditionTips'); // 不满足镂空条件的提示 + if (!isAllPolygons(selection)) { + editor.showMsg(MsgType.warning, hollowTips); + return; + } + // sort by area + selection.sort((shape1, shape2) => { + return getArea(shape2.attrs.points) - getArea(shape1.attrs.points); + }); + const [maxShape, ...otherShapes] = selection; + // condition 1 + const invalid = otherShapes.find((shape) => { + return shape.attrs.innerPoints?.length > 0; + }); + if (invalid) { + editor.showMsg(MsgType.warning, hollowTips); + return; + } + // condition 2; condition 3 + const innerNum: number = maxShape.attrs.innerPoints.length; + const arrLen = otherShapes.length; + for (let i = 0; i < arrLen; i++) { + const shape = otherShapes[i]; + const isInner = polygonContains(maxShape, shape); + if (!isInner) { + editor.showMsg(MsgType.warning, hollowTips); + if (maxShape.attrs.innerPoints.length > innerNum) { + maxShape.attrs.innerPoints.splice(innerNum, i); + } + return; + } + maxShape.attrs.innerPoints.push({ points: shape.attrs.points }); + } + if (maxShape.attrs.innerPoints.length === otherShapes.length + innerNum) { + maxShape.attrs.innerPoints.length = innerNum; + holeShape(editor, maxShape, otherShapes); + editor.showMsg(MsgType.success, 'Hollow out Succeeded'); + } + }, +}); + +export const removeHoleSelection = define({ + valid() { + return true; + }, + execute(editor: Editor) { + const object = editor.selection[0] as Polygon; + if (!(object instanceof Polygon) || object.attrs.innerPoints.length === 0) return; + removeHole(editor, object); + }, +}); + +export const clipSelection = define({ + valid() { + return true; + }, + execute(editor: Editor, isClipFirst: boolean = false) { + const selection = editor.selection as Polygon[]; + let condition = isAllPolygons(selection); + if (!condition) { + editor.showMsg(MsgType.warning, editor.lang('clipConditionTips')); + return; + } + const [targetShape, ...ohterShapes] = selection; + const targetPoints = polygon2PointsVector(targetShape).points; + ohterShapes.forEach((shape) => { + const shapePoints = polygon2PointsVector(shape).points; + const pos = shapePositionRelation(targetPoints, shapePoints); + if (pos == POSTYPE.disjoint) { + condition = false; + return; + } + }); + if (!condition) { + editor.showMsg(MsgType.warning, editor.lang('clipCannotTips')); + return; + } + if (isClipFirst) cropFirstPolygon(editor, selection); + else cropOtherPolygon(editor, selection); + }, +}); +export const cutSelectionOther = define({ + valid(editor: Editor) { + return checkClip(editor); + }, + execute(editor: Editor) { + clipSelection.execute(editor, false); + }, +}); +export const cutSelectionFirst = define({ + valid(editor: Editor) { + return checkClip(editor); + }, + execute(editor: Editor) { + clipSelection.execute(editor, true); + }, +}); +function checkClip(editor: Editor) { + const polys = editor.selection.filter((e) => e instanceof Polygon); + const selectValid = Boolean(polys && polys.length > 1); + const modeValid = editor.state.modeConfig.op === OPType.EDIT; + return selectValid && modeValid; +} +function holeShape(editor: Editor, shape: Polygon, hole: Polygon | Polygon[]) { + const holes = Array.isArray(hole) ? hole : [hole]; + const pos = shape.position(); + const holesPoints = polygonsHollow(shape, holes); + if (pos.x !== 0 || pos.y !== 0) { + holesPoints.forEach((inner) => { + inner.points.forEach((point) => { + point.x -= pos.x; + point.y -= pos.y; + }); + }); + } + let { innerPoints } = shape.clonePointsData(); + innerPoints = innerPoints.concat(holesPoints); + + editor.cmdManager.withGroup(() => { + editor.cmdManager.execute('delete-object', holes); + editor.cmdManager.execute('update-transform', { + objects: shape, + transforms: { innerPoints }, + }); + }); + + editor.selectObject(shape, true); +} +function removeHole(editor: Editor, shape: Polygon) { + const innerShapes: Polygon[] = []; + const { innerPoints } = shape.clonePointsData(); + innerPoints.forEach((inner) => { + const points = getShapeRealPoint(shape, inner.points); + const newPoly = new Polygon({ points, innerPoints: [] }); + editor.initIDInfo(newPoly); + innerShapes.push(newPoly); + }); + editor.cmdManager.withGroup(() => { + editor.cmdManager.execute('add-object', innerShapes); + editor.cmdManager.execute('update-transform', { + objects: shape, + transforms: { innerPoints: [] }, + }); + }); +} +function clipMultipleByOne(editor: Editor, cropPoly: Polygon, otherPoly: Polygon[]): Polygon[] { + let newPolyArr: Polygon[] = []; + otherPoly.forEach((cliped) => { + const shapes = polygonToClip(cliped, cropPoly); + shapes.forEach((poly) => { + editor.initIDInfo(poly); + poly.userData.classId = cliped.userData.classId; + poly.userData.classType = cliped.userData.classType; + poly.userData.classVersion = cliped.userData.classVersion; + poly.userData.sourceId = cliped.userData.sourceId; + editor.mainView.updateObjectByUserData(poly); + }); + newPolyArr = newPolyArr.concat(shapes); + }); + return newPolyArr; +} +function clipOneByMultiple(editor: Editor, poly: Polygon, polys: Polygon[]) { + let failed: Polygon[] = []; + let clipedArr: Polygon[] = []; + try { + clipedArr = polygonToClips(poly, polys); + } catch (error) { + failed = polys; + } + if (failed.length > 0) { + console.log('========== clip all failed, and clip one by one'); + clipedArr = [poly]; + failed = []; + polys.forEach((clipPoly) => { + try { + const clipeds = clipMultipleByOne(editor, clipPoly, clipedArr); + if (clipeds.length > 0) clipedArr = clipeds; + } catch (error) { + failed.push(clipPoly); + } + }); + } + if (failed.length > 0) { + editor.showMsg( + MsgType.error, + editor.lang('shareFailed', { + polygons: failed.map((e) => `#${e.userData.trackName}`).join(','), + }), + ); + } + if (clipedArr.length === 0) return [poly]; + return clipedArr; +} +function cropFirstPolygon(editor: Editor, polygonList: Polygon[]) { + const [firstPoly, ...otherPoly] = polygonList; + const clipedArr = clipOneByMultiple(editor, firstPoly, otherPoly); + clipedArr.forEach((poly) => { + editor.initIDInfo(poly); + poly.userData.classId = firstPoly.userData.classId; + poly.userData.classType = firstPoly.userData.classType; + poly.userData.classVersion = firstPoly.userData.classVersion; + poly.userData.sourceId = firstPoly.userData.sourceId; + editor.mainView.updateObjectByUserData(poly); + }); + editor.cmdManager.withGroup(() => { + if (editor.state.isSeriesFrame) { + editor.cmdManager.execute('add-track', editor.createTrackObj(clipedArr)); + } + editor.cmdManager.execute('add-object', clipedArr); + editor.cmdManager.execute('delete-object', firstPoly); + }); +} +function cropOtherPolygon(editor: Editor, polygonList: Polygon[]) { + const [firstPoly, ...otherPoly] = polygonList; + const newPolyArr: Polygon[] = clipMultipleByOne(editor, firstPoly, otherPoly); + if (newPolyArr.length === 0) return; + editor.cmdManager.withGroup(() => { + if (editor.state.isSeriesFrame) { + editor.cmdManager.execute('add-track', editor.createTrackObj(newPolyArr)); + } + editor.cmdManager.execute('add-object', newPolyArr); + editor.cmdManager.execute('delete-object', otherPoly); + }); +} diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/common.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/common.ts new file mode 100644 index 00000000..5bc9cc8d --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/common.ts @@ -0,0 +1,71 @@ +import { AnnotateModeEnum, AnnotateObject, ITransform, ToolAction } from './../../../types'; +import Editor from '../../../Editor'; +import { define } from '../define'; +import { Event } from '../../../configs'; + +export const deleteSelection = define({ + valid() { + return true; + }, + execute(editor: Editor) { + const { currentDrawTool, currentEditTool } = editor.mainView; + + const tool = currentDrawTool || currentEditTool; + if (tool && tool.checkAction(ToolAction.del)) { + tool.onAction(ToolAction.del); + } else { + const selection = editor.selection; + if (selection.length === 0) return; + editor.cmdManager.execute('delete-object', selection); + editor.emit(Event.ANNOTATE_HANDLE_DELETE, { objects: selection, type: 1 }); + editor.selectObject(); + } + }, +}); +export const adjustObject = define({ + valid(editor: Editor) { + const { selection, state } = editor; + return ( + selection.length === 1 && + state.annotateMode === AnnotateModeEnum.INSTANCE && + editor.isDefaultStatus() + ); + }, + execute(editor: Editor, args?: any) { + const object = editor.selection[0]; + if (!object) return; + const rect = object.getSelfRect(true); + const position = object.getAbsolutePosition(editor.mainView.stage); + const { backgroundWidth, backgroundHeight } = editor.mainView; + const left = position.x + rect.x; + const up = position.y + rect.y; + const right = left + rect.width; + const down = up + rect.height; + + const key = args.split('+').pop(); + let { x, y } = object.attrs; + switch (key) { + case 'up': + if (up <= 0) return; + y--; + break; + case 'down': + if (down >= backgroundHeight) return; + y++; + break; + case 'left': + if (left <= 0) return; + x--; + break; + case 'right': + if (right >= backgroundWidth) return; + x++; + break; + } + let objects: AnnotateObject[] = []; + let transforms: ITransform[] = []; + objects = [object]; + transforms = [{ x, y }]; + editor.cmdManager.execute('update-transform', { objects, transforms }); + }, +}); diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/index.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/index.ts new file mode 100644 index 00000000..c0bd0782 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/index.ts @@ -0,0 +1,6 @@ +export * from './common'; +export * from './tool'; +export * from './undo-redo'; +export * from './clip'; +export * from './ui'; +export * from './seriesFrame'; diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/seriesFrame.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/seriesFrame.ts new file mode 100644 index 00000000..18879779 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/seriesFrame.ts @@ -0,0 +1,51 @@ +import Editor from '../../../Editor'; +import { define } from '../define'; +import { OPType, StatusType, AnnotateModeEnum } from '../../../types'; + +export const toNextFrame = define({ + valid(editor: Editor) { + return editor.isDefaultStatus(); + }, + execute(editor: Editor) { + changeFrame(editor, 1); + }, +}); +export const previousFrame = define({ + valid(editor: Editor) { + return editor.isDefaultStatus(); + }, + execute(editor: Editor) { + changeFrame(editor, -1); + }, +}); +export const copyToNext = define({ + valid(editor: Editor) { + return canCopy(editor); + }, + execute(editor: Editor) { + editor.dataManager.copyForward(); + }, +}); +export const copyToLast = define({ + valid(editor: Editor) { + return canCopy(editor); + }, + execute(editor: Editor) { + editor.dataManager.copyBackWard(); + }, +}); + +function changeFrame(editor: Editor, type: number) { + const { frames, frameIndex } = editor.state; + const index = frameIndex + type; + if (index < 0 || index >= frames.length) return; + editor.switchFrame(index); +} +function canCopy(editor: Editor) { + const { state } = editor; + return ( + (state.modeConfig.op === OPType.EDIT || state.modeConfig.name === 'all') && + state.annotateMode === AnnotateModeEnum.INSTANCE && + state.status === StatusType.Default + ); +} diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/tool.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/tool.ts new file mode 100644 index 00000000..23a8ac4b --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/tool.ts @@ -0,0 +1,99 @@ +import Editor from '../../../Editor'; +import { define } from '../define'; +import { LineDrawMode, ToolAction } from '../../../types'; +import { Event, Cursor } from '../../../configs'; +import { PolylineTool } from 'image-editor/ImageView/shapeTool'; + +export const changeTool = define({ + valid() { + return true; + }, + canBlocked() { + return false; + }, + execute(editor: Editor, args?: any) { + return () => { + editor.emit(Event.TOOL_CHANGE, args); + }; + }, + end(editor: Editor, executeCallback: Function) { + executeCallback(); + }, +}); + +export const selectTool = define({ + valid() { + return true; + }, + canBlocked() { + return false; + }, + execute(editor: Editor) { + editor.mainView.disableDraw(); + editor.selectObject(); + }, +}); + +export const drawTool = define({ + valid() { + return true; + }, + canBlocked() { + return false; + }, + execute(editor: Editor, type: string) { + const state = editor.state; + if (state.activeTool !== type) editor.mainView.enableDraw(type); + }, +}); + +export const stopCurrentDraw = define({ + valid() { + return true; + }, + canBlocked() { + return false; + }, + execute(editor: Editor) { + const { currentDrawTool, currentEditTool } = editor.mainView; + const tool = currentDrawTool || currentEditTool; + + if (tool && tool.checkAction(ToolAction.stop)) { + tool.onAction(ToolAction.stop); + } + editor.mainView.setCursor(editor.mainView.cursor || Cursor.auto); + }, +}); + +export const lineToolDrawMode = define({ + valid(editor: Editor) { + const curDrawTool = editor.mainView.currentDrawTool; + return Boolean(curDrawTool && curDrawTool instanceof PolylineTool); + }, + canBlocked() { + return false; + }, + execute(editor: Editor, key: string) { + const cfg = editor.state.toolConfig; + if (key === 'a') { + cfg.lineMode = + cfg.lineMode === LineDrawMode.horizontal ? LineDrawMode.default : LineDrawMode.horizontal; + } else { + cfg.lineMode = + cfg.lineMode === LineDrawMode.vertical ? LineDrawMode.default : LineDrawMode.vertical; + } + const curDrawTool = editor.mainView.currentDrawTool; + if (curDrawTool && curDrawTool instanceof PolylineTool) { + curDrawTool.updateHolder(); + } + // if (cfg.lineMode === LineDrawMode.default) { + // editor.visibleMessageBox(false); + // } else if (cfg.lineMode === LineDrawMode.horizontal) { + // const txt = editor.lang('Horizontal Drawing Model'); + // editor.visibleMessageBox(true, txt); + // } else if (cfg.lineMode === LineDrawMode.vertical) { + // const txt = editor.lang('Vertical Drawing Model'); + // editor.visibleMessageBox(true, txt); + // } + }, +}); diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/ui.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/ui.ts new file mode 100644 index 00000000..774b1ddd --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/ui.ts @@ -0,0 +1,40 @@ +import Editor from '../../../Editor'; +import { define } from '../define'; +import { Event } from '../../../configs'; + +export const toggleHelpLine = define({ + valid() { + return true; + }, + canBlocked: () => false, + execute(editor: Editor) { + const { config } = editor.state; + config.helperLine.showLine = !config.helperLine.showLine; + }, +}); +export const toggleClassTitle = define({ + valid() { + return true; + }, + canBlocked: () => false, + execute(editor: Editor) { + const { config } = editor.state; + config.showClassTitle = !config.showClassTitle; + if (config.showClassTitle) editor.emit(Event.DRAW); + }, +}); +export const toggleClassView = define({ + valid() { + return true; + }, + execute(editor: Editor) { + const { config } = editor.state; + const selection = editor.selection; + + if (config.showClassView) { + config.showClassView = false; + } else if (selection.length === 1) { + editor.emit(Event.SHOW_CLASS_INFO, selection[0]); + } + }, +}); diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/action/undo-redo.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/undo-redo.ts new file mode 100644 index 00000000..3b945bbc --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/action/undo-redo.ts @@ -0,0 +1,28 @@ +import { ToolAction } from './../../../types'; +import Editor from '../../../Editor'; +import { define } from '../define'; + +export const undo = define({ + valid() { + return true; + }, + canBlocked: () => false, + execute(editor: Editor) { + const { currentDrawTool, currentEditTool } = editor.mainView; + const tool = currentDrawTool || currentEditTool; + if (tool && tool.checkAction(ToolAction.undo)) { + tool.onAction(ToolAction.undo); + } else { + editor.cmdManager.undo(); + } + }, +}); + +export const redo = define({ + valid() { + return true; + }, + execute(editor: Editor) { + editor.cmdManager.redo(); + }, +}); diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/define.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/define.ts new file mode 100644 index 00000000..e4918fa5 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/define.ts @@ -0,0 +1,27 @@ +import type Editor from '../../Editor'; + +export interface IActionOption { + name?: string; + end?: (e: Editor, executeCallback: Function, ...args: any[]) => void; + isContinue?: (e: Editor) => boolean; + canBlocked?: (e: Editor) => boolean; + valid?: (e: Editor) => boolean; + execute: (e: Editor, ...args: any[]) => Promise | void | Error | Function; + [key: string]: any; +} + +export type IAction = Required; + +function noop() {} +const defaultOption: IAction = { + name: '', + valid: () => true, + execute: noop, + end: noop, + canBlocked: () => true, + isContinue: () => false, +}; + +export function define(option: IActionOption): IAction { + return Object.assign({}, defaultOption, option); +} diff --git a/frontend/image-tool/src/package/image-editor/common/ActionManager/index.ts b/frontend/image-tool/src/package/image-editor/common/ActionManager/index.ts new file mode 100644 index 00000000..7b510b63 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/ActionManager/index.ts @@ -0,0 +1,117 @@ +import Editor from '../../Editor'; +import * as Actions from './action'; +import { IAction } from './define'; +import { StatusType, ToolAction } from '../../types'; +import { Event } from '../../configs'; + +export type IActionName = keyof typeof Actions; + +Object.keys(Actions).forEach((name) => { + Actions[name as IActionName].name = name; +}); + +export const AllActions = Object.keys(Actions) as IActionName[]; + +export default class ActionManager { + editor: Editor; + actions: Record; + currentAction: IAction | undefined = undefined; + blockStatus: StatusType[] = [ + StatusType.Loading, + StatusType.Confirm, + StatusType.Modal, + StatusType.Play, + StatusType.Create, + ]; + constructor(editor: Editor) { + this.editor = editor; + this.actions = { ...Actions }; + } + + registryAction(name: string, action: IAction) { + this.actions[name] = action; + } + + async execute(name: T | T[], ...args: any[]): Promise { + let action = undefined; + if (Array.isArray(name)) { + action = this.getEnableAction(name); + } else { + action = this.actions[name] as IAction; + } + + if (!action) return; + const canBlocked = action.canBlocked(this.editor); + if (this.isBlocked() && canBlocked) { + console.log(`action ${name} blocked`); + return; + } + + let result; + let preAction; + if (action.valid(this.editor)) { + this.editor.emit(Event.ACTION_START, name); + console.log('action start:', action.name); + preAction = this.currentAction; + this.currentAction = action; + try { + result = await action.execute(this.editor, ...args); + } catch (e) { + console.error('action error:', name); + console.error(e); + } + this.currentAction = preAction; + console.log('action end:', name); + action.end(this.editor, result); + this.editor.emit(Event.ACTION_END, name); + } + + return result; + } + + isBlocked() { + // return false; + const { status, blocked } = this.editor.state; + console.log(this.currentAction); + return this.currentAction || blocked || this.blockStatus.includes(status); + } + + getEnableAction(names: IActionName[]) { + const config = this.editor.state.modeConfig; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + const action = this.actions[name] as IAction; + if (action && config.actions[name] && action.valid(this.editor)) return action; + } + + return undefined; + } + + stopCurrentAction() { + if (!this.currentAction) return; + + this.currentAction.end(this.editor, () => {}); + console.log(`stop action: ${this.currentAction.name}`); + + this.currentAction = undefined; + } + + handleEsc() { + const { currentDrawTool, currentEditTool } = this.editor.mainView; + const tool = currentDrawTool || currentEditTool; + if (tool && tool.checkAction(ToolAction.esc)) { + tool.onAction(ToolAction.esc); + } else if (this.currentAction) { + this.stopCurrentAction(); + } else if (this.editor.selection.length > 0) { + this.editor.selectObject(); + } + } + + isActionValid(name: IActionName) { + const action = this.actions[name] as IAction; + if (!action) return false; + + return action.valid(this.editor); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/BSError.ts b/frontend/image-tool/src/package/image-editor/common/BSError.ts new file mode 100644 index 00000000..4e131f28 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/BSError.ts @@ -0,0 +1,10 @@ +export default class BSError { + code: string; + message: string; + oriError: any; + constructor(code?: string, message?: string, oriError?: any) { + this.code = code || ''; + this.message = message || ''; + this.oriError = oriError; + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/CmdBase.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/CmdBase.ts new file mode 100644 index 00000000..3a0d86b1 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/CmdBase.ts @@ -0,0 +1,27 @@ +import Editor from '../../Editor'; +export default class CmdBase { + name: string = ''; + createTime: number = 0; + updateTime: number = 0; + editor: Editor; + data: T; + undoData: D | undefined = undefined; + constructor(editor: Editor, data: T) { + this.createTime = new Date().getTime(); + this.updateTime = this.createTime; + this.data = data; + this.editor = editor; + } + redo() { + throw new Error('need implement redo method'); + } + undo() { + throw new Error('need implement undo method'); + } + + canMerge(cmd: CmdBase) { + return false; + } + + merge(cmd: CmdBase) {} +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/CmdGroup.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/CmdGroup.ts new file mode 100644 index 00000000..8c2e8951 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/CmdGroup.ts @@ -0,0 +1,28 @@ +import Editor from '../../Editor'; +import CmdBase from './CmdBase'; + +export default class CmdGroup extends CmdBase { + cmds: CmdBase[] = []; + constructor(editor: Editor) { + super(editor, undefined); + } + redo() { + try { + this.cmds.forEach((e) => { + e.redo(); + }); + } catch (error) { + console.error(error); + } + } + undo() { + try { + const cmds = [...this.cmds].reverse(); + cmds.forEach((e) => { + e.undo(); + }); + } catch (error) { + console.error(error); + } + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/AddObject.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/AddObject.ts new file mode 100644 index 00000000..823bc92a --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/AddObject.ts @@ -0,0 +1,42 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +import { AnnotateObject, IFrame } from '../../../types'; +import Konva from 'konva'; + +export interface IAddObjectItem { + group?: Konva.Group; + objects: AnnotateObject | AnnotateObject[]; + frame?: IFrame; +} + +export type IAddObjectOption = IAddObjectItem[] | AnnotateObject | AnnotateObject[]; + +export default class AddObject extends CmdBase { + name: string = 'add-object'; + redo(): void { + const editor = this.editor; + const frame = editor.getCurrentFrame(); + + if (!Array.isArray(this.data) || !(this.data[0] as any).objects) { + const objects = Array.isArray(this.data) ? this.data : [this.data]; + const data: IAddObjectItem[] = [{ objects: objects as AnnotateObject[] }]; + this.data = data; + } + + const datas = this.data as IAddObjectItem[]; + + datas.forEach((data) => { + if (!data.frame) data.frame = frame; + editor.dataManager.addAnnotates(data.objects, data.frame); + }); + } + undo(): void { + const editor = this.editor; + + const datas = this.data as IAddObjectItem[]; + + datas.forEach((data) => { + editor.dataManager.removeAnnotates(data.objects, data.frame); + }); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/AddTrack.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/AddTrack.ts new file mode 100644 index 00000000..de8344ab --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/AddTrack.ts @@ -0,0 +1,30 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +import { IUserData } from '../../../types'; +import { createUUID } from '../../../utils'; + +export type IAddTrackOption = Partial | Partial[]; + +export default class AddTrack extends CmdBase { + redo(): void { + const editor = this.editor; + + let data = this.data; + if (!Array.isArray(data)) data = [data]; + + data.forEach((e: Partial) => { + if (!e.trackId) e.trackId = createUUID(); + editor.trackManager.addTrackObject(e.trackId, e); + }); + } + undo(): void { + const editor = this.editor; + + let data = this.data; + if (!Array.isArray(data)) data = [data]; + + data.forEach((e: Partial) => { + editor.trackManager.removeTrackObject(e.trackId as string); + }); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/DeleteObject.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/DeleteObject.ts new file mode 100644 index 00000000..7272cd2e --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/DeleteObject.ts @@ -0,0 +1,68 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +import type { AnnotateObject, IFrame } from '../../../types'; + +export interface IDeleteObjectItem { + objects: AnnotateObject | AnnotateObject[]; + frame?: IFrame; +} + +interface IUndoData { + subObjects: AnnotateObject[]; + topObjects: AnnotateObject[]; +} + +export type IDeleteObjectOption = IDeleteObjectItem | AnnotateObject | AnnotateObject[]; + +export default class DeleteObject extends CmdBase { + name: string = 'delete-object'; + redo(): void { + const editor = this.editor; + const frame = editor.getCurrentFrame(); + + if (!(this.data as any).objects) { + const objects = Array.isArray(this.data) ? this.data : [this.data]; + const _data: IDeleteObjectItem = { objects: objects as AnnotateObject[] }; + this.data = _data; + } + + const data = this.data as IDeleteObjectItem; + if (!data.frame) data.frame = frame; + const objects = data.objects as AnnotateObject[]; + + if (!this.undoData) { + const { subObjects, topObjects } = getObjects(objects); + this.undoData = { + subObjects, + topObjects, + }; + } + + const { subObjects, topObjects } = this.undoData; + editor.dataManager.removeAnnotates([...subObjects, ...topObjects], data.frame); + } + undo(): void { + const editor = this.editor; + const data = this.data as IDeleteObjectItem; + + if (!this.undoData) return; + + const { topObjects, subObjects } = this.undoData; + editor.dataManager.addAnnotates([...subObjects, ...topObjects], data.frame); + } +} + +function getObjects(objects: AnnotateObject[]) { + const map: any = {}; + objects.forEach((e) => { + map[e.uuid] = true; + }); + + const subObjects = [] as AnnotateObject[]; + const topObjects = [] as AnnotateObject[]; + objects.forEach((e) => { + topObjects.push(e); + }); + + return { subObjects, topObjects }; +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/DeleteTrack.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/DeleteTrack.ts new file mode 100644 index 00000000..16b92f60 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/DeleteTrack.ts @@ -0,0 +1,35 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +// import { IUserData } from '../../../type'; +import { IUserData } from '../../../types'; + +export type IDeleteTrackOption = string | string[]; + +export default class DeleteTrack extends CmdBase { + redo(): void { + const editor = this.editor; + + let data = this.data; + if (!Array.isArray(data)) data = [data]; + + if (!this.undoData) { + this.undoData = data.map((trackId) => + editor.trackManager.getTrackObject(trackId), + ) as IUserData[]; + } + + data.forEach((trackId) => { + editor.trackManager.removeTrackObject(trackId); + }); + } + undo(): void { + const editor = this.editor; + + if (!this.undoData) return; + + const data = this.undoData; + data.forEach((obj) => { + editor.trackManager.addTrackObject(obj.trackId as string, obj); + }); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateAttrs.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateAttrs.ts new file mode 100644 index 00000000..f9f85e84 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateAttrs.ts @@ -0,0 +1,52 @@ +import CmdBase from '../CmdBase'; +import * as THREE from 'three'; +import type { ICmdOption } from './index'; +import { ITransform, IShapeConfig } from '../../../types'; +import { AnnotateObject } from '../../../ImageView/shape'; +import * as utils from '../../../utils'; + +export interface IUpdateAttrsOption { + objects: AnnotateObject[] | AnnotateObject; + data: IShapeConfig[] | IShapeConfig; +} + +export default class UpdateAttrs extends CmdBase { + name: string = 'update-points'; + redo(): void { + let { data, objects } = this.data; + const editor = this.editor; + + if (!Array.isArray(objects)) objects = [objects]; + + if (!this.undoData) { + const undoData = [] as IShapeConfig[]; + + const attrKeys = Object.keys(Array.isArray(data) ? data[0] : data); + objects.forEach((object, index) => { + const copeData = pickAttrs(object, attrKeys) as IShapeConfig; + undoData.push(copeData); + }); + + this.undoData = undoData; + } + + // editor.dataManager.setAnnotatesAttrs(objects, data); + } + undo(): void { + if (!this.undoData) return; + const editor = this.editor; + let { objects } = this.data; + + if (!Array.isArray(objects)) objects = [objects]; + + // editor.dataManager.setAnnotatesAttrs(objects, this.undoData); + } +} + +function pickAttrs(object: AnnotateObject, attrs: string[]) { + const newObj: any = {}; + attrs.forEach((attr) => { + newObj[attr] = object.getAttr(attr); + }); + return newObj; +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateTrack.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateTrack.ts new file mode 100644 index 00000000..f8412f30 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateTrack.ts @@ -0,0 +1,57 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +import { IUserData } from '../../../types'; +import { cloneDeep } from 'lodash'; + +interface ITrackItem { + trackId: string; + data: Partial; +} + +export type IUpdateTrackOption = ITrackItem[]; + +export default class UpdateTrack extends CmdBase< + ICmdOption['update-track'], + ICmdOption['update-track'] +> { + redo(): void { + const editor = this.editor; + + // let { tracks, objects } = this.data; + + if (!this.undoData) { + const undoData: ITrackItem[] = []; + this.data.forEach((data) => { + let oldData = editor.trackManager.getTrackObject(data.trackId) || {}; + oldData = cloneDeep(oldData); + undoData.push({ trackId: data.trackId, data: oldData }); + }); + + this.undoData = undoData; + } + + this.data.forEach((data) => { + editor.trackManager.updateTrackData(data.trackId, data.data); + }); + + const trackIds = this.data.map((e) => e.trackId); + const objects = editor.trackManager.getObjects(trackIds); + // test + (this as any).objects = objects; + if (objects.length > 0) editor.mainView.updateObjectByUserData(objects); + } + undo(): void { + const editor = this.editor; + + if (!this.undoData) return; + + this.undoData.forEach((data) => { + editor.trackManager.updateTrackData(data.trackId, data.data); + }); + + const trackIds = this.data.map((e) => e.trackId); + const objects = editor.trackManager.getObjects(trackIds); + + if (objects.length > 0) editor.mainView.updateObjectByUserData(objects); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateTransform.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateTransform.ts new file mode 100644 index 00000000..725498b7 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateTransform.ts @@ -0,0 +1,66 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +import { ITransform } from '../../../types'; +import { AnnotateObject } from '../../../ImageView/shape'; +import { cloneDeep } from 'lodash'; + +export interface IUpdateTransformOption { + objects: AnnotateObject[] | AnnotateObject; + transforms: ITransform[] | ITransform; + fromData?: ITransform[] | ITransform; +} + +export default class UpdateTransform extends CmdBase< + ICmdOption['update-transform'], + ICmdOption['update-transform'] +> { + name: string = 'update-transform'; + redo(): void { + let editor = this.editor; + + let { objects, transforms } = this.data; + if (!Array.isArray(objects)) objects = [objects]; + + if (!this.undoData) { + let undoData: IUpdateTransformOption = { + objects: objects, + transforms: [], + }; + objects.forEach((object, index) => { + const changePro = Array.isArray(transforms) + ? transforms[index] || transforms[0] + : transforms; + const clonePro: Record = {}; + Object.keys(changePro).forEach((key) => { + clonePro[key] = cloneDeep(object.attrs[key]); + }); + (undoData.transforms as ITransform[]).push(clonePro); + }); + this.undoData = undoData; + } + + editor.dataManager.setAnnotatesTransform(objects, transforms); + } + undo(): void { + if (!this.undoData) return; + let editor = this.editor; + let { transforms, objects } = this.undoData; + if (!Array.isArray(objects)) objects = [objects]; + + editor.dataManager.setAnnotatesTransform(objects, transforms); + } + canMerge(cmd: CmdBase): boolean { + let offsetTime = Math.abs(this.updateTime - cmd.updateTime); + return cmd instanceof UpdateTransform && + this.data.objects === cmd.data.objects && + !Array.isArray(this.data.transforms) && + offsetTime < 500 + ? true + : false; + } + + merge(cmd: UpdateTransform) { + Object.assign(this.data.transforms, cmd.data.transforms); + this.updateTime = new Date().getTime(); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateUserData.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateUserData.ts new file mode 100644 index 00000000..78ab0eea --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/UpdateUserData.ts @@ -0,0 +1,68 @@ +import CmdBase from '../CmdBase'; +import type { ICmdOption } from './index'; +import { IUserData, AnnotateObject } from '../../../types'; +import * as utils from '../../../utils'; + +export interface IUpdateUserDataOption { + objects: AnnotateObject[] | AnnotateObject; + data: IUserData[] | IUserData; +} + +export default class UpdateUserData extends CmdBase< + ICmdOption['update-user-data'], + ICmdOption['update-user-data'] +> { + name: string = 'update-user-data'; + redo(): void { + const editor = this.editor; + + let { data, objects } = this.data; + + if (!Array.isArray(objects)) objects = [objects]; + + if (!this.undoData) { + const undoData: IUpdateUserDataOption = { + objects: objects, + data: [], + // transform: { objects: [], transforms: [] }, + }; + + const attrKeys = Object.keys(Array.isArray(data) ? data[0] : data); + objects.forEach((object, index) => { + const copeData = utils.pickAttrs(object.userData, attrKeys) as IUserData; + undoData.data.push(copeData); + }); + + this.undoData = undoData; + } + + editor.dataManager.setAnnotatesUserData(objects, data); + } + undo(): void { + const editor = this.editor; + + if (!this.undoData) return; + + let { data, objects } = this.undoData; + if (!Array.isArray(objects)) objects = [objects]; + + editor.dataManager.setAnnotatesUserData(objects, data); + } + + canMerge(cmd: UpdateUserData): boolean { + const data = this.data; + const offsetTime = Math.abs(this.updateTime - cmd.updateTime); + const valid = + cmd instanceof UpdateUserData && + data.objects === data.objects && + !Array.isArray(data.data) && + offsetTime < 1000; + + return valid; + } + + merge(cmd: UpdateUserData) { + Object.assign(this.data.data, cmd.data.data); + this.updateTime = new Date().getTime(); + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/index.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/index.ts new file mode 100644 index 00000000..149c37a1 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/cmd/index.ts @@ -0,0 +1,33 @@ +import AddObject, { IAddObjectOption } from './AddObject'; +import DeleteObject, { IDeleteObjectOption } from './DeleteObject'; +import UpdateUserData, { IUpdateUserDataOption } from './UpdateUserData'; +import UpdateTransform, { IUpdateTransformOption } from './UpdateTransform'; +import UpdateAttrs, { IUpdateAttrsOption } from './UpdateAttrs'; +import AddTrack, { IAddTrackOption } from './AddTrack'; +import UpdateTrack, { IUpdateTrackOption } from './UpdateTrack'; +import DeleteTrack, { IDeleteTrackOption } from './DeleteTrack'; + +export interface ICmdOption { + 'add-object': IAddObjectOption; + 'delete-object': IDeleteObjectOption; + 'update-user-data': IUpdateUserDataOption; + 'update-transform': IUpdateTransformOption; + 'update-attrs': IUpdateAttrsOption; + 'add-track': IAddTrackOption; + 'update-track': IUpdateTrackOption; + 'delete-track': IDeleteTrackOption; +} + +type Name = keyof ICmdOption; +const CMD: Record = { + 'add-object': AddObject, + 'delete-object': DeleteObject, + 'update-user-data': UpdateUserData, + 'update-transform': UpdateTransform, + 'update-attrs': UpdateAttrs, + 'add-track': AddTrack, + 'update-track': UpdateTrack, + 'delete-track': DeleteTrack, +}; + +export default CMD; diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/index.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/index.ts new file mode 100644 index 00000000..d3e36e57 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/index.ts @@ -0,0 +1,147 @@ +import CmdBase from './CmdBase'; +import CMD from './cmd/index'; +import Editor from '../../Editor'; +import type { ICmdOption } from './cmd/index'; +import { Event } from '../../configs'; +import CmdGroup from './CmdGroup'; +import EventEmitter from 'eventemitter3'; + +export type ICmdName = keyof typeof CMD; + +export interface ICmdBackup { + cmds: CmdBase[]; + index: number; +} + +export default class CmdManager extends EventEmitter { + static Cmd = CMD; + editor: Editor; + cmds: CmdBase[] = []; + index: number = -1; + max: number = 20; + backup: ICmdBackup | undefined = undefined; + private _group: CmdGroup | undefined = undefined; + constructor(editor: Editor) { + super(); + this.editor = editor; + } + + execute(name: T | CmdGroup, data?: ICmdOption[T]) { + let cmd = {} as CmdBase; + if (name instanceof CmdGroup) { + cmd = name; + } else { + const CmdCtr = CmdManager.Cmd[name]; + if (!CmdCtr) return; + cmd = new CmdCtr(this.editor, data as any); + cmd.name = name; + } + + if (this._group) { + this._group.cmds.push(cmd); + return; + } + + this.cmds = this.cmds.slice(0, this.index + 1); + + const last = this.cmds[this.index]; + if (last && last.canMerge(cmd)) { + last.merge(cmd); + last.redo(); + } else { + cmd.redo(); + this.cmds.push(cmd); + } + + if (this.cmds.length > this.max) { + this.cmds = this.cmds.slice(-this.max); + } + + this.index = this.cmds.length - 1; + this.emit(Event.EXECUTE, { cmd, last }); + } + + addExecuteManually(cmd: CmdBase) { + if (this._group) { + this._group.cmds.push(cmd); + return; + } + + this.cmds = this.cmds.slice(0, this.index + 1); + this.cmds.push(cmd); + if (this.cmds.length > this.max) { + this.cmds = this.cmds.slice(-this.max); + } + this.index = this.cmds.length - 1; + this.emit(Event.EXECUTE, { cmd }); + } + + undo() { + if (this.index < 0 || this.cmds.length === 0) return; + + const cmd = this.cmds[this.index]; + + cmd.undo(); + + this.index--; + this.emit(Event.UNDO, { cmd }); + } + + redo() { + if (this.cmds.length === 0 || this.index >= this.cmds.length - 1) return; + + const cmd = this.cmds[this.index + 1]; + + cmd.redo(); + + this.index++; + this.emit(Event.REDO, { cmd }); + } + + withGroup(groupFn: () => void) { + if (this._group) { + groupFn(); + return; + } + + const group = new CmdGroup(this.editor); + this._group = group; + + let errFlag = false; + try { + groupFn(); + } catch (error) { + console.log(error); + errFlag = true; + } + + this._group = undefined; + + if (errFlag) return; + + if (group.cmds.length > 0) this.execute(group); + } + + reset() { + this.cmds = []; + this.index = -1; + this.emit(Event.RESET); + } + + saveHistory() { + const backup: ICmdBackup = { + cmds: this.cmds, + index: this.index, + }; + this.backup = backup; + this.cmds = []; + this.index = -1; + } + restoreHistory() { + if (this.backup) { + this.cmds = this.backup.cmds; + this.index = this.backup.index; + this.backup = undefined; + } + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/CmdManager/type.ts b/frontend/image-tool/src/package/image-editor/common/CmdManager/type.ts new file mode 100644 index 00000000..595ad516 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/CmdManager/type.ts @@ -0,0 +1,2 @@ +export type { ICmdName } from './index'; +// export type { ICmdOption } from './cmd/index'; diff --git a/frontend/image-tool/src/package/image-editor/common/DataManager.ts b/frontend/image-tool/src/package/image-editor/common/DataManager.ts new file mode 100644 index 00000000..7ba14623 --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/DataManager.ts @@ -0,0 +1,291 @@ +import { + IFrame, + AnnotateModeEnum, + AnnotateObject, + Const, + ITransform, + IObjectSource, + IShapeConfig, + IUserData, + ModelCodeEnum, + MsgType, +} from '../types'; +import * as utils from '../utils'; +import * as _utils from '../ImageView/utils'; +import Editor from '../Editor'; +import { Event } from '../configs'; +import { ShapeRoot } from '../ImageView'; +import { __UNSERIES__ } from '..'; + +export default class DataManager { + editor: Editor; + // Multi series frames; seriesFrame is named scene + sceneMap: Map = new Map(); + sceneId: string = __UNSERIES__; + // annotations container map + dataMap: Map = new Map(); + // annotations source map + sourceMap: Record = {}; + + constructor(editor: Editor) { + this.editor = editor; + } + /** + * Scene + */ + setSceneDataByFrames(data: IFrame[]) { + this.clearSceneMap(); + data.forEach((e) => { + const key = e.sceneId ? String(e.sceneId) : __UNSERIES__; + let arr = this.sceneMap.get(key); + if (!arr) { + arr = []; + this.sceneMap.set(key, arr); + } + arr.push(e); + }); + } + getFramesBySceneIndex(index: number) { + const arr = Array.from(this.sceneMap.values()); + return arr[index] || []; + } + getFramesBySceneId(id: string) { + return this.sceneMap.get(id + '') || []; + } + clearSceneMap() { + this.sceneMap.clear(); + } + /** + * source + */ + setSources(frame: IFrame, data: IObjectSource[]) { + this.sourceMap[String(frame.id)] = data; + } + getSources(frame: IFrame) { + return this.sourceMap[String(frame.id)]; + } + clearSource(frame?: IFrame) { + if (frame) { + delete this.sourceMap[String(frame.id)]; + } else { + this.sourceMap = {}; + } + } + + /** + * annotations + */ + // set roots + setFrameRoot(frameId: string, roots: ShapeRoot | ShapeRoot[]) { + if (!Array.isArray(roots)) roots = [roots]; + roots.forEach((root) => { + const key = utils.formatId(frameId, root.type); + this.dataMap.set(key, root); + }); + } + getFrameRoot(frameId?: string, type?: AnnotateModeEnum) { + type = type || this.editor.state.annotateMode; + frameId = frameId || this.editor.getCurrentFrame()?.id || ''; + const key = utils.formatId(frameId, type); + return this.dataMap.get(key) as ShapeRoot; + } + hasObject(uuid: string, frame?: IFrame) { + return !!this.getObject(uuid, frame); + } + // get specific annotation + getObject(uuid: string, frame?: IFrame) { + frame = frame || this.editor.getCurrentFrame(); + const root_ins = this.getFrameRoot(frame.id, AnnotateModeEnum.INSTANCE); + const root_seg = this.getFrameRoot(frame.id, AnnotateModeEnum.SEGMENTATION); + return root_ins?.hasMap.get(uuid) || root_seg?.hasMap.get(uuid); + } + // get frame annotations + getFrameObject(frameId: string, type?: AnnotateModeEnum) { + type = type || this.editor.state.annotateMode; + const root = this.getFrameRoot(frameId, type); + return root ? root.children : undefined; + } + addAnnotates(objects: AnnotateObject[] | AnnotateObject, frame?: IFrame) { + if (!Array.isArray(objects)) objects = [objects]; + frame = frame || this.editor.getCurrentFrame(); + const root = this.getFrameRoot(frame.id); + if (!root) return; + + root.addObjects(objects); + this.onAnnotatesAdd(objects, frame); + } + removeAnnotates(objects: AnnotateObject[] | AnnotateObject, frame?: IFrame) { + if (!Array.isArray(objects)) objects = [objects]; + frame = frame || this.editor.getCurrentFrame(); + const root = this.getFrameRoot(frame.id); + if (!root) return; + const removeMap = {} as Record; + const selectionMap = this.editor.selectionMap; + let delFlag = false; + _utils.traverse(objects, (e) => { + removeMap[e.uuid] = true; + if (selectionMap[e.uuid]) { + delFlag = true; + delete selectionMap[e.uuid]; + } + }); + if (delFlag) this.editor.updateSelect(); + root.removeObjects(objects); + this.onAnnotatesRemove(objects, frame); + } + setAnnotatesTransform( + objects: AnnotateObject[] | AnnotateObject, + datas: ITransform | ITransform[], + ) { + if (!Array.isArray(objects)) objects = [objects]; + + objects.forEach((obj, index) => { + const data = Array.isArray(datas) ? datas[index] : datas; + obj.setAttrs(data); + }); + + this.editor.emit(Event.ANNOTATE_TRANSFORM, objects, datas); + this.onAnnotatesChange(objects, 'transform', datas); + } + onAnnotatesAdd(objects: AnnotateObject[], frame?: IFrame) { + frame = frame || this.editor.getCurrentFrame(); + frame.needSave = true; + + this.editor.emit(Event.ANNOTATE_ADD, objects, frame); + } + onAnnotatesChange( + objects: AnnotateObject[], + type?: 'userData' | 'transform' | 'attrs' | 'group' | 'positionIndex' | 'other', + data?: any, + ) { + objects.forEach((obj) => { + const frame = obj.frame; + if (frame) frame.needSave = true; + obj.userData.resultStatus = Const.True_Value; + }); + this.editor.emit(Event.ANNOTATE_CHANGE, objects, type, data); + } + onAnnotatesRemove(objects: AnnotateObject[], frame?: IFrame) { + frame = frame || this.editor.getCurrentFrame(); + frame.needSave = true; + + this.editor.emit(Event.ANNOTATE_REMOVE, objects, frame); + } + setAnnotatesUserData(objects: AnnotateObject[] | AnnotateObject, datas: IUserData | IUserData[]) { + if (!Array.isArray(objects)) objects = [objects]; + + objects.forEach((obj, index) => { + const data = Array.isArray(datas) ? datas[index] : datas; + Object.assign(obj.userData, data); + }); + this.editor.mainView.updateObjectByUserData(objects); + this.editor.mainView.updateToolStyleByClass(); + + this.editor.emit(Event.ANNOTATE_USER_DATA, objects, datas); + this.onAnnotatesChange(objects, 'userData', datas); + } + setAnnotatesVisible(objects: AnnotateObject | AnnotateObject[], data: boolean | boolean[]) { + let visibleObjs = Array.isArray(objects) ? objects : [objects]; + if (visibleObjs.length === 0) return; + const attrs: IShapeConfig[] = []; + visibleObjs.forEach((obj, index) => { + let visible = typeof data === 'boolean' ? data : data[index]; + if (typeof visible !== 'boolean') visible = (data as boolean[])[0]; + obj.showVisible = visible; + attrs.push({ visible }); + }); + + this.editor.emit(Event.ANNOTATE_VISIBLE, visibleObjs); + } + + /** + * scene + */ + copyForward() { + return this.track({ + direction: 'FORWARD', + object: this.editor.selection.length > 0 ? 'select' : 'all', + method: 'copy', + frameN: 1, + }); + } + copyBackWard() { + return this.track({ + direction: 'BACKWARD', + object: this.editor.selection.length > 0 ? 'select' : 'all', + method: 'copy', + frameN: 1, + }); + } + async track(option: { + method: 'copy' | ModelCodeEnum; + object: 'select' | 'all'; + direction: 'BACKWARD' | 'FORWARD'; + frameN: number; + }) { + const editor = this.editor; + const { frameIndex, frames } = editor.state; + const curId = frames[frameIndex].id; + + const getToDataId = () => { + const ids = [] as string[]; + const forward = option.direction === 'FORWARD' ? 1 : -1; + const frameN = option.frameN; + if (frameN <= 0) return ids; + for (let i = 1; i <= frameN; i++) { + const frame = frames[frameIndex + forward * i]; + if (frame) ids.push(frame.id); + } + return ids; + }; + const ids = getToDataId(); + if (ids.length === 0) return; + + const getObjects = () => { + let objects: AnnotateObject[] = []; + if (option.object === 'all') { + const root = this.getFrameRoot(curId); + objects = root.allObjects.filter((e) => root.renderFilter(e)); + } else { + editor.selection.forEach((e) => { + objects.push(e); + }); + } + return objects; + }; + + let objects = getObjects(); + + if (objects.length === 0) { + editor.showMsg(MsgType.warning, editor.lang('track-no-source')); + return; + } + const data = { ...option }; + this.editor.emit(Event.MODEL_RUN_TRACK, data); + const startTm = Date.now(); + utils.copyData(editor, curId, ids, objects); + editor.showMsg(MsgType.success, editor.lang('copy-ok')); + this.gotoNext(ids[0]); + const modelTm = Date.now() - startTm; + this.editor.emit(Event.MODEL_RUN_TRACK_SUCCESS, { time: modelTm }); + } + gotoNext(dataId: string) { + const { frames } = this.editor.state; + const index = frames.findIndex((e) => e.id === dataId); + if (index < 0) return; + this.editor.loadFrame(index); + } + + /** + * clear + */ + clear(frame?: IFrame) { + if (frame) { + this.dataMap.delete(utils.formatId(frame.id, AnnotateModeEnum.INSTANCE)); + delete this.sourceMap[frame.id]; + } else { + this.dataMap.clear(); + this.clearSource(); + } + } +} diff --git a/frontend/image-tool/src/package/image-editor/common/HotkeyManager/index.ts b/frontend/image-tool/src/package/image-editor/common/HotkeyManager/index.ts new file mode 100644 index 00000000..18174fea --- /dev/null +++ b/frontend/image-tool/src/package/image-editor/common/HotkeyManager/index.ts @@ -0,0 +1,93 @@ +import hotkeys from 'hotkeys-js'; +import Editor from '../../Editor'; +import type { IHotkeyConfig } from './type'; +import { IActionName } from '../ActionManager'; +import { Event, hotkeyConfig, seriesFrameHotKey } from '../../configs'; + +export enum NameSpace { + All = 'Hotkey_NameSapce_All', +} +export default class HotkeyManager { + editor: Editor; + hotkeyConfigs: IHotkeyConfig[]; + constructor(editor: Editor) { + this.editor = editor; + this.hotkeyConfigs = [...hotkeyConfig]; + } + + initHotkey() { + hotkeyConfig.forEach((config) => { + this.bindConfig(config); + }); + } + + registryHotkey(configs: IHotkeyConfig[]) { + this.hotkeyConfigs = [...this.hotkeyConfigs, ...configs]; + } + + setHotKeyFromAction(actions: Record) { + // hotkeys.unbind(); + hotkeys.deleteScope(NameSpace.All); + hotkeys.setScope(NameSpace.All); + + const filterConfig = this.hotkeyConfigs.filter((e) => { + return Array.isArray(e.action) ? validActions(actions, e.action) : actions[e.action]; + }); + filterConfig.forEach((config) => { + this.bindConfig(config); + }); + this.bindEsc(); + this.editor.emit(Event.HOTKEY); + } + + bindEsc() { + hotkeys('esc', (event, handler) => { + event.preventDefault(); + console.log('esc', '--> '); + this.editor.actionManager.handleEsc(); + }); + } + + bindConfig(config: IHotkeyConfig) { + hotkeys(config.key, (event, handler) => { + event.preventDefault(); + console.log(config.key, '--> action:', config.action); + + this.editor.actionManager.execute(config.action, config.key); + }); + } + + bindSeriesFrameEvent() { + seriesFrameHotKey.forEach((config) => { + this.bindConfig(config); + }); + this.registryHotkey(seriesFrameHotKey); + } +} + +function validActions(actionMap: Record, actions: IActionName[]) { + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + if (actionMap[action]) return true; + } + + return false; +} +hotkeys.filter = filter; +function filter(event: KeyboardEvent) { + const target = (event.target || event.srcElement) as HTMLFormElement; + const tagName = target.tagName; + const type = target.type; + let flag = true; // ignore: isContentEditable === 'true', and