├── .eslintignore ├── .eslintrc.json ├── .firebaserc ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── firebase.json ├── icon └── favicon-512.png ├── package.json ├── public └── favicon.ico ├── scripts └── build.ts ├── src ├── editor-module │ ├── application │ │ ├── app-context.tsx │ │ └── const.ts │ ├── components │ │ ├── base.tsx │ │ ├── dialogs │ │ │ └── fl-dialog.tsx │ │ ├── draggable-popover.tsx │ │ ├── fields │ │ │ ├── fl-color-field.tsx │ │ │ ├── fl-file-field.tsx │ │ │ ├── fl-folder-contents-field.tsx │ │ │ ├── fl-select-field.tsx │ │ │ ├── fl-text-field.tsx │ │ │ ├── form-util.ts │ │ │ └── type.ts │ │ ├── frame-bar │ │ │ ├── frame-bar-button.tsx │ │ │ └── frame-bar.tsx │ │ ├── style-const.ts │ │ ├── tool-bar-button.tsx │ │ └── tool-bar.tsx │ ├── config │ │ └── mui-theme.tsx │ ├── locales │ │ ├── en.json │ │ └── ja.json │ ├── repositories │ │ └── project-repository.ts │ ├── stores │ │ ├── annotation-class-store.ts │ │ ├── camera-calibration-store.ts │ │ └── task-store.ts │ ├── types │ │ ├── const.ts │ │ ├── labos.ts │ │ └── vo.ts │ ├── utils │ │ ├── annotation-class-util.ts │ │ ├── calibration-util.ts │ │ ├── color-util.ts │ │ ├── fl-cube-util.ts │ │ ├── fl-object-camera-util.ts │ │ ├── format-util.ts │ │ ├── interpolation-util.ts │ │ ├── math-util.ts │ │ ├── pcd-util.ts │ │ ├── task-annotation-util.ts │ │ ├── view-util.ts │ │ └── workspace-util.ts │ └── views │ │ ├── annotation-classes │ │ ├── class-list.tsx │ │ └── instance-list.tsx │ │ ├── pages │ │ └── three-annotation │ │ │ ├── base-view-index.tsx │ │ │ ├── calibration-edit-dialog.tsx │ │ │ ├── class-form-dialog.tsx │ │ │ ├── class-list-dialog.tsx │ │ │ ├── hot-key.tsx │ │ │ ├── image-dialog.tsx │ │ │ ├── index.tsx │ │ │ ├── label-side-panel.tsx │ │ │ ├── label-tool-bar.tsx │ │ │ ├── label-view-index.tsx │ │ │ ├── side-panel.tsx │ │ │ └── tool-bar.tsx │ │ └── task-three │ │ ├── drei-html.tsx │ │ ├── fl-annotation-controls.tsx │ │ ├── fl-const.ts │ │ ├── fl-cube-model.ts │ │ ├── fl-cube.tsx │ │ ├── fl-cubes.tsx │ │ ├── fl-label-main-view.tsx │ │ ├── fl-label-secondary-view.tsx │ │ ├── fl-main-camera-controls.ts │ │ ├── fl-main-controls.tsx │ │ ├── fl-object-camera-controls.ts │ │ ├── fl-object-controls.tsx │ │ ├── fl-pcd-points.ts │ │ ├── fl-pcd.tsx │ │ ├── fl-three-editor.tsx │ │ ├── fl-transform-controls-gizmo.ts │ │ └── fl-transform-controls.ts ├── electron-app │ ├── @types │ │ ├── global.d.ts │ │ └── import-png.d.ts │ ├── main.ts │ ├── node │ │ ├── file-driver.ts │ │ └── workspace.ts │ ├── preload.ts │ └── web │ │ ├── App.scss │ │ ├── App.tsx │ │ ├── asset │ │ └── favicon-200.png │ │ ├── context │ │ └── workspace.ts │ │ ├── index.html │ │ ├── index.scss │ │ ├── index.tsx │ │ ├── locales │ │ ├── en.json │ │ └── ja.json │ │ ├── repositories │ │ └── project-fs-repository.ts │ │ ├── title-bar.tsx │ │ └── views │ │ └── pages │ │ ├── start │ │ └── index.tsx │ │ └── workspace │ │ ├── form.tsx │ │ └── index.tsx ├── file-module │ └── reference-target-util.ts └── web-app │ ├── App.scss │ ├── App.tsx │ ├── images │ ├── autoware-main-logo-whitebg.png │ ├── copy.png │ ├── feature__2d3d.png │ ├── feature__birdeye.png │ ├── feature__label_view.png │ ├── feature__sequence_interpolation.png │ ├── github.png │ ├── hero.png │ ├── logo.png │ ├── pcd.png │ ├── pcd_frames.png │ ├── pcd_image.png │ ├── side-left.png │ └── side-right.png │ ├── index.html │ ├── index.scss │ ├── index.tsx │ ├── locales │ ├── en.json │ └── ja.json │ ├── repositories │ └── project-web-repository.ts │ ├── title-bar.tsx │ └── views │ └── pages │ ├── edit │ ├── form.tsx │ └── index.tsx │ ├── new │ ├── form.tsx │ └── index.tsx │ └── start │ ├── github-icon.tsx │ └── index.tsx ├── tsconfig.json ├── tsconfig.main.json ├── webpack.config.prod.ts ├── webpack.config.ts ├── webpack.config.web-dev.ts ├── webpack.config.web-prod.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | docs/ 4 | build/ 5 | public/ 6 | release/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "browser": true, 6 | "commonjs": true 7 | }, 8 | "settings": { 9 | "react": { 10 | "version": "detect" 11 | } 12 | }, 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "sourceType": "module", 16 | "ecmaVersion": 2020, 17 | "ecmaFeatures": { 18 | "jsx": true 19 | } 20 | }, 21 | "plugins": ["react", "react-hooks", "@typescript-eslint"], 22 | "extends": [ 23 | "eslint:recommended", 24 | "plugin:@typescript-eslint/recommended", 25 | "plugin:react/recommended", 26 | "plugin:react-hooks/recommended", 27 | "prettier" 28 | ], 29 | "rules": { 30 | "react/prop-types": "off", 31 | "react/react-in-jsx-scope": "off", 32 | "react/display-name": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "fastlabel-3d-annotation-3b7ec2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | release/ 3 | dist/ 4 | public/ 5 | .env 6 | .DS_Store 7 | .firebase/ 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | public/ 4 | docs/ 5 | build/ 6 | release/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxBracketSameLine": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The Apache License 2.0 2 | 3 | Copyright [FastLabel & Human Dataware Lab.] [name of copyright owner] 4 | 5 | Licensed under the Apache License, Version 2.0 (the “License”); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automan Tools 2 | 3 | AutomanTools is an open-source annotation tools for self-driving AI. 4 | 5 | ## Feature 6 | 7 | - Annotate 3D objects for PCD data 8 | - Set your custom classes 9 | - Export labels 10 | - Sequence labeling 11 | 12 | ## Usage 13 | 14 | ### preparation 15 | 16 | ```sh 17 | $ yarn install 18 | ``` 19 | 20 | _Note that you will need to have [Node.js](https://nodejs.org/) and [Yarn](https://yarnpkg.com/) installed._ 21 | 22 | ### webapp 23 | 24 | development 25 | 26 | ```sh 27 | $ yarn webdev 28 | ``` 29 | 30 | build 31 | 32 | ```sh 33 | $ yarn webbuild 34 | ``` 35 | 36 | ### electron 37 | 38 | ```sh 39 | $ yarn dev 40 | ``` 41 | 42 | build 43 | 44 | ``` 45 | $ yarn package 46 | ``` 47 | 48 | A boilerplate for [Electron](https://www.electronjs.org/), [React](https://reactjs.org/) and [TypeScript](https://www.typescriptlang.org/) projects with hot reload capabilities. 49 | 50 | ## How to operate the screen 51 | 52 | Below is a sample operation which Selected Type [PCD-with frame] and after imported Reference Resources. 53 | 54 | ### Preparation 55 | 56 | - create some annotation class in Annotation Class Dialog. 57 | 58 | ### Put an annotation 59 | 60 | - select an annotation class by side menu. 61 | - click on main camera. 62 | (\*The default Sequence labeling mode is ON. If you put an annotation in view that put all frame.) 63 | 64 | ### Change view frame 65 | 66 | - click on frame bar that is below of toolbar. 67 | 68 | ### Remove the annotation on frame 69 | 70 | - select the annotation in side menu 71 | - click Turn off in frames, that remove annotation from current frame to end of frame. 72 | - click Turn on at the current frame, that remove annotation at the current frame. 73 | 74 | ## Export Data structure 75 | 76 | ```json 77 | [ 78 | { 79 | "id": "d2458f99-8ba8-4594-8d8e-f37d2e747491", 80 | "annotationClassId": "3016a58c-a802-403c-a9bc-b662246856ff", 81 | "type": "cuboid", 82 | "title": "Bicycle", 83 | "value": "bicycle", 84 | "color": "#A295D6", 85 | "points": { 86 | "0001": [ // number of frame 87 | 3.36, // coordinate x 88 | 2.2, // coordinate y 89 | 0, // coordinate z 90 | 0, // rotation X 91 | 0, // rotation y 92 | 0, // rotation z 93 | 1, // length x 94 | 1, // length y 95 | 1 // length z 96 | ], 97 | "0002": [ 98 | 3.36, 99 | 2.2, 100 | 0, 101 | 0, 102 | 0, 103 | 0, 104 | 1, 105 | 1, 106 | 1 107 | ], 108 | }, 109 | "pointsMeta": { 110 | "0001": { // number of frame 111 | "autogenerated": false 112 | }, 113 | "0002": { 114 | "autogenerated": true 115 | }, 116 | }, 117 | ``` 118 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "public", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /icon/favicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/icon/favicon-512.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastlabel-3d-annotation", 3 | "description": "AutomanTools is an open-source software for self-driving AI.", 4 | "version": "0.0.3", 5 | "author": "FastLabel & Human Dataware Lab.", 6 | "license": "Apache License 2.0", 7 | "main": "dist/main.js", 8 | "scripts": { 9 | "webdev": "run-p webdev:*", 10 | "webdev:webpack": "webpack --config webpack.config.web-dev.ts --watch", 11 | "webdev:start": " wait-on ./public/index.html && cross-env NODE_ENV=\"development\" webpack-dev-server --history-api-fallback --config webpack.config.web-dev.ts", 12 | "webprod": "run-p webprod:*", 13 | "webprod:webpack": "webpack --config webpack.config.web-prod.ts --watch", 14 | "webprod:start": " wait-on ./public/index.html && cross-env NODE_ENV=\"development\" webpack-dev-server --history-api-fallback --config webpack.config.web-prod.ts", 15 | "webbuild": "webpack --config webpack.config.web-prod.ts --progress", 16 | "start": "run-s build serve", 17 | "predev": "rimraf dist", 18 | "dev": "run-p dev:*", 19 | "dev:electron": "wait-on ./dist/index.html && cross-env NODE_ENV=\"development\" electron .", 20 | "dev:tsc": "tsc -w -p tsconfig.main.json", 21 | "dev:webpack": "webpack --watch", 22 | "serve": "electron .", 23 | "prebuild": "rimraf dist release", 24 | "build": "webpack --config webpack.config.prod.ts --progress", 25 | "build:pack": "ts-node ./scripts/build.ts", 26 | "package": "run-s build build:pack", 27 | "lint": "run-s lint:*", 28 | "lint:eslint": "eslint . --ext .ts,.tsx --fix", 29 | "lint:prettier": "prettier --write ." 30 | }, 31 | "dependencies": { 32 | "@emotion/react": "^11.9.0", 33 | "@emotion/styled": "^11.8.1", 34 | "@mui/icons-material": "^5.6.2", 35 | "@mui/lab": "^5.0.0-alpha.81", 36 | "@mui/material": "^5.7.0", 37 | "@mui/styles": "^5.7.0", 38 | "@react-three/fiber": "^7.0.6", 39 | "firebase": "^9.6.2", 40 | "i18next": "^20.3.5", 41 | "i18next-browser-languagedetector": "^6.1.2", 42 | "lodash.debounce": "^4.0.8", 43 | "lodash.throttle": "^4.1.1", 44 | "notistack": "^2.0.5", 45 | "re-resizable": "^6.9.0", 46 | "react": "^17.0.2", 47 | "react-dom": "^17.0.2", 48 | "react-draggable": "^4.4.3", 49 | "react-dropzone": "^11.3.4", 50 | "react-i18next": "^11.11.4", 51 | "react-router-dom": "^5.2.0", 52 | "three": "^0.131.3", 53 | "typeface-roboto": "^1.1.13", 54 | "unstated-next": "^1.1.0", 55 | "uuid": "^8.3.2", 56 | "yaml": "^1.10.2" 57 | }, 58 | "devDependencies": { 59 | "@types/lodash.debounce": "^4.0.7", 60 | "@types/lodash.throttle": "^4.1.6", 61 | "@types/mini-css-extract-plugin": "^2.0.1", 62 | "@types/node": "^16.4.0", 63 | "@types/react": "^17.0.14", 64 | "@types/react-dom": "^17.0.9", 65 | "@types/react-router-dom": "^5.1.8", 66 | "@types/three": "^0.131.0", 67 | "@types/uuid": "^8.3.1", 68 | "cross-env": "^7.0.3", 69 | "css-loader": "^6.2.0", 70 | "electron": "^13.1.7", 71 | "electron-builder": "^22.11.7", 72 | "electron-search-devtools": "^1.2.6", 73 | "html-webpack-plugin": "^5.3.2", 74 | "ifdef-loader": "^2.3.0", 75 | "mini-css-extract-plugin": "^2.1.0", 76 | "npm-run-all": "^4.1.5", 77 | "rimraf": "^3.0.2", 78 | "sass": "^1.35.2", 79 | "sass-loader": "^12.1.0", 80 | "ts-loader": "^9.2.3", 81 | "ts-node": "^10.1.0", 82 | "typescript": "^4.3.5", 83 | "webpack": "^5.45.1", 84 | "webpack-cli": "^4.7.2", 85 | "webpack-dev-server": "^4.3.0" 86 | }, 87 | "optionalDependencies": { 88 | "@typescript-eslint/eslint-plugin": "^4.28.4", 89 | "@typescript-eslint/parser": "^4.28.4", 90 | "electron-reload": "^1.5.0", 91 | "eslint": "^7.31.0", 92 | "eslint-config-prettier": "^8.3.0", 93 | "eslint-plugin-react": "^7.24.0", 94 | "eslint-plugin-react-hooks": "^4.2.0", 95 | "prettier": "^2.3.2", 96 | "wait-on": "^6.0.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/public/favicon.ico -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'electron-builder'; 2 | 3 | build({ 4 | config: { 5 | productName: 'Automan', 6 | copyright: '© 2021 FastLabel and other contributors.', 7 | files: ['dist/**/*'], 8 | directories: { 9 | output: 'release', 10 | }, 11 | win: { 12 | target: ['nsis'], 13 | publisherName: 'FastLabel', 14 | icon: 'icon/favicon-512.png', 15 | fileAssociations: [ 16 | { 17 | ext: ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'ico', 'svg', 'webp'], 18 | description: 'Image files', 19 | }, 20 | ], 21 | }, 22 | nsis: { 23 | oneClick: false, 24 | perMachine: false, 25 | createDesktopShortcut: false, 26 | createStartMenuShortcut: true, 27 | artifactName: '${productName}-${version}-${platform}-installer.${ext}', 28 | }, 29 | mac: { 30 | category: 'public.app-category.photography', 31 | target: { 32 | target: 'default', 33 | arch: ['x64', 'arm64'], 34 | }, 35 | icon: 'icon/favicon-512.png', 36 | // dmg should not use below it make error 37 | // extendInfo: { 38 | // }, 39 | identity: null, 40 | }, 41 | linux: { 42 | target: ['AppImage'], 43 | }, 44 | }, 45 | }).catch((err: any) => console.log(err)); 46 | -------------------------------------------------------------------------------- /src/editor-module/application/app-context.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type AppContextObj = { 4 | mode: 'electron' | 'web'; 5 | }; 6 | 7 | export const AppContext = React.createContext({ 8 | mode: 'web', 9 | }); 10 | 11 | export const useContextApp = (): AppContextObj => React.useContext(AppContext); 12 | -------------------------------------------------------------------------------- /src/editor-module/application/const.ts: -------------------------------------------------------------------------------- 1 | export const ApplicationConst = { 2 | name: 'Automan', 3 | /** 4 | * version change of store json file structure changed 5 | */ 6 | version: '0.1.0.alpha', 7 | }; 8 | -------------------------------------------------------------------------------- /src/editor-module/components/base.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import React, { FC } from 'react'; 3 | 4 | type Props = {}; 5 | 6 | const Base: FC = () => { 7 | return
; 8 | }; 9 | export default Base; 10 | -------------------------------------------------------------------------------- /src/editor-module/components/dialogs/fl-dialog.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from '@mui/icons-material/Close'; 2 | import MuiDialogActions from '@mui/material/DialogActions'; 3 | import MuiDialogContent from '@mui/material/DialogContent'; 4 | import MuiDialogTitle from '@mui/material/DialogTitle'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Typography from '@mui/material/Typography'; 7 | import { WithStyles } from '@mui/styles'; 8 | import createStyles from '@mui/styles/createStyles'; 9 | import withStyles from '@mui/styles/withStyles'; 10 | import React from 'react'; 11 | 12 | const styles = () => 13 | createStyles({ 14 | root: { 15 | m: 0, 16 | p: 2, 17 | }, 18 | closeButton: { 19 | position: 'absolute', 20 | right: 8, 21 | top: 8, 22 | color: 'palette.grey.500', 23 | }, 24 | }); 25 | 26 | export interface DialogTitleProps extends WithStyles { 27 | id: string; 28 | children: React.ReactNode; 29 | onClose?: () => void; 30 | } 31 | 32 | export const FLDialogTitle = withStyles(styles)((props: DialogTitleProps) => { 33 | const { children, classes, onClose, id } = props; 34 | return ( 35 | 36 | {children} 37 | {onClose ? ( 38 | 43 | 44 | 45 | ) : null} 46 | 47 | ); 48 | }); 49 | 50 | export const FLDialogContent = withStyles((theme) => ({ 51 | root: { 52 | padding: theme.spacing(2), 53 | }, 54 | }))(MuiDialogContent); 55 | 56 | export const FLDialogActions = withStyles((theme) => ({ 57 | root: { 58 | margin: 0, 59 | padding: theme.spacing(1), 60 | }, 61 | }))(MuiDialogActions); 62 | -------------------------------------------------------------------------------- /src/editor-module/components/draggable-popover.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Box from '@mui/material/Box'; 5 | import Popover from '@mui/material/Popover'; 6 | import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; 7 | import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; 8 | 9 | const useStyles = makeStyles(() => 10 | createStyles({ 11 | content: { 12 | position: 'relative', 13 | }, 14 | popoverPaper: { 15 | maxWidth: 'initial', 16 | maxHeight: 'initial', 17 | }, 18 | }) 19 | ); 20 | 21 | type Props = { 22 | handle?: string; 23 | open: boolean; 24 | }; 25 | 26 | type Position = { 27 | xRate: number; 28 | yRate: number; 29 | }; 30 | 31 | const DraggablePopover: FC = ({ handle, open, children }) => { 32 | const styles = useStyles(); 33 | const popoverRef = useRef(undefined); 34 | 35 | const [currentPosition, setCurrentPosition] = useState({ 36 | xRate: 0, 37 | yRate: 0, 38 | }); 39 | 40 | const onDrag = useCallback((e: DraggableEvent, data: DraggableData) => { 41 | const xRate = data.lastX < 0 ? 0 : data.lastX; 42 | const yRate = data.lastY < -38 ? -38 : data.lastY; 43 | setCurrentPosition({ xRate, yRate }); 44 | }, []); 45 | 46 | useEffect(() => { 47 | if (popoverRef.current && open) { 48 | popoverRef.current.style.inset = ''; 49 | popoverRef.current.style.top = '60px'; 50 | popoverRef.current.style.left = '-16px'; 51 | } 52 | }, [popoverRef, open]); 53 | 54 | return ( 55 | 62 | 70 | 71 | {children} 72 | 73 | 74 | 75 | ); 76 | }; 77 | export default DraggablePopover; 78 | -------------------------------------------------------------------------------- /src/editor-module/components/fields/fl-color-field.tsx: -------------------------------------------------------------------------------- 1 | import { TextField, Theme } from '@mui/material'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Box from '@mui/material/Box'; 5 | import Grid from '@mui/material/Grid'; 6 | import Typography from '@mui/material/Typography'; 7 | import React, { FC } from 'react'; 8 | import { FormUtil } from './form-util'; 9 | import { FormAction, FormState } from './type'; 10 | 11 | const AnnotationColors = [ 12 | '#0033CC', 13 | '#428BCA', 14 | '#44AD8E', 15 | '#A8D695', 16 | '#5CB85C', 17 | '#69D100', 18 | '#004E00', 19 | '#34495E', 20 | '#7F8C8D', 21 | '#A295D6', 22 | '#5843AD', 23 | '#8E44AD', 24 | '#FFECDB', 25 | '#AD4363', 26 | '#D10069', 27 | '#CC0033', 28 | '#FF0000', 29 | '#D9534F', 30 | '#D1D100', 31 | '#F0AD4E', 32 | '#AD8D43', 33 | ]; 34 | 35 | const useStyles = makeStyles((theme: Theme) => 36 | createStyles({ 37 | tag: () => ({ 38 | borderRadius: '50%', 39 | width: '16px', 40 | height: '16px', 41 | }), 42 | colorBox: () => ({ 43 | borderRadius: 4, 44 | width: theme.spacing(4), 45 | height: theme.spacing(4), 46 | minWidth: theme.spacing(4), 47 | minHeight: theme.spacing(4), 48 | maxWidth: theme.spacing(4), 49 | maxHeight: theme.spacing(4), 50 | '&:hover': { 51 | cursor: 'pointer', 52 | }, 53 | }), 54 | }) 55 | ); 56 | 57 | type Props = { 58 | label: string; 59 | form: [ 60 | name: string, 61 | obj: FormState, 62 | dispatch: React.Dispatch 63 | ]; 64 | }; 65 | 66 | const FLColorField: FC = ({ label, form }) => { 67 | const styles = useStyles(); 68 | const [name, obj, dispatch] = form; 69 | const formValue = FormUtil.resolve(name, obj.data); 70 | const onClickColor = (color: string) => { 71 | dispatch({ type: 'change', name, value: color }); 72 | }; 73 | return ( 74 | 75 | 76 | 77 | {label} 78 | 79 |
82 |
83 | 84 | { 90 | const newValue = e.target.value.trim(); 91 | dispatch({ type: 'change', name, value: newValue }); 92 | }} 93 | /> 94 | 95 | 96 | 97 | {AnnotationColors.map((annotationColor) => ( 98 | 99 | onClickColor(annotationColor)}> 104 | 105 | ))} 106 | 107 | 108 |
109 | ); 110 | }; 111 | 112 | export default FLColorField; 113 | -------------------------------------------------------------------------------- /src/editor-module/components/fields/fl-file-field.tsx: -------------------------------------------------------------------------------- 1 | import { InputAdornment, TextField, Theme } from '@mui/material'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Box from '@mui/material/Box'; 5 | import Typography from '@mui/material/Typography'; 6 | import FolderIcon from '@mui/icons-material/Folder'; 7 | import React, { FC } from 'react'; 8 | import { FormUtil } from './form-util'; 9 | import { FormAction, FormState } from './type'; 10 | 11 | const useStyles = makeStyles((theme) => 12 | createStyles({ 13 | margin: { 14 | margin: theme.spacing(1), 15 | }, 16 | }) 17 | ); 18 | 19 | type Props = { 20 | label: string; 21 | form: [ 22 | name: string, 23 | obj: FormState, 24 | dispatch: React.Dispatch 25 | ]; 26 | }; 27 | 28 | const FLFileField: FC = ({ label, form }) => { 29 | const classes = useStyles(); 30 | const [name, obj, dispatch] = form; 31 | const formValue = FormUtil.resolve(name, obj.data); 32 | return ( 33 | 34 | 35 | 36 | {label} 37 | 38 | 39 | 40 | 48 | 49 | 50 | ), 51 | }} 52 | onChange={(e) => { 53 | const newValue = e.target.value; 54 | dispatch({ type: 'change', name, value: newValue }); 55 | }} 56 | /> 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default FLFileField; 63 | -------------------------------------------------------------------------------- /src/editor-module/components/fields/fl-select-field.tsx: -------------------------------------------------------------------------------- 1 | import { TextField } from '@mui/material'; 2 | import Box from '@mui/material/Box'; 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import Typography from '@mui/material/Typography'; 5 | import React, { FC } from 'react'; 6 | import { FormUtil } from './form-util'; 7 | import { FormAction, FormState } from './type'; 8 | 9 | type Props = { 10 | label: string; 11 | items: { key: string; label: string; value?: string }[]; 12 | form: [ 13 | name: string, 14 | obj: FormState, 15 | dispatch: React.Dispatch 16 | ]; 17 | disabled?: boolean; 18 | }; 19 | 20 | const FLSelectField: FC = ({ label, items, form, disabled }) => { 21 | const [name, obj, dispatch] = form; 22 | const formValue = FormUtil.resolve(name, obj.data); 23 | return ( 24 | 25 | 26 | 27 | {label} 28 | 29 | 30 | 31 | { 39 | const newValue = e.target.value; 40 | dispatch({ type: 'change', name, value: newValue }); 41 | }}> 42 | {items.map((item) => ( 43 | 44 | {item.label} 45 | 46 | ))} 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default FLSelectField; 54 | -------------------------------------------------------------------------------- /src/editor-module/components/fields/fl-text-field.tsx: -------------------------------------------------------------------------------- 1 | import { TextField } from '@mui/material'; 2 | import Box from '@mui/material/Box'; 3 | import Typography from '@mui/material/Typography'; 4 | import React, { FC, useCallback } from 'react'; 5 | import { FormUtil } from './form-util'; 6 | import { FormAction, FormState } from './type'; 7 | 8 | type Props = { 9 | label: string; 10 | mode?: 'list' | 'grid'; 11 | readonly?: boolean; 12 | inputType?: 'text' | 'number'; 13 | form: [ 14 | name: string, 15 | obj: FormState, 16 | dispatch?: React.Dispatch 17 | ]; 18 | }; 19 | 20 | const FLTextField: FC = ({ 21 | label, 22 | mode = 'grid', 23 | readonly = false, 24 | inputType = 'text', 25 | form, 26 | }) => { 27 | const [name, obj, dispatch] = form; 28 | let formValue = FormUtil.resolve(name, obj.data); 29 | if (inputType === 'number') { 30 | formValue = Number(formValue).toFixed(2); 31 | } 32 | const onChange = useCallback( 33 | (e) => { 34 | if (dispatch) { 35 | const newValue = e.target.value; 36 | dispatch({ type: 'change', name, value: newValue }); 37 | } 38 | }, 39 | [dispatch] 40 | ); 41 | return ( 42 | 43 | 47 | 48 | {label} 49 | 50 | 51 | 52 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default FLTextField; 71 | -------------------------------------------------------------------------------- /src/editor-module/components/fields/form-util.ts: -------------------------------------------------------------------------------- 1 | export const FormUtil = { 2 | resolve: (name: string, data: { [key: string]: any }): any => { 3 | const splitName = name.split('.'); 4 | return splitName.reduce((c, n) => c[n], data); 5 | }, 6 | update: ( 7 | name: string, 8 | value: any, 9 | data: T 10 | ): T => { 11 | const newData = { ...data }; 12 | const splitName = name.split('.'); 13 | const lastName = splitName.pop(); 14 | const parent: { [key: string]: any } = splitName.reduce( 15 | (c, n) => c[n], 16 | newData 17 | ); 18 | if (!lastName) { 19 | throw new Error(`lastName is undefined name:${name}`); 20 | } 21 | parent[lastName] = value; 22 | return newData; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/editor-module/components/fields/type.ts: -------------------------------------------------------------------------------- 1 | export type FormAction = 2 | | { 3 | type: 'change'; 4 | name: string; 5 | value: any; 6 | } 7 | | { 8 | type: 'init'; 9 | data: any; 10 | }; 11 | 12 | export type FormState = { 13 | data: T; 14 | helper?: any; 15 | }; 16 | -------------------------------------------------------------------------------- /src/editor-module/components/frame-bar/frame-bar-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckIcon from '@mui/icons-material/Check'; 3 | import CheckCircleIcon from '@mui/icons-material/CheckCircle'; 4 | import Tooltip from '@mui/material/Tooltip'; 5 | import Box from '@mui/material/Box'; 6 | import { Theme } from '@mui/material/styles'; 7 | import makeStyles from '@mui/styles/makeStyles'; 8 | import createStyles from '@mui/styles/createStyles'; 9 | 10 | export type Mark = 'Circle' | 'Check' | 'CheckCircle'; 11 | 12 | export const FRAME_BUTTON_WIDTH = 18; 13 | export const FRAME_BUTTON_MIN_WIDTH = 14; 14 | 15 | type Props = { 16 | frameIndex: number; 17 | onClick: (frame: number) => void; 18 | onDoubleClick: (frame: number) => void; 19 | mark?: Mark; 20 | } & StylesProps; 21 | 22 | type StylesProps = { 23 | color: string; 24 | isActive: boolean; 25 | outlined: boolean; 26 | }; 27 | 28 | const useStyles = makeStyles(() => 29 | createStyles({ 30 | button: { 31 | width: '100%', 32 | minWidth: FRAME_BUTTON_WIDTH, 33 | height: 56, 34 | cursor: 'pointer', 35 | flex: '1 1 auto', 36 | display: 'flex', 37 | alignItems: 'center', 38 | justifyContent: 'center', 39 | padding: 0, 40 | border: 0, 41 | '&:focus': { 42 | outline: 0, 43 | }, 44 | }, 45 | buttonInner: { 46 | width: '70%', 47 | minWidth: FRAME_BUTTON_MIN_WIDTH, 48 | height: ({ isActive }) => (isActive ? 56 : 28), 49 | backgroundColor: ({ color }) => { 50 | if (color === 'blue') { 51 | return 'rgb(79, 143, 240)'; 52 | } else if (color === 'lightBlue') { 53 | return 'rgb(153, 200, 255)'; 54 | } else if (color === 'gray') { 55 | return 'rgb(189, 189, 189)'; 56 | } else { 57 | return color; 58 | } 59 | }, 60 | '&:hover': { 61 | height: ({ isActive }) => (isActive ? 56 : 34), 62 | }, 63 | border: ({ outlined }) => (outlined ? '2px solid black' : ''), 64 | }, 65 | markContainer: { 66 | height: '100%', 67 | display: 'flex', 68 | alignItems: 'center', 69 | justifyContent: 'center', 70 | }, 71 | mark: { 72 | fontSize: '0.75rem', 73 | color: '#FFFFFF', 74 | }, 75 | }) 76 | ); 77 | 78 | const FrameButton: React.FC = ({ 79 | frameIndex, 80 | onClick, 81 | onDoubleClick, 82 | color, 83 | isActive, 84 | outlined, 85 | mark, 86 | }) => { 87 | const styles = useStyles({ color, isActive, outlined }); 88 | const renderMark = (mark: Mark) => { 89 | switch (mark) { 90 | case 'Circle': 91 | return ( 92 | 93 | 94 | 95 | ); 96 | case 'Check': 97 | return ; 98 | case 'CheckCircle': 99 | return ; 100 | } 101 | }; 102 | return ( 103 | 104 | 117 | 118 | ); 119 | }; 120 | 121 | export default FrameButton; 122 | -------------------------------------------------------------------------------- /src/editor-module/components/frame-bar/frame-bar.tsx: -------------------------------------------------------------------------------- 1 | import Paper from '@mui/material/Paper'; 2 | import React from 'react'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import createStyles from '@mui/styles/createStyles'; 5 | import FrameButton, { FRAME_BUTTON_WIDTH, Mark } from './frame-bar-button'; 6 | import { TaskAnnotationVO } from '@fl-three-editor/types/vo'; 7 | import { TASK_SIDEBAR_WIDTH } from '../style-const'; 8 | import { FormatUtil } from '@fl-three-editor/utils/format-util'; 9 | 10 | const FRAME_BUTTONS_ROOT_LEFT_PADDING = 4; 11 | const FRAME_BUTTONS_ROOT_RIGHT_PADDING = 4; 12 | 13 | const useStyles = makeStyles(() => 14 | createStyles({ 15 | frameButtonsRoot: { 16 | paddingLeft: FRAME_BUTTONS_ROOT_LEFT_PADDING, 17 | paddingRight: FRAME_BUTTONS_ROOT_RIGHT_PADDING, 18 | backgroundColor: 'rgb(239, 239, 239)', 19 | }, 20 | frameButtonsContainer: { 21 | display: 'flex', 22 | justifyContent: 'space-between', 23 | alignItems: 'center', 24 | }, 25 | }) 26 | ); 27 | 28 | type Props = { 29 | windowWidth: number; 30 | totalFrames: number; 31 | currentFrame: number; 32 | selectedTaskAnnotations: TaskAnnotationVO[] | undefined; 33 | hasAnnotation: (frameNo: string) => boolean; 34 | onClickFrameButton: (frame: number) => void; 35 | onDoubleClickFrameButton: (frame: number) => void; 36 | }; 37 | 38 | const FrameBar: React.FC = ({ 39 | windowWidth, 40 | totalFrames, 41 | currentFrame, 42 | selectedTaskAnnotations, 43 | hasAnnotation, 44 | onClickFrameButton, 45 | onDoubleClickFrameButton, 46 | }) => { 47 | const styles = useStyles(); 48 | 49 | const frameButtonProps = React.useCallback( 50 | (frame: number) => { 51 | const frameNo = FormatUtil.number2FrameNo(frame); 52 | let color = 'gray'; 53 | let hasCircle = false; 54 | const outlined = false; 55 | let mark: Mark | undefined = undefined; 56 | if (hasAnnotation(frameNo)) { 57 | color = 'lightBlue'; 58 | } 59 | 60 | if (selectedTaskAnnotations && selectedTaskAnnotations.length === 1) { 61 | const taskAnnotation = selectedTaskAnnotations[0]; 62 | const points = taskAnnotation.points; 63 | const pointsMeta = taskAnnotation.pointsMeta; 64 | if (points[frameNo] && pointsMeta[frameNo]) { 65 | color = 'blue'; 66 | hasCircle = !pointsMeta[frameNo].autogenerated; 67 | mark = hasCircle ? 'Circle' : undefined; 68 | } 69 | } 70 | return { color, outlined, mark }; 71 | }, 72 | [hasAnnotation, selectedTaskAnnotations] 73 | ); 74 | 75 | const frameButtons = React.useMemo(() => { 76 | const contentWidth = 77 | windowWidth - 78 | TASK_SIDEBAR_WIDTH - 79 | FRAME_BUTTONS_ROOT_LEFT_PADDING - 80 | FRAME_BUTTONS_ROOT_RIGHT_PADDING; 81 | const maxFrameButtonCount = Math.min( 82 | totalFrames, 83 | Math.floor(contentWidth / FRAME_BUTTON_WIDTH) 84 | ); 85 | const currentPage = Math.floor((currentFrame - 1) / maxFrameButtonCount); 86 | let startIndex = currentPage * maxFrameButtonCount + 1; 87 | let lastIndex = (currentPage + 1) * maxFrameButtonCount + 1; 88 | if (lastIndex > totalFrames) { 89 | lastIndex = totalFrames + 1; 90 | startIndex = lastIndex - maxFrameButtonCount; 91 | } 92 | const buttons = []; 93 | for (let index = startIndex, key = 0; index < lastIndex; index++, key++) { 94 | const { color, outlined, mark } = frameButtonProps(index); 95 | const isActive = currentFrame === index; 96 | buttons.push( 97 | 107 | ); 108 | } 109 | return buttons; 110 | }, [ 111 | frameButtonProps, 112 | onClickFrameButton, 113 | onDoubleClickFrameButton, 114 | currentFrame, 115 | totalFrames, 116 | windowWidth, 117 | ]); 118 | return ( 119 | 120 |
121 |
122 |
{frameButtons}
123 |
124 |
125 |
126 | ); 127 | }; 128 | 129 | export default FrameBar; 130 | -------------------------------------------------------------------------------- /src/editor-module/components/style-const.ts: -------------------------------------------------------------------------------- 1 | export const TASK_SIDEBAR_WIDTH = 350; 2 | -------------------------------------------------------------------------------- /src/editor-module/components/tool-bar-button.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import { 6 | createTheme, 7 | ThemeProvider, 8 | StyledEngineProvider, 9 | } from '@mui/material/styles'; 10 | import Tooltip from '@mui/material/Tooltip'; 11 | import React, { FC } from 'react'; 12 | 13 | declare module '@mui/styles/defaultTheme' { 14 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 15 | interface DefaultTheme extends Theme {} 16 | } 17 | 18 | const theme = createTheme({ 19 | components: { 20 | MuiIconButton: { 21 | styleOverrides: { 22 | root: { 23 | borderRadius: 'initial', 24 | }, 25 | }, 26 | }, 27 | MuiTouchRipple: { 28 | styleOverrides: { 29 | rippleVisible: { 30 | animation: 'initial', 31 | }, 32 | child: { 33 | borderRadius: 'initial', 34 | }, 35 | }, 36 | }, 37 | }, 38 | }); 39 | 40 | const useStyles = makeStyles(() => 41 | createStyles({ 42 | icon: () => ({ 43 | marginRight: theme.spacing(1), 44 | }), 45 | }) 46 | ); 47 | 48 | type Props = { 49 | toolTip: string; 50 | icon: JSX.Element; 51 | dense?: true; 52 | disabled?: boolean; 53 | active?: boolean; 54 | onClick?: () => void; 55 | }; 56 | 57 | export const ToolBarBoxButtonThemeProvider: FC = ({ children }) => { 58 | return ( 59 | 60 | {children} 61 | 62 | ); 63 | }; 64 | 65 | const ToolBarButton: FC = ({ 66 | toolTip, 67 | icon, 68 | dense, 69 | disabled, 70 | active, 71 | onClick, 72 | }) => { 73 | const classes = useStyles(); 74 | if (disabled) { 75 | return ( 76 | 82 | {icon} 83 | 84 | ); 85 | } 86 | return ( 87 | 88 | 94 | {icon} 95 | 96 | 97 | ); 98 | }; 99 | export default ToolBarButton; 100 | -------------------------------------------------------------------------------- /src/editor-module/components/tool-bar.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import List from '@mui/material/List'; 3 | import ListItem from '@mui/material/ListItem'; 4 | import React, { CSSProperties, FC } from 'react'; 5 | 6 | type Props = { 7 | className?: string; 8 | style?: CSSProperties; 9 | }; 10 | 11 | const ToolBar: FC = ({ className, style, children }) => { 12 | return ( 13 | 18 | 19 | {children} 20 | 21 | 22 | ); 23 | }; 24 | export default ToolBar; 25 | -------------------------------------------------------------------------------- /src/editor-module/config/mui-theme.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material/styles'; 2 | 3 | // Create a theme instance. 4 | const muiTheme = createTheme({ 5 | typography: { 6 | button: { 7 | textTransform: 'none', 8 | }, 9 | fontFamily: ['"Helvetica Neue"'].join(','), 10 | }, 11 | components: { 12 | MuiIconButton: { 13 | defaultProps: { 14 | tabIndex: -1, 15 | }, 16 | }, 17 | MuiIcon: { 18 | defaultProps: { 19 | tabIndex: -1, 20 | }, 21 | }, 22 | MuiListItem: { 23 | defaultProps: { 24 | tabIndex: -1, 25 | }, 26 | }, 27 | MuiSlider: { 28 | defaultProps: { 29 | tabIndex: -1, 30 | }, 31 | }, 32 | MuiPaper: { 33 | defaultProps: { 34 | variant: 'outlined', 35 | }, 36 | }, 37 | MuiTable: { 38 | defaultProps: { 39 | size: 'small', 40 | }, 41 | }, 42 | MuiTextField: { 43 | defaultProps: { 44 | variant: 'outlined', 45 | size: 'small', 46 | InputProps: { 47 | classes: { 48 | input: "{'font-size': '0.875rem'}", 49 | }, 50 | }, 51 | SelectProps: { 52 | MenuProps: { 53 | anchorOrigin: { 54 | vertical: 'bottom', 55 | horizontal: 'left', 56 | }, 57 | transformOrigin: { 58 | vertical: 'top', 59 | horizontal: 'left', 60 | }, 61 | }, 62 | }, 63 | }, 64 | }, 65 | MuiButton: { 66 | defaultProps: { 67 | variant: 'contained', 68 | color: 'inherit', 69 | disableElevation: true, 70 | }, 71 | styleOverrides: { 72 | root: { 73 | '&.MuiButtonGroup-groupedOutlined': { 74 | borderColor: 'rgba(0, 0, 0, 0.23)', 75 | color: 'inherit', 76 | '&:hover': { 77 | backgroundColor: 'rgba(0, 0, 0, 0.04)', 78 | borderColor: 'rgba(0, 0, 0, 0.23)', 79 | }, 80 | }, 81 | }, 82 | }, 83 | }, 84 | MuiTableCell: { 85 | styleOverrides: { 86 | head: { 87 | cursor: 'default', 88 | }, 89 | }, 90 | }, 91 | MuiLink: { 92 | styleOverrides: { 93 | root: { 94 | cursor: 'pointer', 95 | }, 96 | }, 97 | }, 98 | MuiToolbar: { 99 | styleOverrides: { 100 | root: { 101 | paddingLeft: '16px !important', 102 | paddingRight: '16px !important', 103 | }, 104 | }, 105 | }, 106 | MuiOutlinedInput: { 107 | styleOverrides: { 108 | inputSizeSmall: { 109 | paddingTop: '8.5px', 110 | paddingBottom: '8.5px', 111 | fontSize: '0.9rem', 112 | }, 113 | }, 114 | }, 115 | }, 116 | palette: { 117 | primary: { 118 | main: '#1565c0', 119 | contrastText: '#ffffff', 120 | }, 121 | secondary: { 122 | main: '#D32F2F', 123 | contrastText: '#ffffff', 124 | }, 125 | background: { 126 | default: '#fff', 127 | }, 128 | }, 129 | }); 130 | 131 | export default muiTheme; 132 | -------------------------------------------------------------------------------- /src/editor-module/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "c_select_item-label__pcd_only": "PCD", 3 | "c_select_item-label__pcd_image": "PCD-with image", 4 | "c_select_item-label__pcd_image_frames": "PCD-with frame", 5 | "c_file-pcd_only__description_main": "Drag and drop a PCD file", 6 | "c_file-pcd_only__description_sub": "Only PCD is supported", 7 | "c_file-pcd_only__description_btn": "Upload PCD file", 8 | "c_file-pcd_only__description_btnUpdate": "Change file", 9 | "c_file-pcd_image__description_main": "Drag and drop PCD / Image / Calibration", 10 | "c_file-pcd_image__description_sub": "Supports PCD, images [PNG / JPEG], calibration [YAML]", 11 | "c_file-pcd_image__description_btn": "Upload PCD / Image / Calibration", 12 | "c_file-pcd_image__description_btnUpdate": "Change file", 13 | "c_file-pcd_image_frames__description_main": "Drag and drop a folder", 14 | "c_file-pcd_image_frames__description_sub": "Only supports specified formats", 15 | "c_file-pcd_image_frames__description_btn": "Upload folder", 16 | "c_file-pcd_image_frames__description_btnUpdate": "Change file", 17 | "$instance-list": "#Comment#", 18 | "instanceList-label__positionX": "Coordinate X", 19 | "instanceList-label__positionY": "Coordinate Y", 20 | "instanceList-label__positionZ": "Coordinate Z", 21 | "instanceList-label__rotationX": "Rotation X", 22 | "instanceList-label__rotationY": "Rotation Y", 23 | "instanceList-label__rotationZ": "Rotation Z", 24 | "instanceList-label__sizeX": "Length X", 25 | "instanceList-label__sizeY": "Length Y", 26 | "instanceList-label__sizeZ": "Length Z", 27 | "menu_item-label__addFromFrame": "Add to frame", 28 | "menu_item-label__removeFromFrame": "Remove from frame", 29 | "menu_item-label__removeAll": "Remove", 30 | "menu_item-label__LabelView": "Label View", 31 | "$class-form-dialog": "#Comment#", 32 | "classForm-header_label__create": "Create Annotation Class", 33 | "classForm-header_label__edit": "Edit Annotation Class", 34 | "classForm-label__title": "Name", 35 | "classForm-label__value": "Value", 36 | "classForm-label__color": "Color", 37 | "classForm-label__defaultSizeX": "Length X [default]", 38 | "classForm-label__defaultSizeY": "Length Y [default]", 39 | "classForm-label__defaultSizeZ": "Length Z [default]", 40 | "classForm-action_label__saveCreate": "Save And Create", 41 | "classForm-action_label__save": "Save", 42 | "classForm-action_label__create": "Create", 43 | "$class-list-dialog": "#Comment#", 44 | "classList-header_label": "Annotation Class", 45 | "classList-action_label__close": "Close", 46 | "classList-action_label__create": "Create", 47 | "$side-panel": "#Comment#", 48 | "sidePanel-header_label__annotationClasses": "Annotation Class", 49 | "sidePanel-header_label__taskAnnotation": "Annotation", 50 | "sidePanel-count_label__taskAnnotation": "Task: {{taskCount}}", 51 | "sidePanel-action_label__otherFrameFilter": "Other Frames", 52 | "sidePanel-action_label__cancel": "Cancel", 53 | "sidePanel-action_label__save": "Save", 54 | "sidePanel-message__save": "Saved", 55 | "$tool-bar": "#Comment#", 56 | "toolBar-label__birdsEyeView": "Bird’s Eye View", 57 | "toolBar-label__selectMode_control": "Pan & zoom scene", 58 | "toolBar-label__selectMode_select": "Select figure", 59 | "toolBar-label__showLabel": "Show label", 60 | "toolBar-label__showImage": "Show Image", 61 | "toolBar-label__interpolation": "Frame Interpolation", 62 | "toolBar-label__copyPrevFrameObject": "Copy figure from previous frame", 63 | "toolBar-message__copyPrevFrame": "Copied to displayed frame", 64 | "toolBar-label__save": "Save", 65 | "toolBar-message__save": "Saved", 66 | "toolBar-label__export": "Export", 67 | "toolBar-message__export": "Exported in {{exportPath}}", 68 | "toolBar-label__movePrevFrame": "Back prev frame", 69 | "toolBar-label__moveNextFrame": "Go next frame", 70 | "labelToolBar-label__return": "Back", 71 | "$fl-three-editor": "#Comment#", 72 | "flThreeEditor-label__top": "TOP", 73 | "flThreeEditor-label__side": "SIDE", 74 | "flThreeEditor-label__front": "FRONT", 75 | "sidePanel-action_label__annotationOff": "Turn off in frames", 76 | "sidePanel-action_label__annotationOn": "Turn on in subsequent frames", 77 | "sidePanel-action_label__annotationOffCurrent": "Turn off at the current frame", 78 | "sidePanel-action_label__annotationOnCurrent": "Turn on at the current frame", 79 | "task.edit.reset": "Reset Task", 80 | "task.edit.reset.description": "Are you sure to reset task? The changes of task annotations are not saved.", 81 | "close": "Close", 82 | "task.save.title": "Confirmation", 83 | "task.save.description": "Do you want to save before returning to the task list?", 84 | "task.save.yes": "Save and return", 85 | "task.save.no": "Return without saving" 86 | } 87 | -------------------------------------------------------------------------------- /src/editor-module/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "c_select_item-label__pcd_only": "PCD", 3 | "c_select_item-label__pcd_image": "PCD - 画像付き", 4 | "c_select_item-label__pcd_image_frames": "PCD - 連続した情報", 5 | "c_file-pcd_only__description_main": "PCDをドラッグ&ドロップしてください", 6 | "c_file-pcd_only__description_sub": "PCDのみをサポートしています", 7 | "c_file-pcd_only__description_btn": "PCDをアップロード", 8 | "c_file-pcd_only__description_btnUpdate": "ファイルを変更", 9 | "c_file-pcd_image__description_main": "PCD/画像/キャリブレーションをドラッグ&ドロップしてください", 10 | "c_file-pcd_image__description_sub": "PCD、画像[PNG/JPEG]、キャリブレーション[YAML]をサポートしています", 11 | "c_file-pcd_image__description_btn": "PCD/画像/キャリブレーションをアップロード", 12 | "c_file-pcd_image__description_btnUpdate": "ファイルを変更", 13 | "c_file-pcd_image_frames__description_main": "フォルダをドラッグ&ドロップしてください", 14 | "c_file-pcd_image_frames__description_sub": "特定の形式のみサポートしています", 15 | "c_file-pcd_image_frames__description_btn": "フォルダをアップロード", 16 | "c_file-pcd_image_frames__description_btnUpdate": "ファイルを変更", 17 | "$instance-list": "#Comment#", 18 | "instanceList-label__positionX": "座標 X", 19 | "instanceList-label__positionY": "座標 Y", 20 | "instanceList-label__positionZ": "座標 Z", 21 | "instanceList-label__rotationX": "回転 X", 22 | "instanceList-label__rotationY": "回転 Y", 23 | "instanceList-label__rotationZ": "回転 Z", 24 | "instanceList-label__sizeX": "全長 X", 25 | "instanceList-label__sizeY": "全幅 Y", 26 | "instanceList-label__sizeZ": "全高 Z", 27 | "menu_item-label__addFromFrame": "フレームに追加", 28 | "menu_item-label__removeFromFrame": "フレームから削除", 29 | "menu_item-label__removeAll": "フレームから削除", 30 | "menu_item-label__LabelView": "ラベルビュー", 31 | "$class-form-dialog": "#Comment#", 32 | "classForm-header_label__create": "アノテーションクラスを作成", 33 | "classForm-header_label__edit": "アノテーションクラスを編集", 34 | "classForm-label__title": "名称", 35 | "classForm-label__value": "値", 36 | "classForm-label__color": "色", 37 | "classForm-label__defaultSizeX": "全長 X(初期値)", 38 | "classForm-label__defaultSizeY": "全長 Y(初期値)", 39 | "classForm-label__defaultSizeZ": "全長 Z(初期値)", 40 | "classForm-action_label__saveCreate": "保存して新規作成", 41 | "classForm-action_label__save": "保存", 42 | "classForm-action_label__create": "作成", 43 | "$class-list-dialog": "#Comment#", 44 | "classList-header_label": "アノテーションクラス", 45 | "classList-action_label__close": "閉じる", 46 | "classList-action_label__create": "新規作成", 47 | "$side-panel": "#Comment#", 48 | "sidePanel-header_label__annotationClasses": "アノテーションクラス", 49 | "sidePanel-header_label__taskAnnotation": "アノテーション", 50 | "sidePanel-count_label__taskAnnotation": "件数: {{taskCount}}", 51 | "sidePanel-action_label__otherFrameFilter": "他のフレーム", 52 | "sidePanel-action_label__cancel": "取り消す", 53 | "sidePanel-action_label__save": "保存", 54 | "sidePanel-message__save": "保存しました", 55 | "$tool-bar": "#Comment#", 56 | "toolBar-label__birdsEyeView": "鳥瞰図表示", 57 | "toolBar-label__selectMode_control": "カメラのズーム・移動", 58 | "toolBar-label__selectMode_select": "オブジェクトの選択", 59 | "toolBar-label__showLabel": "ラベル表示", 60 | "toolBar-label__showImage": "画像表示", 61 | "toolBar-label__interpolation": "フレーム補完", 62 | "toolBar-label__copyPrevFrameObject": "前のフレームのオブジェクトをコピー", 63 | "toolBar-message__copyPrevFrame": "表示しているフレームにコピーしました", 64 | "toolBar-label__save": "保存", 65 | "toolBar-message__save": "保存しました", 66 | "toolBar-label__export": "出力", 67 | "toolBar-message__export": "{{exportPath}}に出力しました", 68 | "toolBar-label__movePrevFrame": "次のフレームへ移動", 69 | "toolBar-label__moveNextFrame": "前のフレームへ移動", 70 | "labelToolBar-label__return": "戻る", 71 | "$fl-three-editor": "#Comment#", 72 | "flThreeEditor-label__top": "上面", 73 | "flThreeEditor-label__side": "側面", 74 | "flThreeEditor-label__front": "前面", 75 | "sidePanel-action_label__annotationOff": "以降のフレームをOFFにする", 76 | "sidePanel-action_label__annotationOn": "以降のフレームをONにする", 77 | "sidePanel-action_label__annotationOffCurrent": "現在のフレームをOFFにする", 78 | "sidePanel-action_label__annotationOnCurrent": "現在のフレームをONにする", 79 | "task.edit.reset": "取り消す", 80 | "task.edit.reset.description": "タスクの変更内容を取り消してもよろしいでしょうか?", 81 | "close": "閉じる", 82 | "task.save.title": "確認", 83 | "task.save.description": "タスク一覧に戻る前に変更を保存しますか?", 84 | "task.save.yes": "保存して戻る", 85 | "task.save.no": "保存しないで戻る" 86 | } 87 | -------------------------------------------------------------------------------- /src/editor-module/repositories/project-repository.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-empty-function */ 3 | import React from 'react'; 4 | import { ProjectType } from '../types/const'; 5 | import { 6 | AnnotationClassVO, 7 | TaskAnnotationVO, 8 | TaskFrameVO, 9 | TaskROMVO, 10 | } from '../types/vo'; 11 | 12 | export type ProjectRepository = { 13 | create(vo: { 14 | projectId: string; 15 | type: ProjectType; 16 | targets: File[]; 17 | }): Promise<{ projectId: string; errorCode?: string }>; 18 | load( 19 | projectId: string, 20 | taskId?: string, 21 | frameNo?: string 22 | ): Promise<{ taskROM: TaskROMVO; taskAnnotations: TaskAnnotationVO[] }>; 23 | loadAnnotationClasses( 24 | projectId: string 25 | ): Promise<{ annotationClasses: AnnotationClassVO[] }>; 26 | saveAnnotationClasses(vo: { 27 | projectId: string; 28 | annotationClasses: AnnotationClassVO[]; 29 | }): Promise; 30 | loadFrameResource( 31 | projectId: string, 32 | taskId: string, 33 | frameNo: string, 34 | pcdTopicId: string, 35 | imageTopics?: { [topicId: string]: string } 36 | ): Promise; 37 | saveFrameTaskAnnotations(vo: TaskAnnotationVO[]): Promise; 38 | exportTaskAnnotations( 39 | vo: TaskAnnotationVO[] 40 | ): Promise<{ status?: boolean; path?: string; message?: string }>; 41 | }; 42 | 43 | const STUB: ProjectRepository = { 44 | create(vo: { 45 | projectId: string; 46 | type: ProjectType; 47 | targets: File[]; 48 | }): Promise<{ projectId: string }> { 49 | return new Promise((resolve, reject) => {}); 50 | }, 51 | load( 52 | projectId: string, 53 | taskId: string, 54 | frameNo?: string 55 | ): Promise<{ taskROM: TaskROMVO; taskAnnotations: TaskAnnotationVO[] }> { 56 | return new Promise((resolve, reject) => {}); 57 | }, 58 | loadAnnotationClasses( 59 | projectId: string 60 | ): Promise<{ annotationClasses: AnnotationClassVO[] }> { 61 | return new Promise((resolve, reject) => {}); 62 | }, 63 | saveAnnotationClasses(vo: { 64 | annotationClasses: AnnotationClassVO[]; 65 | }): Promise { 66 | return new Promise((resolve, reject) => {}); 67 | }, 68 | loadFrameResource( 69 | projectId: string, 70 | taskId: string, 71 | frameNo: string, 72 | pcdTopicId: string, 73 | imageTopics?: { [topicId: string]: string } 74 | ): Promise { 75 | return new Promise((resolve, reject) => {}); 76 | }, 77 | saveFrameTaskAnnotations(vo: TaskAnnotationVO[]): Promise { 78 | return new Promise((resolve, reject) => {}); 79 | }, 80 | exportTaskAnnotations( 81 | vo: TaskAnnotationVO[] 82 | ): Promise<{ status?: boolean; path?: string; message?: string }> { 83 | return new Promise((resolve, reject) => {}); 84 | }, 85 | }; 86 | 87 | export const ProjectRepositoryContext = 88 | React.createContext(STUB); 89 | -------------------------------------------------------------------------------- /src/editor-module/stores/annotation-class-store.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, useContext, useEffect, useReducer } from 'react'; 2 | import { createContainer } from 'unstated-next'; 3 | import { ProjectRepositoryContext } from '../repositories/project-repository'; 4 | import { AnnotationClassVO } from '../types/vo'; 5 | 6 | export type ClassesAction = 7 | | { 8 | type: 'init'; 9 | projectId: string; 10 | data: AnnotationClassVO[]; 11 | } 12 | | { 13 | type: 'add' | 'update'; 14 | vo: AnnotationClassVO; 15 | } 16 | | { 17 | type: 'remove'; 18 | annotationClassId: string; 19 | } 20 | | { 21 | type: 'fetch'; 22 | projectId: string; 23 | } 24 | | { 25 | type: 'fetched'; 26 | data: AnnotationClassVO[]; 27 | } 28 | | { 29 | type: 'save'; 30 | callbackSaved?: () => void; 31 | } 32 | | { 33 | type: 'saved'; 34 | } 35 | | { 36 | type: 'end'; 37 | }; 38 | 39 | export type AnnotationClassState = 40 | | { 41 | status: 'none'; 42 | } 43 | | { 44 | status: 'loading'; 45 | projectId: string; 46 | } 47 | | { 48 | status: 'ready'; 49 | projectId: string; 50 | data: AnnotationClassVO[]; 51 | } 52 | | { 53 | status: 'saving'; 54 | projectId: string; 55 | data: AnnotationClassVO[]; 56 | } 57 | | { 58 | status: 'saved'; 59 | projectId: string; 60 | data: AnnotationClassVO[]; 61 | }; 62 | 63 | const reducer: () => Reducer = () => { 64 | return (state, action) => { 65 | switch (action.type) { 66 | case 'init': 67 | if (state.status !== 'none' && state.status !== 'saved') 68 | throw new Error(`illegal state ${JSON.stringify(state)}`); 69 | return { 70 | status: 'ready', 71 | projectId: action.projectId, 72 | data: action.data, 73 | }; 74 | case 'add': 75 | if (state.status !== 'ready') 76 | throw new Error(`illegal state ${JSON.stringify(state)}`); 77 | return { ...state, data: state.data.concat([action.vo]) }; 78 | case 'update': 79 | if (state.status !== 'ready') 80 | throw new Error(`illegal state ${JSON.stringify(state)}`); 81 | return { 82 | ...state, 83 | status: 'ready', 84 | data: state.data.map((c) => { 85 | if (c.id === action.vo.id) { 86 | return action.vo; 87 | } 88 | return c; 89 | }), 90 | }; 91 | case 'remove': 92 | if (state.status !== 'ready') 93 | throw new Error(`illegal state ${JSON.stringify(state)}`); 94 | return { 95 | ...state, 96 | data: state.data.filter((c) => c.id !== action.annotationClassId), 97 | }; 98 | case 'fetch': 99 | if (state.status === 'saving') 100 | throw new Error(`illegal state ${JSON.stringify(state)}`); 101 | return { ...state, status: 'loading', projectId: action.projectId }; 102 | case 'fetched': 103 | if (state.status !== 'loading') 104 | throw new Error(`illegal state ${JSON.stringify(state)}`); 105 | return { ...state, status: 'ready', data: action.data }; 106 | case 'save': 107 | if (state.status !== 'ready') 108 | throw new Error(`illegal state ${JSON.stringify(state)}`); 109 | return { 110 | ...state, 111 | status: 'saving', 112 | callbackSaved: action.callbackSaved, 113 | }; 114 | case 'saved': 115 | if (state.status !== 'saving') 116 | throw new Error(`illegal state ${JSON.stringify(state)}`); 117 | return { ...state, status: 'saved' }; 118 | case 'end': 119 | if (state.status !== 'saved') 120 | throw new Error(`illegal state ${JSON.stringify(state)}`); 121 | return { status: 'none' }; 122 | } 123 | }; 124 | }; 125 | 126 | const useAnnotationClass = () => { 127 | const projectRepository = useContext(ProjectRepositoryContext); 128 | const [annotationClass, dispatchAnnotationClass] = useReducer(reducer(), { 129 | status: 'none', 130 | }); 131 | 132 | useEffect(() => { 133 | if (annotationClass.status === 'loading') { 134 | projectRepository 135 | .loadAnnotationClasses(annotationClass.projectId) 136 | .then((res) => { 137 | dispatchAnnotationClass({ 138 | type: 'fetched', 139 | data: res.annotationClasses, 140 | }); 141 | }); 142 | } else if (annotationClass.status === 'saving') { 143 | projectRepository 144 | .saveAnnotationClasses({ 145 | projectId: annotationClass.projectId, 146 | annotationClasses: annotationClass.data, 147 | }) 148 | .then(() => { 149 | dispatchAnnotationClass({ type: 'saved' }); 150 | }); 151 | } 152 | }, [annotationClass]); 153 | return { 154 | annotationClass, 155 | dispatchAnnotationClass, 156 | }; 157 | }; 158 | export default createContainer(useAnnotationClass); 159 | -------------------------------------------------------------------------------- /src/editor-module/stores/camera-calibration-store.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, useEffect, useMemo, useReducer, useState } from 'react'; 2 | import { Matrix4, PerspectiveCamera } from 'three'; 3 | import { createContainer } from 'unstated-next'; 4 | import { FormUtil } from '../components/fields/form-util'; 5 | import { FormAction, FormState } from '../components/fields/type'; 6 | import { CameraInternal, TaskCalibrationVO } from '../types/vo'; 7 | 8 | export type CameraCalibration = { 9 | fixFov: number; 10 | }; 11 | 12 | const flipMatrix = new Matrix4(); 13 | flipMatrix.set(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1); 14 | 15 | const formReducer: Reducer, FormAction> = ( 16 | state, 17 | action 18 | ) => { 19 | switch (action.type) { 20 | case 'change': 21 | // TODO validation 22 | return { 23 | data: FormUtil.update(action.name, action.value, state.data), 24 | helper: state.helper, 25 | }; 26 | case 'init': 27 | return { data: action.data, helper: {} }; 28 | } 29 | }; 30 | 31 | const useCameraCalibration = () => { 32 | const [open, setOpen] = useState(false); 33 | const [calibration, updateCalibration] = useState(); 34 | 35 | const [cameraInternal, updateCameraInternal] = useState(); 36 | 37 | const [form, dispatchForm] = useReducer(formReducer, { 38 | data: { fixFov: 0 }, 39 | helper: {}, 40 | }); 41 | 42 | useEffect(() => { 43 | if (!calibration) { 44 | return; 45 | } 46 | const mat = calibration.cameraMat; 47 | const cameraMatrix = new Matrix4(); 48 | cameraMatrix.set(...mat[0], 0, ...mat[1], 0, ...mat[2], 0, 0, 0, 0, 1); 49 | const cameraMatrixT = cameraMatrix.clone().transpose(); 50 | 51 | const [width, height] = calibration.imageSize; 52 | const fy = cameraMatrixT.elements[5]; 53 | const cx = cameraMatrixT.elements[2]; 54 | const cy = cameraMatrixT.elements[6]; 55 | const fullWidth = cx * 2; 56 | const fullHeight = cy * 2; 57 | 58 | const offsetX = cx - width / 2; 59 | const offsetY = cy - height / 2; 60 | 61 | const fov = (2 * Math.atan(fullHeight / (2 * fy)) * 180) / Math.PI; 62 | const distance = 1000; 63 | 64 | updateCameraInternal((pre) => ({ 65 | ...pre, 66 | width, 67 | height, 68 | fy, 69 | fov: form.data.fixFov || fov, 70 | distance, 71 | fullWidth, 72 | fullHeight, 73 | offsetX, 74 | offsetY, 75 | })); 76 | }, [calibration]); 77 | 78 | useEffect(() => { 79 | updateCameraInternal((pre) => { 80 | if (pre) { 81 | const newState = { ...pre, fov: form.data.fixFov }; 82 | console.log(newState); 83 | return newState; 84 | } 85 | return pre; 86 | }); 87 | }, [form]); 88 | 89 | const calibrationCamera = useMemo(() => { 90 | if (!cameraInternal || !calibration) { 91 | return undefined; 92 | } 93 | const cameraExtrinsicMatrix = new Matrix4(); 94 | const extrinsic = calibration.cameraExtrinsicMat; 95 | cameraExtrinsicMatrix.set( 96 | ...extrinsic[0], 97 | ...extrinsic[1], 98 | ...extrinsic[2], 99 | ...extrinsic[3] 100 | ); 101 | // Flip the calibration information along with all axes. 102 | const flipMatrix = new Matrix4(); 103 | flipMatrix.set(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1); 104 | 105 | // NOTE: THREE.Matrix4.elements contains matrices in column-major order, but not row-major one. 106 | // So, we need the transposed matrix to get the elements in row-major order. 107 | const cameraExtrinsicMatrixFlipped = flipMatrix.premultiply( 108 | cameraExtrinsicMatrix 109 | ); 110 | const cameraExtrinsicMatrixFlippedT = cameraExtrinsicMatrixFlipped 111 | .clone() 112 | .transpose(); 113 | 114 | const imageCamera = new PerspectiveCamera( 115 | cameraInternal.fov, 116 | // cameraInternal.fullWidth / cameraInternal.fullHeight, 117 | cameraInternal.width / cameraInternal.height, 118 | 1, 119 | cameraInternal.distance 120 | ); 121 | // imageCamera.setViewOffset( 122 | // cameraInternal.fullWidth, 123 | // cameraInternal.fullHeight, 124 | // cameraInternal.offsetX, 125 | // cameraInternal.offsetY, 126 | // cameraInternal.width, 127 | // cameraInternal.height 128 | // ); 129 | const [ 130 | n11, 131 | n12, 132 | n13, 133 | n14, 134 | n21, 135 | n22, 136 | n23, 137 | n24, 138 | n31, 139 | n32, 140 | n33, 141 | n34, 142 | n41, 143 | n42, 144 | n43, 145 | n44, 146 | ] = cameraExtrinsicMatrixFlippedT.elements; 147 | imageCamera.matrix.set( 148 | n11, 149 | n12, 150 | n13, 151 | n14, 152 | n21, 153 | n22, 154 | n23, 155 | n24, 156 | n31, 157 | n32, 158 | n33, 159 | n34, 160 | n41, 161 | n42, 162 | n43, 163 | n44 164 | ); 165 | imageCamera.matrixWorld.copy(imageCamera.matrix); 166 | imageCamera.updateProjectionMatrix(); 167 | imageCamera.matrixAutoUpdate = false; 168 | return imageCamera; 169 | }, [cameraInternal, calibration]); 170 | 171 | return { 172 | open, 173 | setOpen, 174 | calibrationCamera, 175 | cameraInternal, 176 | updateCalibration, 177 | form, 178 | dispatchForm, 179 | }; 180 | }; 181 | export default createContainer(useCameraCalibration); 182 | -------------------------------------------------------------------------------- /src/editor-module/types/const.ts: -------------------------------------------------------------------------------- 1 | export enum ProjectType { 2 | pcd_only = 'pcd_only', 3 | pcd_image = 'pcd_image', 4 | pcd_image_frames = 'pcd_image_frames', 5 | } 6 | 7 | export enum AnnotationType { 8 | cuboid = 'cuboid', 9 | } 10 | 11 | export enum ContentResourceType { 12 | pcd = 'pcd', 13 | image_png = 'image_png', 14 | image_jpeg = 'image_jpeg', 15 | } 16 | -------------------------------------------------------------------------------- /src/editor-module/types/labos.ts: -------------------------------------------------------------------------------- 1 | export type PCDResult = { 2 | position: any; 3 | normal: any; 4 | color: any; 5 | name: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/editor-module/types/vo.ts: -------------------------------------------------------------------------------- 1 | import { AnnotationType, ProjectType } from './const'; 2 | 3 | export type ThreeSize = { 4 | x: number; 5 | y: number; 6 | z: number; 7 | }; 8 | 9 | export type ProjectVO = { 10 | id: string; 11 | type: ProjectType; 12 | createdAt: string; 13 | updatedAt: string; 14 | }; 15 | 16 | export type ProjectAnnotationClassVO = { 17 | projectId: string; 18 | annotationClasses: AnnotationClassVO[]; 19 | createdAt: string; 20 | updatedAt: string; 21 | }; 22 | 23 | export type TaskVO = { 24 | // [ref] scope=task 25 | id: string; 26 | frames: string[]; 27 | pcdTopicId: string; 28 | imageTopics: TaskImageTopicVO[]; 29 | createdAt: string; 30 | updatedAt: string; 31 | }; 32 | 33 | export type TaskImageTopicVO = { 34 | topicId: string; 35 | extension: string; 36 | calibration?: boolean; 37 | }; 38 | 39 | export type ThreePoints = [ 40 | px: number, 41 | py: number, 42 | pz: number, 43 | ax: number, 44 | ay: number, 45 | az: number, 46 | sx: number, 47 | sy: number, 48 | sz: number 49 | ]; 50 | 51 | export type ThreePointsMeta = { 52 | autogenerated?: boolean; 53 | }; 54 | 55 | export type TaskAnnotationOriginVO = { 56 | id: string; 57 | annotationClassId: string; 58 | points: { [frameNo: string]: ThreePoints }; 59 | pointsMeta: { [frameNo: string]: ThreePointsMeta }; 60 | createdAt: string; 61 | updatedAt: string; 62 | }; 63 | 64 | export type TaskAnnotationVO = { 65 | type: AnnotationType; 66 | title: string; 67 | value: string; 68 | color: string; 69 | } & TaskAnnotationOriginVO; 70 | 71 | export type AnnotationClassVO = { 72 | id: string; 73 | type: AnnotationType; 74 | title: string; 75 | value: string; 76 | color: string; 77 | defaultSize: ThreeSize; 78 | createdAt: string; 79 | updatedAt: string; 80 | }; 81 | 82 | // ---- task store VO 83 | export type TaskROMVO = { 84 | // [ref] ProjectAnnotationClassVO 85 | projectId: string; 86 | annotationClasses: AnnotationClassVO[]; 87 | 88 | // [ref] TaskVO 89 | taskId: string; 90 | frames: string[]; 91 | pcdTopicId: string; 92 | imageTopics: TaskImageTopicVO[]; 93 | calibrations: { [topicID: string]: TaskCalibrationVO }; 94 | }; 95 | 96 | export type TaskFrameVO = { 97 | currentFrame: string; 98 | pcdResource: any; 99 | imageResources: { [key: string]: any }; 100 | }; 101 | 102 | export type TaskCalibrationVO = { 103 | cameraExtrinsicMat: [x: number, y: number, z: number, w: number][]; 104 | cameraMat: [x: number, y: number, z: number][]; 105 | distCoeff: number[][]; 106 | imageSize: [width: number, height: number]; 107 | }; 108 | 109 | export type CameraVo = { 110 | internal: CameraInternal; 111 | }; 112 | 113 | export type CameraInternal = { 114 | width: number; 115 | height: number; 116 | fy: number; 117 | fov?: number; 118 | distance: number; 119 | fullWidth: number; 120 | fullHeight: number; 121 | offsetX: number; 122 | offsetY: number; 123 | }; 124 | -------------------------------------------------------------------------------- /src/editor-module/utils/annotation-class-util.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | import { AnnotationType } from '../types/const'; 3 | import { AnnotationClassVO } from '../types/vo'; 4 | 5 | export const AnnotationClassUtil = { 6 | create: (): AnnotationClassVO => { 7 | const now = new Date(); 8 | return { 9 | id: uuid().toString(), 10 | type: AnnotationType.cuboid, 11 | title: '', 12 | value: '', 13 | color: '#' + Math.random().toString(16).substr(-6), 14 | defaultSize: { x: 1, y: 1, z: 1 }, 15 | createdAt: now.toISOString(), 16 | updatedAt: now.toISOString(), 17 | }; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/editor-module/utils/calibration-util.ts: -------------------------------------------------------------------------------- 1 | import { TaskCalibrationVO } from '../types/vo'; 2 | 3 | type CalibrationYaml = { 4 | CameraExtrinsicMat: any; 5 | CameraMat: any; 6 | DistCoeff: any; 7 | ImageSize: [width: number, height: number]; 8 | }; 9 | 10 | const convertArray = (item: { 11 | cols: number; 12 | rows: number; 13 | dt: string; 14 | data: number[]; 15 | }): any => { 16 | const { cols, rows, data } = item; 17 | const len = data.length; 18 | const result = []; 19 | for (let i = 0; i < len; ) { 20 | const end = i + rows; 21 | result.push(data.slice(i, end)); 22 | i = end; 23 | } 24 | return result; 25 | }; 26 | 27 | export const CalibrationUtil = { 28 | convertYamlToVo: (yamlObj: CalibrationYaml): TaskCalibrationVO => { 29 | return { 30 | cameraExtrinsicMat: convertArray(yamlObj.CameraExtrinsicMat), 31 | cameraMat: convertArray(yamlObj.CameraMat), 32 | distCoeff: convertArray(yamlObj.DistCoeff), 33 | imageSize: yamlObj.ImageSize, 34 | }; 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/editor-module/utils/color-util.ts: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import PcdUtil from './pcd-util'; 3 | 4 | const calculateMean = (arr: number[]) => { 5 | let total = 0; 6 | const len = arr.length; 7 | for (let i = 0; i < len; i++) { 8 | total += arr[i]; 9 | } 10 | return total / len; 11 | }; 12 | 13 | const standardDeviation = (arr: number[], optMean?: number) => { 14 | const mean = optMean || calculateMean(arr); 15 | let variance = 0; 16 | const len = arr.length; 17 | for (let i = 0; i < len; i++) { 18 | variance += Math.pow(arr[i] - mean, 2); 19 | } 20 | variance = variance / len; 21 | return Math.pow(variance, 0.5); 22 | }; 23 | 24 | const ColorUtil = { 25 | normalizeColors(vertices: Float32Array, color?: Color) { 26 | const normalizedIntensities = []; 27 | const colors: number[] = []; 28 | 29 | const { max, min, points } = PcdUtil.getMaxMin(vertices, 'z'); 30 | const maxColor = max; 31 | const minColor = min; 32 | const intensities = points; 33 | 34 | const mean = calculateMean(intensities); 35 | const sd = standardDeviation(intensities, mean); 36 | const filteredIntensities = intensities.filter( 37 | (i) => Math.abs(i - mean) < sd 38 | ); 39 | const range = filteredIntensities.reduce( 40 | (r, i) => { 41 | if (i < r.min) { 42 | r.min = i; 43 | } 44 | if (i > r.max) { 45 | r.max = i; 46 | } 47 | return r; 48 | }, 49 | { min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY } 50 | ); 51 | 52 | // normalize colors 53 | // if greater than 2 sd from mean, set to max color 54 | // if less than 2 sd from mean, set to min color 55 | // ortherwise normalize color based on min and max z-coordinates 56 | intensities.forEach((intensity, i) => { 57 | let r = intensity; 58 | if (intensity - mean >= 2 * sd) { 59 | r = 1; 60 | } else if (mean - intensity >= 2 * sd) { 61 | r = 0; 62 | } else { 63 | r = (intensity - range.min) / (range.max - range.min); 64 | } 65 | normalizedIntensities.push(r); 66 | const color = new Color(r, 0, 1 - r).multiplyScalar(r * 5); 67 | const baseIndex = i * 3; 68 | colors[baseIndex] = color.r; 69 | colors[baseIndex + 1] = color.g; 70 | colors[baseIndex + 2] = color.b; 71 | }); 72 | return colors; 73 | }, 74 | }; 75 | 76 | export default ColorUtil; 77 | -------------------------------------------------------------------------------- /src/editor-module/utils/fl-cube-util.ts: -------------------------------------------------------------------------------- 1 | import { ThreeEvent } from '@react-three/fiber'; 2 | import { BoxGeometry, Mesh, MeshBasicMaterial, Object3D, Vector3 } from 'three'; 3 | import { ThreePoints } from '../types/vo'; 4 | 5 | /** 6 | * Cube 7 | */ 8 | export const FlCubeUtil = { 9 | valid: (object: Object3D) => { 10 | const scaleGrp = object.children[0]; 11 | if (!scaleGrp) { 12 | return false; 13 | } 14 | // TODO add condition 15 | return true; 16 | }, 17 | getId: (object: Object3D) => { 18 | return object.name; 19 | }, 20 | setScale: (object: Object3D, scale: Vector3) => { 21 | const scaleGrp = object.children[0]; 22 | scaleGrp.scale.copy(scale); 23 | }, 24 | getScale: (object: Object3D) => { 25 | const scaleGrp = object.children[0]; 26 | return scaleGrp.scale; 27 | }, 28 | getColor: (object: Object3D) => { 29 | const cube = object.children[0].children[1] as Mesh< 30 | BoxGeometry, 31 | MeshBasicMaterial 32 | >; 33 | return cube.material.color; 34 | }, 35 | getPointsVo: (object: Object3D): ThreePoints => { 36 | const scale = FlCubeUtil.getScale(object); 37 | // prevent NaN with decimal value 38 | // to Number prevent Nan in editing. 39 | // Basically it should prevent make annotation class. 40 | // It's for preventive measures. 41 | return [ 42 | Number(object.position.x), 43 | Number(object.position.y), 44 | Number(object.position.z), 45 | Number(object.rotation.x), 46 | Number(object.rotation.y), 47 | Number(object.rotation.z), 48 | Number(scale.x), 49 | Number(scale.y), 50 | Number(scale.z), 51 | ]; 52 | }, 53 | resolveByOnClick: (event: ThreeEvent) => { 54 | const scaleGrp = event.eventObject.parent; 55 | if (scaleGrp && scaleGrp.parent) { 56 | return scaleGrp.parent; 57 | } 58 | throw new Error(`Can't resolve event ${event}`); 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/editor-module/utils/fl-object-camera-util.ts: -------------------------------------------------------------------------------- 1 | import { OrthographicCamera, Vector3 } from 'three'; 2 | 3 | const DISTANCE = 0.1; 4 | 5 | export type ControlKey = 6 | | 'T_BOX' 7 | | 'S_TL' 8 | | 'S_TR' 9 | | 'S_BL' 10 | | 'S_BR' 11 | | 'R_POINT'; 12 | 13 | export type ControlType = 'top' | 'side' | 'front'; 14 | 15 | export const FlObjectCameraUtil = { 16 | reset: (controlType: ControlType, camera: OrthographicCamera) => { 17 | switch (controlType) { 18 | case 'top': 19 | camera.position.set(0, 0, 10); 20 | break; 21 | case 'side': 22 | camera.position.set(0, 10, 0); 23 | break; 24 | case 'front': 25 | camera.position.set(10, 0, 0); 26 | break; 27 | } 28 | camera.far = 20; 29 | }, 30 | adjust: ( 31 | controlType: ControlType, 32 | camera: OrthographicCamera, 33 | objectScale: Vector3 34 | ) => { 35 | switch (controlType) { 36 | case 'top': 37 | camera.position.set(0, 0, objectScale.z / 2); 38 | camera.far = objectScale.z + DISTANCE; 39 | break; 40 | case 'side': 41 | camera.position.set(0, objectScale.y / 2, 0); 42 | camera.far = objectScale.y + DISTANCE; 43 | break; 44 | case 'front': 45 | camera.position.set(objectScale.x / 2, 0, 0); 46 | camera.far = objectScale.x + DISTANCE; 47 | break; 48 | } 49 | camera.updateProjectionMatrix(); 50 | }, 51 | adjustFar: ( 52 | controlType: ControlType, 53 | camera: OrthographicCamera, 54 | objectScale: Vector3 55 | ) => { 56 | switch (controlType) { 57 | case 'top': 58 | camera.position.z = objectScale.z / 2; 59 | camera.far = objectScale.z + DISTANCE; 60 | break; 61 | case 'side': 62 | camera.position.y = objectScale.y / 2; 63 | camera.far = objectScale.y + DISTANCE; 64 | break; 65 | case 'front': 66 | camera.position.x = objectScale.x / 2; 67 | camera.far = objectScale.x + DISTANCE; 68 | break; 69 | } 70 | camera.updateProjectionMatrix(); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/editor-module/utils/format-util.ts: -------------------------------------------------------------------------------- 1 | export const FormatUtil = { 2 | omitVal: (val: string, len: number): string => { 3 | return '…' + val.slice(val.length - len); 4 | }, 5 | number2FrameNo: (frame: number): string => { 6 | return ('0000' + frame).slice(-4); 7 | }, 8 | frameNo2Number: (frameNo: string): number => { 9 | return parseInt(frameNo); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/editor-module/utils/interpolation-util.ts: -------------------------------------------------------------------------------- 1 | import { ThreePoints, ThreePointsMeta } from '@fl-three-editor/types/vo'; 2 | import { updateFlCubeObject3d } from '@fl-three-editor/views/task-three/fl-cube-model'; 3 | import { Group } from 'three'; 4 | import { MathUtil } from './math-util'; 5 | 6 | const _interpolation = ( 7 | frameNo: string, 8 | pointsMeta: { [frameNo: string]: ThreePointsMeta }, 9 | points: { [frameNo: string]: ThreePoints }, 10 | newPoints: ThreePoints, 11 | onCopy: (frameNo: string, copyPoints: ThreePoints) => void, 12 | onUpdate: (frameNo: string, updatedPoints: ThreePoints) => void 13 | ): void => { 14 | const changedFrameNo = frameNo; 15 | const { updateBaseFrame, copyTargets, updateTargets } = Object.keys(points) 16 | .sort() 17 | .reduce<{ 18 | lower: boolean; 19 | end: boolean; 20 | updateBaseFrame: string; 21 | prevFrameNo: number; 22 | copyTargets: string[]; 23 | updateTargets: string[]; 24 | }>( 25 | (prev, frameNo) => { 26 | if (prev.end) { 27 | return prev; 28 | } 29 | prev.lower = frameNo < changedFrameNo; 30 | if (changedFrameNo === frameNo) { 31 | prev.prevFrameNo = parseInt(frameNo); 32 | return prev; 33 | } 34 | 35 | const intFrameNo = parseInt(frameNo); 36 | if ( 37 | (prev.prevFrameNo !== -1 && prev.prevFrameNo + 1 !== intFrameNo) || 38 | !pointsMeta[frameNo].autogenerated 39 | ) { 40 | if (prev.lower) { 41 | prev.prevFrameNo = -1; 42 | prev.updateTargets.length = 0; 43 | prev.updateBaseFrame = frameNo; 44 | } else { 45 | prev.end = true; 46 | } 47 | return prev; 48 | } 49 | if (prev.lower) { 50 | prev.updateTargets.push(frameNo); 51 | } else { 52 | prev.copyTargets.push(frameNo); 53 | } 54 | prev.prevFrameNo = intFrameNo; 55 | return prev; 56 | }, 57 | { 58 | lower: true, 59 | end: false, 60 | prevFrameNo: -1, 61 | updateBaseFrame: '', 62 | copyTargets: [], 63 | updateTargets: [], 64 | } 65 | ); 66 | copyTargets.forEach((frameNo) => { 67 | onCopy(frameNo, newPoints); 68 | }); 69 | 70 | const updatedFrameCount = updateTargets.length; 71 | if (updatedFrameCount > 0) { 72 | const basePoints = points[updateBaseFrame]; 73 | // calc diffs 74 | const diffPoints = basePoints.map( 75 | (value, index) => value - newPoints[index] 76 | ); 77 | // to each frame amount 78 | const frameAmountPoints = diffPoints.map((value) => 79 | MathUtil.round(value !== 0 ? value / updatedFrameCount : 0) 80 | ); 81 | updateTargets.reduce((prev, frameNo) => { 82 | // add amount 83 | const added = prev.map( 84 | (value, index) => value - frameAmountPoints[index] 85 | ) as ThreePoints; 86 | onUpdate(frameNo, added); 87 | return added; 88 | }, basePoints); 89 | } 90 | }; 91 | 92 | export const InterpolationUtil = { 93 | interpolation: ( 94 | frameNo: string, 95 | pointsMeta: { [frameNo: string]: ThreePointsMeta }, 96 | points: { [frameNo: string]: ThreePoints }, 97 | newPoints: ThreePoints 98 | ): void => { 99 | _interpolation( 100 | frameNo, 101 | pointsMeta, 102 | points, 103 | newPoints, 104 | (frameNo, copyPoints) => { 105 | points[frameNo] = copyPoints; 106 | }, 107 | (frameNo, updatedPoints) => { 108 | points[frameNo] = updatedPoints; 109 | } 110 | ); 111 | }, 112 | interpolation3D: ( 113 | frameNo: string, 114 | pointsMeta: { [frameNo: string]: ThreePointsMeta }, 115 | framesPoints: { 116 | [frameNo: string]: ThreePoints; 117 | }, 118 | framesObject: { 119 | [frameNo: string]: Group; 120 | }, 121 | newPoints: ThreePoints 122 | ): void => { 123 | _interpolation( 124 | frameNo, 125 | pointsMeta, 126 | framesPoints, 127 | newPoints, 128 | (frameNo, copyPoints) => { 129 | framesPoints[frameNo] = copyPoints; 130 | }, 131 | (frameNo, updatedPoints) => { 132 | framesPoints[frameNo] = updatedPoints; 133 | updateFlCubeObject3d(framesObject[frameNo], updatedPoints); 134 | } 135 | ); 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/editor-module/utils/math-util.ts: -------------------------------------------------------------------------------- 1 | export const MathUtil = { 2 | round(value: number): number { 3 | return Math.round(value * 100) / 100; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/editor-module/utils/task-annotation-util.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | import { 3 | AnnotationClassVO, 4 | TaskAnnotationOriginVO, 5 | TaskAnnotationVO, 6 | ThreePoints, 7 | ThreePointsMeta, 8 | } from '../types/vo'; 9 | 10 | const STORE_KEY = [ 11 | 'id', 12 | 'annotationClassId', 13 | 'points', 14 | 'pointsMeta', 15 | 'createdAt', 16 | 'updatedAt', 17 | ]; 18 | 19 | export const TaskAnnotationUtil = { 20 | create: ( 21 | annotationClass: AnnotationClassVO, 22 | frameNo: string, 23 | frames: string[], 24 | initPoints: ThreePoints, 25 | autogenerated?: boolean 26 | ): TaskAnnotationVO => { 27 | const now = new Date(); 28 | const { id, type, title, value, color } = annotationClass; 29 | 30 | const points: { [frameNo: string]: ThreePoints } = {}; 31 | const pointsMeta: { [frameNo: string]: ThreePointsMeta } = {}; 32 | 33 | if (autogenerated) { 34 | const currentFrame = parseInt(frameNo); 35 | const autogeneratedFrame = frames.filter( 36 | (frame) => parseInt(frame) > currentFrame 37 | ); 38 | autogeneratedFrame.forEach((frame) => { 39 | points[frame] = initPoints; 40 | pointsMeta[frame] = { autogenerated: true }; 41 | }); 42 | } 43 | 44 | points[frameNo] = initPoints; 45 | pointsMeta[frameNo] = { autogenerated: false }; 46 | return { 47 | id: uuid().toString(), 48 | annotationClassId: id, 49 | type, 50 | title, 51 | value, 52 | color, 53 | points, 54 | pointsMeta, 55 | createdAt: now.toISOString(), 56 | updatedAt: now.toISOString(), 57 | }; 58 | }, 59 | formSaveJson: (vo: TaskAnnotationVO): TaskAnnotationOriginVO => { 60 | const source = vo as any; 61 | return STORE_KEY.reduce((r, key) => { 62 | r[key] = source[key]; 63 | return r; 64 | }, {}); 65 | }, 66 | merge: ( 67 | voList: TaskAnnotationOriginVO[], 68 | classVoList: AnnotationClassVO[] 69 | ): TaskAnnotationVO[] => { 70 | const classVoObj = classVoList.reduce<{ [key: string]: AnnotationClassVO }>( 71 | (r, c) => { 72 | r[c.id] = c; 73 | return r; 74 | }, 75 | {} 76 | ); 77 | return voList.map((vo) => { 78 | const { type, title, value, color, defaultSize } = 79 | classVoObj[vo.annotationClassId]; 80 | return { ...vo, type, title, value, color } as TaskAnnotationVO; 81 | }); 82 | }, 83 | copyFramePoints: ( 84 | voList: TaskAnnotationVO[], 85 | frameNo: { source: string; dist: string }, 86 | targetId?: string 87 | ): TaskAnnotationVO[] => { 88 | const now = new Date(); 89 | return voList.map((vo) => { 90 | if (targetId && targetId !== vo.id) { 91 | return vo; 92 | } 93 | if (vo.points[frameNo.source] && !vo.points[frameNo.dist]) { 94 | vo.points[frameNo.dist] = vo.points[frameNo.source]; 95 | vo.updatedAt = now.toISOString(); 96 | } 97 | return vo; 98 | }); 99 | }, 100 | findNearestFramePoints: ( 101 | vo: TaskAnnotationVO, 102 | frameNo: string 103 | ): { prev: string; next: string } => { 104 | return Object.keys(vo.points) 105 | .sort() 106 | .reduce<{ prev: string; next: string }>( 107 | (r, f) => { 108 | if (f < r.prev) { 109 | r.prev = f; 110 | } 111 | if (f > r.next) { 112 | r.next = f; 113 | } 114 | return r; 115 | }, 116 | { prev: frameNo, next: frameNo } 117 | ); 118 | }, 119 | formatFrameNo: (frameNo: number): string => ('0000' + frameNo).slice(-4), 120 | }; 121 | -------------------------------------------------------------------------------- /src/editor-module/utils/view-util.ts: -------------------------------------------------------------------------------- 1 | export const ViewUtils = { 2 | offsetHeight: (offset = 0, isElectron?: boolean): string => { 3 | let total = offset; 4 | if (isElectron) { 5 | total += 39; 6 | } 7 | return total > 0 ? `${total}px` : '0px'; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/editor-module/utils/workspace-util.ts: -------------------------------------------------------------------------------- 1 | import { TFunction } from 'react-i18next'; 2 | import { ProjectType } from '../types/const'; 3 | 4 | const TargetItemTypes = [ 5 | { key: ProjectType.pcd_only, label: 'c_select_item-label__pcd_only' }, 6 | { key: ProjectType.pcd_image, label: 'c_select_item-label__pcd_image' }, 7 | { 8 | key: ProjectType.pcd_image_frames, 9 | label: 'c_select_item-label__pcd_image_frames', 10 | }, 11 | ]; 12 | 13 | const CONST_FILE_PROPS: any = { 14 | pcd_only: { 15 | descriptionKey: { 16 | main: 'c_file-pcd_only__description_main', 17 | sub: 'c_file-pcd_only__description_sub', 18 | btn: 'c_file-pcd_only__description_btn', 19 | btnUpdate: 'c_file-pcd_only__description_btnUpdate', 20 | }, 21 | accept: '.pcd', 22 | maxFiles: 1, 23 | }, 24 | pcd_image: { 25 | descriptionKey: { 26 | main: 'c_file-pcd_image__description_main', 27 | sub: 'c_file-pcd_image__description_sub', 28 | btn: 'c_file-pcd_image__description_btn', 29 | btnUpdate: 'c_file-pcd_image__description_btnUpdate', 30 | }, 31 | accept: ['.pcd', 'image/jpeg', 'image/png', '.yaml'], 32 | }, 33 | pcd_image_frames: { 34 | descriptionKey: { 35 | main: 'c_file-pcd_image_frames__description_main', 36 | sub: 'c_file-pcd_image_frames__description_sub', 37 | btn: 'c_file-pcd_image_frames__description_btn', 38 | btnUpdate: 'c_file-pcd_image_frames__description_btnUpdate', 39 | }, 40 | mode: 'folder', 41 | }, 42 | }; 43 | 44 | export const WorkspaceUtil = { 45 | targetItemTypes: (t: TFunction<'translation'>) => { 46 | return TargetItemTypes.map((item) => { 47 | const r = { ...item }; 48 | r.label = t(r.label); 49 | return r; 50 | }); 51 | }, 52 | folderContentsProps: (t: TFunction<'translation'>, typeValue: string) => { 53 | const fileProps = CONST_FILE_PROPS[typeValue]; 54 | fileProps.description = {}; 55 | fileProps.description.main = t(fileProps.descriptionKey.main); 56 | fileProps.description.sub = t(fileProps.descriptionKey.sub); 57 | fileProps.description.btn = t(fileProps.descriptionKey.btn); 58 | fileProps.description.btnUpdate = t(fileProps.descriptionKey.btnUpdate); 59 | return fileProps; 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/editor-module/views/annotation-classes/class-list.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Chip from '@mui/material/Chip'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import List from '@mui/material/List'; 7 | import ListItem from '@mui/material/ListItem'; 8 | import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; 9 | import ListItemText from '@mui/material/ListItemText'; 10 | import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; 11 | import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; 12 | import React, { FC } from 'react'; 13 | import { AnnotationType } from '../../types/const'; 14 | import { AnnotationClassVO } from '../../types/vo'; 15 | 16 | const useStyles = makeStyles((theme) => 17 | createStyles({ 18 | flexGrow: { 19 | flexGrow: 1, 20 | }, 21 | hotKey: { 22 | fontSize: '0.5rem', 23 | height: 20, 24 | borderRadius: 4, 25 | marginRight: theme.spacing(1), 26 | }, 27 | markCuboid: { 28 | marginRight: theme.spacing(1), 29 | borderWidth: 1, 30 | borderStyle: 'solid', 31 | borderColor: '#ffffff', 32 | boxSizing: 'border-box', 33 | minWidth: theme.spacing(2), 34 | minHeight: theme.spacing(2), 35 | }, 36 | }) 37 | ); 38 | 39 | type Props = { 40 | classes: AnnotationClassVO[]; 41 | invisibleClasses?: Set; 42 | selectedId?: string; 43 | onClickItem?: (item: AnnotationClassVO) => void; 44 | onClickToggleInvisible?: (item: AnnotationClassVO, visible: boolean) => void; 45 | }; 46 | 47 | const ClassList: FC = ({ 48 | classes, 49 | invisibleClasses, 50 | selectedId, 51 | onClickItem, 52 | onClickToggleInvisible, 53 | }) => { 54 | const styleClasses = useStyles(); 55 | const getClassTagStyle = (type: AnnotationType): any => { 56 | return styleClasses.markCuboid; 57 | }; 58 | return ( 59 | 60 | {classes.map((item, index) => { 61 | const hidden = invisibleClasses?.has(item.id); 62 | return ( 63 | onClickItem && onClickItem(item)}> 69 | 73 | 77 | {index < 9 ? ( 78 | 85 | ) : null} 86 | {onClickToggleInvisible ? ( 87 | 88 | onClickToggleInvisible(item, false)}> 92 | {hidden ? ( 93 | 94 | ) : ( 95 | 96 | )} 97 | 98 | 99 | ) : undefined} 100 | 101 | ); 102 | })} 103 | 104 | ); 105 | }; 106 | 107 | export default ClassList; 108 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/calibration-edit-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import Grid from '@mui/material/Grid'; 3 | import React, { FC } from 'react'; 4 | import DraggablePopover from '../../../components/draggable-popover'; 5 | import FLTextField from '../../../components/fields/fl-text-field'; 6 | import CameraCalibrationStore from '../../../stores/camera-calibration-store'; 7 | 8 | /** 9 | * under developing. It's for debug tool. 10 | * @returns 11 | */ 12 | const CalibrationEditDialog: FC = () => { 13 | const { open, form, dispatchForm } = CameraCalibrationStore.useContainer(); 14 | return ( 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | export default CalibrationEditDialog; 31 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/class-form-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import Grid from '@mui/material/Grid'; 4 | import React, { 5 | FC, 6 | Reducer, 7 | useCallback, 8 | useEffect, 9 | useReducer, 10 | useState, 11 | } from 'react'; 12 | import { useTranslation } from 'react-i18next'; 13 | import { 14 | FLDialogActions, 15 | FLDialogContent, 16 | FLDialogTitle, 17 | } from '../../../components/dialogs/fl-dialog'; 18 | import FLTextField from '../../../components/fields/fl-text-field'; 19 | import { FormUtil } from '../../../components/fields/form-util'; 20 | import { FormAction, FormState } from '../../../components/fields/type'; 21 | import { AnnotationClassVO } from '../../../types/vo'; 22 | import { AnnotationClassUtil } from '../../../utils/annotation-class-util'; 23 | import FLColorField from './../../../components/fields/fl-color-field'; 24 | 25 | type Props = { 26 | open: boolean; 27 | classVo?: AnnotationClassVO; 28 | onClose: () => void; 29 | onSubmit: (vo: AnnotationClassVO, type: 'add' | 'update') => Promise; 30 | }; 31 | 32 | const formReducer: Reducer, FormAction> = ( 33 | state, 34 | action 35 | ) => { 36 | switch (action.type) { 37 | case 'change': 38 | // TODO validation 39 | return { 40 | data: FormUtil.update(action.name, action.value, state.data), 41 | helper: state.helper, 42 | }; 43 | case 'init': 44 | return { data: action.data, helper: {} }; 45 | } 46 | }; 47 | 48 | const ClassFormDialog: FC = ({ open, classVo, onClose, onSubmit }) => { 49 | const handleClose = useCallback(() => onClose(), []); 50 | const [t] = useTranslation(); 51 | 52 | const [submitType, setSubmitType] = useState<'add' | 'update'>( 53 | classVo ? 'update' : 'add' 54 | ); 55 | 56 | const initialForm = { 57 | data: classVo || AnnotationClassUtil.create(), 58 | helper: {}, 59 | }; 60 | 61 | const [form, dispatchForm] = useReducer(formReducer, initialForm); 62 | 63 | useEffect(() => { 64 | setSubmitType(classVo ? 'update' : 'add'); 65 | dispatchForm({ 66 | type: 'init', 67 | data: classVo || AnnotationClassUtil.create(), 68 | }); 69 | }, [open, classVo]); 70 | 71 | const handleClickSaveCreate = () => { 72 | onSubmit(form.data, submitType).then(() => { 73 | setSubmitType('add'); 74 | dispatchForm({ type: 'init', data: AnnotationClassUtil.create() }); 75 | }); 76 | }; 77 | 78 | const handleClickSaveClose = () => { 79 | onSubmit(form.data, submitType).then(() => { 80 | onClose(); 81 | }); 82 | }; 83 | 84 | const componentCode = 'class-form-dialog-title'; 85 | return ( 86 | 91 | 92 | {submitType === 'update' 93 | ? t('classForm-header_label__edit') 94 | : t('classForm-header_label__create')} 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 107 | 108 | 109 | 112 | 113 | 114 | 118 | 119 | 120 | 124 | 125 | 126 | 130 | 131 | 132 | 133 | 134 | 137 | 142 | 143 | 144 | ); 145 | }; 146 | 147 | export default ClassFormDialog; 148 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/class-list-dialog.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@mui/material/Button'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import DialogActions from '@mui/material/DialogActions'; 4 | import DialogContent from '@mui/material/DialogContent'; 5 | import React, { FC, useCallback, useEffect } from 'react'; 6 | import { useTranslation } from 'react-i18next'; 7 | import { FLDialogTitle } from '../../../components/dialogs/fl-dialog'; 8 | import AnnotationClassStore from '../../../stores/annotation-class-store'; 9 | import { AnnotationClassVO } from '../../../types/vo'; 10 | import ClassList from '../../annotation-classes/class-list'; 11 | import ClassFormDialog from './class-form-dialog'; 12 | 13 | type Props = { 14 | // 15 | }; 16 | 17 | const ClassListDialog: FC = () => { 18 | const [open, setOpen] = React.useState(false); 19 | const [t] = useTranslation(); 20 | const [formDialog, setFormDialog] = React.useState<{ 21 | open: boolean; 22 | classVo?: AnnotationClassVO; 23 | }>({ open: false }); 24 | 25 | const { annotationClass, dispatchAnnotationClass } = 26 | AnnotationClassStore.useContainer(); 27 | 28 | useEffect(() => { 29 | setOpen(annotationClass.status === 'ready'); 30 | }, [annotationClass]); 31 | 32 | const handleClose = useCallback(() => { 33 | dispatchAnnotationClass({ type: 'save' }); 34 | }, []); 35 | 36 | const handleFormSubmit = ( 37 | vo: AnnotationClassVO, 38 | type: 'add' | 'update' = 'add' 39 | ) => { 40 | return new Promise((resolve) => { 41 | dispatchAnnotationClass({ type, vo }); 42 | resolve(); 43 | }); 44 | }; 45 | 46 | const componentCode = 'class-list-dialog-title'; 47 | return ( 48 | 49 | 54 | 55 | {t('classList-header_label')} 56 | 57 | 58 | {annotationClass.status === 'ready' ? ( 59 | 60 | 63 | setFormDialog({ open: true, classVo: item }) 64 | } 65 | /> 66 | 67 | ) : undefined} 68 | 69 | 70 | 73 | 79 | 80 | 81 | 82 | setFormDialog({ open: false })} 85 | classVo={formDialog.classVo} 86 | onSubmit={handleFormSubmit} 87 | /> 88 | 89 | ); 90 | }; 91 | 92 | export default ClassListDialog; 93 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/hot-key.tsx: -------------------------------------------------------------------------------- 1 | import createStyles from '@mui/styles/createStyles'; 2 | import makeStyles from '@mui/styles/makeStyles'; 3 | import React, { FC, useCallback, useEffect, useMemo } from 'react'; 4 | import TaskStore from '../../../stores/task-store'; 5 | import { AnnotationClassVO } from '../../../types/vo'; 6 | import { FlMainCameraControls } from '../../task-three/fl-main-camera-controls'; 7 | 8 | const useStyles = makeStyles(() => 9 | createStyles({ 10 | root: { 11 | width: '100%', 12 | height: '100%', 13 | '&:focus': { 14 | outline: 'none', 15 | }, 16 | }, 17 | }) 18 | ); 19 | 20 | type Props = { 21 | mainControlsRef?: React.RefObject; 22 | }; 23 | 24 | const KEY_ROTATE_SEED = Math.PI / 180; 25 | const KEY_PAN_SEED = 7; 26 | const KEY_DOLLY_SEED = Math.pow(0.95, 1); 27 | 28 | const HotKey: FC = ({ mainControlsRef }) => { 29 | const styles = useStyles(); 30 | const { 31 | taskRom, 32 | taskFrame, 33 | taskEditor, 34 | updateTaskAnnotations, 35 | selectAnnotationClass, 36 | resetSelectMode, 37 | } = TaskStore.useContainer(); 38 | 39 | const memoTaskStore = useMemo(() => { 40 | let enableHotKey = false; 41 | let currentFrameNo = ''; 42 | let selectingTaskAnnotationIds: string[] = []; 43 | let annotationClasses: AnnotationClassVO[] = []; 44 | if (taskRom.status !== 'loaded' || taskFrame.status === 'none') { 45 | return { 46 | enableHotKey, 47 | currentFrameNo, 48 | selectingTaskAnnotationIds, 49 | annotationClasses, 50 | }; 51 | } 52 | enableHotKey = taskEditor.pageMode === 'threeEdit'; 53 | // hasMultiFrame = taskRom.frames.length > 1; 54 | currentFrameNo = taskFrame.currentFrame; 55 | annotationClasses = taskRom.annotationClasses; 56 | 57 | if (taskEditor.editorState.mode === 'selecting_taskAnnotation') { 58 | selectingTaskAnnotationIds = 59 | taskEditor.editorState.selectingTaskAnnotations.map((vo) => vo.id); 60 | } 61 | return { 62 | enableHotKey, 63 | currentFrameNo, 64 | selectingTaskAnnotationIds, 65 | annotationClasses, 66 | }; 67 | }, [taskRom, taskFrame, taskEditor]); 68 | 69 | const onKeyDown = useCallback( 70 | (event: KeyboardEvent) => { 71 | if (!memoTaskStore.enableHotKey) { 72 | return; 73 | } 74 | const controls = mainControlsRef?.current; 75 | const { key } = event; 76 | 77 | let handled = true; 78 | switch (key) { 79 | case '1': 80 | case '2': 81 | case '3': 82 | case '4': 83 | case '5': 84 | case '6': 85 | case '7': 86 | case '8': 87 | case '9': 88 | // eslint-disable-next-line no-case-declarations 89 | const selectAnnotationClassIdx = Number(key) - 1; 90 | // eslint-disable-next-line no-case-declarations 91 | const annClasses = memoTaskStore.annotationClasses; 92 | if (annClasses.length > Number(key) - 1) { 93 | selectAnnotationClass(annClasses[selectAnnotationClassIdx]); 94 | } 95 | break; 96 | case 'ArrowRight': 97 | if (controls) { 98 | controls.rotate(KEY_ROTATE_SEED, 0); 99 | } 100 | break; 101 | case 'ArrowLeft': 102 | if (controls) { 103 | controls.rotate(-KEY_ROTATE_SEED, 0); 104 | } 105 | break; 106 | case 'ArrowDown': 107 | if (controls) { 108 | controls.rotate(0, -KEY_ROTATE_SEED); 109 | } 110 | break; 111 | case 'ArrowUp': 112 | if (controls) { 113 | controls.rotate(0, KEY_ROTATE_SEED); 114 | } 115 | break; 116 | case 'q': 117 | if (controls) { 118 | // TODO move forward 119 | controls.pan(0, 0, -KEY_DOLLY_SEED); 120 | } 121 | break; 122 | case 'e': 123 | if (controls) { 124 | // TODO move back 125 | controls.pan(0, 0, KEY_DOLLY_SEED); 126 | } 127 | break; 128 | case 'a': 129 | if (controls) { 130 | // move left 131 | controls.pan(KEY_PAN_SEED, 0, 0); 132 | } 133 | break; 134 | case 'w': 135 | if (controls) { 136 | // TODO move up 137 | controls.pan(0, KEY_PAN_SEED, 0); 138 | } 139 | break; 140 | case 's': 141 | if (controls) { 142 | // TODO move down 143 | controls.pan(0, -KEY_PAN_SEED, 0); 144 | } 145 | break; 146 | case 'd': 147 | if (controls) { 148 | // move right 149 | controls.pan(-KEY_PAN_SEED, 0, 0); 150 | } 151 | break; 152 | case 'Escape': 153 | resetSelectMode(); 154 | break; 155 | case 'Delete': 156 | // current only supported single select 157 | updateTaskAnnotations({ 158 | type: event.shiftKey ? 'removeAll' : 'removeFrame', 159 | id: memoTaskStore.selectingTaskAnnotationIds[0], 160 | frameNo: memoTaskStore.currentFrameNo, 161 | }); 162 | break; 163 | default: 164 | handled = false; 165 | } 166 | if (handled) { 167 | event.preventDefault(); 168 | event.stopPropagation(); 169 | return; 170 | } 171 | console.log(key); 172 | }, 173 | [mainControlsRef, memoTaskStore] 174 | ); 175 | 176 | useEffect(() => { 177 | const oldOnKeyDown = onKeyDown; 178 | document.addEventListener('keydown', onKeyDown); 179 | return () => { 180 | document.removeEventListener('keydown', oldOnKeyDown); 181 | }; 182 | }, [onKeyDown]); 183 | return <>; 184 | }; 185 | export default HotKey; 186 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/image-dialog.tsx: -------------------------------------------------------------------------------- 1 | import ArrowBackIosOutlinedIcon from '@mui/icons-material/ArrowBackIosOutlined'; 2 | import ArrowForwardIosOutlinedIcon from '@mui/icons-material/ArrowForwardIosOutlined'; 3 | import { Theme } from '@mui/material'; 4 | import Box from '@mui/material/Box'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import createStyles from '@mui/styles/createStyles'; 7 | import makeStyles from '@mui/styles/makeStyles'; 8 | import { Canvas, useThree } from '@react-three/fiber'; 9 | import React, { FC, useEffect, useMemo, useState } from 'react'; 10 | import { Texture, TextureLoader } from 'three'; 11 | import DraggablePopover from '../../../components/draggable-popover'; 12 | import ToolBar from '../../../components/tool-bar'; 13 | import CameraCalibrationStore from '../../../stores/camera-calibration-store'; 14 | import TaskStore from '../../../stores/task-store'; 15 | import FLCubes from '../../task-three/fl-cubes'; 16 | 17 | const useStyles = makeStyles(() => 18 | createStyles({ 19 | prevButton: { 20 | display: 'flex', 21 | position: 'absolute', 22 | height: '100%', 23 | alignItems: 'center', 24 | top: 0, 25 | left: 0, 26 | }, 27 | nextButton: { 28 | display: 'flex', 29 | position: 'absolute', 30 | height: '100%', 31 | alignItems: 'center', 32 | top: 0, 33 | right: 0, 34 | }, 35 | }) 36 | ); 37 | 38 | type LocalState = { 39 | open: boolean; 40 | width: number; 41 | height: number; 42 | tex?: Texture; 43 | }; 44 | 45 | type SceneBackgroundProps = { 46 | tex?: Texture; 47 | }; 48 | 49 | const _SceneBackground: FC = ({ tex }) => { 50 | const { gl, scene } = useThree(); 51 | useEffect(() => { 52 | scene.background = tex || null; 53 | }, [tex]); 54 | return <>; 55 | }; 56 | 57 | const ImagePopover: FC = () => { 58 | const styles = useStyles(); 59 | 60 | const [state, setState] = useState({ 61 | open: false, 62 | width: 0, 63 | height: 0, 64 | }); 65 | 66 | const { taskAnnotations, taskFrame, topicImageDialog, moveTopicImage } = 67 | TaskStore.useContainer(); 68 | 69 | const { open, setOpen, calibrationCamera } = 70 | CameraCalibrationStore.useContainer(); 71 | 72 | useEffect(() => { 73 | if (topicImageDialog.open) { 74 | new TextureLoader().load(topicImageDialog.currentImageData, (tex) => { 75 | const imageAspect = tex.image ? tex.image.width / tex.image.height : 1; 76 | const width = 800; 77 | const height = width / imageAspect; 78 | setState({ open: true, width, height, tex }); 79 | }); 80 | return; 81 | } 82 | setState({ 83 | open: false, 84 | width: 0, 85 | height: 0, 86 | }); 87 | }, [topicImageDialog]); 88 | 89 | const frameNo = useMemo(() => { 90 | if (taskFrame.status === 'loaded') { 91 | return taskFrame.currentFrame; 92 | } 93 | return ''; 94 | }, [taskFrame]); 95 | 96 | return ( 97 | 98 | 101 | {/* } 104 | onClick={() => setOpen((pre) => !pre)} 105 | active={open} 106 | /> */} 107 | 108 | {topicImageDialog.open && ( 109 | <> 110 | 111 | 112 | <_SceneBackground tex={state.tex} /> 113 | 114 | 115 | 116 | {topicImageDialog.hasPrev && ( 117 | 118 | 119 | moveTopicImage('prev')} 122 | size="large"> 123 | 124 | 125 | 126 | 127 | )} 128 | {topicImageDialog.hasNext && ( 129 | 130 | 131 | moveTopicImage('next')} 134 | size="large"> 135 | 136 | 137 | 138 | 139 | )} 140 | 141 | )} 142 | 143 | ); 144 | }; 145 | 146 | export default ImagePopover; 147 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/index.tsx: -------------------------------------------------------------------------------- 1 | import Grid from '@mui/material/Grid'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import React from 'react'; 5 | import { useParams } from 'react-router-dom'; 6 | import TaskStore, { 7 | TaskEditorViewMode, 8 | } from '@fl-three-editor/stores/task-store'; 9 | import BaseViewIndex from './base-view-index'; 10 | import ClassListDialog from './class-list-dialog'; 11 | import ImageDialog from './image-dialog'; 12 | import LabelViewIndex from './label-view-index'; 13 | 14 | const useStyles = makeStyles(() => 15 | createStyles({ 16 | root: { 17 | flexGrow: 1, 18 | height: (props: Props) => props.height || '100vh', 19 | width: '100vw', 20 | }, 21 | }) 22 | ); 23 | 24 | type Props = { 25 | height?: '100%' | '100vh'; 26 | }; 27 | 28 | const ThreeAnnotationPage: React.FC = (props) => { 29 | const [windowWidth, setWindowWidth] = React.useState(0); 30 | const classes = useStyles(props); 31 | const { projectId } = useParams<{ projectId: string }>(); 32 | const [initialed, setInitialed] = React.useState(false); 33 | 34 | const { open, taskEditorViewMode } = TaskStore.useContainer(); 35 | 36 | React.useEffect(() => { 37 | const onWindowResize = () => { 38 | setWindowWidth(window.innerWidth); 39 | }; 40 | setWindowWidth(window.innerWidth); 41 | window.addEventListener('resize', onWindowResize); 42 | 43 | return () => window.removeEventListener('resize', onWindowResize); 44 | 45 | // eslint-disable-next-line react-hooks/exhaustive-deps 46 | }, []); 47 | 48 | // initialize Editor 49 | React.useEffect(() => { 50 | const taskId = ''; 51 | setInitialed(false); 52 | open(projectId, taskId); 53 | // eslint-disable-next-line react-hooks/exhaustive-deps 54 | }, [projectId]); 55 | 56 | return ( 57 | 58 | 59 | {taskEditorViewMode === TaskEditorViewMode.base__normal && ( 60 | 65 | )} 66 | {taskEditorViewMode === TaskEditorViewMode.anno__multi_frame_view && ( 67 | 68 | )} 69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default ThreeAnnotationPage; 77 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/label-tool-bar.tsx: -------------------------------------------------------------------------------- 1 | import ToolBar from '@fl-three-editor/components/tool-bar'; 2 | import ToolBarButton, { 3 | ToolBarBoxButtonThemeProvider, 4 | } from '@fl-three-editor/components/tool-bar-button'; 5 | import React from 'react'; 6 | import { useTranslation } from 'react-i18next'; 7 | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; 8 | import Box from '@mui/material/Box'; 9 | import TaskStore from '@fl-three-editor/stores/task-store'; 10 | import DynamicFeedIcon from '@mui/icons-material/DynamicFeed'; 11 | 12 | type Props = { 13 | onEndLabelView: () => void; 14 | }; 15 | 16 | const LabelToolBar: React.FC = ({ onEndLabelView }) => { 17 | const [t] = useTranslation(); 18 | const { taskToolBar, updateTaskToolBar } = TaskStore.useContainer(); 19 | 20 | return ( 21 | 22 | 23 | } 26 | onClick={() => { 27 | onEndLabelView(); 28 | }} 29 | /> 30 | 31 | } 35 | onClick={() => 36 | updateTaskToolBar((pre) => ({ 37 | ...pre, 38 | interpolation: !pre.interpolation, 39 | })) 40 | } 41 | /> 42 | 43 | 44 | ); 45 | }; 46 | export default LabelToolBar; 47 | -------------------------------------------------------------------------------- /src/editor-module/views/pages/three-annotation/label-view-index.tsx: -------------------------------------------------------------------------------- 1 | import { useContextApp } from '@fl-three-editor/application/app-context'; 2 | import CameraCalibrationStore from '@fl-three-editor/stores/camera-calibration-store'; 3 | import TaskStore from '@fl-three-editor/stores/task-store'; 4 | import { PCDResult } from '@fl-three-editor/types/labos'; 5 | import { ThreePoints } from '@fl-three-editor/types/vo'; 6 | import PcdUtil from '@fl-three-editor/utils/pcd-util'; 7 | import { ViewUtils } from '@fl-three-editor/utils/view-util'; 8 | import FlLabelMainView from '@fl-three-editor/views/task-three/fl-label-main-view'; 9 | import { FlMainCameraControls } from '@fl-three-editor/views/task-three/fl-main-camera-controls'; 10 | import Grid from '@mui/material/Grid'; 11 | import Stack from '@mui/material/Stack'; 12 | import React from 'react'; 13 | import { Group, PerspectiveCamera, Vector3 } from 'three'; 14 | import { 15 | buildFlCubeObject3d, 16 | extractFlCubeObject3d, 17 | } from './../../task-three/fl-cube-model'; 18 | import LabelSidePanel from './label-side-panel'; 19 | import LabelToolBar from './label-tool-bar'; 20 | 21 | const LabelViewIndex: React.FC = () => { 22 | const { mode } = useContextApp(); 23 | const { calibrationCamera } = CameraCalibrationStore.useContainer(); 24 | 25 | const mainControlsRef = React.createRef(); 26 | const { 27 | loadStatus, 28 | taskRom, 29 | taskFrames, 30 | labelViewState, 31 | labelViewPageState, 32 | endLabelView, 33 | updateTaskAnnotations, 34 | } = TaskStore.useContainer(); 35 | 36 | const resolveCameraHelper = React.useCallback( 37 | (calibrationCamera?: PerspectiveCamera) => { 38 | if (!calibrationCamera) { 39 | return undefined; 40 | } 41 | const helperCamera = calibrationCamera.clone(); 42 | helperCamera.far = 30; 43 | return ; 44 | }, 45 | [] 46 | ); 47 | const [pcd, setPcd] = React.useState(); 48 | 49 | React.useEffect(() => { 50 | const targetFrameNo = labelViewPageState?.selectedFrame || ''; 51 | const targetFrame = taskFrames[targetFrameNo]; 52 | if ( 53 | targetFrame && 54 | targetFrame.status !== 'none' && 55 | targetFrame.pcdResource 56 | ) { 57 | const pcd = targetFrame.pcdResource; 58 | setPcd(pcd); 59 | } 60 | // eslint-disable-next-line react-hooks/exhaustive-deps 61 | }, [taskFrames, labelViewPageState]); 62 | 63 | const [currentPoints, setCurrentPoints] = React.useState<{ 64 | [frameNo: string]: ThreePoints; 65 | }>({}); 66 | 67 | const [framesObject, framesPoints] = React.useMemo(() => { 68 | const resultObj: { [frameNo: string]: Group } = {}; 69 | // copy for prevent leak object 70 | const resultPoints: { [frameNo: string]: ThreePoints } = {}; 71 | if (labelViewState && taskRom.status === 'loaded') { 72 | taskRom.frames.forEach((frameNo) => { 73 | const flCube = buildFlCubeObject3d(labelViewState.target, frameNo); 74 | if (flCube) { 75 | resultObj[frameNo] = flCube; 76 | resultPoints[frameNo] = labelViewState.target.points[ 77 | frameNo 78 | ].concat() as ThreePoints; 79 | } 80 | }); 81 | } 82 | return [resultObj, resultPoints]; 83 | // eslint-disable-next-line react-hooks/exhaustive-deps 84 | }, []); 85 | 86 | const target = React.useMemo(() => { 87 | if (labelViewPageState) { 88 | return framesObject[labelViewPageState.selectedFrame]; 89 | } 90 | return undefined; 91 | }, [framesObject, labelViewPageState]); 92 | 93 | React.useEffect(() => { 94 | setCurrentPoints(framesPoints); 95 | }, [framesPoints]); 96 | 97 | if (!labelViewState || !labelViewPageState) { 98 | return <>; 99 | } 100 | return ( 101 | <> 102 | 103 | 108 | { 110 | const points = Object.keys(framesObject).reduce<{ 111 | [frameNo: string]: ThreePoints; 112 | }>((r, key) => { 113 | r[key] = extractFlCubeObject3d(framesObject[key]); 114 | return r; 115 | }, {}); 116 | const newVo = { ...labelViewState.target, points }; 117 | updateTaskAnnotations({ 118 | type: 'updateTaskAnnotation', 119 | vo: newVo, 120 | }); 121 | endLabelView(); 122 | }} 123 | /> 124 | {loadStatus === 'loaded' && ( 125 | 131 | )} 132 | 133 | 134 | 135 | 140 | 141 | 142 | ); 143 | }; 144 | export default LabelViewIndex; 145 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-annotation-controls.tsx: -------------------------------------------------------------------------------- 1 | import { ReactThreeFiber, ThreeEvent, useThree } from '@react-three/fiber'; 2 | import * as React from 'react'; 3 | import { useMemo } from 'react'; 4 | import { 5 | Camera, 6 | EventDispatcher, 7 | Group, 8 | Object3D, 9 | OrthographicCamera, 10 | Scene, 11 | Vector3, 12 | } from 'three'; 13 | import { AnnotationClassVO, ThreePoints } from '../../types/vo'; 14 | import FLCube from './fl-cube'; 15 | 16 | type Prop = ReactThreeFiber.Overwrite< 17 | ReactThreeFiber.Object3DNode< 18 | FLAnnotationControlsImpl, 19 | typeof FLAnnotationControlsImpl 20 | >, 21 | { 22 | target?: ReactThreeFiber.Vector3; 23 | camera?: Camera; 24 | domElement?: HTMLElement; 25 | preObject?: AnnotationClassVO; 26 | onPutObject?: ( 27 | evt: ThreeEvent, 28 | preObject: AnnotationClassVO 29 | ) => void; 30 | } 31 | >; 32 | 33 | const FLAnnotationControls = React.forwardRef( 34 | function FLAnnotationControls( 35 | { camera, domElement, preObject, onPutObject = (f) => f, ...restProps }, 36 | ref 37 | ) { 38 | const invalidate = useThree(({ invalidate }) => invalidate); 39 | const defaultCamera = useThree(({ camera }) => camera); 40 | const gl = useThree(({ gl }) => gl); 41 | const scene = useThree(({ scene }) => scene); 42 | 43 | // const performance = useThree(({ performance }) => performance) 44 | const explCamera = camera || defaultCamera; 45 | const explDomElement = domElement || gl.domElement; 46 | 47 | const preCube = React.createRef(); 48 | const controls = React.useMemo( 49 | () => new FLAnnotationControlsImpl(explCamera, scene), 50 | [explCamera, scene] 51 | ); 52 | 53 | const points = useMemo(() => { 54 | if (preObject) { 55 | const { x, y, z } = preObject.defaultSize; 56 | return [0, 0, 0, 0, 0, 0, x, y, z]; 57 | } 58 | return undefined; 59 | }, [preObject]) as ThreePoints; 60 | 61 | React.useEffect(() => { 62 | controls.connect(explDomElement); 63 | return () => { 64 | controls.dispose(); 65 | }; 66 | // eslint-disable-next-line react-hooks/exhaustive-deps 67 | }, [controls, explDomElement]); 68 | 69 | React.useEffect(() => { 70 | if (preCube.current) { 71 | controls.attach(preCube.current); 72 | } else { 73 | controls.detach(); 74 | } 75 | return () => { 76 | controls.detach(); 77 | }; 78 | // eslint-disable-next-line react-hooks/exhaustive-deps 79 | }, [controls, preCube]); 80 | return ( 81 | <> 82 | 88 | {preObject && points && ( 89 | { 95 | onPutObject(e, preObject); 96 | }} 97 | /> 98 | )} 99 | 100 | ); 101 | } 102 | ); 103 | 104 | const _startEvent = { type: 'start' }; 105 | const _endEvent = { type: 'end' }; 106 | 107 | export class FLAnnotationControlsImpl extends EventDispatcher { 108 | private _domElementKeyEvents: HTMLElement | null = null; 109 | 110 | camera: Camera; 111 | scene: Scene; 112 | object?: Object3D; 113 | domElement?: HTMLElement; 114 | enabled: boolean; 115 | connect: (domElement: HTMLElement) => void; 116 | attach: (object: Object3D) => void; 117 | detach: () => void; 118 | dispose: () => void; 119 | 120 | constructor(camera: Camera, scene: Scene) { 121 | super(); 122 | this.camera = camera; 123 | this.scene = scene; 124 | this.enabled = true; 125 | 126 | const onPointerMove = (event: PointerEvent) => { 127 | if (this.enabled === false) return; 128 | 129 | // adjust preObject3D 130 | if (this.object) { 131 | const canvasElement = event.target as HTMLCanvasElement; 132 | const pos = new Vector3(0, 0, 0); 133 | const pMouse = new Vector3( 134 | (event.offsetX / canvasElement.width) * 2 - 1, 135 | -(event.offsetY / canvasElement.height) * 2 + 1, 136 | 1 137 | ); 138 | pMouse.unproject(camera); 139 | 140 | const cam = camera.position; 141 | if ( 142 | camera instanceof OrthographicCamera && 143 | camera.isOrthographicCamera 144 | ) { 145 | pos.x = pMouse.x; 146 | pos.y = pMouse.y; 147 | } else { 148 | const m = pMouse.z / (pMouse.z - cam.z); 149 | pos.x = pMouse.x + (cam.x - pMouse.x) * m; 150 | pos.y = pMouse.y + (cam.y - pMouse.y) * m; 151 | } 152 | this.object.position.copy(pos); 153 | } 154 | }; 155 | 156 | this.connect = (domElement: HTMLElement) => { 157 | if ((domElement as any) === document) { 158 | console.error( 159 | 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' 160 | ); 161 | } 162 | this.domElement = domElement; 163 | this.domElement.style.touchAction = 'none'; 164 | }; 165 | 166 | this.dispose = () => { 167 | this.detach(); 168 | }; 169 | 170 | this.attach = (object: Object3D) => { 171 | if (!this.domElement) return; 172 | this.object = object; 173 | 174 | this.domElement.addEventListener('pointermove', onPointerMove); 175 | }; 176 | 177 | this.detach = () => { 178 | this.object = undefined; 179 | if (this.domElement) { 180 | this.domElement.removeEventListener('pointermove', onPointerMove); 181 | } 182 | }; 183 | } 184 | } 185 | 186 | export default FLAnnotationControls; 187 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-const.ts: -------------------------------------------------------------------------------- 1 | export const MAIN_C_RESIZE = { debounce: 100 }; 2 | export const MAIN_FOOTER_BOX_H = 360; 3 | 4 | export const LABEL_C_RESIZE = { debounce: 200 }; 5 | export const LABEL_BOX_H = 240; 6 | 7 | export const ANNOTATION_OPACITY = 50; 8 | 9 | export const THREE_SX_PROPS = { 10 | canvasSx: { backgroundColor: 'black' }, 11 | }; 12 | 13 | export const THREE_STYLES = { 14 | baseBackgroundColor: '#1a1a1a', 15 | }; 16 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-cube-model.ts: -------------------------------------------------------------------------------- 1 | import { TaskAnnotationVO, ThreePoints } from '@fl-three-editor/types/vo'; 2 | import { 3 | Group, 4 | Vector3, 5 | Mesh, 6 | BoxGeometry, 7 | MeshBasicMaterial, 8 | Object3D, 9 | BufferGeometry, 10 | BufferAttribute, 11 | LineSegments, 12 | LineBasicMaterial, 13 | } from 'three'; 14 | 15 | export const buildFlCubeObject3d = ( 16 | taskAnnotationVo: TaskAnnotationVO, 17 | frameNo: string 18 | ): Group | undefined => { 19 | const points = taskAnnotationVo.points[frameNo]; 20 | if (!points) { 21 | return undefined; 22 | } 23 | const id = taskAnnotationVo.id; 24 | const [px, py, pz, ax, ay, az, sx, sy, sz] = points; 25 | const color = taskAnnotationVo.color; 26 | 27 | // dAssistance 28 | const [width, height, depth] = [0.5, 0.1, 0.1]; 29 | const dAssistancePosition = new Vector3((1 / 4) * 3, 0, 0); 30 | 31 | const material = new MeshBasicMaterial({ color }); 32 | 33 | const root = new Group(); 34 | root.name = id; 35 | root.userData = { type: 'cube' }; 36 | root.rotation.set(ax, ay, az); 37 | root.position.set(px, py, pz); 38 | 39 | const scaleNode = new Group(); 40 | scaleNode.scale.set(sx, sy, sz); 41 | root.add(scaleNode); 42 | 43 | const meshCubeDirection = new Mesh(); 44 | meshCubeDirection.position.copy(dAssistancePosition); 45 | meshCubeDirection.userData = { type: 'cube-direction' }; 46 | meshCubeDirection.geometry = new BoxGeometry(width, height, depth); 47 | meshCubeDirection.material = material; 48 | scaleNode.add(meshCubeDirection); 49 | 50 | const meshCubeBox = new Mesh(); 51 | meshCubeBox.userData = { type: 'cube-box' }; 52 | meshCubeBox.geometry = new BoxGeometry(1, 1, 1); 53 | meshCubeBox.material = new MeshBasicMaterial({ 54 | color, 55 | opacity: 0.5, 56 | transparent: true, 57 | }); 58 | scaleNode.add(meshCubeBox); 59 | 60 | const indices = new Uint16Array([ 61 | 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7, 62 | ]); 63 | const positions = new Float32Array(8 * 3); 64 | positions.set([ 65 | 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, -0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, 66 | -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 67 | ]); 68 | const geometry = new BufferGeometry(); 69 | geometry.setIndex(new BufferAttribute(indices, 1)); 70 | geometry.setAttribute('position', new BufferAttribute(positions, 3)); 71 | geometry.computeBoundingSphere(); 72 | 73 | const lineSegments = new LineSegments( 74 | geometry, 75 | new LineBasicMaterial({ color: color }) 76 | ); 77 | scaleNode.add(lineSegments); 78 | 79 | return root; 80 | }; 81 | 82 | export const extractFlCubeObject3d = ( 83 | flCube: Group | Object3D 84 | ): ThreePoints => { 85 | if (flCube.children[0] === undefined) { 86 | console.error(flCube); 87 | return [ 88 | flCube.position.x, 89 | flCube.position.y, 90 | flCube.position.z, 91 | flCube.rotation.x, 92 | flCube.rotation.y, 93 | flCube.rotation.z, 94 | 1, 95 | 1, 96 | 1, 97 | ]; 98 | } 99 | return [ 100 | flCube.position.x, 101 | flCube.position.y, 102 | flCube.position.z, 103 | flCube.rotation.x, 104 | flCube.rotation.y, 105 | flCube.rotation.z, 106 | flCube.children[0].scale.x, 107 | flCube.children[0].scale.y, 108 | flCube.children[0].scale.z, 109 | ]; 110 | }; 111 | 112 | export const updateFlCubeObject3d = ( 113 | flCube: Group | Object3D, 114 | points: ThreePoints 115 | ): void => { 116 | if (flCube.children[0] === undefined) { 117 | console.error(flCube); 118 | } 119 | const [px, py, pz, ax, ay, az, sx, sy, sz] = points; 120 | flCube.position.setX(px); 121 | flCube.position.setY(py); 122 | flCube.position.setZ(pz); 123 | flCube.rotation.set(ax, ay, az); 124 | flCube.children[0].scale.setX(sx); 125 | flCube.children[0].scale.setY(sy); 126 | flCube.children[0].scale.setZ(sz); 127 | }; 128 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-cubes.tsx: -------------------------------------------------------------------------------- 1 | import { ThreeEvent } from '@react-three/fiber'; 2 | import React from 'react'; 3 | import { Object3D } from 'three'; 4 | import { TaskAnnotationVO } from '../../types/vo'; 5 | import FLCube from './fl-cube'; 6 | 7 | type Props = { 8 | frameNo: string; 9 | annotations: TaskAnnotationVO[]; 10 | selectedTaskAnnotations?: TaskAnnotationVO[]; 11 | selectable?: boolean; 12 | showLabel?: boolean; 13 | hoveredLabelAnnotationId?: string; 14 | annotationOpacity?: number; 15 | onClick?: (event: ThreeEvent) => void; 16 | onLabelMouseOver?: (hoveredId: string) => void; 17 | }; 18 | 19 | const FLCubes = React.forwardRef( 20 | ( 21 | { 22 | frameNo, 23 | annotations, 24 | selectedTaskAnnotations, 25 | selectable = false, 26 | showLabel = false, 27 | hoveredLabelAnnotationId, 28 | annotationOpacity, 29 | onClick = (f) => f, 30 | onLabelMouseOver = (f) => f, 31 | }, 32 | ref 33 | ) => { 34 | const selectedTaskAnnotationSet = React.useMemo( 35 | () => new Set(selectedTaskAnnotations?.map((a) => a.id)), 36 | [selectedTaskAnnotations] 37 | ); 38 | const getLabelTitle = ( 39 | taskAnnotation: TaskAnnotationVO, 40 | hoveredLabelAnnotationId?: string 41 | ) => { 42 | const isHovered = hoveredLabelAnnotationId === taskAnnotation.id; 43 | const attributeValue = ''; 44 | const labelTitleSep = isHovered ? '\n' : ': '; 45 | return attributeValue 46 | ? taskAnnotation.title + labelTitleSep + attributeValue 47 | : taskAnnotation.title; 48 | }; 49 | return ( 50 | 51 | {annotations 52 | .map((a) => { 53 | const points = a.points[frameNo as any]; 54 | if (points) { 55 | return ( 56 | 69 | ); 70 | } 71 | return undefined; 72 | }) 73 | .filter((r) => !!r)} 74 | 75 | ); 76 | } 77 | ); 78 | 79 | export default FLCubes; 80 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-label-main-view.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import { Canvas, useThree } from '@react-three/fiber'; 3 | import React from 'react'; 4 | import { Euler, Group, Vector3 } from 'three'; 5 | import { PCDResult } from '../../types/labos'; 6 | import { LABEL_C_RESIZE, THREE_STYLES, THREE_SX_PROPS } from './fl-const'; 7 | import { extractFlCubeObject3d } from './fl-cube-model'; 8 | import { FlMainCameraControls } from './fl-main-camera-controls'; 9 | import FLMainControls from './fl-main-controls'; 10 | import { FlPcdPoints } from './fl-pcd-points'; 11 | 12 | type ResetEffectProps = { 13 | pcd?: PCDResult; 14 | target?: Group; 15 | baseSize?: number; 16 | mainControlsRef?: React.RefObject; 17 | }; 18 | 19 | const ResetEffect: React.FC = ({ 20 | pcd, 21 | target, 22 | baseSize, 23 | mainControlsRef, 24 | }) => { 25 | const gl = useThree((state) => state.gl); 26 | const scene = useThree((state) => state.scene); 27 | React.useEffect(() => { 28 | scene.clear(); 29 | if (target) { 30 | scene.add(target); 31 | } 32 | if (pcd) { 33 | const points = FlPcdPoints.buildPointsMesh(pcd, baseSize); 34 | scene.add(points); 35 | } 36 | gl.clear(); 37 | gl.resetState(); 38 | if (mainControlsRef?.current && target) { 39 | const [px, py, pz, ax, ay, az, sx, sy, sz] = 40 | extractFlCubeObject3d(target); 41 | const offset = Math.max(sx, sy, sz, 20); 42 | const positionX = px - offset; 43 | const positionY = py; 44 | const positionZ = pz + offset; 45 | mainControlsRef.current.point( 46 | new Vector3(px, py, pz), 47 | new Vector3(positionX, positionY, positionZ) 48 | ); 49 | } 50 | // eslint-disable-next-line react-hooks/exhaustive-deps 51 | }, [target, gl, scene]); 52 | return <>; 53 | }; 54 | 55 | type Props = { 56 | position0?: Vector3; 57 | target?: Group; 58 | pcd?: PCDResult; 59 | cameraHelper?: JSX.Element; 60 | mainControlsRef?: React.RefObject; 61 | }; 62 | 63 | const FlLabelMainView: React.FC = ({ 64 | position0, 65 | target, 66 | pcd, 67 | cameraHelper, 68 | mainControlsRef, 69 | }) => { 70 | const rootRef = React.createRef(); 71 | const orthographic = false; 72 | 73 | React.useEffect(() => { 74 | if (rootRef.current) { 75 | const root = rootRef.current; 76 | const handleResize = () => { 77 | const canvas = root.getElementsByTagName('canvas') as any; 78 | for (const c of canvas) { 79 | c.width = 0; 80 | c.height = 0; 81 | c.style.width = ''; 82 | c.style.height = ''; 83 | } 84 | }; 85 | window.addEventListener('resize', handleResize); 86 | return () => window.removeEventListener('resize', handleResize); 87 | } 88 | }, [rootRef]); 89 | 90 | return ( 91 | 99 | 109 | 114 | 120 | {cameraHelper} 121 | 122 | 123 | ); 124 | }; 125 | export default FlLabelMainView; 126 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-label-secondary-view.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import Grid from '@mui/material/Grid'; 3 | import Stack from '@mui/material/Stack'; 4 | import Typography from '@mui/material/Typography'; 5 | import { Canvas } from '@react-three/fiber'; 6 | import React from 'react'; 7 | import { Euler, Event, Object3D } from 'three'; 8 | import { 9 | LABEL_BOX_H, 10 | MAIN_C_RESIZE, 11 | ANNOTATION_OPACITY, 12 | THREE_SX_PROPS, 13 | } from './fl-const'; 14 | import FLObjectControls from './fl-object-controls'; 15 | 16 | type Props = { 17 | frameNo: string; 18 | target: Object3D; 19 | onClickCapture: (event: Event, frameNo: string) => void; 20 | selected: boolean; 21 | bgSub?: JSX.Element; 22 | onObjectChange: (event: Event, frameNo: string) => void; 23 | }; 24 | 25 | const FlLabelSecondaryView: React.FC = ({ 26 | frameNo, 27 | target, 28 | onClickCapture, 29 | selected, 30 | bgSub, 31 | onObjectChange, 32 | }) => { 33 | const [near, far] = React.useMemo(() => [0.03, 20], []); 34 | const handleObjectChange = React.useCallback( 35 | (event) => onObjectChange(event, frameNo), 36 | [frameNo, onObjectChange] 37 | ); 38 | return ( 39 | { 42 | onClickCapture(event, frameNo); 43 | }} 44 | sx={{ 45 | backgroundColor: selected ? '#595959' : undefined, 46 | ':hover': { 47 | backgroundColor: '#757575', 48 | }, 49 | }}> 50 | {frameNo} 51 | 52 | 53 | 54 | 63 | {bgSub} 64 | 70 | 71 | 72 | 73 | 74 | 75 | 84 | {bgSub} 85 | 91 | 92 | 93 | 94 | 95 | 96 | 105 | {bgSub} 106 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | }; 119 | export default FlLabelSecondaryView; 120 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-main-controls.tsx: -------------------------------------------------------------------------------- 1 | import { ThreeEvent, useFrame, useThree } from '@react-three/fiber'; 2 | import React, { createRef, FC, useEffect } from 'react'; 3 | import { Vector3 } from 'three'; 4 | import { AnnotationClassVO } from '../../types/vo'; 5 | import FLAnnotationControls, { 6 | FLAnnotationControlsImpl, 7 | } from './fl-annotation-controls'; 8 | import { FlMainCameraControls } from './fl-main-camera-controls'; 9 | 10 | type Props = { 11 | orthographic: boolean; 12 | position0?: Vector3; 13 | preObject?: AnnotationClassVO; 14 | mainControlsRef?: React.RefObject; 15 | onPutObject?: ( 16 | evt: ThreeEvent, 17 | preObject: AnnotationClassVO 18 | ) => void; 19 | }; 20 | 21 | const FLMainControls: FC = ({ 22 | orthographic, 23 | position0, 24 | preObject, 25 | mainControlsRef, 26 | onPutObject = (f) => f, 27 | }) => { 28 | const gl = useThree(({ gl }) => gl); 29 | const camera = useThree(({ camera }) => camera); 30 | const mainControls = React.useMemo( 31 | () => new FlMainCameraControls(camera), 32 | [camera] 33 | ); 34 | 35 | const annotation = createRef(); 36 | 37 | // TODO adjust position only first time 38 | useFrame(() => { 39 | if (mainControls.enabled) mainControls.update(); 40 | }); 41 | 42 | useEffect(() => { 43 | const putMode = !!preObject; 44 | 45 | if (annotation.current) { 46 | annotation.current.enabled = putMode; 47 | } 48 | }, [annotation]); 49 | 50 | useEffect(() => { 51 | mainControls.connect(gl.domElement); 52 | if (position0 && mainControls.enabled) { 53 | mainControls.object.position.copy( 54 | position0.clone().setZ(position0.z + 50) 55 | ); 56 | mainControls.saveState(); 57 | } 58 | }, [mainControls]); 59 | return ( 60 | <> 61 | 67 | 77 | 78 | ); 79 | }; 80 | 81 | export default FLMainControls; 82 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-object-controls.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, useThree } from '@react-three/fiber'; 2 | import React, { FC, useCallback, useMemo } from 'react'; 3 | import { Event, Object3D, OrthographicCamera } from 'three'; 4 | import { 5 | ControlType, 6 | FlObjectCameraUtil, 7 | } from '../../utils/fl-object-camera-util'; 8 | import { FLObjectCameraControls } from './fl-object-camera-controls'; 9 | import { FLTransformControls } from './fl-transform-controls'; 10 | 11 | const [zoom, distance] = [5, 5]; 12 | 13 | const FLObjectControls: FC<{ 14 | control: ControlType; 15 | annotationOpacity: number; 16 | target?: Object3D; 17 | onObjectChange?: (event: Event) => void; 18 | }> = ({ control, annotationOpacity, target, onObjectChange = (f) => f }) => { 19 | const invalidate = useThree(({ invalidate }) => invalidate); 20 | const gl = useThree(({ gl }) => gl); 21 | const camera = useThree(({ camera }) => camera); 22 | 23 | const initCamera = useCallback( 24 | (orbit: FLObjectCameraControls) => { 25 | const camera = orbit.object as OrthographicCamera; 26 | camera.zoom = zoom; 27 | 28 | orbit.target.set(0, 0, 0); 29 | 30 | switch (control) { 31 | case 'top': 32 | camera.up.set(0, 0, -1); 33 | break; 34 | case 'side': 35 | camera.up.set(0, -1, 0); 36 | break; 37 | case 'front': 38 | camera.up.set(-1, 0, 0); 39 | break; 40 | } 41 | FlObjectCameraUtil.reset(control, camera); 42 | camera.lookAt(orbit.target); 43 | camera.updateProjectionMatrix(); 44 | orbit.update(); 45 | orbit.saveState(); 46 | }, 47 | [control] 48 | ); 49 | 50 | const [orbitControls, transformControls] = useMemo(() => { 51 | const orbit = new FLObjectCameraControls(camera, control); 52 | orbit.minZoom = zoom; 53 | orbit.maxZoom = 200; 54 | initCamera(orbit); 55 | 56 | const transform = new FLTransformControls( 57 | camera, 58 | gl.domElement, 59 | control, 60 | orbit 61 | ); 62 | return [orbit, transform]; 63 | }, [control]); 64 | 65 | React.useEffect(() => { 66 | const callback = (e: THREE.Event) => { 67 | invalidate(); 68 | }; 69 | orbitControls.connect(gl.domElement); 70 | orbitControls.addEventListener('change', callback); 71 | orbitControls.listenToKeyEvents(document.body); 72 | return () => { 73 | orbitControls.dispose(); 74 | }; 75 | // eslint-disable-next-line react-hooks/exhaustive-deps 76 | }, [orbitControls, invalidate]); 77 | 78 | React.useEffect(() => { 79 | if (target) { 80 | transformControls.attach(target); 81 | } else { 82 | transformControls.detach(); 83 | initCamera(orbitControls); 84 | } 85 | // eslint-disable-next-line react-hooks/exhaustive-deps 86 | }, [transformControls, target]); 87 | 88 | React.useEffect(() => { 89 | const old = transformControls; 90 | old.addEventListener('objectChange', onObjectChange); 91 | return () => { 92 | old.removeEventListener('objectChange', onObjectChange); 93 | }; 94 | }, [transformControls, onObjectChange]); 95 | 96 | React.useEffect(() => { 97 | transformControls.setAnnotationOpacity(annotationOpacity); 98 | }, [annotationOpacity, transformControls]); 99 | 100 | useFrame(() => { 101 | if (!transformControls.isDragging() && orbitControls.enabled) 102 | orbitControls.update(); 103 | }); 104 | 105 | return ( 106 | <> 107 | 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default FLObjectControls; 114 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-pcd-points.ts: -------------------------------------------------------------------------------- 1 | import { PCDResult } from '@fl-three-editor/types/labos'; 2 | import ColorUtil from '@fl-three-editor/utils/color-util'; 3 | import { 4 | BufferGeometry, 5 | Float32BufferAttribute, 6 | Points, 7 | PointsMaterial, 8 | } from 'three'; 9 | 10 | const _buildGeometry = (pcd: PCDResult): BufferGeometry => { 11 | const geometry = new BufferGeometry(); 12 | if (pcd.position.length > 0) 13 | geometry.setAttribute( 14 | 'position', 15 | new Float32BufferAttribute(pcd.position, 3) 16 | ); 17 | if (pcd.normal.length > 0) 18 | geometry.setAttribute('normal', new Float32BufferAttribute(pcd.normal, 3)); 19 | if (pcd.color.length > 0) { 20 | geometry.setAttribute('color', new Float32BufferAttribute(pcd.color, 3)); 21 | } else { 22 | geometry.setAttribute( 23 | 'color', 24 | new Float32BufferAttribute(ColorUtil.normalizeColors(pcd.position), 3) 25 | ); 26 | } 27 | geometry.computeBoundingSphere(); 28 | return geometry; 29 | }; 30 | 31 | const _buildMaterial = (baseSize = 0.005): PointsMaterial => { 32 | const material = new PointsMaterial({ size: baseSize * 8 }); 33 | material.vertexColors = true; 34 | return material; 35 | }; 36 | 37 | export const FlPcdPoints = { 38 | buildGeometry: _buildGeometry, 39 | buildMaterial: _buildMaterial, 40 | buildPointsMesh: (pcd: PCDResult, baseSize?: number): Points => { 41 | const geometry = _buildGeometry(pcd); 42 | const material = _buildMaterial(baseSize); 43 | return new Points(geometry, material); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/editor-module/views/task-three/fl-pcd.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { PCDResult } from '../../types/labos'; 3 | import { FlPcdPoints } from './fl-pcd-points'; 4 | 5 | type Props = { 6 | pcd: PCDResult; 7 | baseSize?: number; 8 | }; 9 | 10 | const FLPcd: FC = ({ pcd, baseSize = 0.005 }) => { 11 | const geometry = FlPcdPoints.buildGeometry(pcd); 12 | // build material 13 | const material = FlPcdPoints.buildMaterial(baseSize); 14 | return ; 15 | }; 16 | export default FLPcd; 17 | -------------------------------------------------------------------------------- /src/electron-app/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | workspace: Workspace; 4 | appApi: AppApi; 5 | } 6 | } 7 | 8 | export interface Workspace { 9 | openFolderDialog: () => Promise; 10 | save: (param: WKSaveParam) => Promise; 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | load: (param: WKLoadParam) => Promise>; 13 | exist: (param: WKLoadParam) => Promise>; 14 | checkWorkspace: (param: WKCheckParam) => Promise; 15 | export: (param: WKExportPram) => Promise; 16 | } 17 | 18 | export type WKJsonSaveCommand = { 19 | method: 'json'; 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | resource: any; 22 | }; 23 | 24 | export type WKCopyCommand = { 25 | method: 'copy'; 26 | fromPath: string; 27 | extension: string; 28 | }; 29 | 30 | export type WKSaveCommand = WKJsonSaveCommand | WKCopyCommand; 31 | 32 | export type WKSkeleton = { 33 | meta?: { 34 | project?: J; 35 | annotation_classes?: J; 36 | }; 37 | target?: any; 38 | // [frameNo: string]: { [topicId: string]: R | J; } | J 39 | // target_info?: J; 40 | // calibration?: { [topicId: string]: J; } | 'folder' 41 | output?: { 42 | annotation_data: J; 43 | }; 44 | }; 45 | 46 | export type WKCheckResult = 47 | | { 48 | code: 'valid_new_wk'; 49 | valid: true; 50 | } 51 | | { 52 | code: 'invalid_folder_not_empty'; 53 | valid: false; 54 | }; 55 | 56 | export type WKCheckParam = { 57 | wkDir: string; 58 | }; 59 | 60 | export type WKLoadParam = { 61 | wkDir: string; 62 | query: WKSkeleton; 63 | }; 64 | 65 | export type WKSaveParam = { 66 | wkDir: string; 67 | query: WKSkeleton; 68 | }; 69 | export type WKExportPram = { 70 | fileName: string; 71 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 72 | dataJson: any; 73 | }; 74 | 75 | export type WKExportResult = 76 | | { 77 | status: true; 78 | path: string; 79 | } 80 | | { 81 | status: false; 82 | message: message; 83 | }; 84 | 85 | export interface AppApi { 86 | close: () => Promise; 87 | restore: () => Promise; 88 | maximize: () => Promise; 89 | minimize: () => Promise; 90 | 91 | resized: (listener: () => Promise) => Electron.IpcRenderer; 92 | removeResized: () => Electron.IpcRenderer; 93 | 94 | maximized: (listener: () => Promise) => Electron.IpcRenderer; 95 | removeMaximized: () => Electron.IpcRenderer; 96 | 97 | unMaximized: (listener: () => Promise) => Electron.IpcRenderer; 98 | removeUnMaximized: () => Electron.IpcRenderer; 99 | 100 | getFocus: (listener: () => Promise) => Electron.IpcRenderer; 101 | removeGetFocus: () => Electron.IpcRenderer; 102 | 103 | getBlur: (listener: () => Promise) => Electron.IpcRenderer; 104 | removeGetBlur: () => Electron.IpcRenderer; 105 | } 106 | -------------------------------------------------------------------------------- /src/electron-app/@types/import-png.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | -------------------------------------------------------------------------------- /src/electron-app/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, dialog, ipcMain, session } from 'electron'; 2 | import { searchDevtools } from 'electron-search-devtools'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { WKExportPram } from './@types/global'; 6 | import { WorkSpaceDriver } from './node/workspace'; 7 | 8 | const isDev = process.env.NODE_ENV === 'development'; 9 | 10 | /// #if DEBUG 11 | const execPath = 12 | process.platform === 'win32' 13 | ? '../node_modules/electron/dist/electron.exe' 14 | : '../node_modules/.bin/electron'; 15 | 16 | if (isDev) { 17 | // eslint-disable-next-line @typescript-eslint/no-var-requires 18 | require('electron-reload')(__dirname, { 19 | electron: path.resolve(__dirname, execPath), 20 | forceHardReset: true, 21 | hardResetMethod: 'exit', 22 | }); 23 | } 24 | /// #endif 25 | 26 | const createWindow = () => { 27 | const mainWindow = new BrowserWindow({ 28 | width: 1920, 29 | height: 1080, 30 | title: 'FastLabel 3D Annotation', 31 | frame: false, 32 | webPreferences: { 33 | preload: path.join(__dirname, 'preload.js'), 34 | nodeIntegration: true, 35 | // contextIsolation: false, 36 | enableWebSQL: false, 37 | nativeWindowOpen: true, 38 | }, 39 | }); 40 | 41 | mainWindow.setMenuBarVisibility(false); 42 | 43 | ipcMain.handle('workspace/openFolderDialog', async () => { 44 | const dirPath = await dialog 45 | .showOpenDialog(mainWindow, { 46 | properties: ['openDirectory', 'createDirectory'], 47 | }) 48 | .then((result) => { 49 | if (result.canceled) return; 50 | return result.filePaths[0]; 51 | }) 52 | .catch((err) => console.log(err)); 53 | return dirPath; 54 | }); 55 | 56 | ipcMain.handle('workspace/save', async (event, param) => { 57 | const result = await WorkSpaceDriver.saveQuery(param) 58 | .then() 59 | .catch((err) => console.log(err)); 60 | return result; 61 | }); 62 | 63 | ipcMain.handle('workspace/load', async (event, param) => { 64 | const result = await WorkSpaceDriver.loadQuery(param) 65 | .then((r) => r) 66 | .catch((error) => console.log(error)); 67 | return result; 68 | }); 69 | 70 | ipcMain.handle('workspace/exist', async (event, param) => { 71 | const result = await WorkSpaceDriver.exist(param) 72 | .then((r) => r) 73 | .catch((error) => console.log(error)); 74 | return result; 75 | }); 76 | 77 | ipcMain.handle('workspace/checkWorkspace', async (event, param) => { 78 | const result = await WorkSpaceDriver.checkWorkspace(param) 79 | .then((r) => r) 80 | .catch((error) => console.log(error)); 81 | return result; 82 | }); 83 | 84 | ipcMain.handle('workspace/export', async (event, param: WKExportPram) => { 85 | const path = dialog.showSaveDialogSync(mainWindow, { 86 | buttonLabel: '保存', 87 | defaultPath: param.fileName, 88 | filters: [{ name: '*', extensions: ['json'] }], 89 | properties: ['createDirectory'], 90 | }); 91 | // キャンセルで閉じた場合 92 | if (path === undefined) { 93 | return { status: undefined }; 94 | } 95 | 96 | // ファイルの内容を返却 97 | try { 98 | fs.writeFileSync(path, JSON.stringify(param.dataJson, null, 2)); 99 | return { status: true, path: path }; 100 | } catch (error: any) { 101 | return { status: false, message: error.message }; 102 | } 103 | }); 104 | 105 | ipcMain.handle('minimize', () => mainWindow.minimize()); 106 | ipcMain.handle('maximize', () => mainWindow.maximize()); 107 | ipcMain.handle('restore', () => mainWindow.unmaximize()); 108 | ipcMain.handle('close', () => mainWindow.close()); 109 | 110 | mainWindow.on('maximize', () => mainWindow.webContents.send('maximized')); 111 | mainWindow.on('unmaximize', () => mainWindow.webContents.send('unMaximized')); 112 | mainWindow.on('resized', () => { 113 | if (mainWindow.isMaximized()) return; 114 | mainWindow.webContents.send('resized'); 115 | }); 116 | mainWindow.on('focus', () => mainWindow.webContents.send('get-focus')); 117 | mainWindow.on('blur', () => mainWindow.webContents.send('get-blur')); 118 | 119 | if (isDev) mainWindow.webContents.openDevTools({ mode: 'detach' }); 120 | 121 | // bootstrap remote 122 | 123 | mainWindow.loadFile('dist/index.html'); 124 | mainWindow.once('ready-to-show', () => mainWindow.show()); 125 | }; 126 | 127 | app.disableHardwareAcceleration(); 128 | 129 | app.whenReady().then(async () => { 130 | if (isDev) { 131 | const devtools = await searchDevtools('REACT'); 132 | if (devtools) { 133 | await session.defaultSession.loadExtension(devtools, { 134 | allowFileAccess: true, 135 | }); 136 | } 137 | } 138 | createWindow(); 139 | }); 140 | 141 | app.once('window-all-closed', () => app.quit()); 142 | -------------------------------------------------------------------------------- /src/electron-app/node/file-driver.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import YAML from 'yaml'; 4 | 5 | const resolveParent = ( 6 | pathStr: string, 7 | fileFunc: () => Promise 8 | ): Promise => { 9 | const dirPath = path.dirname(pathStr); 10 | return new Promise((resolver, reject) => { 11 | try { 12 | fs.stat(dirPath, (err, stats) => { 13 | if (err) { 14 | FileDriver.makeDir(dirPath).then(() => { 15 | fileFunc().then(() => resolver(pathStr)); 16 | }); 17 | } else { 18 | fileFunc().then(() => resolver(pathStr)); 19 | } 20 | }); 21 | } catch (error) { 22 | reject(error); 23 | } 24 | }); 25 | }; 26 | 27 | export const FileDriver = { 28 | makeDir: (pathStr: string): Promise => { 29 | return new Promise((resolver, reject) => { 30 | try { 31 | fs.mkdir(pathStr, { recursive: true }, () => resolver(pathStr)); 32 | } catch (error) { 33 | reject(error); 34 | } 35 | }); 36 | }, 37 | remove: (pathStr: string): Promise => { 38 | return new Promise((resolver, reject) => { 39 | try { 40 | fs.rm(pathStr, () => resolver(pathStr)); 41 | } catch (error) { 42 | reject(error); 43 | } 44 | }); 45 | }, 46 | removeDir: (pathStr: string): Promise => { 47 | return new Promise((resolver, reject) => { 48 | try { 49 | fs.rmdir(pathStr, { recursive: true }, () => resolver(pathStr)); 50 | } catch (error) { 51 | reject(error); 52 | } 53 | }); 54 | }, 55 | saveFile: (pathStr: string, file: File): Promise => { 56 | return resolveParent( 57 | pathStr, 58 | () => 59 | new Promise((resolver, reject) => { 60 | file.arrayBuffer().then((b) => { 61 | fs.writeFile(pathStr, Buffer.from(b), () => resolver()); 62 | }); 63 | }) 64 | ); 65 | }, 66 | copyFile: (pathStr: string, srcPath: string): Promise => { 67 | console.debug('copyFile :' + pathStr); 68 | return resolveParent( 69 | pathStr, 70 | () => 71 | new Promise((resolver, reject) => { 72 | fs.copyFile(srcPath, pathStr, () => resolver()); 73 | }) 74 | ); 75 | }, 76 | saveJson: (pathStr: string, data: T): Promise => { 77 | console.debug('saveJson :' + pathStr); 78 | return resolveParent( 79 | pathStr, 80 | () => 81 | new Promise((resolver, reject) => { 82 | fs.writeFile(pathStr, JSON.stringify(data), 'utf8', () => resolver()); 83 | }) 84 | ); 85 | }, 86 | exist: (pathStr: string): Promise => { 87 | return new Promise((resolver, reject) => { 88 | fs.readFile(pathStr, (err, data) => { 89 | if (err) reject(err); 90 | resolver(!!data); 91 | }); 92 | }); 93 | }, 94 | emptyDir: (pathStr: string): Promise => { 95 | return new Promise((resolver, reject) => { 96 | fs.readdir(pathStr, (err, files) => { 97 | if (err) reject(err); 98 | resolver(files.length === 0); 99 | }); 100 | }); 101 | }, 102 | load: (pathStr: string): Promise => { 103 | console.debug('load :' + pathStr); 104 | return new Promise((resolver, reject) => { 105 | fs.readFile(pathStr, (err, data) => { 106 | if (err) reject(err); 107 | const ab = new ArrayBuffer(data.length); 108 | const view = new Uint8Array(ab); 109 | for (let i = 0; i < data.length; ++i) { 110 | view[i] = data[i]; 111 | } 112 | resolver(ab); 113 | }); 114 | }); 115 | }, 116 | loadText: (pathStr: string): Promise => { 117 | return new Promise((resolver, reject) => { 118 | fs.readFile(pathStr, 'utf8', (err, data) => { 119 | if (err) reject(err); 120 | resolver(data); 121 | }); 122 | }); 123 | }, 124 | loadJson: (pathStr: string): Promise => { 125 | console.debug('loadJson :' + pathStr); 126 | return new Promise((resolver, reject) => { 127 | fs.readFile(pathStr, 'utf8', (err, data) => { 128 | if (err) reject(err); 129 | if (data) { 130 | resolver(JSON.parse(data)); 131 | } else { 132 | resolver({} as any); 133 | } 134 | }); 135 | }); 136 | }, 137 | loadYaml: (pathStr: string): Promise => { 138 | console.debug('loadYaml :' + pathStr); 139 | return new Promise((resolver, reject) => { 140 | fs.readFile(pathStr, 'utf8', (err, data) => { 141 | if (err) reject(err); 142 | resolver(YAML.parse(data)); 143 | }); 144 | }); 145 | }, 146 | }; 147 | -------------------------------------------------------------------------------- /src/electron-app/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron'; 2 | import { 3 | WKCheckParam, 4 | WKExportPram, 5 | WKLoadParam, 6 | WKSaveParam, 7 | } from './@types/global'; 8 | 9 | contextBridge.exposeInMainWorld('workspace', { 10 | openFolderDialog: async (): Promise => 11 | await ipcRenderer.invoke('workspace/openFolderDialog'), 12 | save: async (param: WKSaveParam) => 13 | ipcRenderer.invoke('workspace/save', param), 14 | load: async (param: WKLoadParam) => 15 | ipcRenderer.invoke('workspace/load', param), 16 | exist: async (param: WKLoadParam) => 17 | ipcRenderer.invoke('workspace/exist', param), 18 | checkWorkspace: async (param: WKCheckParam) => 19 | ipcRenderer.invoke('workspace/checkWorkspace', param), 20 | export: async (param: WKExportPram) => 21 | await ipcRenderer.invoke('workspace/export', param), 22 | }); 23 | 24 | contextBridge.exposeInMainWorld('appApi', { 25 | close: async () => ipcRenderer.invoke('close'), 26 | minimize: async () => ipcRenderer.invoke('minimize'), 27 | maximize: async () => ipcRenderer.invoke('maximize'), 28 | restore: async () => ipcRenderer.invoke('restore'), 29 | 30 | resized: (listener: () => Promise) => 31 | ipcRenderer.on('resized', listener), 32 | removeResized: () => ipcRenderer.removeAllListeners('resized'), 33 | 34 | maximized: (listener: () => Promise) => 35 | ipcRenderer.on('maximized', listener), 36 | removeMaximized: () => ipcRenderer.removeAllListeners('maximized'), 37 | 38 | unMaximized: (listener: () => Promise) => 39 | ipcRenderer.on('unMaximized', listener), 40 | removeUnMaximized: () => ipcRenderer.removeAllListeners('unMaximized'), 41 | 42 | getFocus: (listener: () => Promise) => 43 | ipcRenderer.on('get-focus', listener), 44 | removeGetFocus: () => ipcRenderer.removeAllListeners('get-focus'), 45 | 46 | getBlur: (listener: () => Promise) => 47 | ipcRenderer.on('get-blur', listener), 48 | removeGetBlur: () => ipcRenderer.removeAllListeners('get-blur'), 49 | }); 50 | -------------------------------------------------------------------------------- /src/electron-app/web/App.scss: -------------------------------------------------------------------------------- 1 | $titlebar-height: 36px; 2 | 3 | @media (prefers-color-scheme: dark) { 4 | .container { 5 | background: #1e1e1e; 6 | color: #ffffff; 7 | } 8 | } 9 | 10 | @media (prefers-color-scheme: light) { 11 | .container { 12 | background: #ffffff; 13 | color: #1e1e1e; 14 | } 15 | } 16 | 17 | html, 18 | body, 19 | #root { 20 | height: 100%; 21 | overflow: hidden; 22 | } 23 | 24 | .container { 25 | height: 100%; 26 | 27 | .content { 28 | height: calc(100% - #{$titlebar-height}); 29 | overflow: hidden; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | 35 | .content-darwin { 36 | height: 100%; 37 | overflow: hidden; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | -webkit-app-region: drag; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/electron-app/web/App.tsx: -------------------------------------------------------------------------------- 1 | import { AppContext } from '@fl-three-editor/application/app-context'; 2 | import { ProjectRepositoryContext } from '@fl-three-editor/repositories/project-repository'; 3 | import AnnotationClassStore from '@fl-three-editor/stores/annotation-class-store'; 4 | import CameraCalibrationStore from '@fl-three-editor/stores/camera-calibration-store'; 5 | import TaskStore from '@fl-three-editor/stores/task-store'; 6 | import ThreeAnnotationPage from '@fl-three-editor/views/pages/three-annotation/index'; 7 | import { SnackbarProvider } from 'notistack'; 8 | import React from 'react'; 9 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 10 | import 'typeface-roboto/index.css'; 11 | import './App.scss'; 12 | import WorkspaceContext from './context/workspace'; 13 | import { useProjectFsRepository } from './repositories/project-fs-repository'; 14 | import TitleBar from './title-bar'; 15 | import StartPage from './views/pages/start/index'; 16 | import WorkspacePage from './views/pages/workspace/index'; 17 | 18 | export const App = (): JSX.Element => { 19 | const workspace = WorkspaceContext.useContainer(); 20 | const projectFsRepository = useProjectFsRepository(workspace); 21 | return ( 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/electron-app/web/asset/favicon-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/electron-app/web/asset/favicon-200.png -------------------------------------------------------------------------------- /src/electron-app/web/context/workspace.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { createContainer } from 'unstated-next'; 3 | 4 | const useWorkspace = () => { 5 | const [workspaceFolder, setWorkspaceFolder] = useState(''); 6 | const [forceUpdate, setForceUpdate] = useState(false); 7 | 8 | const folderName = useMemo(() => { 9 | if (workspaceFolder) { 10 | let paths = workspaceFolder.split('\\'); 11 | if (paths.length > 1) { 12 | paths = workspaceFolder.split('/'); 13 | } 14 | return paths[paths.length - 1]; 15 | } 16 | return ''; 17 | }, [workspaceFolder]); 18 | 19 | return { 20 | workspaceFolder, 21 | setWorkspaceFolder, 22 | forceUpdate, 23 | setForceUpdate, 24 | folderName, 25 | }; 26 | }; 27 | 28 | export default createContainer(useWorkspace); 29 | -------------------------------------------------------------------------------- /src/electron-app/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FastLabel 3D Annotation 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/electron-app/web/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow: hidden; 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/electron-app/web/index.tsx: -------------------------------------------------------------------------------- 1 | import muiTheme from '@fl-three-editor/config/mui-theme'; 2 | import editorEnJson from '@fl-three-editor/locales/en.json'; 3 | import editorJaJson from '@fl-three-editor/locales/ja.json'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import { 6 | ThemeProvider, 7 | Theme, 8 | StyledEngineProvider, 9 | } from '@mui/material/styles'; 10 | import i18n from 'i18next'; 11 | import LanguageDetector from 'i18next-browser-languagedetector'; 12 | import React, { Suspense } from 'react'; 13 | import ReactDOM from 'react-dom'; 14 | import { initReactI18next } from 'react-i18next'; 15 | import { App } from './App'; 16 | import WorkspaceContext from './context/workspace'; 17 | import './index.scss'; 18 | import enJson from './locales/en.json'; 19 | import jaJson from './locales/ja.json'; 20 | 21 | declare module '@mui/styles/defaultTheme' { 22 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 23 | interface DefaultTheme extends Theme {} 24 | } 25 | 26 | i18n 27 | .use(LanguageDetector) 28 | .use(initReactI18next) 29 | .init({ 30 | resources: { 31 | en: { translation: { ...editorEnJson, ...enJson } }, 32 | ja: { translation: { ...editorJaJson, ...jaJson } }, 33 | }, 34 | fallbackLng: 'en', 35 | interpolation: { escapeValue: false }, 36 | }); 37 | 38 | ReactDOM.render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | , 49 | document.getElementById('root') 50 | ); 51 | -------------------------------------------------------------------------------- /src/electron-app/web/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "$pages/start/index": "#Comment#", 3 | "app_start-header_label__open": "Open", 4 | "app_start-header_action_label__folder_open": "Open Folder", 5 | "$pages/workspace/form": "#Comment#", 6 | "app_workspaceForm-label_workspaceFolder": "Workspace Folder", 7 | "app_workspaceForm-label_type": "Type", 8 | "app_workspaceForm-label_targets": "Target", 9 | "$pages/workspace/index": "#Comment#", 10 | "app_workspace-header_label": "Create Workspace", 11 | "app_workspace-action_label__create": "Create", 12 | "app_workspace-action_label__back": "Back", 13 | "app_workspace-message__invalid_folder_not_empty": "Must be select an empty folder", 14 | "$title-bar": "#Comment#", 15 | "titleBar-menu_label__file": "File", 16 | "titleBar-menu_item_label__createWorkspace": "Create Workspace", 17 | "titleBar-menu_item_label__openWorkspace": "Open Workspace", 18 | "titleBar-menu_item_label__close": "Close" 19 | } 20 | -------------------------------------------------------------------------------- /src/electron-app/web/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "$pages/start/index": "#Comment#", 3 | "app_start-header_label__open": "開く", 4 | "app_start-header_action_label__folder_open": "フォルダを開く", 5 | "$pages/workspace/form": "#Comment#", 6 | "app_workspaceForm-label_workspaceFolder": "ワークスペースフォルダ", 7 | "app_workspaceForm-label_type": "タイプ", 8 | "app_workspaceForm-label_targets": "対象", 9 | "$pages/workspace/index": "#Comment#", 10 | "app_workspace-header_label": "ワークスペースを作成", 11 | "app_workspace-action_label__create": "ワークスペースを作成", 12 | "app_workspace-action_label__back": "戻る", 13 | "app_workspace-message__invalid_folder_not_empty": "ワークスペースを新規作成する場合は空のフォルダを選択してください", 14 | "$title-bar": "#Comment#", 15 | "titleBar-menu_label__file": "ファイル", 16 | "titleBar-menu_item_label__createWorkspace": "新規ワークスペースを開く", 17 | "titleBar-menu_item_label__openWorkspace": "ワークスペースを開く", 18 | "titleBar-menu_item_label__close": "ウインドウを閉じる" 19 | } 20 | -------------------------------------------------------------------------------- /src/electron-app/web/views/pages/start/index.tsx: -------------------------------------------------------------------------------- 1 | import { ApplicationConst } from '@fl-three-editor/application/const'; 2 | import FolderOpenIcon from '@mui/icons-material/FolderOpen'; 3 | import { Button, Grid, Typography } from '@mui/material'; 4 | import createStyles from '@mui/styles/createStyles'; 5 | import makeStyles from '@mui/styles/makeStyles'; 6 | import React, { FC, useCallback } from 'react'; 7 | import { useTranslation } from 'react-i18next'; 8 | import { useHistory } from 'react-router-dom'; 9 | import WorkspaceContext from '../../../context/workspace'; 10 | 11 | const useStyles = makeStyles(() => 12 | createStyles({ 13 | root: { 14 | flexGrow: 1, 15 | height: '100%', 16 | width: '100vw', 17 | }, 18 | main: { 19 | minHeight: 380, 20 | }, 21 | }) 22 | ); 23 | 24 | const workspaceApi = window.workspace; 25 | 26 | const StartPage: FC = () => { 27 | const classes = useStyles(); 28 | const history = useHistory(); 29 | const [t] = useTranslation(); 30 | const workspaceStore = WorkspaceContext.useContainer(); 31 | 32 | const onClickStartButton = useCallback(() => { 33 | // TODO check folder content and control moved page 34 | const selectFolder = workspaceApi.openFolderDialog(); 35 | selectFolder.then((wkDir) => { 36 | if (!wkDir) return; 37 | workspaceStore.setWorkspaceFolder(wkDir); 38 | workspaceApi 39 | .load({ wkDir, query: { meta: { project: true } } }) 40 | .then((res) => { 41 | if (res.meta?.project) { 42 | const projectId = res.meta?.project.projectId; 43 | history.push(`/threeannotation/${projectId}`); 44 | return; 45 | } 46 | history.push('/workspace?from='); 47 | }) 48 | .catch((err) => { 49 | // not exist meta/project.json 50 | history.push('/workspace?from='); 51 | }); 52 | }); 53 | }, []); 54 | 55 | return ( 56 | 62 | 63 | 64 | 65 | 66 | {ApplicationConst.name} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {t('app_start-header_label__open')} 77 | 78 | 79 | 80 | 87 | 88 | 89 | 90 | {/* 91 | 92 | 最近 93 | 94 | */} 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export default StartPage; 104 | -------------------------------------------------------------------------------- /src/electron-app/web/views/pages/workspace/form.tsx: -------------------------------------------------------------------------------- 1 | import FLFileField from '@fl-three-editor/components/fields/fl-file-field'; 2 | import FLFolderContentsField from '@fl-three-editor/components/fields/fl-folder-contents-field'; 3 | import FLSelectField from '@fl-three-editor/components/fields/fl-select-field'; 4 | import { FormUtil } from '@fl-three-editor/components/fields/form-util'; 5 | import { FormAction, FormState } from '@fl-three-editor/components/fields/type'; 6 | import { ProjectType } from '@fl-three-editor/types/const'; 7 | import { WorkspaceUtil } from '@fl-three-editor/utils/workspace-util'; 8 | import Grid from '@mui/material/Grid'; 9 | import createStyles from '@mui/styles/createStyles'; 10 | import makeStyles from '@mui/styles/makeStyles'; 11 | import React, { FC, useMemo } from 'react'; 12 | import { useTranslation } from 'react-i18next'; 13 | 14 | const useStyles = makeStyles(() => createStyles({})); 15 | 16 | type Props = { 17 | form: FormState; 18 | dispatchForm: React.Dispatch; 19 | }; 20 | 21 | export type WorkspaceFormState = { 22 | workspaceFolder?: string; 23 | type?: ProjectType; 24 | targets?: File[]; 25 | }; 26 | 27 | const WorkspaceForm: FC = ({ form, dispatchForm }) => { 28 | const classes = useStyles(); 29 | const [t] = useTranslation(); 30 | const typeValue = FormUtil.resolve('type', form.data); 31 | 32 | const folderContentsProps = useMemo( 33 | () => WorkspaceUtil.folderContentsProps(t, typeValue), 34 | [typeValue] 35 | ); 36 | 37 | const targetItemTypes = useMemo(() => WorkspaceUtil.targetItemTypes(t), []); 38 | 39 | return ( 40 | 41 | 42 | 46 | 47 | 48 | 53 | 54 | 55 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default WorkspaceForm; 66 | -------------------------------------------------------------------------------- /src/file-module/reference-target-util.ts: -------------------------------------------------------------------------------- 1 | import { ProjectType } from '@fl-three-editor/types/const'; 2 | import { TaskImageTopicVO } from '@fl-three-editor/types/vo'; 3 | 4 | const IMAGE_EXTENSION = [ 5 | 'jpg', 6 | 'jpeg', 7 | 'JPG', 8 | 'JPEG', 9 | 'jpe', 10 | 'jfif', 11 | 'pjpeg', 12 | 'pjp', 13 | 'png', 14 | 'PNG', 15 | ]; 16 | 17 | export const ReferenceTargetUtil = { 18 | reduceFiles: ( 19 | type: ProjectType, 20 | files: File[], 21 | resolveBlobFile?: (file: File, extension: string) => any, 22 | resolveJsonFile?: (jsonObj: any) => any 23 | ) => { 24 | const calibration = new Set(); 25 | const frames = new Set(); 26 | const topicIds = new Set(); 27 | 28 | const targetQuery = files.reduce( 29 | (res, f) => { 30 | if (f.name === '.DS_Store') { 31 | return res; 32 | } 33 | const [topicId, extension] = f.name.split('.'); 34 | const targetInfo = res.target_info; 35 | 36 | const delimiter = f.path.includes('\\') ? '\\' : '/'; 37 | const paths = f.path.split(delimiter); 38 | let parent = paths[paths.length - 2]; 39 | const cFrameNo = Number(parent); 40 | if ( 41 | !( 42 | parent === 'calibration' || 43 | (!Number.isNaN(cFrameNo) && Number.isInteger(cFrameNo)) 44 | ) 45 | ) { 46 | if (type === ProjectType.pcd_image_frames) { 47 | throw new Error( 48 | 'folder structure is not supported !! path:' + f.path 49 | ); 50 | } 51 | parent = 52 | extension === 'yaml' || extension === 'yml' 53 | ? 'calibration' 54 | : '0001'; 55 | } 56 | 57 | if (!res[parent]) { 58 | res[parent] = {}; 59 | if (parent !== 'calibration') { 60 | frames.add(parent); 61 | } 62 | } 63 | if (extension === 'pcd') { 64 | res[parent][topicId] = resolveBlobFile 65 | ? resolveBlobFile(f, extension) 66 | : f; 67 | targetInfo.pcdTopicId = topicId; 68 | return res; 69 | } else if (IMAGE_EXTENSION.includes(extension)) { 70 | res[parent][topicId] = resolveBlobFile 71 | ? resolveBlobFile(f, extension) 72 | : f; 73 | if (!topicIds.has(topicId)) { 74 | topicIds.add(topicId); 75 | targetInfo.imageTopics.push({ 76 | topicId, 77 | extension, 78 | } as TaskImageTopicVO); 79 | } 80 | return res; 81 | } else if (extension === 'yaml' || extension === 'yml') { 82 | res[parent][topicId] = resolveBlobFile 83 | ? resolveBlobFile(f, extension) 84 | : f; 85 | calibration.add(topicId); 86 | return res; 87 | } 88 | throw new Error('Non supported file !'); 89 | }, 90 | { 91 | target_info: { 92 | frames: [], 93 | pcdTopicId: '', 94 | imageTopics: [], 95 | }, 96 | } 97 | ); 98 | targetQuery.target_info.frames = Array.from(frames.values()).sort(); 99 | targetQuery.target_info.imageTopics.forEach((t: TaskImageTopicVO) => { 100 | t.calibration = calibration.has(t.topicId); 101 | }); 102 | if (resolveJsonFile) { 103 | targetQuery.target_info = resolveJsonFile(targetQuery.target_info); 104 | } 105 | return targetQuery; 106 | }, 107 | timestamp: () => { 108 | const date = new Date(); 109 | const yyyy = `${date.getFullYear()}`; 110 | const MM = `0${date.getMonth() + 1}`.slice(-2); 111 | const dd = `0${date.getDate()}`.slice(-2); 112 | const HH = `0${date.getHours()}`.slice(-2); 113 | const mm = `0${date.getMinutes()}`.slice(-2); 114 | const ss = `0${date.getSeconds()}`.slice(-2); 115 | const ms = `00${date.getMilliseconds()}`.slice(-3); 116 | return `${yyyy}${MM}${dd}${HH}${mm}${ss}${ms}`; 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /src/web-app/App.scss: -------------------------------------------------------------------------------- 1 | $titlebar-height: 48px; 2 | 3 | html, 4 | body, 5 | #root { 6 | height: 100%; 7 | } 8 | 9 | .container { 10 | height: 100%; 11 | 12 | .content { 13 | overflow: hidden; 14 | } 15 | 16 | .content-darwin { 17 | height: 100vh; 18 | overflow: hidden; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | -webkit-app-region: drag; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/web-app/App.tsx: -------------------------------------------------------------------------------- 1 | import { AppContext } from '@fl-three-editor/application/app-context'; 2 | import { ProjectRepositoryContext } from '@fl-three-editor/repositories/project-repository'; 3 | import AnnotationClassStore from '@fl-three-editor/stores/annotation-class-store'; 4 | import CameraCalibrationStore from '@fl-three-editor/stores/camera-calibration-store'; 5 | import TaskStore from '@fl-three-editor/stores/task-store'; 6 | import ThreeAnnotationPage from '@fl-three-editor/views/pages/three-annotation/index'; 7 | import { SnackbarProvider } from 'notistack'; 8 | import React from 'react'; 9 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 10 | import 'typeface-roboto/index.css'; 11 | import './App.scss'; 12 | import { useProjectWebRepository } from './repositories/project-web-repository'; 13 | import EditPage from './views/pages/edit'; 14 | import NewPage from './views/pages/new'; 15 | import StartPage from './views/pages/start/index'; 16 | 17 | export const App = (): JSX.Element => { 18 | const projectWebRepository = useProjectWebRepository(); 19 | return ( 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/web-app/images/autoware-main-logo-whitebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/autoware-main-logo-whitebg.png -------------------------------------------------------------------------------- /src/web-app/images/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/copy.png -------------------------------------------------------------------------------- /src/web-app/images/feature__2d3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/feature__2d3d.png -------------------------------------------------------------------------------- /src/web-app/images/feature__birdeye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/feature__birdeye.png -------------------------------------------------------------------------------- /src/web-app/images/feature__label_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/feature__label_view.png -------------------------------------------------------------------------------- /src/web-app/images/feature__sequence_interpolation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/feature__sequence_interpolation.png -------------------------------------------------------------------------------- /src/web-app/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/github.png -------------------------------------------------------------------------------- /src/web-app/images/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/hero.png -------------------------------------------------------------------------------- /src/web-app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/logo.png -------------------------------------------------------------------------------- /src/web-app/images/pcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/pcd.png -------------------------------------------------------------------------------- /src/web-app/images/pcd_frames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/pcd_frames.png -------------------------------------------------------------------------------- /src/web-app/images/pcd_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/pcd_image.png -------------------------------------------------------------------------------- /src/web-app/images/side-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/side-left.png -------------------------------------------------------------------------------- /src/web-app/images/side-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastlabel/AutomanTools/3c28932b51a45b578d011947e79bb860ef61597c/src/web-app/images/side-right.png -------------------------------------------------------------------------------- /src/web-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | Automan 11 | 12 | 13 | 14 |
15 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/web-app/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Roboto', sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /src/web-app/index.tsx: -------------------------------------------------------------------------------- 1 | import muiTheme from '@fl-three-editor/config/mui-theme'; 2 | import editorEnJson from '@fl-three-editor/locales/en.json'; 3 | import editorJaJson from '@fl-three-editor/locales/ja.json'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import { 6 | ThemeProvider, 7 | Theme, 8 | StyledEngineProvider, 9 | } from '@mui/material/styles'; 10 | import i18n from 'i18next'; 11 | import LanguageDetector from 'i18next-browser-languagedetector'; 12 | import React, { Suspense } from 'react'; 13 | import ReactDOM from 'react-dom'; 14 | import { initReactI18next } from 'react-i18next'; 15 | import { App } from './App'; 16 | import './index.scss'; 17 | import enJson from './locales/en.json'; 18 | import jaJson from './locales/ja.json'; 19 | 20 | declare module '@mui/styles/defaultTheme' { 21 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 22 | interface DefaultTheme extends Theme {} 23 | } 24 | 25 | i18n 26 | .use(LanguageDetector) 27 | .use(initReactI18next) 28 | .init({ 29 | lng: 'en', 30 | resources: { 31 | en: { translation: { ...editorEnJson, ...enJson } }, 32 | ja: { translation: { ...editorJaJson, ...jaJson } }, 33 | }, 34 | fallbackLng: 'en', 35 | interpolation: { escapeValue: false }, 36 | }); 37 | 38 | ReactDOM.render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | , 47 | document.getElementById('root') 48 | ); 49 | -------------------------------------------------------------------------------- /src/web-app/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "$pages/start/index": "#Comment#", 3 | "web_start-header_label": "Open", 4 | "web_start-action_label__new": "Get Started", 5 | "web_start-action_label__edit": "Edit", 6 | "$pages/common/workspaceForm": "#Comment#", 7 | "web_workspaceForm-label__type": "Type", 8 | "web_workspaceForm-label__targets": "Reference Resources", 9 | "web_workspaceForm-action_label__back": "Back", 10 | "$pages/edit/*": "#Comment#", 11 | "web_edit-label__editTargets": "Edit target", 12 | "web_edit-description_main__editTargets": "Drag and drop the file to edit", 13 | "web_edit-description_sub__editTargets": "Supports tool exported json file", 14 | "web_edit-description_btn__editTargets": "Upload Edit File", 15 | "web_edit-header_label": "Edit Annotation", 16 | "web_edit-action_label_edit": "Edit", 17 | "web_edit-message_empty_edit_target": "Required the Edit target", 18 | "$pages/new/*": "#Comment#", 19 | "web_new-header_label": "Create Annotation", 20 | "web_new-action_label_new": "Start", 21 | "web_hero_start_button": "Start on Web", 22 | "web_hero_download_button": "Download app", 23 | "web_hero_visit_github_button": "View on GitHub", 24 | "web_hero_describe_for_mobile": "Automan is designed for PC. Please view from a PC when using.", 25 | "web_copy_title": "3D annotations quickly and easily.", 26 | "web_copy_caption_1": "Automan can be used on the web or by installing an app.", 27 | "web_copy_caption_2": "You can annotate 3D data on your device.", 28 | "web_features_title": "Main features of Automan", 29 | "web_features_caption_1": "Sequence interpolation between frames", 30 | "web_features_caption_2": "3D data labeling", 31 | "web_features_caption_2_1": "- Bird's-eye view display", 32 | "web_features_caption_2_2": "- 3 view point display", 33 | "web_features_caption_3": "2D/3D calibration", 34 | "web_features_caption_4": "Adjust an annotation with multi frames [Label View]", 35 | "web_data_type_title": "Annotate 3D data in three different ways", 36 | "web_data_type_caption_1": "It supports not only annotation of pcd data, but also annotation", 37 | "web_data_type_caption_2": "in comparison with images and It can also be used to annotate continuous 3D data.", 38 | "web_data_type_pcd": "3D file (.pcd)", 39 | "web_data_type_pcd_image": "3D data with images", 40 | "web_data_type_pcd_frames": "Continuous 3D data", 41 | "web_sample_download": "Download sample", 42 | "web_data_type_button": "Start on Web", 43 | "web_usage_title": "How to use Automan", 44 | "web_usage_new_title": "New Annotation", 45 | "web_usage_new_step_1": "Upload .pcd", 46 | "web_usage_new_step_2": "Create labels for annotations", 47 | "web_usage_new_step_3": "Annotate 3D data", 48 | "web_usage_new_step_4": "Download annotation results as JSON", 49 | "web_usage_edit_title": "Edit Annotation", 50 | "web_usage_edit_step_1": "Upload the output JSON and .pcd data", 51 | "web_usage_edit_step_2": "Edit annotations in 3D data", 52 | "web_usage_edit_step_3": "Download annotation results as JSON", 53 | "web_usage_new_button": "Start a new annotation", 54 | "web_usage_edit_button": "Edit annotation", 55 | "web_footer_caption": "Automan is a joint OSS project between FastLabel and Human Dataware Lab., Inc." 56 | } 57 | -------------------------------------------------------------------------------- /src/web-app/locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "$pages/start/index": "#Comment#", 3 | "web_start-header_label": "開く", 4 | "web_start-action_label__new": "新しく始める", 5 | "web_start-action_label__edit": "編集する", 6 | "$pages/common/workspaceForm": "#Comment#", 7 | "web_workspaceForm-label__type": "タイプ", 8 | "web_workspaceForm-label__targets": "参照リソース", 9 | "web_workspaceForm-action_label__back": "戻る", 10 | "$pages/edit/*": "#Comment#", 11 | "web_edit-label__editTargets": "編集対象", 12 | "web_edit-description_main__editTargets": "編集するファイルをドラッグ&ドロップしてください", 13 | "web_edit-description_sub__editTargets": "ツールで出力したJSONファイルのみをサポートしています", 14 | "web_edit-description_btn__editTargets": "ファイルをアップロード", 15 | "web_edit-header_label": "アノテーションを編集する", 16 | "web_edit-action_label_edit": "編集", 17 | "web_edit-message_empty_edit_target": "編集対象が設定されてません", 18 | "$pages/new/*": "#Comment#", 19 | "web_new-header_label": "3Dアノテーションを始める", 20 | "web_new-action_label_new": "開始", 21 | "web_hero_start_button": "Webで試す", 22 | "web_hero_download_button": "アプリをダウンロード", 23 | "web_hero_visit_github_button": "GitHubを見る", 24 | "web_hero_describe_for_mobile": "AutomanはPC向けに設計されております。ご利用する場合はPCより閲覧してください。", 25 | "web_copy_title": "3Dアノテーションを手元で、簡単に。", 26 | "web_copy_caption_1": "Automanはウェブ上、またはアプリをインストールして、", 27 | "web_copy_caption_2": "手元の端末で3Dデータのアノテーションを行うことができます。", 28 | "web_features_title": "Automanの主な機能", 29 | "web_features_caption_1": "フレーム間の線形補完", 30 | "web_features_caption_2": "3Dのデータラベリング", 31 | "web_features_caption_2_1": "- 鳥観図表示", 32 | "web_features_caption_2_2": "- 3視点表示", 33 | "web_features_caption_3": "2D/3Dのキャリブレーション", 34 | "web_features_caption_4": "単一のアノテーションの複数フレーム調整 [Label View]", 35 | "web_data_type_title": "3つの方法で3Dデータをアノテーション", 36 | "web_data_type_caption_1": "pcdデータのアノテーションだけでなく、画像と比較したアノテーションや、", 37 | "web_data_type_caption_2": "連続した3Dデータのアノテーションにも対応しています。", 38 | "web_data_type_pcd": "3Dデータ(.pcd)", 39 | "web_data_type_pcd_image": "画像つき3Dデータ", 40 | "web_data_type_pcd_frames": "連続した3Dデータ", 41 | "web_sample_download": "サンプルをダウンロード", 42 | "web_data_type_button": "今すぐWebで始める", 43 | "web_usage_title": "Automanの使い方", 44 | "web_usage_new_title": "新しく始める", 45 | "web_usage_new_step_1": "pcdデータをアップロード", 46 | "web_usage_new_step_2": "アノテーションのラベルを作成", 47 | "web_usage_new_step_3": "3Dデータをアノテーション", 48 | "web_usage_new_step_4": "アノテーション結果をJSONでダウンロード", 49 | "web_usage_edit_title": "アノテーションを編集", 50 | "web_usage_edit_step_1": "出力されたJSONと.pcdデータをアップロード", 51 | "web_usage_edit_step_2": "3Dデータのアノテーションを編集", 52 | "web_usage_edit_step_3": "アノテーション結果をJSONでダウンロード", 53 | "web_usage_new_button": "アノテーションを始める", 54 | "web_usage_edit_button": "アノテーションを編集する", 55 | "web_footer_caption": "Automan は FastLabel と Human Dataware Lab., Inc. による共同の OSS のプロジェクトです。" 56 | } 57 | -------------------------------------------------------------------------------- /src/web-app/title-bar.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import AppBar from '@mui/material/AppBar'; 3 | import Toolbar from '@mui/material/Toolbar'; 4 | import React, { FC } from 'react'; 5 | 6 | const TitleBar: FC = () => { 7 | return ( 8 | 9 | 10 | 11 | Automan 12 | 13 | 14 | 15 | ); 16 | }; 17 | export default TitleBar; 18 | -------------------------------------------------------------------------------- /src/web-app/views/pages/edit/form.tsx: -------------------------------------------------------------------------------- 1 | import FLFolderContentsField from '@fl-three-editor/components/fields/fl-folder-contents-field'; 2 | import FLSelectField from '@fl-three-editor/components/fields/fl-select-field'; 3 | import { FormUtil } from '@fl-three-editor/components/fields/form-util'; 4 | import { FormAction, FormState } from '@fl-three-editor/components/fields/type'; 5 | import { ProjectType } from '@fl-three-editor/types/const'; 6 | import { WorkspaceUtil } from '@fl-three-editor/utils/workspace-util'; 7 | import Grid from '@mui/material/Grid'; 8 | import createStyles from '@mui/styles/createStyles'; 9 | import makeStyles from '@mui/styles/makeStyles'; 10 | import React, { FC, useMemo } from 'react'; 11 | import { useTranslation } from 'react-i18next'; 12 | 13 | const useStyles = makeStyles(() => createStyles({})); 14 | 15 | type Props = { 16 | form: FormState; 17 | dispatchForm: React.Dispatch; 18 | }; 19 | 20 | export type WorkspaceFormState = { 21 | editTargets?: File[]; 22 | type?: ProjectType; 23 | targets?: File[]; 24 | }; 25 | 26 | const WorkspaceForm: FC = ({ form, dispatchForm }) => { 27 | const classes = useStyles(); 28 | const [t] = useTranslation(); 29 | 30 | const typeValue = FormUtil.resolve('type', form.data); 31 | 32 | const folderContentsProps = useMemo( 33 | () => WorkspaceUtil.folderContentsProps(t, typeValue), 34 | [typeValue] 35 | ); 36 | 37 | const targetItemTypes = useMemo(() => WorkspaceUtil.targetItemTypes(t), []); 38 | 39 | return ( 40 | 41 | 42 | 54 | 55 | 56 | 61 | 62 | 63 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default WorkspaceForm; 74 | -------------------------------------------------------------------------------- /src/web-app/views/pages/new/form.tsx: -------------------------------------------------------------------------------- 1 | import FLFolderContentsField from '@fl-three-editor/components/fields/fl-folder-contents-field'; 2 | import FLSelectField from '@fl-three-editor/components/fields/fl-select-field'; 3 | import { FormUtil } from '@fl-three-editor/components/fields/form-util'; 4 | import { FormAction, FormState } from '@fl-three-editor/components/fields/type'; 5 | import { ProjectType } from '@fl-three-editor/types/const'; 6 | import { WorkspaceUtil } from '@fl-three-editor/utils/workspace-util'; 7 | import Grid from '@mui/material/Grid'; 8 | import createStyles from '@mui/styles/createStyles'; 9 | import makeStyles from '@mui/styles/makeStyles'; 10 | import React, { FC, useMemo } from 'react'; 11 | import { useTranslation } from 'react-i18next'; 12 | 13 | const useStyles = makeStyles(() => createStyles({})); 14 | 15 | type Props = { 16 | form: FormState; 17 | dispatchForm: React.Dispatch; 18 | }; 19 | 20 | export type WorkspaceFormState = { 21 | type?: ProjectType; 22 | targets?: File[]; 23 | }; 24 | 25 | const WorkspaceForm: FC = ({ form, dispatchForm }) => { 26 | const [t] = useTranslation(); 27 | 28 | const typeValue = FormUtil.resolve('type', form.data); 29 | 30 | const folderContentsProps = useMemo( 31 | () => WorkspaceUtil.folderContentsProps(t, typeValue), 32 | [typeValue] 33 | ); 34 | 35 | const targetItemTypes = useMemo(() => WorkspaceUtil.targetItemTypes(t), []); 36 | 37 | return ( 38 | 39 | 40 | 45 | 46 | 47 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default WorkspaceForm; 58 | -------------------------------------------------------------------------------- /src/web-app/views/pages/new/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormUtil } from '@fl-three-editor/components/fields/form-util'; 2 | import { FormAction, FormState } from '@fl-three-editor/components/fields/type'; 3 | import { ProjectRepositoryContext } from '@fl-three-editor/repositories/project-repository'; 4 | import { ProjectType } from '@fl-three-editor/types/const'; 5 | import Box from '@mui/material/Box'; 6 | import Button from '@mui/material/Button'; 7 | import Grid from '@mui/material/Grid'; 8 | import Typography from '@mui/material/Typography'; 9 | import createStyles from '@mui/styles/createStyles'; 10 | import makeStyles from '@mui/styles/makeStyles'; 11 | import { useSnackbar } from 'notistack'; 12 | import React, { FC, Reducer, useEffect, useReducer } from 'react'; 13 | import { useTranslation } from 'react-i18next'; 14 | import { useHistory, useLocation } from 'react-router-dom'; 15 | import { v4 as uuid } from 'uuid'; 16 | import WorkspaceForm, { WorkspaceFormState } from './form'; 17 | 18 | const useStyles = makeStyles(() => 19 | createStyles({ 20 | root: { 21 | flexGrow: 1, 22 | backgroundColor: '#F5F5F5', 23 | display: 'flex', 24 | justifyContent: 'center', 25 | alignItems: 'center', 26 | minHeight: '100vh', 27 | }, 28 | main: { 29 | width: '100%', 30 | maxWidth: 720, 31 | padding: 24, 32 | backgroundColor: '#FFF', 33 | borderRadius: 16, 34 | }, 35 | item: { 36 | width: '100%', 37 | }, 38 | itemGlow: { 39 | flexGrow: 1, 40 | width: '100%', 41 | }, 42 | }) 43 | ); 44 | 45 | const formReducer: Reducer, FormAction> = ( 46 | state, 47 | action 48 | ) => { 49 | let newState: WorkspaceFormState = {}; 50 | switch (action.type) { 51 | case 'change': 52 | newState = FormUtil.update(action.name, action.value, state.data); 53 | if (action.name === 'type') { 54 | newState = FormUtil.update('targets', [], newState); 55 | } 56 | if (newState.type && newState.targets && newState.targets.length > 0) { 57 | return { data: newState, helper: { validState: 'valid' } }; 58 | } else { 59 | return { data: newState, helper: { validState: 'error' } }; 60 | } 61 | case 'init': 62 | return { data: action.data, helper: {} }; 63 | } 64 | }; 65 | 66 | const NewPage: FC = () => { 67 | const classes = useStyles(); 68 | const history = useHistory(); 69 | const [t] = useTranslation(); 70 | const { enqueueSnackbar } = useSnackbar(); 71 | 72 | const search = useLocation().search; 73 | const queryParam = new URLSearchParams(search); 74 | const formStartPage = !queryParam.get('from'); 75 | 76 | const projectRepository = React.useContext(ProjectRepositoryContext); 77 | 78 | const initialForm = { 79 | data: { 80 | workspaceFolder: '', 81 | type: ProjectType.pcd_only, 82 | }, 83 | helper: {}, 84 | }; 85 | 86 | const [form, dispatchForm] = useReducer(formReducer, initialForm); 87 | 88 | const handleCreate = () => { 89 | projectRepository 90 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 | .create({ ...form.data, projectId: uuid().toString() } as any) 92 | .then(({ projectId, errorCode }) => { 93 | if (errorCode) { 94 | switch (errorCode) { 95 | default: 96 | enqueueSnackbar(errorCode, { variant: 'error' }); 97 | return; 98 | } 99 | } 100 | history.push(`/threeannotation/${projectId}`); 101 | }) 102 | .catch((err) => enqueueSnackbar(err, { variant: 'error' })); 103 | }; 104 | 105 | const handleBack = () => { 106 | history.push('/'); 107 | }; 108 | 109 | useEffect(() => { 110 | // 111 | }, [form]); 112 | 113 | return ( 114 | 115 | 121 | 122 | 123 | {t('web_new-header_label')} 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {formStartPage && ( 133 | 136 | )} 137 | 138 | 139 | 146 | 147 | 148 | 149 | 150 | 151 | ); 152 | }; 153 | 154 | export default NewPage; 155 | -------------------------------------------------------------------------------- /src/web-app/views/pages/start/github-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SvgIcon from '@mui/material/SvgIcon'; 3 | 4 | const GitHubIcon: React.FC = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | export default GitHubIcon; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "baseUrl": "./", 8 | "paths": { 9 | "@fl-three-editor/*": ["src/editor-module/*"], 10 | "@fl-file/*": ["src/file-module/*"] 11 | }, 12 | "lib": ["DOM", "ES2020"], 13 | "jsx": "react", 14 | "strict": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true, 18 | "forceConsistentCasingInFileNames": true 19 | }, 20 | "ts-node": { 21 | "compilerOptions": { 22 | "target": "ES2015", 23 | "module": "CommonJS" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.main.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ".", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src/electron-app/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /webpack.config.prod.ts: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import path from 'path'; 4 | import { Configuration } from 'webpack'; 5 | 6 | const base: Configuration = { 7 | mode: 'production', 8 | node: { 9 | __dirname: false, 10 | __filename: false, 11 | }, 12 | resolve: { 13 | alias: { 14 | '@fl-three-editor': path.resolve(__dirname, 'src/editor-module/'), 15 | '@fl-file': path.resolve(__dirname, 'src/file-module/'), 16 | }, 17 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'], 18 | }, 19 | output: { 20 | path: path.resolve(__dirname, 'dist'), 21 | publicPath: './', 22 | filename: '[name].js', 23 | assetModuleFilename: 'asset/[name][ext]', 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | exclude: /node_modules/, 30 | use: [ 31 | { loader: 'ts-loader' }, 32 | { loader: 'ifdef-loader', options: { DEBUG: false } }, 33 | ], 34 | }, 35 | { 36 | test: /\.s?css$/, 37 | use: [ 38 | MiniCssExtractPlugin.loader, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | sourceMap: false, 43 | importLoaders: 1, 44 | }, 45 | }, 46 | { 47 | loader: 'sass-loader', 48 | options: { 49 | sourceMap: false, 50 | }, 51 | }, 52 | ], 53 | }, 54 | { 55 | test: /\.(jpg|png)$/, 56 | type: 'asset/inline', 57 | }, 58 | { 59 | test: /\.(bmp|ico|gif|jpe?g|png|svg|ttf|eot|woff?2?)$/, 60 | type: 'asset/resource', 61 | }, 62 | ], 63 | }, 64 | stats: 'errors-only', 65 | performance: { hints: false }, 66 | optimization: { minimize: true }, 67 | devtool: undefined, 68 | }; 69 | 70 | const main: Configuration = { 71 | ...base, 72 | target: 'electron-main', 73 | entry: { 74 | main: './src/electron-app/main.ts', 75 | }, 76 | }; 77 | 78 | const preload: Configuration = { 79 | ...base, 80 | target: 'electron-preload', 81 | entry: { 82 | preload: './src/electron-app/preload.ts', 83 | }, 84 | }; 85 | 86 | const renderer: Configuration = { 87 | ...base, 88 | target: 'web', 89 | entry: { 90 | index: './src/electron-app/web/index.tsx', 91 | }, 92 | plugins: [ 93 | new MiniCssExtractPlugin(), 94 | new HtmlWebpackPlugin({ 95 | template: './src/electron-app//web/index.html', 96 | minify: true, 97 | inject: 'body', 98 | filename: 'index.html', 99 | scriptLoading: 'blocking', 100 | }), 101 | ], 102 | }; 103 | 104 | export default [main, preload, renderer]; 105 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import path from 'path'; 4 | import { Configuration } from 'webpack'; 5 | 6 | const config: Configuration = { 7 | mode: 'development', 8 | target: 'web', 9 | node: { 10 | __dirname: false, 11 | __filename: false, 12 | }, 13 | resolve: { 14 | alias: { 15 | '@fl-three-editor': path.resolve(__dirname, 'src/editor-module/'), 16 | '@fl-file': path.resolve(__dirname, 'src/file-module/'), 17 | }, 18 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'], 19 | }, 20 | entry: { 21 | app: './src/electron-app/web/index.tsx', 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, 'dist'), 25 | publicPath: './', 26 | filename: '[name].js', 27 | assetModuleFilename: 'assets/[name][ext]', 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.tsx?$/, 33 | exclude: /(node_modules|tests|mocks)/, 34 | use: 'ts-loader', 35 | }, 36 | { 37 | test: /\.s?css$/, 38 | use: [ 39 | MiniCssExtractPlugin.loader, 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | sourceMap: true, 44 | importLoaders: 1, 45 | }, 46 | }, 47 | { 48 | loader: 'sass-loader', 49 | options: { 50 | sourceMap: true, 51 | }, 52 | }, 53 | ], 54 | }, 55 | { 56 | test: /\.(jpg|png)$/, 57 | type: 'asset/inline', 58 | }, 59 | { 60 | test: /\.(bmp|ico|gif|jpe?g|png|svg|ttf|eot|woff?2?)$/, 61 | type: 'asset/resource', 62 | }, 63 | ], 64 | }, 65 | plugins: [ 66 | new MiniCssExtractPlugin(), 67 | new HtmlWebpackPlugin({ 68 | template: './src/electron-app/web/index.html', 69 | filename: 'index.html', 70 | scriptLoading: 'blocking', 71 | inject: 'body', 72 | minify: false, 73 | }), 74 | ], 75 | stats: 'errors-only', 76 | performance: { hints: false }, 77 | optimization: { minimize: false }, 78 | devtool: 'inline-source-map', 79 | }; 80 | 81 | export default config; 82 | -------------------------------------------------------------------------------- /webpack.config.web-dev.ts: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import path from 'path'; 4 | import { Configuration } from 'webpack'; 5 | 6 | const config: Configuration = { 7 | mode: 'development', 8 | target: 'web', 9 | node: { 10 | __dirname: false, 11 | __filename: false, 12 | }, 13 | resolve: { 14 | alias: { 15 | '@fl-three-editor': path.resolve(__dirname, 'src/editor-module/'), 16 | '@fl-file': path.resolve(__dirname, 'src/file-module/'), 17 | }, 18 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'], 19 | }, 20 | entry: { 21 | app: './src/web-app/index.tsx', 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, 'public'), 25 | publicPath: './', 26 | filename: '[name].js', 27 | assetModuleFilename: 'assets/[name][ext]', 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.tsx?$/, 33 | exclude: /(node_modules|tests|mocks)/, 34 | use: 'ts-loader', 35 | }, 36 | { 37 | test: /\.s?css$/, 38 | use: [ 39 | MiniCssExtractPlugin.loader, 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | sourceMap: true, 44 | importLoaders: 1, 45 | }, 46 | }, 47 | { 48 | loader: 'sass-loader', 49 | options: { 50 | sourceMap: true, 51 | }, 52 | }, 53 | ], 54 | }, 55 | { 56 | test: /\.(jpg|png)$/, 57 | type: 'asset/inline', 58 | }, 59 | { 60 | test: /\.(bmp|ico|gif|jpe?g|png|svg|ttf|eot|woff?2?)$/, 61 | type: 'asset/resource', 62 | }, 63 | ], 64 | }, 65 | plugins: [ 66 | new MiniCssExtractPlugin(), 67 | new HtmlWebpackPlugin({ 68 | template: './src/web-app/index.html', 69 | filename: 'index.html', 70 | scriptLoading: 'blocking', 71 | inject: 'body', 72 | minify: false, 73 | }), 74 | ], 75 | stats: 'errors-only', 76 | performance: { hints: false }, 77 | optimization: { minimize: false }, 78 | devtool: 'inline-source-map', 79 | }; 80 | 81 | export default config; 82 | -------------------------------------------------------------------------------- /webpack.config.web-prod.ts: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import path from 'path'; 4 | import { Configuration } from 'webpack'; 5 | 6 | const config: Configuration = { 7 | mode: 'production', 8 | target: 'web', 9 | node: { 10 | __dirname: false, 11 | __filename: false, 12 | }, 13 | resolve: { 14 | alias: { 15 | '@fl-three-editor': path.resolve(__dirname, 'src/editor-module/'), 16 | '@fl-file': path.resolve(__dirname, 'src/file-module/'), 17 | }, 18 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'], 19 | }, 20 | entry: { 21 | app: './src/web-app/index.tsx', 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, 'public'), 25 | publicPath: './', 26 | filename: '[name].js', 27 | assetModuleFilename: 'assets/[name][ext]', 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.tsx?$/, 33 | exclude: /(node_modules|tests|mocks)/, 34 | use: 'ts-loader', 35 | }, 36 | { 37 | test: /\.s?css$/, 38 | use: [ 39 | MiniCssExtractPlugin.loader, 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | sourceMap: false, 44 | importLoaders: 1, 45 | }, 46 | }, 47 | { 48 | loader: 'sass-loader', 49 | options: { 50 | sourceMap: false, 51 | }, 52 | }, 53 | ], 54 | }, 55 | { 56 | test: /\.(jpg|png)$/, 57 | type: 'asset/inline', 58 | }, 59 | { 60 | test: /\.(bmp|ico|gif|jpe?g|png|svg|ttf|eot|woff?2?)$/, 61 | type: 'asset/resource', 62 | }, 63 | ], 64 | }, 65 | plugins: [ 66 | new MiniCssExtractPlugin(), 67 | new HtmlWebpackPlugin({ 68 | template: './src/web-app/index.html', 69 | filename: 'index.html', 70 | scriptLoading: 'blocking', 71 | inject: 'body', 72 | minify: true, 73 | }), 74 | ], 75 | stats: 'errors-only', 76 | performance: { hints: false }, 77 | optimization: { minimize: true }, 78 | devtool: 'inline-source-map', 79 | }; 80 | 81 | export default config; 82 | --------------------------------------------------------------------------------