Compare commits
365 Commits
v2021.1.2
...
v2024.1.1-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
524b135142 | ||
|
|
623b4e5b84 | ||
|
|
9370937280 | ||
|
|
7eb4645ee2 | ||
|
|
5136dad535 | ||
|
|
5a4eb54693 | ||
|
|
12774591a4 | ||
|
|
6666b22fc1 | ||
|
|
fa87af9c26 | ||
|
|
1d6abf6ba9 | ||
|
|
363e1d8fd4 | ||
|
|
76e3c6d5a5 | ||
|
|
0898dfe2f7 | ||
|
|
d61225eba3 | ||
|
|
5b2be119e7 | ||
|
|
63147786b9 | ||
|
|
f3fb0109f9 | ||
|
|
cb401e1c7a | ||
|
|
adc30336d2 | ||
|
|
df45bc2d73 | ||
|
|
c5b42a1191 | ||
|
|
959c162fc2 | ||
|
|
8446c94508 | ||
|
|
89908fc181 | ||
|
|
25a4f24b06 | ||
|
|
7f98941b23 | ||
|
|
441caf03c0 | ||
|
|
47bd077bbb | ||
|
|
ededc4f130 | ||
|
|
1aa6bc80c9 | ||
|
|
cd83e220d7 | ||
|
|
67d8680a32 | ||
|
|
ad4f462fd6 | ||
|
|
7f94962791 | ||
|
|
c8c9e779ab | ||
|
|
760de0ff86 | ||
|
|
9991f8670c | ||
|
|
82e3da622f | ||
|
|
8b9a198d0b | ||
|
|
b37948cf5e | ||
|
|
5f3b5d2f19 | ||
|
|
0e06737272 | ||
|
|
43b78fae35 | ||
|
|
6e8e3a0cba | ||
|
|
2881741226 | ||
|
|
74f1779961 | ||
|
|
b3a3ab71bd | ||
|
|
ce0d28da93 | ||
|
|
65d5494ab3 | ||
|
|
5267b2c70d | ||
|
|
3c85291610 | ||
|
|
43eefcf1c5 | ||
|
|
2cb87c5a88 | ||
|
|
9d0f1a34a8 | ||
|
|
7f283640c4 | ||
|
|
9e371de1cb | ||
|
|
9f3a735c59 | ||
|
|
6e8e379b71 | ||
|
|
f601275fb2 | ||
|
|
306677e56f | ||
|
|
08892b9e68 | ||
|
|
de394418f6 | ||
|
|
8751764721 | ||
|
|
2f2396fe57 | ||
|
|
fd8f16b615 | ||
|
|
f623e4a1cc | ||
|
|
8397b43bef | ||
|
|
24a0c9c270 | ||
|
|
652a653c9a | ||
|
|
816bbca2f1 | ||
|
|
454f8a1773 | ||
|
|
dd9795028d | ||
|
|
715ef62c85 | ||
|
|
7593c5ed05 | ||
|
|
f1cadc1e1e | ||
|
|
4a94775639 | ||
|
|
f6756bdb9a | ||
|
|
b1546b8038 | ||
|
|
f813048462 | ||
|
|
013ff5e7c0 | ||
|
|
a723d3dc5c | ||
|
|
58419cfe38 | ||
|
|
7b8fb3385b | ||
|
|
f63283e187 | ||
|
|
6d2eae7f20 | ||
|
|
80f479344d | ||
|
|
6bdb158b33 | ||
|
|
c148331b69 | ||
|
|
2d586fe1c0 | ||
|
|
78ffa415fc | ||
|
|
14224574cf | ||
|
|
dcd917870c | ||
|
|
2b8f900768 | ||
|
|
0bb563a6e2 | ||
|
|
cdf045d887 | ||
|
|
767c0471d9 | ||
|
|
cf68f2a450 | ||
|
|
8e724ef60f | ||
|
|
a4554d9bd4 | ||
|
|
9ac1050264 | ||
|
|
abe32a1aae | ||
|
|
bf4a4db874 | ||
|
|
545e016d04 | ||
|
|
5b86360b9b | ||
|
|
a2dfe48679 | ||
|
|
3edc8750ec | ||
|
|
b4c93e5d34 | ||
|
|
0255798d6c | ||
|
|
6886663688 | ||
|
|
3c53dcbb7b | ||
|
|
6491698c0b | ||
|
|
bd66f90881 | ||
|
|
241961ae7a | ||
|
|
8ae7977477 | ||
|
|
deb8f97ee9 | ||
|
|
e58c27caa2 | ||
|
|
f6e3c9b3ee | ||
|
|
88ed2ebf51 | ||
|
|
5f39123bde | ||
|
|
37a7d378fd | ||
|
|
811fef1212 | ||
|
|
d0162b0ed0 | ||
|
|
9d6997180d | ||
|
|
a985c6cf3a | ||
|
|
167a4661ca | ||
|
|
a16ac4af57 | ||
|
|
d9f99f9c9b | ||
|
|
357d8a518a | ||
|
|
073714f0bc | ||
|
|
39f6ab8805 | ||
|
|
5c66785095 | ||
|
|
53c67a07e4 | ||
|
|
7c985e3a84 | ||
|
|
80e16ece87 | ||
|
|
86b9d4b037 | ||
|
|
e12f360a29 | ||
|
|
d0641d0cb6 | ||
|
|
871aa8b44b | ||
|
|
beaee9f6c0 | ||
|
|
11f5069148 | ||
|
|
6716d41a62 | ||
|
|
63b3cfe7e1 | ||
|
|
967be84b4b | ||
|
|
16ca2671f0 | ||
|
|
5e977445ee | ||
|
|
8117b5814b | ||
|
|
087429dab9 | ||
|
|
dbe7464ea9 | ||
|
|
ebef19af3d | ||
|
|
bde023c025 | ||
|
|
0f427bb52b | ||
|
|
05198ef294 | ||
|
|
b263fe19cc | ||
|
|
e68e6f3181 | ||
|
|
326701b74f | ||
|
|
af6f5eb0c4 | ||
|
|
0b5256df12 | ||
|
|
971b471f92 | ||
|
|
aaa886bd73 | ||
|
|
7c49cfe625 | ||
|
|
ea293f57d2 | ||
|
|
dc663657ff | ||
|
|
eedbfe3d49 | ||
|
|
1ab5b66829 | ||
|
|
d0bf64af6c | ||
|
|
8028d1887c | ||
|
|
74b807343e | ||
|
|
15fbe29d34 | ||
|
|
550194152a | ||
|
|
3a10f49b54 | ||
|
|
7ff630dc44 | ||
|
|
4088a394f3 | ||
|
|
78ab5e7c1d | ||
|
|
14d263a567 | ||
|
|
cf1a45d35b | ||
|
|
2ebc27aa3b | ||
|
|
95c55f08cf | ||
|
|
4382b8ea3f | ||
|
|
b1905954bc | ||
|
|
548f52e117 | ||
|
|
1971744589 | ||
|
|
6c51d8ab51 | ||
|
|
8330bf9d92 | ||
|
|
e1b39a1723 | ||
|
|
915f784d9d | ||
|
|
96006fc501 | ||
|
|
4fd7533456 | ||
|
|
bb63af601d | ||
|
|
da1aabae3a | ||
|
|
643db9c435 | ||
|
|
ec7bef7a4b | ||
|
|
b72f4ca2a9 | ||
|
|
ffd741ec0a | ||
|
|
678961e4f2 | ||
|
|
4c004fc780 | ||
|
|
41a00bc90f | ||
|
|
dcad7f34a2 | ||
|
|
72d8f49145 | ||
|
|
df852410b0 | ||
|
|
3c7165bb0d | ||
|
|
f193a2331a | ||
|
|
c7aa84ca41 | ||
|
|
209cdbf45f | ||
|
|
e03ec862a8 | ||
|
|
8169da5ad4 | ||
|
|
916431b4ff | ||
|
|
7dd1719fbd | ||
|
|
b408a58e9e | ||
|
|
a64697e714 | ||
|
|
e971db2f52 | ||
|
|
7b6afd545b | ||
|
|
0f99044468 | ||
|
|
1412155c50 | ||
|
|
b1280e49d5 | ||
|
|
aaac6a4fbb | ||
|
|
b68b0ca5f6 | ||
|
|
45d99f1f6b | ||
|
|
a42fef67f2 | ||
|
|
bd4d74c192 | ||
|
|
c4500ce12b | ||
|
|
81d19672d2 | ||
|
|
04bde1b230 | ||
|
|
4f355f2749 | ||
|
|
5e604cf98d | ||
|
|
2d7a88e231 | ||
|
|
27198a3e32 | ||
|
|
fbf6fb304e | ||
|
|
d24a8d4188 | ||
|
|
def40484e3 | ||
|
|
aff163fc6a | ||
|
|
c392d5fa4d | ||
|
|
8dbd428359 | ||
|
|
ccd3a512d6 | ||
|
|
bfc5e45cd0 | ||
|
|
a1b09100e0 | ||
|
|
2bf7a77885 | ||
|
|
d1bfb86ab4 | ||
|
|
07904589df | ||
|
|
5540bbf115 | ||
|
|
c827afb25f | ||
|
|
87e7c3ca74 | ||
|
|
4d5904dd6d | ||
|
|
9bf589ebc6 | ||
|
|
1e4a92c71f | ||
|
|
4ad9d97508 | ||
|
|
2c6b0ddac3 | ||
|
|
dafee954e0 | ||
|
|
5ac541642e | ||
|
|
ad0474d42a | ||
|
|
4b4a0a1cd9 | ||
|
|
a764ace7f2 | ||
|
|
a3bcd3ac4f | ||
|
|
661f8b2c04 | ||
|
|
72717cecf0 | ||
|
|
971ff3ac40 | ||
|
|
b80e436f02 | ||
|
|
be1a053cbe | ||
|
|
f4555dc545 | ||
|
|
54fdd1db51 | ||
|
|
1805785cc6 | ||
|
|
e62f6419b5 | ||
|
|
fa7824c616 | ||
|
|
9090aa6bcc | ||
|
|
5655ca6890 | ||
|
|
50fdfd8bce | ||
|
|
3120a6439b | ||
|
|
ab3e8c8db7 | ||
|
|
5144819ce2 | ||
|
|
d779fe23f0 | ||
|
|
b2a3f34433 | ||
|
|
b09a6d6a2d | ||
|
|
9893cf1f7e | ||
|
|
fc91daf397 | ||
|
|
a3e205cb6f | ||
|
|
553bed32b5 | ||
|
|
6c91feaf3f | ||
|
|
4ddb9aa08f | ||
|
|
4aadebdbb2 | ||
|
|
da8d70f887 | ||
|
|
80a0d8de1c | ||
|
|
3ad476bc28 | ||
|
|
e6d8e05b91 | ||
|
|
46fa17dfd8 | ||
|
|
43c35286f3 | ||
|
|
3d317f7035 | ||
|
|
a161bd5be9 | ||
|
|
0f730fc28d | ||
|
|
12e06b09c3 | ||
|
|
641101f574 | ||
|
|
6a1201432c | ||
|
|
e77a06bfa6 | ||
|
|
8b0b18bd07 | ||
|
|
1766f3bf0f | ||
|
|
4578fa756c | ||
|
|
d6e1e28fc2 | ||
|
|
49048c3998 | ||
|
|
8b079d9b20 | ||
|
|
1522adaa0e | ||
|
|
3cd57b8b43 | ||
|
|
c944967476 | ||
|
|
dbd631da61 | ||
|
|
f731ae37d2 | ||
|
|
0a8da1a0bd | ||
|
|
be5f0880c8 | ||
|
|
a02cd4a50e | ||
|
|
efd31543f6 | ||
|
|
a151f23319 | ||
|
|
822811c853 | ||
|
|
23834c87f4 | ||
|
|
f103a6b712 | ||
|
|
3257736ffa | ||
|
|
9df25eda88 | ||
|
|
5a13739818 | ||
|
|
5ca39e7f84 | ||
|
|
ffe34f00fe | ||
|
|
a5cc0808c4 | ||
|
|
08fafe2607 | ||
|
|
a2af7d9273 | ||
|
|
a731c7a8db | ||
|
|
7e74da5cff | ||
|
|
0977fd0dff | ||
|
|
3241ef7b1b | ||
|
|
f922466d41 | ||
|
|
243f06da2d | ||
|
|
44e91a184d | ||
|
|
e6b0f398b6 | ||
|
|
5a475e1071 | ||
|
|
f8def88e4d | ||
|
|
db09e5209f | ||
|
|
9fdd945a52 | ||
|
|
00b8e7d1c5 | ||
|
|
798b8e398a | ||
|
|
affb27038b | ||
|
|
6767781a41 | ||
|
|
6007cc752d | ||
|
|
9cf5c77d69 | ||
|
|
9dc5481d1c | ||
|
|
3948650e6c | ||
|
|
49fcdb64ed | ||
|
|
1e715ce4bb | ||
|
|
f9fd7a0b45 | ||
|
|
e9a3c2d1b8 | ||
|
|
129575dd23 | ||
|
|
ba8d2691fc | ||
|
|
f3d3a59ca0 | ||
|
|
b653fc7db1 | ||
|
|
71ee03a531 | ||
|
|
4a2493ff2e | ||
|
|
0b20111824 | ||
|
|
449977e63b | ||
|
|
b0504cbef5 | ||
|
|
fccb395564 | ||
|
|
b510417541 | ||
|
|
0330467874 | ||
|
|
4c15a46cda | ||
|
|
d59f8d1227 | ||
|
|
bbc8a3137b | ||
|
|
951c038f19 | ||
|
|
1b0c5af86e | ||
|
|
a113bd4192 | ||
|
|
d3c23345da | ||
|
|
2e1b3d0f83 | ||
|
|
58b39f47aa | ||
|
|
dc0f8cf296 | ||
|
|
3d34d1ca40 |
167
.clang-format
Normal file
@@ -0,0 +1,167 @@
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
AccessModifierOffset: -1
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveMacros: false
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: WithoutElse
|
||||
AllowShortLoopsOnASingleLine: true
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DeriveLineEnding: false
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^<ext/.*\.h>'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
- Regex: '^<.*'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentCaseLabels: true
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 2
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Never
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 1
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 200
|
||||
PointerAlignment: Left
|
||||
RawStringFormats:
|
||||
- Language: Cpp
|
||||
Delimiters:
|
||||
- cc
|
||||
- CC
|
||||
- cpp
|
||||
- Cpp
|
||||
- CPP
|
||||
- 'c++'
|
||||
- 'C++'
|
||||
CanonicalDelimiter: ''
|
||||
BasedOnStyle: google
|
||||
- Language: TextProto
|
||||
Delimiters:
|
||||
- pb
|
||||
- PB
|
||||
- proto
|
||||
- PROTO
|
||||
EnclosingFunctions:
|
||||
- EqualsProto
|
||||
- EquivToProto
|
||||
- PARSE_PARTIAL_TEXT_PROTO
|
||||
- PARSE_TEST_PROTO
|
||||
- PARSE_TEXT_PROTO
|
||||
- ParseTextOrDie
|
||||
- ParseTextProtoOrDie
|
||||
CanonicalDelimiter: ''
|
||||
BasedOnStyle: google
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
SpacesInAngles: false
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
Standard: Auto
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
...
|
||||
37
.gitattributes
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# Set default behavior to automatically normalize line endings (LF on check-in).
|
||||
* text=auto
|
||||
|
||||
# Force batch scripts to always use CRLF line endings so that if a repo is accessed
|
||||
# in Windows via a file share from Linux, the scripts will work.
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
*.{ics,[iI][cC][sS]} text eol=crlf
|
||||
|
||||
# Force bash scripts to always use LF line endings so that if a repo is accessed
|
||||
# in Unix via a file share from Windows, the scripts will work.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Ensure Spotless does not try to use CRLF line endings on Windows in the local repo.
|
||||
*.gradle text eol=lf
|
||||
*.java text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.h text eol=lf
|
||||
*.hpp text eol=lf
|
||||
*.inc text eol=lf
|
||||
*.inl text eol=lf
|
||||
*.cpp text eol=lf
|
||||
|
||||
# Frontend Files
|
||||
*.js text eol=lf
|
||||
*.vue text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.so binary
|
||||
*.dll binary
|
||||
*.webp binary
|
||||
1
.github/CODEOWNERS
vendored
@@ -1,3 +1,2 @@
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
* @PhotonVision/program-devs
|
||||
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is and what the expected behavior should have been.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Screenshots / Videos**
|
||||
If applicable, add screenshots to help explain your problem. Additionally, provide journalctl logs and settings zip export.
|
||||
|
||||
**Platform:**
|
||||
- Hardware Platform (ex. Raspberry Pi 4, Windows x64):
|
||||
- Network Configuration (Connection between the Radio and any devices in between, such as a Network Switch):
|
||||
- PhotonVision Version:
|
||||
- Browser (with Version) (Chrome, Edge, Firefox, etc.):
|
||||
- Camera(s) Used:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
324
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build-client:
|
||||
name: "PhotonClient Build"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build Production Client
|
||||
run: npm run build
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-client/dist/
|
||||
build-examples:
|
||||
name: "Build Examples"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
# Need to publish to maven local first, so that C++ sim can pick it up
|
||||
# Still haven't figured out how to make the vendordep file be copied before trying to build examples
|
||||
- name: Publish photonlib to maven local
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew publishtomavenlocal -x check
|
||||
- name: Build Java examples
|
||||
working-directory: photonlib-java-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew build -x check --max-workers 2
|
||||
- name: Build C++ examples
|
||||
working-directory: photonlib-cpp-examples
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew copyPhotonlib -x check
|
||||
./gradlew build -x check --max-workers 2
|
||||
build-gradle:
|
||||
name: "Gradle Build"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags --force
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Gradle Build
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:build photon-lib:build -x check --max-workers 2
|
||||
- name: Gradle Tests
|
||||
run: ./gradlew testHeadless -i --max-workers 1 --stacktrace
|
||||
- name: Gradle Coverage
|
||||
run: ./gradlew jacocoTestReport --max-workers 1
|
||||
- name: Publish Coverage Report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
- name: Publish Core Coverage Report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./photon-core/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
build-offline-docs:
|
||||
name: "Build Offline Docs"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: 'PhotonVision/photonvision-docs.git'
|
||||
ref: master
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
||||
pip install -r requirements.txt
|
||||
- name: Build the docs
|
||||
run: |
|
||||
make html
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: built-docs
|
||||
path: build/html
|
||||
build-photonlib-host:
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: 11
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2022
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
- os: macos-12
|
||||
artifact-name: macOS
|
||||
architecture: x64
|
||||
- os: ubuntu-22.04
|
||||
artifact-name: Linux
|
||||
|
||||
name: "Photonlib - Build Host - ${{ matrix.artifact-name }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: git fetch --tags --force
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:build --max-workers 1
|
||||
- run: ./gradlew photon-lib:publish photon-targeting:publish
|
||||
name: Publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push'
|
||||
build-photonlib-docker:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- container: wpilib/roborio-cross-ubuntu:2024-22.04
|
||||
artifact-name: Athena
|
||||
- container: wpilib/raspbian-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Raspbian
|
||||
- container: wpilib/aarch64-cross-ubuntu:bullseye-22.04
|
||||
artifact-name: Aarch64
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
container: ${{ matrix.container }}
|
||||
name: "Photonlib - Build Docker - ${{ matrix.artifact-name }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Config Git
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/photonvision/photonvision
|
||||
- name: Build PhotonLib
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:build --max-workers 1
|
||||
- name: Publish
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-lib:publish
|
||||
env:
|
||||
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
|
||||
if: github.event_name == 'push'
|
||||
build-package:
|
||||
needs: [build-client, build-gradle, build-offline-docs]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
artifact-name: Win64
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: macos-latest
|
||||
artifact-name: macOS
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: ubuntu-latest
|
||||
artifact-name: Linux
|
||||
architecture: x64
|
||||
arch-override: none
|
||||
- os: macos-latest
|
||||
artifact-name: macOSArm
|
||||
architecture: x64
|
||||
arch-override: macarm64
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm32
|
||||
architecture: x64
|
||||
arch-override: linuxarm32
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
architecture: x64
|
||||
arch-override: linuxarm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build fat JAR - ${{ matrix.artifact-name }}"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: |
|
||||
rm -rf photon-server/src/main/resources/web/*
|
||||
mkdir -p photon-server/src/main/resources/web/docs
|
||||
if: ${{ (matrix.os) != 'windows-latest' }}
|
||||
- run: |
|
||||
del photon-server\src\main\resources\web\*.*
|
||||
mkdir photon-server\src\main\resources\web\docs
|
||||
if: ${{ (matrix.os) == 'windows-latest' }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-server/src/main/resources/web/
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar --max-workers 2 -PArchOverride=${{ matrix.arch-override }}
|
||||
if: ${{ (matrix.arch-override != 'none') }}
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew photon-server:shadowJar --max-workers 2
|
||||
if: ${{ (matrix.arch-override == 'none') }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
path: photon-server/build/libs
|
||||
build-image:
|
||||
needs: [build-package]
|
||||
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: RaspberryPi
|
||||
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.1.3_arm64
|
||||
- os: ubuntu-latest
|
||||
artifact-name: LinuxArm64
|
||||
image_suffix: limelight2
|
||||
image_url: https://api.github.com/repos/photonvision/photon-pi-gen/releases/tags/v2023.2.2_limelight-arm64
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "Build image - ${{ matrix.image_url }}"
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: jar-${{ matrix.artifact-name }}
|
||||
- name: Generate image
|
||||
run: |
|
||||
chmod +x scripts/generatePiImage.sh
|
||||
./scripts/generatePiImage.sh ${{ matrix.image_url }} ${{ matrix.image_suffix }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
name: Upload image
|
||||
with:
|
||||
name: image-${{ matrix.image_suffix }}
|
||||
path: photonvision*.xz
|
||||
release:
|
||||
needs: [build-package, build-image]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# Download literally every single artifact. This also downloads client and docs,
|
||||
# but the filtering below won't pick these up (I hope)
|
||||
- uses: actions/download-artifact@v2
|
||||
- run: find
|
||||
# Push to dev release
|
||||
- uses: pyTooling/Actions/releaser@r0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: 'Dev'
|
||||
rm: true
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
if: github.event_name == 'push'
|
||||
# Upload all jars and xz archives
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
**/*.xz
|
||||
**/*.jar
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
88
.github/workflows/lint-format.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
name: Lint and Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
wpiformat:
|
||||
name: "wpiformat"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install wpiformat
|
||||
run: pip3 install wpiformat
|
||||
- name: Run
|
||||
run: wpiformat
|
||||
- name: Check output
|
||||
run: git --no-pager diff --exit-code HEAD
|
||||
- name: Generate diff
|
||||
run: git diff HEAD > wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wpiformat fixes
|
||||
path: wpiformat-fixes.patch
|
||||
if: ${{ failure() }}
|
||||
javaformat:
|
||||
name: "Java Formatting"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
|
||||
client-lint-format:
|
||||
name: "PhotonClient Lint and Formatting"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check Linting
|
||||
run: npm run lint-ci
|
||||
- name: Check Formatting
|
||||
run: npm run format-ci
|
||||
server-index:
|
||||
name: "Check server index.html not changed"
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch all history and metadata
|
||||
run: |
|
||||
git fetch --prune --unshallow
|
||||
git checkout -b pr
|
||||
git branch -f master origin/master
|
||||
- name: Check index.html not changed
|
||||
run: git --no-pager diff --exit-code origin/master photon-server/src/main/resources/web/index.html
|
||||
222
.github/workflows/main.yml
vendored
@@ -1,222 +0,0 @@
|
||||
# This workflow builds the client (UI), the server, builds the JAR.
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||
# events but only for the master branch
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
# This job builds the client (web view).
|
||||
build-client:
|
||||
|
||||
# Let all steps run within the photon-client dir.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-client
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Grab the docker container.
|
||||
container:
|
||||
image: docker://node:10
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Setup Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
|
||||
# Run npm
|
||||
- run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
|
||||
# Upload client artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-client/dist/
|
||||
|
||||
build-server:
|
||||
# Let all steps run within the photon-server dir.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-server
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- name: Install Java 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Run Gradle build.
|
||||
- name: Gradle Build
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
./gradlew build -x check
|
||||
|
||||
# Run Tests Generate Coverage Report.
|
||||
- name: Gradle Test and Coverage
|
||||
run: ./gradlew jacocoTestReport
|
||||
|
||||
# Publish Coverage Report.
|
||||
- name: Publish Coverage Report
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./photon-server/build/reports/jacoco/test/jacocoTestReport.xml
|
||||
|
||||
build-offline-docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout docs.
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'PhotonVision/photonvision-docs.git'
|
||||
ref: master
|
||||
|
||||
# Install Python.
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install sphinx sphinx_rtd_theme sphinx-tabs sphinxext-opengraph doc8
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Check the docs
|
||||
run: |
|
||||
make linkcheck
|
||||
make lint
|
||||
|
||||
- name: Build the docs
|
||||
run: |
|
||||
make html
|
||||
|
||||
# Upload docs artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: built-docs
|
||||
path: build/html
|
||||
|
||||
build-package:
|
||||
needs: [build-client, build-server, build-offline-docs]
|
||||
|
||||
# Let all steps run within the photon-server dir.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-server
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Clear any existing web resources.
|
||||
- run: |
|
||||
rm -rf src/main/resources/web/*
|
||||
mkdir -p src/main/resources/web/docs
|
||||
|
||||
# Download client artifact to resources folder.
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: built-client
|
||||
path: photon-server/src/main/resources/web/
|
||||
|
||||
# Download docs artifact to resources folder.
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: built-docs
|
||||
path: photon-server/src/main/resources/web/docs
|
||||
|
||||
|
||||
# Print folder contents.
|
||||
- run: ls
|
||||
working-directory: photon-server/src/main/resources/web/
|
||||
|
||||
# Build fat jar.
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew shadowJar
|
||||
working-directory: photon-server
|
||||
|
||||
# Upload final fat jar as artifact.
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: jar
|
||||
path: photon-server/build/libs
|
||||
|
||||
- uses: eine/tip@master
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: 'Dev'
|
||||
rm: true
|
||||
files: |
|
||||
photon-server/build/libs/*.jar
|
||||
if: github.event_name == 'push'
|
||||
|
||||
check-lint:
|
||||
# Let all steps run within the photon-server dir.
|
||||
defaults:
|
||||
run:
|
||||
working-directory: photon-server
|
||||
|
||||
# The type of runner that the job will run on.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout code.
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
# Install Java 11.
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
# Check server code with Spotless.
|
||||
- run: |
|
||||
chmod +x gradlew
|
||||
./gradlew spotlessCheck
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build-package]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: jar
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: '**/*'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
54
.gitignore
vendored
@@ -10,6 +10,8 @@ Python/app/handlers/__pycache__/
|
||||
|
||||
\.vscode/
|
||||
|
||||
/.vs
|
||||
|
||||
backend/settings/
|
||||
/.vscode/
|
||||
# Compiled class file
|
||||
@@ -30,6 +32,7 @@ backend/settings/
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.xz
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
@@ -104,17 +107,24 @@ fabric.properties
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
|
||||
photon-server/.gradle
|
||||
photon-server/target
|
||||
photon-server/src/main/java/META-INF
|
||||
photon-server/.settings
|
||||
photon-server/.classpath
|
||||
photon-server/.project
|
||||
photon-server/settings
|
||||
photon-server/dependency-reduced-pom.xml
|
||||
# Temporary build files
|
||||
**/.gradle
|
||||
**/target
|
||||
**/src/main/java/META-INF
|
||||
**/.settings
|
||||
**/.classpath
|
||||
**/.project
|
||||
**/dependency-reduced-pom.xml
|
||||
# photon-server/photon-vision.iml
|
||||
|
||||
# compile_commands
|
||||
compile_commands.json
|
||||
|
||||
# clang configuration and clangd cache
|
||||
.clang
|
||||
.clangd/
|
||||
.cache/
|
||||
|
||||
New client/photon-client/*
|
||||
|
||||
*.prefs
|
||||
@@ -126,3 +136,29 @@ photon-server/photon-vision
|
||||
photon-server/src/main/resources/web
|
||||
photon-server/src/main/java/org/photonvision/PhotonVersion.java
|
||||
photon-server/src/main/generated/native/include/org_photonvision_raspi_PicamJNI.h
|
||||
*.bin
|
||||
.gradle
|
||||
.gradle/*
|
||||
photonvision_config
|
||||
build/spotlessJava
|
||||
build/*
|
||||
build
|
||||
photon-lib/src/main/java/org/photonvision/PhotonVersion.java
|
||||
photon-lib/bin/main/images/*
|
||||
/photonlib-java-examples/bin/
|
||||
photon-lib/src/generate/native/include/PhotonVersion.h
|
||||
.gitattributes
|
||||
lib/*
|
||||
photon-server/lib/libapriltag.so
|
||||
photon-server/bin/main/nativelibraries/apriltag/*
|
||||
photon-server/src/main/resources/nativelibraries/apriltag/*
|
||||
|
||||
photonlib-java-examples/*/vendordeps/*
|
||||
photonlib-cpp-examples/*/vendordeps/*
|
||||
|
||||
*/networktables.json
|
||||
*/networktables.json.bck
|
||||
photonlib-cpp-examples/*/networktables.json.bck
|
||||
photonlib-java-examples/*/networktables.json.bck
|
||||
*.sqlite
|
||||
photon-server/src/main/resources/web/index.html
|
||||
|
||||
33
.styleguide
Normal file
@@ -0,0 +1,33 @@
|
||||
cppHeaderFileInclude {
|
||||
\.h$
|
||||
\.hpp$
|
||||
\.inc$
|
||||
\.inl$
|
||||
}
|
||||
|
||||
cppSrcFileInclude {
|
||||
\.cpp$
|
||||
}
|
||||
|
||||
modifiableFileExclude {
|
||||
\.jpg$
|
||||
\.jpeg$
|
||||
\.png$
|
||||
\.gif$
|
||||
\.so$
|
||||
\.dll$
|
||||
\.webp$
|
||||
\.ico$
|
||||
gradlew
|
||||
}
|
||||
|
||||
includeProject {
|
||||
^photonLib/
|
||||
}
|
||||
|
||||
includeOtherLibs {
|
||||
^frc/
|
||||
^networktables/
|
||||
^units/
|
||||
^wpi/
|
||||
}
|
||||
@@ -14,4 +14,3 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
61
README.md
@@ -6,18 +6,65 @@ PhotonVision is the free, fast, and easy-to-use computer vision solution for the
|
||||
|
||||
A copy of the latest Raspberry Pi image is available [here](https://github.com/PhotonVision/photon-pi-gen/releases). A copy of the latest standalone JAR is available [here](https://github.com/PhotonVision/photonvision/releases). If you are a Gloworm user you can find the latest Gloworm image [here](https://github.com/gloworm-vision/pi-gen/releases).
|
||||
|
||||
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/other/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
|
||||
If you are interested in contributing code or documentation to the project, please [read our getting started page for contributors](https://docs.photonvision.org/en/latest/docs/contributing/index.html) and **[join the Discord](https://discord.gg/wYxTwym) to introduce yourself!** We hope to provide a welcoming community to anyone who is interested in helping.
|
||||
|
||||
## Authors
|
||||
|
||||
A list of contributors is available in our documentation on ReadTheDocs.
|
||||
<a href="https://github.com/PhotonVision/photonvision/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=PhotonVision/photonvision" />
|
||||
</a>
|
||||
|
||||
## Gradle Arguments
|
||||
|
||||
Note that these are case sensitive!
|
||||
|
||||
* `-PArchOverride=foobar`: builds for a target system other than your current architecture. Valid overrides are:
|
||||
* linuxathena
|
||||
* linuxarm32
|
||||
* linuxarm64
|
||||
* arm32
|
||||
* arm64
|
||||
* x86-64
|
||||
* x86
|
||||
- `-PtgtIp`: Specifies where `./gradlew deploy` should try to copy the fat JAR to
|
||||
- `-Pprofile`: enables JVM profiling
|
||||
|
||||
## Building
|
||||
|
||||
Gradle is used for all C++ and Java code, and NPM is used for the web UI. Instructions to compile PhotonVision yourself can be found [in our docs](https://docs.photonvision.org/en/latest/docs/contributing/photonvision/build-instructions.html?highlight=npm%20install#compiling-instructions).
|
||||
|
||||
You can run one of the many built in examples straight from the command line, too! They contain a fully featured robot project, and some include simulation support. The projects can be found inside the `photonlib-java-examples` and `photonlib-cpp-examples` subdirectories, respectively. The projects currently available include:
|
||||
|
||||
- photonlib-java-examples:
|
||||
- aimandrange:simulateJava
|
||||
- aimattarget:simulateJava
|
||||
- getinrange:simulateJava
|
||||
- simaimandrange:simulateJava
|
||||
- simposeest:simulateJava
|
||||
- photonlib-cpp-examples:
|
||||
- aimandrange:simulateNative
|
||||
- getinrange:simulateNative
|
||||
|
||||
To run them, use the commands listed below. Photonlib must first be published to your local maven repository, then the `copyPhotonlib` task will copy the generated vendordep json file into each example. After that, the simulateJava/simulateNative task can be used like a normal robot project. Robot simulation with attached debugger is technically possible by using simulateExternalJava and modifying the launch script it exports, though unsupported.
|
||||
|
||||
```
|
||||
~/photonvision$ ./gradlew publishToMavenLocal
|
||||
|
||||
~/photonvision$ cd photonlib-java-examples
|
||||
~/photonvision/photonlib-java-examples$ ./gradlew copyPhotonlib
|
||||
~/photonvision/photonlib-java-examples$ ./gradlew <example-name>:simulateJava
|
||||
|
||||
~/photonvision$ cd photonlib-cpp-examples
|
||||
~/photonvision/photonlib-cpp-examples$ ./gradlew copyPhotonlib
|
||||
~/photonvision/photonlib-cpp-examples$ ./gradlew <example-name>:simulateNative
|
||||
```
|
||||
|
||||
|
||||
## Acknowledgments
|
||||
PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vision/chameleon-vision/). Thank you to everyone who worked on the original project.
|
||||
|
||||
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
* [WPILib](https://github.com/wpilibsuite) - Specifically [cscore](https://github.com/wpilibsuite/allwpilib/tree/master/cscore), [CameraServer](https://github.com/wpilibsuite/allwpilib/tree/master/cameraserver), [NTCore](https://github.com/wpilibsuite/allwpilib/tree/master/ntcore), and [OpenCV](https://github.com/wpilibsuite/thirdparty-opencv).
|
||||
|
||||
* [Apache Commons](https://commons.apache.org/) - Specifically [Commons Math](https://commons.apache.org/proper/commons-math/), and [Commons Lang](https://commons.apache.org/proper/commons-lang/)
|
||||
|
||||
@@ -27,5 +74,11 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis
|
||||
|
||||
* [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson)
|
||||
|
||||
## License
|
||||
## License
|
||||
PhotonVision is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
||||
## Meeting Notes
|
||||
Our meeting notes can be found in the wiki section of this repository.
|
||||
|
||||
* [2020 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2020-Meeting-Notes)
|
||||
* [2021 Meeting Notes](https://github.com/PhotonVision/photonvision/wiki/2021-Meeting-Notes)
|
||||
|
||||
96
build.gradle
Normal file
@@ -0,0 +1,96 @@
|
||||
plugins {
|
||||
id "com.diffplug.spotless" version "6.22.0"
|
||||
id "edu.wpi.first.NativeUtils" version "2024.2.0" apply false
|
||||
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
|
||||
id "edu.wpi.first.GradleRIO" version "2024.1.1-beta-3"
|
||||
id 'edu.wpi.first.WpilibTools' version '1.3.0'
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = "https://maven.photonvision.org/repository/internal/" }
|
||||
}
|
||||
wpilibRepositories.addAllReleaseRepositories(it)
|
||||
wpilibRepositories.addAllDevelopmentRepositories(it)
|
||||
}
|
||||
|
||||
// Configure the version number.
|
||||
apply from: "versioningHelper.gradle"
|
||||
|
||||
ext {
|
||||
wpilibVersion = "2024.1.1-beta-3"
|
||||
openCVversion = "4.8.0-2"
|
||||
joglVersion = "2.4.0-rc-20200307"
|
||||
javalinVersion = "5.6.2"
|
||||
frcYear = "2024"
|
||||
|
||||
pubVersion = versionString
|
||||
isDev = pubVersion.startsWith("dev")
|
||||
|
||||
// A list, for legacy reasons, with only the current platform contained
|
||||
String nativeName = wpilibTools.platformMapper.currentPlatform.platformName;
|
||||
if (nativeName == "linuxx64") nativeName = "linuxx86-64";
|
||||
if (nativeName == "winx64") nativeName = "windowsx86-64";
|
||||
if (nativeName == "macx64") nativeName = "osxx86-64";
|
||||
if (nativeName == "macarm64") nativeName = "osxarm64";
|
||||
jniPlatform = nativeName
|
||||
println("Building for platform " + jniPlatform)
|
||||
}
|
||||
|
||||
wpilibTools.deps.wpilibVersion = wpilibVersion
|
||||
|
||||
// Tell gradlerio what version of things to use (that we care about)
|
||||
// See: https://github.com/wpilibsuite/GradleRIO/blob/main/src/main/java/edu/wpi/first/gradlerio/wpi/WPIVersionsExtension.java
|
||||
wpi.getVersions().getOpencvVersion().convention(openCVversion);
|
||||
wpi.getVersions().getWpilibVersion().convention(wpilibVersion);
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target fileTree('.') {
|
||||
include '**/*.java'
|
||||
exclude '**/build/**', '**/build-*/**', "photon-core\\src\\main\\java\\org\\photonvision\\PhotonVersion.java", "photon-lib\\src\\main\\java\\org\\photonvision\\PhotonVersion.java"
|
||||
}
|
||||
toggleOffOn()
|
||||
googleJavaFormat()
|
||||
indentWithTabs(2)
|
||||
indentWithSpaces(4)
|
||||
removeUnusedImports()
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
groovyGradle {
|
||||
target fileTree('.') {
|
||||
include '**/*.gradle'
|
||||
exclude '**/build/**', '**/build-*/**'
|
||||
}
|
||||
greclipse()
|
||||
indentWithSpaces(4)
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
format 'xml', {
|
||||
target fileTree('.') {
|
||||
include '**/*.xml'
|
||||
exclude '**/build/**', '**/build-*/**', "**/.idea/**"
|
||||
}
|
||||
eclipseWtp('xml')
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces(2)
|
||||
endWithNewline()
|
||||
}
|
||||
format 'misc', {
|
||||
target fileTree('.') {
|
||||
include '**/*.md', '**/.gitignore'
|
||||
exclude '**/build/**', '**/build-*/**'
|
||||
}
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces(2)
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion '8.4'
|
||||
}
|
||||
@@ -2,4 +2,4 @@ coverage:
|
||||
# Turning off commit status to prevent failed checks if coverage decreases
|
||||
status:
|
||||
project: no
|
||||
patch: no
|
||||
patch: no
|
||||
|
||||
8
gradle.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
# The --add-exports flags work around a bug with spotless and JDK 17
|
||||
# https://github.com/diffplug/spotless/issues/834
|
||||
org.gradle.jvmargs= \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
|
||||
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=permwrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=permwrapper/dists
|
||||
249
gradlew
vendored
Executable file
@@ -0,0 +1,249 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
gradlew.bat
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,2 +0,0 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
@@ -1,17 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/recommended',
|
||||
'eslint:recommended'
|
||||
],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
}
|
||||
};
|
||||
21
photon-client/.eslintrc.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-typescript",
|
||||
"@vue/eslint-config-prettier/skip-formatting"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": ["error", "double"],
|
||||
"comma-dangle": ["error", "never"],
|
||||
"comma-spacing": ["error", { "before": false, "after": true }],
|
||||
"semi": ["error", "always"],
|
||||
"eol-last": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"quote-props": ["error", "as-needed"],
|
||||
"no-case-declarations": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-on-event-hyphenation": "off"
|
||||
}
|
||||
}
|
||||
27
photon-client/.gitignore
vendored
@@ -1,21 +1,28 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
components.d.ts
|
||||
|
||||
1
photon-client/.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
src/assets/fonts/PromptRegular.ts
|
||||
8
photon-client/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# PhotonVision Client UI
|
||||
|
||||
## Install Node.js
|
||||
|
||||
Follow [this](https://nodejs.org/en/) link.
|
||||
|
||||
## Project setup
|
||||
Run this one time, this command downloades the packages the UI uses and it might take a short while
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
Run this every development session, this command auto-builds the UI after every change you make
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See Node.js' [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
};
|
||||
1
photon-client/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
photon-client/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Photon Client</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
28238
photon-client/package-lock.json
generated
@@ -1,40 +1,47 @@
|
||||
{
|
||||
"name": "photon-client",
|
||||
"version": "3.0.0",
|
||||
"name": "photonclient",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "run-p build-only",
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/",
|
||||
"lint-ci": "eslint . --max-warnings 0 --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format-ci": "prettier --check src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@femessage/log-viewer": "^1.4.2",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^2.6.11",
|
||||
"downloadjs": "^1.4.7",
|
||||
"material-design-icons-iconfont": "^5.0.1",
|
||||
"msgpack5": "^4.2.1",
|
||||
"vue": "^2.6.12",
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-native-websocket": "git+https://github.com/PhotonVision/vue-native-websocket.git#7a32791",
|
||||
"vue-router": "^3.4.3",
|
||||
"vuetify": "^2.3.10",
|
||||
"vuex": "^3.5.1"
|
||||
"@fontsource/prompt": "^5.0.5",
|
||||
"@mdi/font": "^7.2.96",
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.4.0",
|
||||
"jspdf": "^2.5.1",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.154.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuetify": "^2.6.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/font": "^4.9.95",
|
||||
"@vue/cli-plugin-babel": "^3.12.1",
|
||||
"@vue/cli-plugin-eslint": "^4.5.4",
|
||||
"@vue/cli-service": "^4.5.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"papaparse": "^5.3.0",
|
||||
"sass": "^1.26.10",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue-cli-plugin-vuetify": "^0.6.3",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuetify-loader": "^1.6.0"
|
||||
"@rushstack/eslint-patch": "^1.3.2",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"prettier": "^3.0.0",
|
||||
"@types/node": "^16.11.45",
|
||||
"@types/three": "^0.154.0",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"sass": "~1.32",
|
||||
"sass-loader": "^13.3.2",
|
||||
"terser": "^5.14.2",
|
||||
"typescript": "~4.7.4",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
BIN
photon-client/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||
<title>PhotonVision</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but PhotonVision doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,344 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
|
||||
import { inject } from "vue";
|
||||
import PhotonSidebar from "@/components/app/photon-sidebar.vue";
|
||||
import PhotonLogView from "@/components/app/photon-log-view.vue";
|
||||
import PhotonErrorSnackbar from "@/components/app/photon-error-snackbar.vue";
|
||||
|
||||
const websocket = new AutoReconnectingWebsocket(
|
||||
`ws://${inject("backendHost")}/websocket_data`,
|
||||
() => {
|
||||
useStateStore().$patch({ backendConnected: true });
|
||||
},
|
||||
(data) => {
|
||||
if (data.log !== undefined) {
|
||||
useStateStore().addLogFromWebsocket(data.log);
|
||||
}
|
||||
if (data.settings !== undefined) {
|
||||
useSettingsStore().updateGeneralSettingsFromWebsocket(data.settings);
|
||||
}
|
||||
if (data.cameraSettings !== undefined) {
|
||||
useCameraSettingsStore().updateCameraSettingsFromWebsocket(data.cameraSettings);
|
||||
}
|
||||
if (data.ntConnectionInfo !== undefined) {
|
||||
useStateStore().updateNTConnectionStatusFromWebsocket(data.ntConnectionInfo);
|
||||
}
|
||||
if (data.metrics !== undefined) {
|
||||
useSettingsStore().updateMetricsFromWebsocket(data.metrics);
|
||||
}
|
||||
if (data.updatePipelineResult !== undefined) {
|
||||
useStateStore().updateBackendResultsFromWebsocket(data.updatePipelineResult);
|
||||
}
|
||||
if (data.mutatePipelineSettings !== undefined && data.cameraIndex !== undefined) {
|
||||
useCameraSettingsStore().changePipelineSettingsInStore(data.mutatePipelineSettings, data.cameraIndex);
|
||||
}
|
||||
if (data.calibrationData !== undefined) {
|
||||
useStateStore().updateCalibrationStateValuesFromWebsocket(data.calibrationData);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
useStateStore().$patch({ backendConnected: false });
|
||||
}
|
||||
);
|
||||
|
||||
useStateStore().$patch({ websocket: websocket });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<!-- Although most of the app runs with the "light" theme, the navigation drawer needs to have white text and icons so it uses the dark theme-->
|
||||
<v-navigation-drawer
|
||||
dark
|
||||
app
|
||||
permanent
|
||||
:mini-variant="compact"
|
||||
color="primary"
|
||||
>
|
||||
<v-list>
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="compact ? 'pr-0 pl-0' : ''">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img
|
||||
v-if="!compact"
|
||||
class="logo"
|
||||
src="./assets/logoLarge.png"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="logo"
|
||||
src="./assets/logoSmall.png"
|
||||
>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
link
|
||||
to="dashboard"
|
||||
@click="rollbackPipelineIndex()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
ref="camerasTabOpener"
|
||||
link
|
||||
to="cameras"
|
||||
@click="switchToDriverMode()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="settings"
|
||||
@click="switchToSettingsTab()"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-settings</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="docs"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="this.$vuetify.breakpoint.mdAndUp"
|
||||
link
|
||||
@click.stop="toggleCompactMode"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
mdi-chevron-left
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Advanced Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item style="position: absolute; bottom: 0; left: 0;">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="$store.state.backendConnected">
|
||||
mdi-wifi
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
class="pulse"
|
||||
style="border-radius: 100%;"
|
||||
>
|
||||
mdi-wifi-off
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ $store.state.backendConnected ? "Connected" : "Trying to connect..." }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<photon-sidebar />
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
>
|
||||
<v-container class="main-container" fluid fill-height>
|
||||
<v-layout>
|
||||
<v-flex>
|
||||
<router-view @switch-to-cameras="switchToDriverMode" />
|
||||
<router-view />
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-dialog
|
||||
v-model="$store.state.logsOverlay"
|
||||
width="1500"
|
||||
dark
|
||||
>
|
||||
<logs />
|
||||
</v-dialog>
|
||||
<photon-log-view />
|
||||
<photon-error-snackbar />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Logs from "./views/LogsView"
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Logs
|
||||
},
|
||||
data: () => ({
|
||||
// Used so that we can switch back to the previously selected pipeline after camera calibration
|
||||
previouslySelectedIndices: [],
|
||||
timer: undefined,
|
||||
}),
|
||||
computed: {
|
||||
compact: {
|
||||
get() {
|
||||
if (this.$store.state.compactMode === undefined) {
|
||||
return this.$vuetify.breakpoint.smAndDown;
|
||||
} else {
|
||||
return this.$store.state.compactMode || this.$vuetify.breakpoint.smAndDown;
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
// compactMode is the user's preference for compact mode; it overrides screen size
|
||||
this.$store.commit("compactMode", value);
|
||||
localStorage.setItem("compactMode", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
document.addEventListener("keydown", e => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
this.$store.state.logsOverlay = !this.$store.state.logsOverlay;
|
||||
break;
|
||||
case "z":
|
||||
if (e.ctrlKey && this.$store.getters.canUndo) {
|
||||
this.$store.dispatch('undo', {vm: this});
|
||||
}
|
||||
break;
|
||||
case "y":
|
||||
if (e.ctrlKey && this.$store.getters.canRedo) {
|
||||
this.$store.dispatch('redo', {vm: this});
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
this.$options.sockets.onmessage = (data) => {
|
||||
try {
|
||||
let message = this.$msgPack.decode(data.data);
|
||||
for (let prop in message) {
|
||||
if (message.hasOwnProperty(prop)) {
|
||||
this.handleMessage(prop, message[prop]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error: ' + JSON.stringify(data.data) + " , " + error);
|
||||
}
|
||||
};
|
||||
this.$options.sockets.onopen = () => {
|
||||
this.$store.state.backendConnected = true;
|
||||
this.$store.state.connectedCallbacks.forEach(it => it())
|
||||
};
|
||||
|
||||
let closed = () => {
|
||||
this.$store.state.backendConnected = false;
|
||||
};
|
||||
this.$options.sockets.onclose = closed;
|
||||
this.$options.sockets.onerror = closed;
|
||||
|
||||
this.$connect();
|
||||
},
|
||||
methods: {
|
||||
handleMessage(key, value) {
|
||||
if (key === "logMessage") {
|
||||
this.logMessage(value["logMessage"], value["logLevel"]);
|
||||
} else if (key === "updatePipelineResult") {
|
||||
this.$store.commit('mutatePipelineResults', value)
|
||||
} else if (this.$store.state.hasOwnProperty(key)) {
|
||||
this.$store.commit(key, value);
|
||||
} else if (this.$store.getters.currentPipelineSettings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutatePipeline', {[key]: value});
|
||||
} else if (this.$store.state.settings.hasOwnProperty(key)) {
|
||||
this.$store.commit('mutateSettings', {[key]: value});
|
||||
} else {
|
||||
switch (key) {
|
||||
default: {
|
||||
console.error("Unknown message from backend: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleCompactMode() {
|
||||
this.compact = !this.compact;
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
logMessage(message, levelInt) {
|
||||
this.$store.commit('logString', {
|
||||
['level']: levelInt,
|
||||
['message']: message
|
||||
})
|
||||
},
|
||||
switchToDriverMode() {
|
||||
if (!this.previouslySelectedIndices) this.previouslySelectedIndices = [];
|
||||
|
||||
for (const [i, cameraSettings] of this.$store.state.cameraSettings.entries()) {
|
||||
this.previouslySelectedIndices[i] = cameraSettings.currentPipelineIndex;
|
||||
this.handleInputWithIndex('currentPipeline', -1, i);
|
||||
}
|
||||
},
|
||||
rollbackPipelineIndex()
|
||||
{
|
||||
if (this.previouslySelectedIndices !== null) {
|
||||
for (const [i] of this.$store.state.cameraSettings.entries()) {
|
||||
this.handleInputWithIndex('currentPipeline', this.previouslySelectedIndices[i] || 0, i);
|
||||
}
|
||||
}
|
||||
this.previouslySelectedIndices = null;
|
||||
}
|
||||
,
|
||||
switchToSettingsTab() {
|
||||
this.axios.post('http://' + this.$address + '/api/sendMetrics', {})
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@import "./scss/variables.scss"
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.pulse {
|
||||
animation: pulse-animation 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-animation {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 20px rgba(0, 0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #232c37;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Hacks */
|
||||
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
|
||||
.v-input {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
/* This is unfortunately the only way to override table background color */
|
||||
.theme--dark.v-data-table > .v-data-table__wrapper > table > tbody > tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
@import "vuetify/src/styles/settings/_variables";
|
||||
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@media #{map-get($display-breakpoints, 'md-and-down')} {
|
||||
html {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background-color: #232c37;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#title {
|
||||
color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
Before Width: | Height: | Size: 61 KiB |
@@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg865"
|
||||
sodipodi:docname="eyedropper.svg"
|
||||
inkscape:version="0.92.4 (unknown)">
|
||||
<metadata
|
||||
id="metadata871">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs869" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1040"
|
||||
id="namedview867"
|
||||
showgrid="false"
|
||||
inkscape:zoom="35.541667"
|
||||
inkscape:cx="12.112544"
|
||||
inkscape:cy="10.171169"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="1458"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg865" />
|
||||
<g
|
||||
id="g905"
|
||||
inkscape:export-xdpi="77.2733"
|
||||
inkscape:export-ydpi="77.2733">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path863"
|
||||
d="m 12.28,19.4725 -2.13,-2.13 1.42,-1.41 -7.71,-7.71 -1.86,-4.6 1.5,-1.5 4.6,1.86 7.71,7.71 1.41,-1.42 2.13,2.13 -7.07,7.07 m 8.72,-2.59 c 1.17,1.17 1.17,3.07 0,4.24 -1.17,1.17 -3.07,1.17 -4.24,0 l -1.92,-1.92 4.24,-4.24 1.92,1.92 m -14.03,-11.2 -2.47,-1.06 1.06,2.47 7.44,7.43 1.4,-1.4 z" />
|
||||
<path
|
||||
inkscape:export-ydpi="161.91951"
|
||||
inkscape:export-xdpi="161.91951"
|
||||
d="m 3.5996094,2.6132812 -1.109375,1.109375 1.7246094,4.2636719 7.6503902,7.6503909 a 0.41783994,0.41783994 0 0 1 0,0.591797 l -1.123046,1.115234 1.537109,1.539062 6.480469,-6.480468 -1.53711,-1.53711 -1.115234,1.123047 a 0.41783994,0.41783994 0 0 1 -0.591797,0 L 7.8652344,4.3378906 Z m 0.9101562,1.5917969 a 0.41783994,0.41783994 0 0 1 0.1542969,0.033203 L 7.1347656,5.296875 a 0.41783994,0.41783994 0 0 1 0.1308594,0.089844 l 7.429687,7.4414062 a 0.41783994,0.41783994 0 0 1 0,0.589844 l -1.40039,1.40039 a 0.41783994,0.41783994 0 0 1 -0.589844,0 L 5.265625,7.3867188 A 0.41783994,0.41783994 0 0 1 5.1757812,7.2558594 L 4.1152344,4.7871094 A 0.41783994,0.41783994 0 0 1 4.5097656,4.2050781 Z m 14.5703124,11.3476559 -3.65039,3.650391 1.625,1.625 c 1.010062,1.010062 2.640328,1.010062 3.65039,0 1.010062,-1.010062 1.010062,-2.640327 0,-3.650391 z"
|
||||
id="path889"
|
||||
style="fill:#ffffff;stroke-width:21.16535378;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
inkscape:original="M 3.5 2.1230469 L 2 3.6230469 L 3.859375 8.2226562 L 11.570312 15.931641 L 10.150391 17.341797 L 12.279297 19.472656 L 19.349609 12.402344 L 17.220703 10.273438 L 15.810547 11.693359 L 8.0996094 3.9824219 L 3.5 2.1230469 z M 4.5 4.6230469 L 6.9707031 5.6816406 L 14.400391 13.123047 L 13 14.523438 L 5.5605469 7.0917969 L 4.5 4.6230469 z M 19.080078 14.962891 L 14.839844 19.203125 L 16.759766 21.123047 C 17.929766 22.293047 19.83 22.293047 21 21.123047 C 22.17 19.953047 22.17 18.052813 21 16.882812 L 19.080078 14.962891 z "
|
||||
inkscape:radius="-0.41779816"
|
||||
sodipodi:type="inkscape:offset" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
1
photon-client/src/assets/fonts/PromptRegular.ts
Normal file
40
photon-client/src/assets/images/loading.svg
Normal file
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="margin: auto; background: rgb(255, 216, 68); display: block;"
|
||||
width="600px" height="412px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<circle cx="75" cy="50" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.9166666666666666s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.9166666666666666s"></animate>
|
||||
</circle><circle cx="71.65063509461098" cy="62.5" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.8333333333333334s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.8333333333333334s"></animate>
|
||||
</circle><circle cx="62.5" cy="71.65063509461096" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.75s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.75s"></animate>
|
||||
</circle><circle cx="50" cy="75" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.6666666666666666s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.6666666666666666s"></animate>
|
||||
</circle><circle cx="37.50000000000001" cy="71.65063509461098" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.5833333333333334s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.5833333333333334s"></animate>
|
||||
</circle><circle cx="28.34936490538903" cy="62.5" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.5s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.5s"></animate>
|
||||
</circle><circle cx="25" cy="50" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.4166666666666667s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.4166666666666667s"></animate>
|
||||
</circle><circle cx="28.34936490538903" cy="37.50000000000001" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.3333333333333333s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.3333333333333333s"></animate>
|
||||
</circle><circle cx="37.499999999999986" cy="28.349364905389038" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.25s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.25s"></animate>
|
||||
</circle><circle cx="49.99999999999999" cy="25" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.16666666666666666s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.16666666666666666s"></animate>
|
||||
</circle><circle cx="62.5" cy="28.349364905389034" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="-0.08333333333333333s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="-0.08333333333333333s"></animate>
|
||||
</circle><circle cx="71.65063509461096" cy="37.499999999999986" fill="#89b99a" r="5">
|
||||
<animate attributeName="r" values="2;2;4;2;2" times="0;0.1;0.2;0.3;1" dur="1s" repeatCount="indefinite" begin="0s"></animate>
|
||||
<animate attributeName="fill" values="#89b99a;#89b99a;#43a7ce;#89b99a;#89b99a" repeatCount="indefinite" times="0;0.1;0.2;0.3;1" dur="1s" begin="0s"></animate>
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
57
photon-client/src/assets/images/logoLarge.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1920 680" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-1986,-1146)">
|
||||
<g id="PhotonVision-Header-noBG" transform="matrix(1,0,0,0.62963,-0.666667,1146)">
|
||||
<rect x="1986.67" y="0" width="1920" height="1080" style="fill:none;"/>
|
||||
<g transform="matrix(1.87463,0,0,2.97735,-2372.42,-4617.27)">
|
||||
<g transform="matrix(136.163,0,0,136.163,2412.48,1719.32)">
|
||||
<path d="M0.061,0L0.061,-0.7L0.362,-0.7C0.409,-0.7 0.452,-0.689 0.491,-0.668C0.53,-0.647 0.561,-0.617 0.584,-0.58C0.607,-0.542 0.619,-0.498 0.619,-0.449C0.619,-0.4 0.607,-0.356 0.584,-0.318C0.561,-0.279 0.53,-0.249 0.491,-0.228C0.452,-0.207 0.409,-0.196 0.362,-0.196L0.229,-0.196L0.229,0L0.061,0ZM0.229,-0.337L0.343,-0.337C0.362,-0.337 0.38,-0.341 0.397,-0.35C0.413,-0.359 0.426,-0.371 0.436,-0.388C0.446,-0.405 0.451,-0.425 0.451,-0.448C0.451,-0.471 0.446,-0.491 0.436,-0.508C0.426,-0.525 0.413,-0.538 0.397,-0.547C0.38,-0.556 0.362,-0.56 0.343,-0.56L0.229,-0.56L0.229,-0.337Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2500.17,1719.32)">
|
||||
<path d="M0.061,0L0.061,-0.7L0.229,-0.7L0.229,-0.425L0.502,-0.425L0.502,-0.7L0.67,-0.7L0.67,0L0.502,0L0.502,-0.285L0.229,-0.285L0.229,0L0.061,0Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2599.7,1719.32)">
|
||||
<path d="M0.4,0.013C0.344,0.013 0.293,0.004 0.248,-0.015C0.203,-0.034 0.164,-0.06 0.131,-0.093C0.098,-0.126 0.073,-0.165 0.056,-0.209C0.038,-0.253 0.029,-0.3 0.029,-0.351C0.029,-0.402 0.038,-0.449 0.056,-0.493C0.073,-0.536 0.098,-0.575 0.131,-0.608C0.164,-0.641 0.203,-0.667 0.248,-0.686C0.293,-0.704 0.344,-0.713 0.4,-0.713C0.455,-0.713 0.506,-0.704 0.552,-0.686C0.597,-0.667 0.636,-0.641 0.669,-0.608C0.701,-0.575 0.726,-0.536 0.744,-0.493C0.762,-0.449 0.771,-0.402 0.771,-0.351C0.771,-0.3 0.762,-0.253 0.744,-0.209C0.726,-0.165 0.701,-0.126 0.669,-0.093C0.636,-0.06 0.597,-0.034 0.552,-0.015C0.506,0.004 0.455,0.013 0.4,0.013ZM0.4,-0.13C0.429,-0.13 0.457,-0.136 0.482,-0.147C0.506,-0.158 0.528,-0.173 0.546,-0.193C0.564,-0.213 0.578,-0.236 0.588,-0.264C0.598,-0.291 0.603,-0.32 0.603,-0.351C0.603,-0.382 0.598,-0.411 0.588,-0.438C0.578,-0.465 0.564,-0.488 0.546,-0.508C0.528,-0.528 0.506,-0.544 0.482,-0.555C0.457,-0.566 0.429,-0.571 0.4,-0.571C0.37,-0.571 0.343,-0.566 0.318,-0.555C0.293,-0.544 0.272,-0.528 0.254,-0.508C0.235,-0.488 0.221,-0.465 0.212,-0.438C0.202,-0.411 0.197,-0.382 0.197,-0.351C0.197,-0.32 0.202,-0.291 0.212,-0.264C0.221,-0.236 0.235,-0.213 0.254,-0.193C0.272,-0.173 0.293,-0.158 0.318,-0.147C0.343,-0.136 0.37,-0.13 0.4,-0.13Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2704.41,1719.32)">
|
||||
<path d="M0.421,0C0.378,0 0.341,-0.01 0.309,-0.029C0.276,-0.048 0.251,-0.073 0.233,-0.105C0.214,-0.137 0.205,-0.173 0.205,-0.212L0.205,-0.56L0.015,-0.56L0.015,-0.7L0.562,-0.7L0.562,-0.56L0.373,-0.56L0.373,-0.197C0.373,-0.182 0.378,-0.168 0.389,-0.157C0.4,-0.146 0.413,-0.14 0.429,-0.14L0.484,-0.14L0.484,0L0.421,0Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2778.89,1719.32)">
|
||||
<path d="M0.4,0.013C0.344,0.013 0.293,0.004 0.248,-0.015C0.203,-0.034 0.164,-0.06 0.131,-0.093C0.098,-0.126 0.073,-0.165 0.056,-0.209C0.038,-0.253 0.029,-0.3 0.029,-0.351C0.029,-0.402 0.038,-0.449 0.056,-0.493C0.073,-0.536 0.098,-0.575 0.131,-0.608C0.164,-0.641 0.203,-0.667 0.248,-0.686C0.293,-0.704 0.344,-0.713 0.4,-0.713C0.455,-0.713 0.506,-0.704 0.552,-0.686C0.597,-0.667 0.636,-0.641 0.669,-0.608C0.701,-0.575 0.726,-0.536 0.744,-0.493C0.762,-0.449 0.771,-0.402 0.771,-0.351C0.771,-0.3 0.762,-0.253 0.744,-0.209C0.726,-0.165 0.701,-0.126 0.669,-0.093C0.636,-0.06 0.597,-0.034 0.552,-0.015C0.506,0.004 0.455,0.013 0.4,0.013ZM0.4,-0.13C0.429,-0.13 0.457,-0.136 0.482,-0.147C0.506,-0.158 0.528,-0.173 0.546,-0.193C0.564,-0.213 0.578,-0.236 0.588,-0.264C0.598,-0.291 0.603,-0.32 0.603,-0.351C0.603,-0.382 0.598,-0.411 0.588,-0.438C0.578,-0.465 0.564,-0.488 0.546,-0.508C0.528,-0.528 0.506,-0.544 0.482,-0.555C0.457,-0.566 0.429,-0.571 0.4,-0.571C0.37,-0.571 0.343,-0.566 0.318,-0.555C0.293,-0.544 0.272,-0.528 0.254,-0.508C0.235,-0.488 0.221,-0.465 0.212,-0.438C0.202,-0.411 0.197,-0.382 0.197,-0.351C0.197,-0.32 0.202,-0.291 0.212,-0.264C0.221,-0.236 0.235,-0.213 0.254,-0.193C0.272,-0.173 0.293,-0.158 0.318,-0.147C0.343,-0.136 0.37,-0.13 0.4,-0.13Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2887.69,1719.32)">
|
||||
<path d="M0.058,0L0.058,-0.422C0.058,-0.482 0.071,-0.534 0.096,-0.577C0.121,-0.62 0.157,-0.654 0.202,-0.677C0.247,-0.7 0.298,-0.712 0.356,-0.712C0.414,-0.712 0.465,-0.7 0.51,-0.677C0.555,-0.654 0.59,-0.62 0.616,-0.577C0.641,-0.534 0.654,-0.482 0.654,-0.422L0.654,0L0.488,0L0.488,-0.431C0.488,-0.457 0.483,-0.481 0.471,-0.502C0.461,-0.523 0.445,-0.54 0.426,-0.552C0.407,-0.564 0.383,-0.57 0.356,-0.57C0.329,-0.57 0.306,-0.564 0.286,-0.552C0.266,-0.54 0.251,-0.523 0.24,-0.502C0.229,-0.481 0.224,-0.457 0.224,-0.431L0.224,0L0.058,0Z" style="fill:rgb(255,216,67);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(2.05664,0,0,3.26643,-2793.17,-4723.02)">
|
||||
<g transform="matrix(136.163,0,0,136.163,2412.48,1719.32)">
|
||||
<path d="M0.341,0.012C0.314,0.012 0.29,0.005 0.269,-0.008C0.248,-0.021 0.231,-0.043 0.22,-0.072L0.001,-0.7L0.133,-0.7L0.319,-0.121C0.322,-0.114 0.325,-0.109 0.328,-0.106C0.331,-0.102 0.336,-0.1 0.341,-0.1C0.346,-0.1 0.351,-0.102 0.355,-0.106C0.358,-0.109 0.361,-0.114 0.363,-0.121L0.555,-0.7L0.681,-0.7L0.462,-0.072C0.452,-0.044 0.436,-0.023 0.414,-0.009C0.393,0.005 0.368,0.012 0.341,0.012Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2505.34,1719.32)">
|
||||
<path d="M0.052,0L0.052,-0.105L0.182,-0.105L0.182,-0.595L0.052,-0.595L0.052,-0.7L0.432,-0.7L0.432,-0.595L0.302,-0.595L0.302,-0.105L0.432,-0.105L0.432,0L0.052,0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2571.25,1719.32)">
|
||||
<path d="M0.071,0L0.071,-0.105L0.372,-0.105C0.391,-0.105 0.408,-0.11 0.423,-0.119C0.437,-0.128 0.448,-0.141 0.457,-0.156C0.465,-0.171 0.469,-0.186 0.469,-0.203C0.469,-0.221 0.465,-0.237 0.457,-0.252C0.449,-0.267 0.438,-0.278 0.424,-0.287C0.409,-0.296 0.393,-0.3 0.374,-0.3L0.249,-0.3C0.21,-0.3 0.174,-0.308 0.143,-0.325C0.113,-0.341 0.088,-0.364 0.07,-0.393C0.052,-0.422 0.043,-0.457 0.043,-0.497C0.043,-0.537 0.052,-0.572 0.069,-0.603C0.086,-0.633 0.11,-0.657 0.14,-0.674C0.17,-0.691 0.204,-0.7 0.242,-0.7L0.543,-0.7L0.543,-0.595L0.253,-0.595C0.236,-0.595 0.22,-0.591 0.206,-0.582C0.192,-0.573 0.181,-0.562 0.173,-0.548C0.166,-0.534 0.162,-0.519 0.162,-0.503C0.162,-0.486 0.166,-0.471 0.173,-0.458C0.181,-0.444 0.192,-0.433 0.205,-0.425C0.219,-0.416 0.235,-0.412 0.252,-0.412L0.38,-0.412C0.423,-0.412 0.46,-0.404 0.491,-0.387C0.522,-0.37 0.546,-0.347 0.563,-0.318C0.58,-0.289 0.588,-0.255 0.588,-0.217C0.588,-0.172 0.579,-0.134 0.562,-0.102C0.544,-0.069 0.52,-0.044 0.49,-0.027C0.459,-0.009 0.425,0 0.387,0L0.071,0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2656.62,1719.32)">
|
||||
<path d="M0.052,0L0.052,-0.105L0.182,-0.105L0.182,-0.595L0.052,-0.595L0.052,-0.7L0.432,-0.7L0.432,-0.595L0.302,-0.595L0.302,-0.105L0.432,-0.105L0.432,0L0.052,0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2722.52,1719.32)">
|
||||
<path d="M0.401,0.013C0.346,0.013 0.296,0.003 0.251,-0.016C0.205,-0.035 0.167,-0.061 0.136,-0.094C0.104,-0.127 0.079,-0.166 0.062,-0.21C0.045,-0.254 0.036,-0.301 0.036,-0.351C0.036,-0.401 0.045,-0.448 0.062,-0.492C0.079,-0.535 0.104,-0.574 0.136,-0.607C0.167,-0.64 0.205,-0.666 0.251,-0.685C0.296,-0.704 0.346,-0.713 0.402,-0.713C0.457,-0.713 0.507,-0.704 0.552,-0.685C0.597,-0.666 0.635,-0.64 0.667,-0.607C0.699,-0.574 0.724,-0.535 0.741,-0.492C0.758,-0.448 0.767,-0.401 0.767,-0.351C0.767,-0.301 0.758,-0.254 0.741,-0.21C0.724,-0.166 0.699,-0.127 0.667,-0.094C0.635,-0.061 0.597,-0.035 0.552,-0.016C0.507,0.003 0.457,0.013 0.401,0.013ZM0.401,-0.093C0.436,-0.093 0.469,-0.099 0.499,-0.112C0.529,-0.125 0.555,-0.143 0.578,-0.167C0.6,-0.19 0.617,-0.218 0.629,-0.249C0.641,-0.28 0.647,-0.314 0.647,-0.351C0.647,-0.388 0.641,-0.422 0.629,-0.453C0.617,-0.484 0.6,-0.511 0.578,-0.535C0.555,-0.558 0.529,-0.576 0.499,-0.589C0.469,-0.602 0.436,-0.608 0.401,-0.608C0.366,-0.608 0.334,-0.602 0.304,-0.589C0.274,-0.576 0.248,-0.558 0.226,-0.535C0.203,-0.511 0.186,-0.484 0.173,-0.453C0.161,-0.422 0.155,-0.388 0.155,-0.351C0.155,-0.314 0.161,-0.28 0.173,-0.249C0.186,-0.218 0.203,-0.19 0.226,-0.167C0.248,-0.143 0.274,-0.125 0.304,-0.112C0.334,-0.099 0.366,-0.093 0.401,-0.093Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(136.163,0,0,136.163,2831.86,1719.32)">
|
||||
<path d="M0.069,0L0.069,-0.426C0.069,-0.485 0.081,-0.536 0.106,-0.579C0.13,-0.622 0.163,-0.655 0.206,-0.678C0.249,-0.701 0.298,-0.712 0.353,-0.712C0.408,-0.712 0.457,-0.701 0.5,-0.678C0.543,-0.655 0.576,-0.622 0.601,-0.579C0.626,-0.536 0.638,-0.485 0.638,-0.426L0.638,0L0.518,0L0.518,-0.436C0.518,-0.467 0.511,-0.496 0.498,-0.522C0.485,-0.548 0.466,-0.569 0.442,-0.584C0.417,-0.599 0.388,-0.607 0.353,-0.607C0.32,-0.607 0.291,-0.599 0.266,-0.584C0.241,-0.569 0.222,-0.548 0.209,-0.522C0.195,-0.496 0.188,-0.467 0.188,-0.436L0.188,0L0.069,0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1.47838,0,0,2.34801,-955.236,-8562.95)">
|
||||
<path d="M3055.09,3977.51C3050.3,3984.25 3045,3990.56 3039.21,3996.35C2987.91,4047.65 2917.1,4038.77 2881.16,3976.54C2845.23,3914.3 2857.71,3822.13 2909.01,3770.83C2960.31,3719.53 3031.13,3728.41 3067.06,3790.64C3069.85,3795.48 3072.35,3800.49 3074.56,3805.67L3039.78,3811.64C3012.82,3769.64 2962.9,3764.58 2926.45,3801.04C2888.89,3838.59 2879.76,3906.07 2906.07,3951.63C2932.37,3997.19 2984.22,4003.69 3021.77,3966.14L3021.89,3966.01L3055.09,3977.51ZM3085.02,3841.47C3090.86,3875.56 3086.6,3912.35 3073.22,3944.57L3043.91,3934.42C3056.74,3907.59 3060.53,3875.54 3054.13,3846.78L3085.02,3841.47Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.47838,0,0,2.34801,-955.236,-3103.7)">
|
||||
<path d="M2906.78,1571.77L3111.02,1642.48L3116.61,1626.34L3147.2,1664.74L3099.42,1675.99L3105,1659.86L2910.03,1592.35C2908.25,1585.69 2907.18,1578.77 2906.78,1571.77ZM2917.45,1517.07L3114.77,1483.17L3111.88,1466.34L3157.2,1485.21L3120.78,1518.13L3117.88,1501.3L2910.22,1536.97C2911.99,1530.09 2914.41,1523.4 2917.45,1517.07Z" style="fill:rgb(255,216,67);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
BIN
photon-client/src/assets/images/logoMono.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
25
photon-client/src/assets/images/logoSmall.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 508 507" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-1279,0)">
|
||||
<g id="PhotonVision-Icon-BG" transform="matrix(0.264062,0,0,0.469444,1279.5,0)">
|
||||
<rect x="0" y="0" width="1920" height="1080" style="fill:none;"/>
|
||||
<clipPath id="_clip1">
|
||||
<rect x="0" y="0" width="1920" height="1080"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<g transform="matrix(4.27015,0,0,2.40196,-20444.8,-3235.56)">
|
||||
<circle cx="5012.55" cy="1571.77" r="224.918" style="fill:rgb(0,100,146);"/>
|
||||
</g>
|
||||
<g transform="matrix(4.95901,0,0,2.78944,-13955,-10313.5)">
|
||||
<path d="M3055.09,3977.51C3050.3,3984.25 3045,3990.56 3039.21,3996.35C2987.91,4047.65 2917.1,4038.77 2881.16,3976.54C2845.23,3914.3 2857.71,3822.13 2909.01,3770.83C2960.31,3719.53 3031.13,3728.41 3067.06,3790.64C3069.85,3795.48 3072.35,3800.49 3074.56,3805.67L3039.78,3811.64C3012.82,3769.64 2962.9,3764.58 2926.45,3801.04C2888.89,3838.59 2879.76,3906.07 2906.07,3951.63C2932.37,3997.19 2984.22,4003.69 3021.77,3966.14L3021.89,3966.01L3055.09,3977.51ZM3085.02,3841.47C3090.86,3875.56 3086.6,3912.35 3073.22,3944.57L3043.91,3934.42C3056.74,3907.59 3060.53,3875.54 3054.13,3846.78L3085.02,3841.47Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(4.95901,0,0,2.78944,-13955,-3827.86)">
|
||||
<path d="M2906.78,1571.77L3111.02,1642.48L3116.61,1626.34L3147.2,1664.74L3099.42,1675.99L3105,1659.86L2910.03,1592.35C2908.25,1585.69 2907.18,1578.77 2906.78,1571.77ZM2917.45,1517.07L3114.77,1483.17L3111.88,1466.34L3157.2,1485.21L3120.78,1518.13L3117.88,1501.3L2910.22,1536.97C2911.99,1530.09 2914.41,1523.4 2917.45,1517.07Z" style="fill:rgb(255,216,67);"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
photon-client/src/assets/images/notfound.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 48 KiB |
20
photon-client/src/assets/styles/variables.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@import "@fontsource/prompt";
|
||||
|
||||
$default-font: "Prompt", sans-serif !default;
|
||||
$body-font-family: $default-font;
|
||||
$heading-font-family: $default-font;
|
||||
|
||||
.v-application {
|
||||
font-family: $default-font !important;
|
||||
}
|
||||
|
||||
.v-row-group__header {
|
||||
background: #005281 !important;
|
||||
}
|
||||
.theme--dark.v-data-table
|
||||
> .v-data-table__wrapper
|
||||
> table
|
||||
> tbody
|
||||
> tr:hover:not(.v-data-table__expanded__content):not(.v-data-table__empty-wrapper) {
|
||||
background: #005281 !important;
|
||||
}
|
||||
206
photon-client/src/components/app/photon-3d-visualizer.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
|
||||
import { onBeforeUnmount, onMounted, watchEffect } from "vue";
|
||||
import {
|
||||
ArrowHelper,
|
||||
BoxGeometry,
|
||||
Color,
|
||||
ConeGeometry,
|
||||
Mesh,
|
||||
MeshNormalMaterial,
|
||||
type Object3D,
|
||||
PerspectiveCamera,
|
||||
Quaternion,
|
||||
Scene,
|
||||
Vector3,
|
||||
WebGLRenderer
|
||||
} from "three";
|
||||
import { TrackballControls } from "three/examples/jsm/controls/TrackballControls";
|
||||
|
||||
const props = defineProps<{
|
||||
targets: PhotonTarget[];
|
||||
}>();
|
||||
|
||||
let scene: Scene | undefined;
|
||||
let camera: PerspectiveCamera | undefined;
|
||||
let renderer: WebGLRenderer | undefined;
|
||||
let controls: TrackballControls | undefined;
|
||||
|
||||
let previousTargets: Object3D[] = [];
|
||||
const drawTargets = (targets: PhotonTarget[]) => {
|
||||
// Check here, since if we check in watchEffect this never gets called
|
||||
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
scene.remove(...previousTargets);
|
||||
previousTargets = [];
|
||||
|
||||
targets.forEach((target) => {
|
||||
if (target.pose === undefined) return;
|
||||
|
||||
const geometry = new BoxGeometry(0.3 / 5, 0.2, 0.2);
|
||||
const material = new MeshNormalMaterial();
|
||||
|
||||
const quaternion = new Quaternion(target.pose.qx, target.pose.qy, target.pose.qz, target.pose.qw);
|
||||
|
||||
const cube = new Mesh(geometry, material);
|
||||
cube.position.set(target.pose.x, target.pose.y, target.pose.z);
|
||||
cube.rotation.setFromQuaternion(quaternion);
|
||||
previousTargets.push(cube);
|
||||
|
||||
let arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1);
|
||||
arrow.rotation.setFromQuaternion(quaternion);
|
||||
arrow.rotateZ(-Math.PI / 2);
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
|
||||
previousTargets.push(arrow);
|
||||
|
||||
arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1);
|
||||
arrow.rotation.setFromQuaternion(quaternion);
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
|
||||
previousTargets.push(arrow);
|
||||
|
||||
arrow = new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1);
|
||||
arrow.setRotationFromQuaternion(quaternion);
|
||||
arrow.rotateX(Math.PI / 2);
|
||||
arrow.position.set(target.pose.x, target.pose.y, target.pose.z);
|
||||
previousTargets.push(arrow);
|
||||
});
|
||||
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
const onWindowResize = () => {
|
||||
const container = document.getElementById("container");
|
||||
const canvas = document.getElementById("view");
|
||||
|
||||
if (container === null || canvas === null || camera === undefined || renderer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.style.width = container.clientWidth * 0.75 + "px";
|
||||
canvas.style.height = container.clientWidth * 0.35 + "px";
|
||||
camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
||||
};
|
||||
const resetCamFirstPerson = () => {
|
||||
if (scene === undefined || camera === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.reset();
|
||||
camera.position.set(0.2, 0, 0);
|
||||
camera.up.set(0, 0, 1);
|
||||
controls.target.set(4.0, 0.0, 0.0);
|
||||
controls.update();
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
const resetCamThirdPerson = () => {
|
||||
if (scene === undefined || camera === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
controls.reset();
|
||||
camera.position.set(-1.39, -1.09, 1.17);
|
||||
camera.up.set(0, 0, 1);
|
||||
controls.target.set(4.0, 0.0, 0.0);
|
||||
controls.update();
|
||||
if (previousTargets.length > 0) {
|
||||
scene.add(...previousTargets);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
scene = new Scene();
|
||||
camera = new PerspectiveCamera(75, 800 / 800, 0.1, 1000);
|
||||
|
||||
const canvas = document.getElementById("view");
|
||||
if (canvas === null) return;
|
||||
renderer = new WebGLRenderer({ canvas: canvas });
|
||||
|
||||
scene.background = new Color(0xa9a9a9);
|
||||
|
||||
onWindowResize();
|
||||
window.addEventListener("resize", onWindowResize);
|
||||
|
||||
const referenceFrameCues: Object3D[] = [];
|
||||
referenceFrameCues.push(
|
||||
new ArrowHelper(new Vector3(1, 0, 0).normalize(), new Vector3(0, 0, 0), 1, 0xff0000, 0.1, 0.1)
|
||||
);
|
||||
referenceFrameCues.push(
|
||||
new ArrowHelper(new Vector3(0, 1, 0).normalize(), new Vector3(0, 0, 0), 1, 0x00ff00, 0.1, 0.1)
|
||||
);
|
||||
referenceFrameCues.push(
|
||||
new ArrowHelper(new Vector3(0, 0, 1).normalize(), new Vector3(0, 0, 0), 1, 0x0000ff, 0.1, 0.1)
|
||||
);
|
||||
|
||||
// Draw the Camera Body
|
||||
const camSize = 0.2;
|
||||
const camBodyGeometry = new BoxGeometry(camSize, camSize, camSize);
|
||||
const camLensGeometry = new ConeGeometry(camSize * 0.4, camSize * 0.8, 30);
|
||||
const camMaterial = new MeshNormalMaterial();
|
||||
const camBody = new Mesh(camBodyGeometry, camMaterial);
|
||||
const camLens = new Mesh(camLensGeometry, camMaterial);
|
||||
camBody.position.set(0, 0, 0);
|
||||
camLens.rotateZ(Math.PI / 2);
|
||||
camLens.position.set(camSize * 0.8, 0, 0);
|
||||
referenceFrameCues.push(camBody);
|
||||
referenceFrameCues.push(camLens);
|
||||
|
||||
controls = new TrackballControls(camera, renderer.domElement);
|
||||
controls.rotateSpeed = 1.0;
|
||||
controls.zoomSpeed = 1.2;
|
||||
controls.panSpeed = 0.8;
|
||||
controls.noZoom = false;
|
||||
controls.noPan = false;
|
||||
controls.staticMoving = true;
|
||||
controls.dynamicDampingFactor = 0.3;
|
||||
|
||||
scene.add(...referenceFrameCues);
|
||||
resetCamThirdPerson();
|
||||
|
||||
controls.update();
|
||||
|
||||
const animate = () => {
|
||||
if (scene === undefined || camera === undefined || renderer === undefined || controls === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
drawTargets(props.targets);
|
||||
animate();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", onWindowResize);
|
||||
});
|
||||
watchEffect(() => {
|
||||
drawTargets(props.targets);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="container" style="width: 100%">
|
||||
<v-row>
|
||||
<v-col align-self="stretch" style="display: flex; justify-content: center">
|
||||
<canvas id="view" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="margin-bottom: 24px">
|
||||
<v-col style="display: flex; justify-content: center">
|
||||
<v-btn color="secondary" @click="resetCamFirstPerson"> First Person </v-btn>
|
||||
</v-col>
|
||||
<v-col style="display: flex; justify-content: center">
|
||||
<v-btn color="secondary" @click="resetCamThirdPerson"> Third Person </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
94
photon-client/src/components/app/photon-camera-stream.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import loadingImage from "@/assets/images/loading.svg";
|
||||
import type { StyleValue } from "vue/types/jsx";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
streamType: "Raw" | "Processed";
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
const streamSrc = computed<string>(() => {
|
||||
const port =
|
||||
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
|
||||
if (!useStateStore().backendConnected || port === 0) {
|
||||
return loadingImage;
|
||||
}
|
||||
|
||||
return `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
||||
});
|
||||
const streamDesc = computed<string>(() => `${props.streamType} Stream View`);
|
||||
const streamStyle = computed<StyleValue>(() => {
|
||||
if (useStateStore().colorPickingMode) {
|
||||
return { width: "100%", cursor: "crosshair" };
|
||||
} else if (streamSrc.value !== loadingImage) {
|
||||
return { width: "100%", cursor: "pointer" };
|
||||
}
|
||||
|
||||
return { width: "100%" };
|
||||
});
|
||||
|
||||
const overlayStyle = computed<StyleValue>(() => {
|
||||
if (useStateStore().colorPickingMode || streamSrc.value == loadingImage) {
|
||||
return { display: "none" };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const handleStreamClick = () => {
|
||||
if (!useStateStore().colorPickingMode && streamSrc.value !== loadingImage) {
|
||||
window.open(streamSrc.value);
|
||||
}
|
||||
};
|
||||
const handleCaptureClick = () => {
|
||||
if (props.streamType === "Raw") {
|
||||
useCameraSettingsStore().saveInputSnapshot();
|
||||
} else {
|
||||
useCameraSettingsStore().saveOutputSnapshot();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="stream-container">
|
||||
<img
|
||||
:id="id"
|
||||
crossorigin="anonymous"
|
||||
:src="streamSrc"
|
||||
:alt="streamDesc"
|
||||
:style="streamStyle"
|
||||
@click="handleStreamClick"
|
||||
/>
|
||||
<div class="stream-overlay" :style="overlayStyle">
|
||||
<pv-icon
|
||||
icon-name="mdi-camera-image"
|
||||
tooltip="Capture and save a frame of this stream"
|
||||
class="ma-1 mr-2"
|
||||
@click="handleCaptureClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stream-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stream-overlay {
|
||||
opacity: 0;
|
||||
transition: 0.1s ease;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.stream-container:hover .stream-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
16
photon-client/src/components/app/photon-error-snackbar.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="useStateStore().snackbarData.show"
|
||||
top
|
||||
:color="useStateStore().snackbarData.color"
|
||||
:timeout="useStateStore().snackbarData.timeout"
|
||||
>
|
||||
<p style="padding: 0; margin: 0; text-align: center">
|
||||
{{ useStateStore().snackbarData.message }}
|
||||
</p>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
105
photon-client/src/components/app/photon-log-view.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const selectedLogLevels = ref<LogLevel[]>([LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO]);
|
||||
|
||||
const logs = computed<LogMessage[]>(() =>
|
||||
useStateStore().logMessages.filter((message) => selectedLogLevels.value.includes(message.level))
|
||||
);
|
||||
|
||||
const backendHost = inject<string>("backendHost");
|
||||
|
||||
const getLogColor = (level: LogLevel): string => {
|
||||
switch (level) {
|
||||
case LogLevel.ERROR:
|
||||
return "red";
|
||||
case LogLevel.WARN:
|
||||
return "yellow";
|
||||
case LogLevel.INFO:
|
||||
return "green";
|
||||
case LogLevel.DEBUG:
|
||||
return "white";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getLogLevelFromIndex = (index: number): string => {
|
||||
return LogLevel[index];
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
|
||||
const handleLogExport = () => {
|
||||
exportLogFile.value.click();
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case "`":
|
||||
useStateStore().$patch((state) => (state.showLogModal = !state.showLogModal));
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
|
||||
<v-card dark class="pt-3" color="primary" flat>
|
||||
<v-card-title>
|
||||
View Program Logs
|
||||
<v-btn color="secondary" style="margin-left: auto" depressed @click="handleLogExport">
|
||||
<v-icon left> mdi-download </v-icon>
|
||||
Download Current Log
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
<a
|
||||
ref="exportLogFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${backendHost}/api/utils/photonvision-journalctl.txt`"
|
||||
download="photonvision-journalctl.txt"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<div class="pr-6 pl-6">
|
||||
<v-btn-toggle v-model="selectedLogLevels" dark multiple class="fill mb-4">
|
||||
<v-btn v-for="level in [0, 1, 2, 3]" :key="level" color="secondary" class="fill">
|
||||
{{ getLogLevelFromIndex(level) }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-card-text v-if="logs.length === 0" style="font-size: 18px; font-weight: 600">
|
||||
There are no logs to show
|
||||
</v-card-text>
|
||||
<v-virtual-scroll v-else :items="logs" item-height="50" height="600">
|
||||
<template #default="{ item }">
|
||||
<div :class="[getLogColor(item.level) + '--text', 'log-item']">
|
||||
{{ item.message }}
|
||||
</div>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="white" text @click="() => (useStateStore().showLogModal = false)"> Close </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
122
photon-client/src/components/app/photon-sidebar.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => {
|
||||
return useStateStore().sidebarFolded;
|
||||
},
|
||||
set: (val) => {
|
||||
useStateStore().setSidebarFolded(val);
|
||||
}
|
||||
});
|
||||
|
||||
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
|
||||
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer dark app permanent :mini-variant="compact || !mdAndUp" color="primary">
|
||||
<v-list>
|
||||
<!-- List item for the heading; note that there are some tricks in setting padding and image width make things look right -->
|
||||
<v-list-item :class="compact || !mdAndUp ? 'pr-0 pl-0' : ''" style="display: flex; justify-content: center">
|
||||
<v-list-item-icon class="mr-0">
|
||||
<img v-if="!(compact || !mdAndUp)" class="logo" src="@/assets/images/logoLarge.svg" alt="large logo" />
|
||||
<img v-else class="logo" src="@/assets/images/logoSmall.svg" alt="small logo" />
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item link to="/dashboard">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-view-dashboard</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Documentation</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="mdAndUp" link @click="() => (compact = !compact)">
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="compact || !mdAndUp"> mdi-chevron-right </v-icon>
|
||||
<v-icon v-else> mdi-chevron-left </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Compact Mode</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<div style="position: absolute; bottom: 0; left: 0">
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useSettingsStore().network.runNTServer"> mdi-server </v-icon>
|
||||
<v-icon v-else-if="useStateStore().ntConnectionStatus.connected"> mdi-robot </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-robot-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-if="useSettingsStore().network.runNTServer" class="text-wrap">
|
||||
NetworkTables server running for
|
||||
<span class="accent--text">{{ useStateStore().ntConnectionStatus.clients || 0 }}</span> clients
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-else-if="useStateStore().ntConnectionStatus.connected && useStateStore().backendConnected"
|
||||
class="text-wrap"
|
||||
style="flex-direction: column; display: flex"
|
||||
>
|
||||
NetworkTables Server Connected!
|
||||
<span class="accent--text">
|
||||
{{ useStateStore().ntConnectionStatus.address }}
|
||||
</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-title v-else class="text-wrap" style="flex-direction: column; display: flex">
|
||||
Not connected to NetworkTables Server!
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="useStateStore().backendConnected"> mdi-server-network </v-icon>
|
||||
<v-icon v-else style="border-radius: 100%"> mdi-server-network-off </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="text-wrap">
|
||||
{{ useStateStore().backendConnected ? "Backend connected" : "Trying to connect to backend" }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
512
photon-client/src/components/cameras/CameraCalibrationCard.vue
Normal file
@@ -0,0 +1,512 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { CalibrationBoardTypes, type Resolution, type VideoFormat } from "@/types/SettingTypes";
|
||||
import JsPDF from "jspdf";
|
||||
import { font as PromptRegular } from "@/assets/fonts/PromptRegular";
|
||||
import MonoLogo from "@/assets/images/logoMono.png";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvNumberInput from "@/components/common/pv-number-input.vue";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
|
||||
const settingsValid = ref(true);
|
||||
|
||||
const getCalibrationCoeffs = (resolution: Resolution) => {
|
||||
return useCameraSettingsStore().currentCameraSettings.completeCalibrations.find(
|
||||
(cal) => cal.resolution.width === resolution.width && cal.resolution.height === resolution.height
|
||||
);
|
||||
};
|
||||
const getUniqueVideoResolutions = (): VideoFormat[] => {
|
||||
const uniqueResolutions: VideoFormat[] = [];
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format, index) => {
|
||||
if (
|
||||
!uniqueResolutions.some(
|
||||
(v) => v.resolution.width === format.resolution.width && v.resolution.height === format.resolution.height
|
||||
)
|
||||
) {
|
||||
format.index = index;
|
||||
|
||||
const calib = getCalibrationCoeffs(format.resolution);
|
||||
if (calib !== undefined) {
|
||||
format.standardDeviation = calib.standardDeviation;
|
||||
format.mean = calib.perViewErrors.reduce((a, b) => a + b) / calib.perViewErrors.length;
|
||||
format.horizontalFOV = 2 * Math.atan2(format.resolution.width / 2, calib.intrinsics[0]) * (180 / Math.PI);
|
||||
format.verticalFOV = 2 * Math.atan2(format.resolution.height / 2, calib.intrinsics[4]) * (180 / Math.PI);
|
||||
format.diagonalFOV =
|
||||
2 *
|
||||
Math.atan2(
|
||||
Math.sqrt(
|
||||
format.resolution.width ** 2 +
|
||||
(format.resolution.height / (calib.intrinsics[4] / calib.intrinsics[0])) ** 2
|
||||
) / 2,
|
||||
calib.intrinsics[0]
|
||||
) *
|
||||
(180 / Math.PI);
|
||||
}
|
||||
uniqueResolutions.push(format);
|
||||
}
|
||||
});
|
||||
uniqueResolutions.sort(
|
||||
(a, b) => b.resolution.width + b.resolution.height - (a.resolution.width + a.resolution.height)
|
||||
);
|
||||
return uniqueResolutions;
|
||||
};
|
||||
const getUniqueVideoResolutionStrings = () =>
|
||||
getUniqueVideoResolutions().map<{ name: string; value: number }>((f) => ({
|
||||
name: `${f.resolution.width} X ${f.resolution.height}`,
|
||||
// Index won't ever be undefined
|
||||
value: f.index || 0
|
||||
}));
|
||||
const calibrationDivisors = computed(() =>
|
||||
[1, 2, 4].filter((v) => {
|
||||
const currentRes = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return (currentRes.width / v >= 300 && currentRes.height / v >= 220) || v === 1;
|
||||
})
|
||||
);
|
||||
|
||||
const squareSizeIn = ref(1);
|
||||
const patternWidth = ref(8);
|
||||
const patternHeight = ref(8);
|
||||
const boardType = ref<CalibrationBoardTypes>(CalibrationBoardTypes.Chessboard);
|
||||
|
||||
const downloadCalibBoard = () => {
|
||||
const doc = new JsPDF({ unit: "in", format: "letter" });
|
||||
|
||||
doc.addFileToVFS("Prompt-Regular.tff", PromptRegular);
|
||||
doc.addFont("Prompt-Regular.tff", "Prompt-Regular", "normal");
|
||||
doc.setFont("Prompt-Regular");
|
||||
doc.setFontSize(12);
|
||||
|
||||
const paperWidth = 8.5;
|
||||
const paperHeight = 11.0;
|
||||
|
||||
switch (boardType.value) {
|
||||
case CalibrationBoardTypes.Chessboard:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const chessboardStartX = (paperWidth - patternWidth.value * squareSizeIn.value) / 2;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const chessboardStartY = (paperHeight - patternWidth.value * squareSizeIn.value) / 2;
|
||||
|
||||
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
|
||||
for (let squareX = 0; squareX < patternWidth.value; squareX++) {
|
||||
const xPos = chessboardStartX + squareX * squareSizeIn.value;
|
||||
const yPos = chessboardStartY + squareY * squareSizeIn.value;
|
||||
|
||||
// Only draw the odd squares to create the chessboard pattern
|
||||
if ((xPos + yPos + 0.25) % 2 === 0) {
|
||||
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CalibrationBoardTypes.DotBoard:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const dotgridStartX =
|
||||
(paperWidth - (2 * (patternWidth.value - 1) + ((patternHeight.value - 1) % 2)) * squareSizeIn.value) / 2.0;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const dotgridStartY = (paperHeight - (patternHeight.value - squareSizeIn.value)) / 2;
|
||||
|
||||
for (let squareY = 0; squareY < patternHeight.value; squareY++) {
|
||||
for (let squareX = 0; squareX < patternWidth.value; squareX++) {
|
||||
const xPos = dotgridStartX + (2 * squareX + (squareY % 2)) * squareSizeIn.value;
|
||||
const yPos = dotgridStartY + squareY * squareSizeIn.value;
|
||||
|
||||
doc.circle(xPos, yPos, squareSizeIn.value / 4, "F");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw ruler pattern
|
||||
const lineStartX = 1.0;
|
||||
const lineEndX = paperWidth - lineStartX;
|
||||
const lineY = paperHeight - 1.0;
|
||||
|
||||
doc.setLineWidth(0.01);
|
||||
doc.line(lineStartX, lineY, lineEndX, lineY);
|
||||
|
||||
for (let tickX = lineStartX; tickX <= lineEndX; tickX++) {
|
||||
doc.line(tickX, lineY, tickX, lineY + 0.25);
|
||||
doc.text(`${tickX - 1}${tickX - 1 === 0 ? " in" : ""}`, tickX + 0.1, lineY + 0.25);
|
||||
}
|
||||
|
||||
// Add branding
|
||||
const logoImage = new Image();
|
||||
logoImage.src = MonoLogo;
|
||||
doc.addImage(logoImage, "PNG", 1.0, 0.75, 1.4, 0.5);
|
||||
|
||||
doc.text(`${patternWidth.value} x ${patternHeight.value} | ${squareSizeIn.value}in`, paperWidth - 1, 1.0, {
|
||||
maxWidth: (paperWidth - 2.0) / 2,
|
||||
align: "right"
|
||||
});
|
||||
|
||||
doc.save(`calibrationTarget-${CalibrationBoardTypes[boardType.value]}.pdf`);
|
||||
};
|
||||
|
||||
const importCalibrationFromCalibDB = ref();
|
||||
const openCalibUploadPrompt = () => {
|
||||
importCalibrationFromCalibDB.value.click();
|
||||
};
|
||||
const readImportedCalibration = (payload: Event) => {
|
||||
if (payload.target == null || !payload.target?.files) return;
|
||||
const files: FileList = payload.target.files as FileList;
|
||||
|
||||
files[0].text().then((text) => {
|
||||
useCameraSettingsStore()
|
||||
.importCalibDB({ payload: text, filename: files[0].name })
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: response.status === 200 ? "success" : "error"
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while uploading calibration file! The backend didn't respond to the upload attempt.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while uploading calibration file!",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isCalibrating = ref(false);
|
||||
const startCalibration = () => {
|
||||
useCameraSettingsStore().startPnPCalibration({
|
||||
squareSizeIn: squareSizeIn.value,
|
||||
patternHeight: patternHeight.value,
|
||||
patternWidth: patternWidth.value,
|
||||
boardType: boardType.value
|
||||
});
|
||||
// The Start PnP method already handles updating the backend so only a store update is required
|
||||
useCameraSettingsStore().currentCameraSettings.currentPipelineIndex = WebsocketPipelineType.Calib3d;
|
||||
isCalibrating.value = true;
|
||||
calibCanceled.value = false;
|
||||
};
|
||||
const showCalibEndDialog = ref(false);
|
||||
const calibCanceled = ref(false);
|
||||
const calibSuccess = ref<boolean | undefined>(undefined);
|
||||
const endCalibration = () => {
|
||||
if (!useStateStore().calibrationData.hasEnoughImages) {
|
||||
calibCanceled.value = true;
|
||||
}
|
||||
|
||||
showCalibEndDialog.value = true;
|
||||
// Check if calibration finished cleanly or was canceled
|
||||
useCameraSettingsStore()
|
||||
.endPnPCalibration()
|
||||
.then(() => {
|
||||
calibSuccess.value = true;
|
||||
})
|
||||
.catch(() => {
|
||||
calibSuccess.value = false;
|
||||
})
|
||||
.finally(() => {
|
||||
isCalibrating.value = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
|
||||
<v-card-title>Camera Calibration</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
<pv-select
|
||||
v-model="useStateStore().calibrationData.videoFormatIndex"
|
||||
label="Resolution"
|
||||
:select-cols="7"
|
||||
:disabled="isCalibrating"
|
||||
tooltip="Resolution to calibrate at (you will have to calibrate every resolution you use 3D mode on)"
|
||||
:items="getUniqueVideoResolutionStrings()"
|
||||
/>
|
||||
<pv-select
|
||||
v-show="isCalibrating"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Decimation"
|
||||
tooltip="Resolution to which camera frames are downscaled for detection. Calibration still uses full-res"
|
||||
:items="calibrationDivisors"
|
||||
:select-cols="7"
|
||||
@input="
|
||||
(v) => useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: v }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="boardType"
|
||||
label="Board Type"
|
||||
tooltip="Calibration board pattern to use"
|
||||
:select-cols="7"
|
||||
:items="['Chessboard', 'Dotboard']"
|
||||
:disabled="isCalibrating"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="squareSizeIn"
|
||||
label="Pattern Spacing (in)"
|
||||
tooltip="Spacing between pattern features in inches"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v > 0 || 'Size must be positive']"
|
||||
:label-cols="5"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternWidth"
|
||||
label="Board Width (in)"
|
||||
tooltip="Width of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Width must be at least 4']"
|
||||
:label-cols="5"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="patternHeight"
|
||||
label="Board Height (in)"
|
||||
tooltip="Height of the board in dots or chessboard squares"
|
||||
:disabled="isCalibrating"
|
||||
:rules="[(v) => v >= 4 || 'Height must be at least 4']"
|
||||
:label-cols="5"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-row align="start" class="pb-4 pt-2">
|
||||
<v-simple-table fixed-header height="100%" dense>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resolution</th>
|
||||
<th>Mean Error</th>
|
||||
<th>Standard Deviation</th>
|
||||
<th>Horizontal FOV</th>
|
||||
<th>Vertical FOV</th>
|
||||
<th>Diagonal FOV</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(value, index) in getUniqueVideoResolutions()" :key="index">
|
||||
<td>{{ value.resolution.width }} X {{ value.resolution.height }}</td>
|
||||
<td>{{ value.mean !== undefined ? value.mean.toFixed(2) + "px" : "-" }}</td>
|
||||
<td>
|
||||
{{ value.standardDeviation !== undefined ? value.standardDeviation.toFixed(2) + "px" : "-" }}
|
||||
</td>
|
||||
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
<td>{{ value.diagonalFOV !== undefined ? value.diagonalFOV.toFixed(2) + "°" : "-" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-row justify="center">
|
||||
<v-chip
|
||||
v-show="isCalibrating"
|
||||
label
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'secondary' : 'gray'"
|
||||
>
|
||||
Snapshots: {{ useStateStore().calibrationData.imageCount }} of at least
|
||||
{{ useStateStore().calibrationData.minimumImageCount }}
|
||||
</v-chip>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="isCalibrating">
|
||||
<v-col cols="12" class="pt-0">
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="8"
|
||||
:step="0.1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="8"
|
||||
@input="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:label-cols="4"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="
|
||||
(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)
|
||||
"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="secondary"
|
||||
style="width: 100%"
|
||||
:disabled="!settingsValid"
|
||||
@click="isCalibrating ? useCameraSettingsStore().takeCalibrationSnapshot() : startCalibration()"
|
||||
>
|
||||
{{ isCalibrating ? "Take Snapshot" : "Start Calibration" }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
small
|
||||
:color="useStateStore().calibrationData.hasEnoughImages ? 'accent' : 'red'"
|
||||
:class="useStateStore().calibrationData.hasEnoughImages ? 'black--text' : 'white---text'"
|
||||
style="width: 100%"
|
||||
:disabled="!isCalibrating || !settingsValid"
|
||||
@click="endCalibration"
|
||||
>
|
||||
{{ useStateStore().calibrationData.hasEnoughImages ? "Finish Calibration" : "Cancel Calibration" }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col :cols="6">
|
||||
<v-btn
|
||||
color="accent"
|
||||
small
|
||||
outlined
|
||||
style="width: 100%"
|
||||
:disabled="!settingsValid"
|
||||
@click="downloadCalibBoard"
|
||||
>
|
||||
<v-icon left> mdi-download </v-icon>
|
||||
Generate Board
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col :cols="6">
|
||||
<v-btn color="secondary" :disabled="isCalibrating" small style="width: 100%" @click="openCalibUploadPrompt">
|
||||
<v-icon left> mdi-upload </v-icon>
|
||||
Import From CalibDB
|
||||
</v-btn>
|
||||
<input
|
||||
ref="importCalibrationFromCalibDB"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="readImportedCalibration"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
<v-dialog v-model="showCalibEndDialog" width="500px" :persistent="true">
|
||||
<v-card color="primary" dark>
|
||||
<v-card-title class="pb-8"> Camera Calibration </v-card-title>
|
||||
<div class="ml-3">
|
||||
<v-col style="text-align: center">
|
||||
<template v-if="calibCanceled">
|
||||
<v-icon color="blue" size="70"> mdi-cancel </v-icon>
|
||||
<v-card-text
|
||||
>Camera Calibration has been Canceled, the backend is attempting to cleanly cancel the calibration
|
||||
process.</v-card-text
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="isCalibrating">
|
||||
<v-progress-circular indeterminate :size="70" :width="8" color="accent" />
|
||||
<v-card-text>Camera is being calibrated. This process may take several minutes...</v-card-text>
|
||||
</template>
|
||||
<template v-else-if="calibSuccess">
|
||||
<v-icon color="green" size="70"> mdi-check-bold </v-icon>
|
||||
<v-card-text>
|
||||
Camera has been successfully calibrated for
|
||||
{{
|
||||
getUniqueVideoResolutionStrings().find(
|
||||
(v) => v.value === useStateStore().calibrationData.videoFormatIndex
|
||||
).name
|
||||
}}!
|
||||
</v-card-text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon color="red" size="70"> mdi-close </v-icon>
|
||||
<v-card-text
|
||||
>Camera calibration failed! Make sure that the photos are taken such that the rainbow grid circles align
|
||||
with the corners of the chessboard, and try again. More information is available in the program
|
||||
logs.</v-card-text
|
||||
>
|
||||
</template>
|
||||
</v-col>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn v-if="!isCalibrating" color="white" text @click="showCalibEndDialog = false"> OK </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
text-align: center;
|
||||
|
||||
th,
|
||||
td {
|
||||
background-color: #006492 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
tbody :hover td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
202
photon-client/src/components/cameras/CameraControlCard.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
interface SnapshotMetadata {
|
||||
snapshotName: string;
|
||||
cameraNickname: string;
|
||||
streamType: "input" | "output";
|
||||
timeCreated: Date;
|
||||
}
|
||||
const getSnapshotMetadataFromName = (snapshotName: string): SnapshotMetadata => {
|
||||
snapshotName = snapshotName.replace(/\.[^/.]+$/, "");
|
||||
|
||||
const data = snapshotName.split("_");
|
||||
|
||||
const cameraName = data.slice(0, data.length - 2).join("_");
|
||||
const streamType = data[data.length - 2] as "input" | "output";
|
||||
const dateStr = data[data.length - 1];
|
||||
|
||||
const year = parseInt(dateStr.substring(0, 4), 10);
|
||||
const month = parseInt(dateStr.substring(5, 7), 10) - 1; // Months are zero-based
|
||||
const day = parseInt(dateStr.substring(8, 10), 10);
|
||||
const hours = parseInt(dateStr.substring(11, 13), 10);
|
||||
const minutes = parseInt(dateStr.substring(13, 15), 10);
|
||||
const seconds = parseInt(dateStr.substring(15, 17), 10);
|
||||
const milliseconds = parseInt(dateStr.substring(17), 10);
|
||||
|
||||
return {
|
||||
snapshotName: snapshotName,
|
||||
cameraNickname: cameraName,
|
||||
streamType: streamType,
|
||||
timeCreated: new Date(year, month, day, hours, minutes, seconds, milliseconds)
|
||||
};
|
||||
};
|
||||
|
||||
interface Snapshot {
|
||||
index: number;
|
||||
snapshotName: string;
|
||||
snapshotShortName: string;
|
||||
cameraUniqueName: string;
|
||||
cameraNickname: string;
|
||||
streamType: "input" | "output";
|
||||
timeCreated: Date;
|
||||
snapshotSrc: string;
|
||||
}
|
||||
const imgData = ref<Snapshot[]>([]);
|
||||
const fetchSnapshots = () => {
|
||||
axios
|
||||
.get("/utils/getImageSnapshots")
|
||||
.then((response) => {
|
||||
imgData.value = response.data.map(
|
||||
(snapshotData: { snapshotName: string; cameraUniqueName: string; snapshotData: string }, index) => {
|
||||
const metadata = getSnapshotMetadataFromName(snapshotData.snapshotName);
|
||||
|
||||
return {
|
||||
index: index,
|
||||
snapshotName: snapshotData.snapshotName,
|
||||
snapshotShortName: metadata.snapshotName,
|
||||
cameraUniqueName: snapshotData.cameraUniqueName,
|
||||
cameraNickname: metadata.cameraNickname,
|
||||
streamType: metadata.streamType,
|
||||
timeCreated: metadata.timeCreated,
|
||||
snapshotSrc: "data:image/jpg;base64," + snapshotData.snapshotData
|
||||
};
|
||||
}
|
||||
);
|
||||
showSnapshotViewerDialog.value = true;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const showSnapshotViewerDialog = ref(false);
|
||||
const expanded = ref([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title>Camera Control</v-card-title>
|
||||
<v-row class="pl-6">
|
||||
<v-col>
|
||||
<v-btn color="secondary" @click="fetchSnapshots">
|
||||
<v-icon left> mdi-folder </v-icon>
|
||||
Show Saved Snapshots
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog v-model="showSnapshotViewerDialog">
|
||||
<v-card dark class="pt-3 pl-5 pr-5" color="primary" flat>
|
||||
<v-card-title> View Saved Frame Snapshots </v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text v-if="imgData.length === 0" style="font-size: 18px; font-weight: 600" class="pt-4">
|
||||
There are no snapshots saved
|
||||
</v-card-text>
|
||||
<div v-else class="pb-2">
|
||||
<v-data-table
|
||||
v-model:expanded="expanded"
|
||||
:headers="[
|
||||
{ text: 'Snapshot Name', value: 'snapshotShortName', sortable: false },
|
||||
{ text: 'Camera Unique Name', value: 'cameraUniqueName' },
|
||||
{ text: 'Camera Nickname', value: 'cameraNickname' },
|
||||
{ text: 'Stream Type', value: 'streamType' },
|
||||
{ text: 'Time Created', value: 'timeCreated' },
|
||||
{ text: 'Actions', value: 'actions', sortable: false }
|
||||
]"
|
||||
:items="imgData"
|
||||
group-by="cameraUniqueName"
|
||||
class="elevation-0"
|
||||
item-key="index"
|
||||
show-expand
|
||||
expand-icon="mdi-eye"
|
||||
>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<div style="display: flex; justify-content: center; width: 100%">
|
||||
<img :src="item.snapshotSrc" alt="snapshot-image" class="snapshot-preview pt-2 pb-2" />
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
<!-- eslint-disable-next-line vue/valid-v-slot-->
|
||||
<template #item.actions="{ item }">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<a :download="item.snapshotName" :href="item.snapshotSrc">
|
||||
<v-icon small> mdi-download </v-icon>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<span
|
||||
>Snapshot Timestamps may be incorrect as they depend on when the coprocessor was last connected to the
|
||||
internet</span
|
||||
>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
.v-data-table {
|
||||
text-align: center;
|
||||
background-color: #006492 !important;
|
||||
|
||||
th,
|
||||
td {
|
||||
background-color: #005281 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
tbody :hover tr {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.snapshot-preview {
|
||||
max-width: 55%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 512px) {
|
||||
.snapshot-preview {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
photon-client/src/components/cameras/CameraSettingsCard.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvNumberInput from "@/components/common/pv-number-input.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { ref, watchEffect } from "vue";
|
||||
|
||||
const currentFov = ref();
|
||||
|
||||
const saveCameraSettings = () => {
|
||||
useCameraSettingsStore()
|
||||
.updateCameraSettings({ fov: currentFov.value }, false)
|
||||
.then((response) => {
|
||||
useCameraSettingsStore().currentCameraSettings.fov.value = currentFov.value;
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "success",
|
||||
message: response.data.text || response.data
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
currentFov.value = useCameraSettingsStore().currentCameraSettings.fov.value;
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
currentFov.value = useCameraSettingsStore().currentCameraSettings.fov.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mb-3 pr-6 pb-3" color="primary" dark>
|
||||
<v-card-title>Camera Settings</v-card-title>
|
||||
<div class="ml-5">
|
||||
<pv-select
|
||||
v-model="useStateStore().currentCameraIndex"
|
||||
label="Camera"
|
||||
:items="useCameraSettingsStore().cameraNames"
|
||||
:select-cols="8"
|
||||
@input="
|
||||
(args) => {
|
||||
currentFov = useCameraSettingsStore().cameras[args].fov.value;
|
||||
useCameraSettingsStore().setCurrentCameraIndex(args);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<pv-number-input
|
||||
v-model="currentFov"
|
||||
:tooltip="
|
||||
!useCameraSettingsStore().currentCameraSettings.fov.managedByVendor
|
||||
? 'Field of view (in degrees) of the camera measured across the diagonal of the frame, in a video mode which covers the whole sensor area.'
|
||||
: 'This setting is managed by a vendor'
|
||||
"
|
||||
label="Maximum Diagonal FOV"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.fov.managedByVendor"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<br />
|
||||
<v-btn
|
||||
style="margin-top: 10px"
|
||||
small
|
||||
color="secondary"
|
||||
:disabled="currentFov === useCameraSettingsStore().currentCameraSettings.fov.value"
|
||||
@click="saveCameraSettings"
|
||||
>
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
Save Changes
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
149
photon-client/src/components/cameras/CamerasView.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
import { computed } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
|
||||
const props = defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number[]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
|
||||
const fpsTooLow = computed<boolean>(() => {
|
||||
const currFPS = useStateStore().currentPipelineResults?.fps || 0;
|
||||
const targetFPS = useCameraSettingsStore().currentVideoFormat.fps;
|
||||
const driverMode = useCameraSettingsStore().isDriverMode;
|
||||
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
|
||||
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
|
||||
|
||||
return currFPS - targetFPS < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="mb-3 pb-3 pa-4" color="primary" dark>
|
||||
<v-card-title
|
||||
class="pb-0 mb-2 pl-4 pt-1"
|
||||
style="min-height: 50px; justify-content: space-between; align-content: center"
|
||||
>
|
||||
<div style="display: flex; flex-wrap: wrap">
|
||||
<div>
|
||||
<span class="mr-4" style="white-space: nowrap"> Cameras </span>
|
||||
</div>
|
||||
<div>
|
||||
<v-chip
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
>
|
||||
<span class="pr-1">
|
||||
{{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –
|
||||
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto"
|
||||
color="accent"
|
||||
class="pt-2"
|
||||
/>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<div class="stream-container pb-4">
|
||||
<div class="stream">
|
||||
<photon-camera-stream v-show="value.includes(0)" stream-type="Raw" style="max-width: 100%" />
|
||||
</div>
|
||||
<div class="stream">
|
||||
<photon-camera-stream v-show="value.includes(1)" stream-type="Processed" style="max-width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
<v-divider />
|
||||
<div class="pt-4">
|
||||
<p style="color: white">Stream Display</p>
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill" style="width: 100%">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
>
|
||||
<v-icon>mdi-import</v-icon>
|
||||
<span>Raw</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="fill"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
>
|
||||
<v-icon>mdi-export</v-icon>
|
||||
<span>Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stream-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stream {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 512px) and (max-width: 960px) {
|
||||
.stream-container {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stream {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip
|
||||
:right="right"
|
||||
:bottom="!right"
|
||||
nudge-right="10"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
:class="hoverClass"
|
||||
:color="color"
|
||||
@click="handleClick"
|
||||
v-on="on"
|
||||
>
|
||||
{{ text }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Icon',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['color', 'tooltip', 'text', 'right', 'hover'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
hoverClass: {
|
||||
get() {
|
||||
if (this.hover !== undefined) {
|
||||
return "hover";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('click');
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hover:hover {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<img
|
||||
:id="id"
|
||||
crossOrigin="anonymous"
|
||||
:style="styleObject"
|
||||
:src="src"
|
||||
alt=""
|
||||
@click="e => $emit('click', e)"
|
||||
>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CvImage",
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['address', 'scale', 'maxHeight', 'maxHeightMd', 'maxHeightXl', 'colorPicking', 'id', 'disconnected'],
|
||||
data() {
|
||||
return {
|
||||
seed: 1.0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styleObject: {
|
||||
get() {
|
||||
let ret = {
|
||||
"border-radius": "3px",
|
||||
"display": "block",
|
||||
"object-fit": "contain",
|
||||
"object-position": "50% 50%",
|
||||
"max-width": "100%",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"max-height": this.maxHeight,
|
||||
height: `${this.scale}%`,
|
||||
cursor: (this.colorPicking ? `url(${require("../../assets/eyedropper.svg")}),` : "") + "default",
|
||||
};
|
||||
|
||||
if (this.$vuetify.breakpoint.xl) {
|
||||
ret["max-height"] = this.maxHeightXl;
|
||||
} else if (this.$vuetify.breakpoint.mdAndUp) {
|
||||
ret["max-height"] = this.maxHeightMd;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
src: {
|
||||
get() {
|
||||
return this.disconnected ? require("../../assets/noStream.jpg") : this.address + "?" + this.seed // This prevents caching
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.reload(); // Force reload image on creation
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
this.seed = new Date().getTime();
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (inputCols || 8)">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="inputCols || 8">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
:rules="rules"
|
||||
class="mt-1 pt-2"
|
||||
@keydown="handleKeyboard"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
s
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'Input',
|
||||
components: {
|
||||
TooltippedLabel
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'disabled', 'errorMessage', 'inputCols', 'rules', 'tooltip'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleKeyboard(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.$emit("Enter");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="labelCols || 2">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
color="accent"
|
||||
type="number"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
:rules="rules"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'NumberInput',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'step', 'labelCols', 'rules', 'tooltip', 'disabled'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', parseFloat(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-radio-group
|
||||
v-model="localValue"
|
||||
row
|
||||
dark
|
||||
:mandatory="true"
|
||||
>
|
||||
<v-radio
|
||||
v-for="(name,index) in list"
|
||||
:key="index"
|
||||
color="#ffd843"
|
||||
:label="name"
|
||||
:value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Radio',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['value', 'list', 'disabled'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,131 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col cols="2">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="10">
|
||||
<v-range-slider
|
||||
:value="localValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
color="accent"
|
||||
:step="step"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-text-field
|
||||
dark
|
||||
color="accent"
|
||||
:value="localValue[0]"
|
||||
:max="max"
|
||||
:min="min"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="prependFocused = true"
|
||||
@blur="prependFocused = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-text-field
|
||||
dark
|
||||
color="accent"
|
||||
:value="localValue[1]"
|
||||
:max="max"
|
||||
:min="min"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="appendFocused = true"
|
||||
@blur="appendFocused = false"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: "RangeSlider",
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["name", "min", "max", "value", "step", "tooltip"],
|
||||
data() {
|
||||
return {
|
||||
prependFocused: false,
|
||||
appendFocused: false,
|
||||
currentTempVal: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return Object.values(this.value || [0, 0]);
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
},
|
||||
|
||||
async handleChange(val) {
|
||||
this.currentTempVal = val;
|
||||
|
||||
await this.delay(200).then(() => {
|
||||
let i = 0;
|
||||
if (this.prependFocused === false && this.appendFocused === true) {
|
||||
i = 1;
|
||||
}
|
||||
|
||||
// will get empty string if entry is not a number
|
||||
if (this.currentTempVal !== val || val === "") return;
|
||||
|
||||
let parsed = parseFloat(val);
|
||||
let tmp = this.localValue;
|
||||
tmp[i] = Math.max(this.min, Math.min(parsed, this.max));
|
||||
this.localValue = tmp;
|
||||
|
||||
this.$emit("rollback", this.localValue);
|
||||
});
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.prependFocused || !this.appendFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (selectCols || 9)">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="selectCols || 9">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="indexList"
|
||||
item-text="name"
|
||||
item-value="index"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
:disabled="disabled"
|
||||
:rules="rules"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'Select',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['list', 'name', 'value', 'disabled', 'selectCols', 'rules', 'tooltip'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
indexList() {
|
||||
let list = [];
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
list.push({
|
||||
name: this.list[i],
|
||||
index: i
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="12 - (sliderCols || 8)">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols || 8">
|
||||
<v-slider
|
||||
:value="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
@start="isClicked = true"
|
||||
@end="isClicked = false"
|
||||
@change="handleClick"
|
||||
@input="handleInput"
|
||||
@mousedown="$emit('rollback', localValue)"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-text-field
|
||||
dark
|
||||
color="accent"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
:value="localValue"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
@input="handleChange"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
/>
|
||||
</template>
|
||||
</v-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: "Slider",
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ["min", "max", "name", "value", "step", "sliderCols", "disabled", "tooltip"],
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
isClicked: false,
|
||||
currentBoxVal: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("input", value);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChange(val) {
|
||||
this.currentBoxVal = val;
|
||||
setTimeout(() => {
|
||||
if (this.currentBoxVal !== val) return;
|
||||
// if (this.isFocused) {
|
||||
this.localValue = parseFloat(val);
|
||||
this.$emit("rollback", this.localValue);
|
||||
// }
|
||||
}, 200);
|
||||
},
|
||||
handleInput(val) {
|
||||
if (!this.isFocused && this.isClicked) {
|
||||
this.localValue = val;
|
||||
}
|
||||
},
|
||||
handleClick(val) {
|
||||
if (!this.isFocused) {
|
||||
this.localValue = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
</style>
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
>
|
||||
<v-col :cols="textCols || 2">
|
||||
<tooltipped-label
|
||||
:tooltip="tooltip"
|
||||
:text="name"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col :cols="12 - (textCols || 2)">
|
||||
<v-switch
|
||||
v-model="localValue"
|
||||
dark
|
||||
:disabled="disabled"
|
||||
color="#ffd843"
|
||||
@change="$emit('rollback', localValue)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TooltippedLabel from "./cv-tooltipped-label";
|
||||
|
||||
export default {
|
||||
name: 'CVSwitch',
|
||||
components: {
|
||||
TooltippedLabel,
|
||||
},
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['name', 'value', 'disabled', 'textCols', 'tooltip'],
|
||||
computed: {
|
||||
localValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip
|
||||
:disabled="tooltip === undefined"
|
||||
right
|
||||
open-delay="600"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span
|
||||
style="cursor: text !important;"
|
||||
class="white--text"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>{{ text }}</span>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TooltippedLabel',
|
||||
// eslint-disable-next-line vue/require-prop-types
|
||||
props: ['text', 'tooltip'],
|
||||
}
|
||||
</script>
|
||||
49
photon-client/src/components/common/pv-icon.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
iconName: string;
|
||||
disabled?: boolean;
|
||||
color?: string;
|
||||
tooltip?: string;
|
||||
right?: boolean;
|
||||
hover?: boolean;
|
||||
}>(),
|
||||
{
|
||||
right: false,
|
||||
disabled: false,
|
||||
hover: false
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
|
||||
const hoverClass = props.hover ? "hover" : "";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :right="right" :bottom="!right" nudge-right="10" :disabled="tooltip === undefined">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-icon
|
||||
:class="hoverClass"
|
||||
:color="color"
|
||||
v-bind="attrs"
|
||||
:disabled="disabled"
|
||||
v-on="on"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ iconName }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hover:hover {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
73
photon-client/src/components/common/pv-input.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
errorMessage?: string;
|
||||
placeholder?: string;
|
||||
labelCols?: number;
|
||||
inputCols?: number;
|
||||
rules?: ((v: string) => boolean | string)[];
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
inputCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string): void;
|
||||
(e: "onEnter", value: string): void;
|
||||
(e: "onEscape"): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
const handleKeydown = ({ key }) => {
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
// Explicitly check that all rule props return true
|
||||
if (!props.rules?.every((rule) => rule(localValue.value) === true)) return;
|
||||
|
||||
emit("onEnter", localValue.value);
|
||||
break;
|
||||
case "Escape":
|
||||
emit("onEscape");
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="labelCols || 12 - inputCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
|
||||
<v-col :cols="inputCols">
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
dense
|
||||
color="accent"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:error-messages="errorMessage"
|
||||
:rules="rules"
|
||||
class="mt-1 pt-2"
|
||||
@keydown="handleKeydown"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
56
photon-client/src/components/common/pv-number-input.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
rules?: ((v: number) => boolean | string)[];
|
||||
step?: number;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
labelCols: 2,
|
||||
step: 1
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="labelCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
color="accent"
|
||||
type="number"
|
||||
style="width: 70px"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
:rules="rules"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
51
photon-client/src/components/common/pv-radio.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
inputCols?: number;
|
||||
list: string[];
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
inputCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - inputCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="inputCols">
|
||||
<v-radio-group v-model="localValue" row dark :mandatory="true">
|
||||
<v-radio
|
||||
v-for="(radioName, index) in list"
|
||||
:key="index"
|
||||
color="#ffd843"
|
||||
:label="radioName"
|
||||
:value="index"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
107
photon-client/src/components/common/pv-range-slider.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
// value: [number, number] | WebsocketNumberPair, // Vue doesnt like Union types for the value prop for some reason.
|
||||
value: [number, number];
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
sliderCols?: number;
|
||||
disabled?: boolean;
|
||||
inverted?: boolean;
|
||||
}>(),
|
||||
{
|
||||
step: 1,
|
||||
disabled: false,
|
||||
inverted: false,
|
||||
sliderCols: 10
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: [number, number]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed<[number, number]>({
|
||||
get: (): [number, number] => {
|
||||
return Object.values(props.value) as [number, number];
|
||||
},
|
||||
set: (v) => {
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
v[i] = parseFloat(v[i] as unknown as string);
|
||||
}
|
||||
emit("input", v);
|
||||
}
|
||||
});
|
||||
|
||||
const changeFromSlot = (v: number, i: number) => {
|
||||
// localValue.value must be replaced for a reactive change to take place
|
||||
const temp = localValue.value;
|
||||
temp[i] = v;
|
||||
localValue.value = temp;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - sliderCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols">
|
||||
<v-range-slider
|
||||
v-model="localValue"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
hide-details
|
||||
class="align-center"
|
||||
dark
|
||||
:color="inverted ? 'rgba(255, 255, 255, 0.2)' : 'accent'"
|
||||
:track-color="inverted ? 'accent' : undefined"
|
||||
thumb-color="accent"
|
||||
:step="step"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-text-field
|
||||
:value="localValue[0]"
|
||||
dark
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 0)"
|
||||
/>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
:value="localValue[1]"
|
||||
dark
|
||||
color="accent"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
:max="max"
|
||||
:min="min"
|
||||
:step="step"
|
||||
type="number"
|
||||
style="width: 60px"
|
||||
@input="(v) => changeFromSlot(v, 1)"
|
||||
/>
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
72
photon-client/src/components/common/pv-select.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
interface SelectItem {
|
||||
name: string | number;
|
||||
value: string | number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
selectCols?: number;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
disabled?: boolean;
|
||||
items: string[] | number[] | SelectItem[];
|
||||
}>(),
|
||||
{
|
||||
selectCols: 9,
|
||||
disabled: false
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
// Computed in case items changes
|
||||
const items = computed<SelectItem[]>(() => {
|
||||
// Trivial case for empty list; we have no data
|
||||
if (!props.items.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the prop exists on the object to infer object type
|
||||
if ((props.items[0] as SelectItem).name) {
|
||||
return props.items as SelectItem[];
|
||||
}
|
||||
return props.items.map((v, i) => ({ name: v, value: i }));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - selectCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="selectCols">
|
||||
<v-select
|
||||
v-model="localValue"
|
||||
:items="items"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
item-disabled="disabled"
|
||||
dark
|
||||
color="accent"
|
||||
item-color="secondary"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
72
photon-client/src/components/common/pv-slider.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
sliderCols?: number;
|
||||
}>(),
|
||||
{
|
||||
step: 1,
|
||||
disabled: false,
|
||||
sliderCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", parseFloat(v as unknown as string))
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - sliderCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="sliderCols">
|
||||
<v-slider
|
||||
v-model="localValue"
|
||||
dark
|
||||
class="align-center"
|
||||
:max="max"
|
||||
:min="min"
|
||||
hide-details
|
||||
color="accent"
|
||||
:disabled="disabled"
|
||||
:step="step"
|
||||
>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
v-model="localValue"
|
||||
dark
|
||||
color="accent"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:disabled="disabled"
|
||||
class="mt-0 pt-0"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
style="width: 50px"
|
||||
:step="step"
|
||||
/>
|
||||
</template>
|
||||
</v-slider>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
42
photon-client/src/components/common/pv-switch.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import TooltippedLabel from "@/components/common/pv-tooltipped-label.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
labelCols?: number;
|
||||
switchCols?: number;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
labelCols: 2
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row dense align="center">
|
||||
<v-col :cols="12 - switchCols || labelCols">
|
||||
<tooltipped-label :tooltip="tooltip" :label="label" />
|
||||
</v-col>
|
||||
<v-col :cols="switchCols || 12 - labelCols">
|
||||
<v-switch v-model="localValue" dark :disabled="disabled" color="#ffd843" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
17
photon-client/src/components/common/pv-tooltipped-label.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-tooltip :disabled="tooltip === undefined" right open-delay="300">
|
||||
<template #activator="{ on, attrs }">
|
||||
<span style="cursor: text !important" class="white--text" v-bind="attrs" v-on="on">{{ label }}</span>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,406 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
import { computed, ref } from "vue";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
|
||||
const changeCurrentCameraIndex = (index: number) => {
|
||||
useCameraSettingsStore().setCurrentCameraIndex(index, true);
|
||||
|
||||
switch (useCameraSettingsStore().cameras[index].pipelineSettings.pipelineType) {
|
||||
case PipelineType.Reflective:
|
||||
pipelineType.value = WebsocketPipelineType.Reflective;
|
||||
break;
|
||||
case PipelineType.ColoredShape:
|
||||
pipelineType.value = WebsocketPipelineType.ColoredShape;
|
||||
break;
|
||||
case PipelineType.AprilTag:
|
||||
pipelineType.value = WebsocketPipelineType.AprilTag;
|
||||
break;
|
||||
case PipelineType.Aruco:
|
||||
pipelineType.value = WebsocketPipelineType.Aruco;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Common RegEx used for naming both pipelines and cameras
|
||||
const nameChangeRegex = /^[A-Za-z0-9_ \-)(]*[A-Za-z0-9][A-Za-z0-9_ \-)(.]*$/;
|
||||
|
||||
// Camera Name Edit
|
||||
const isCameraNameEdit = ref(false);
|
||||
const currentCameraName = ref(useCameraSettingsStore().currentCameraSettings.nickname);
|
||||
const startCameraNameEdit = () => {
|
||||
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
|
||||
isCameraNameEdit.value = true;
|
||||
};
|
||||
const checkCameraName = (name: string): string | boolean => {
|
||||
if (!nameChangeRegex.test(name))
|
||||
return "A camera name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
|
||||
if (useCameraSettingsStore().cameraNames.some((cameraName) => cameraName === name))
|
||||
return "This camera name has already been used";
|
||||
|
||||
return true;
|
||||
};
|
||||
const saveCameraNameEdit = (newName: string) => {
|
||||
useCameraSettingsStore()
|
||||
.changeCameraNickname(newName, false)
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "success",
|
||||
message: response.data.text || response.data
|
||||
});
|
||||
useCameraSettingsStore().currentCameraSettings.nickname = newName;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
|
||||
})
|
||||
.finally(() => (isCameraNameEdit.value = false));
|
||||
};
|
||||
const cancelCameraNameEdit = () => {
|
||||
isCameraNameEdit.value = false;
|
||||
currentCameraName.value = useCameraSettingsStore().currentCameraSettings.nickname;
|
||||
};
|
||||
|
||||
// Pipeline Name Edit
|
||||
const pipelineNamesWrapper = computed<{ name: string; value: number }[]>(() => {
|
||||
const pipelineNames = useCameraSettingsStore().pipelineNames.map((name, index) => ({ name: name, value: index }));
|
||||
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineNames.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
}
|
||||
if (useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineNames.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
}
|
||||
|
||||
return pipelineNames;
|
||||
});
|
||||
const isPipelineNameEdit = ref(false);
|
||||
const currentPipelineName = ref(useCameraSettingsStore().currentPipelineSettings.pipelineNickname);
|
||||
const startPipelineNameEdit = () => {
|
||||
currentPipelineName.value = useCameraSettingsStore().currentPipelineSettings.pipelineNickname;
|
||||
isPipelineNameEdit.value = true;
|
||||
};
|
||||
const checkPipelineName = (name: string): string | boolean => {
|
||||
if (!nameChangeRegex.test(name))
|
||||
return "A pipeline name can only contain letters, numbers, spaces, underscores, hyphens, parenthesis, and periods";
|
||||
if (useCameraSettingsStore().pipelineNames.some((pipelineName) => pipelineName === name))
|
||||
return "This pipeline name has already been used";
|
||||
|
||||
return true;
|
||||
};
|
||||
const savePipelineNameEdit = (name: string) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineNickname(name);
|
||||
isPipelineNameEdit.value = false;
|
||||
};
|
||||
const cancelPipelineNameEdit = () => {
|
||||
isPipelineNameEdit.value = false;
|
||||
currentPipelineName.value = useCameraSettingsStore().currentPipelineSettings.pipelineNickname;
|
||||
};
|
||||
|
||||
// Pipeline Creation
|
||||
const showPipelineCreationDialog = ref(false);
|
||||
const newPipelineName = ref("");
|
||||
const newPipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
|
||||
const showCreatePipelineDialog = () => {
|
||||
newPipelineName.value = "";
|
||||
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
showPipelineCreationDialog.value = true;
|
||||
};
|
||||
const createNewPipeline = () => {
|
||||
const type = newPipelineType.value;
|
||||
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
|
||||
useCameraSettingsStore().createNewPipeline(newPipelineName.value, type);
|
||||
showPipelineCreationDialog.value = false;
|
||||
};
|
||||
const cancelPipelineCreation = () => {
|
||||
showPipelineCreationDialog.value = false;
|
||||
newPipelineName.value = "";
|
||||
newPipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
};
|
||||
|
||||
// Pipeline Deletion
|
||||
const showPipelineDeletionConfirmationDialog = ref(false);
|
||||
const confirmDeleteCurrentPipeline = () => {
|
||||
useCameraSettingsStore().deleteCurrentPipeline();
|
||||
showPipelineDeletionConfirmationDialog.value = false;
|
||||
};
|
||||
|
||||
// Pipeline Type Change
|
||||
const showPipelineTypeChangeDialog = ref(false);
|
||||
const pipelineTypesWrapper = computed<{ name: string; value: number }[]>(() => {
|
||||
const pipelineTypes = [
|
||||
{ name: "Reflective", value: WebsocketPipelineType.Reflective },
|
||||
{ name: "Colored Shape", value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: "AprilTag", value: WebsocketPipelineType.AprilTag },
|
||||
{ name: "Aruco", value: WebsocketPipelineType.Aruco }
|
||||
];
|
||||
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
pipelineTypes.push({ name: "Driver Mode", value: WebsocketPipelineType.DriverMode });
|
||||
}
|
||||
if (useCameraSettingsStore().isCalibrationMode) {
|
||||
pipelineTypes.push({ name: "3D Calibration Mode", value: WebsocketPipelineType.Calib3d });
|
||||
}
|
||||
|
||||
return pipelineTypes;
|
||||
});
|
||||
const pipelineType = ref<WebsocketPipelineType>(useCameraSettingsStore().currentWebsocketPipelineType);
|
||||
const currentPipelineType = computed<WebsocketPipelineType>({
|
||||
get: () => {
|
||||
if (useCameraSettingsStore().isDriverMode) return WebsocketPipelineType.DriverMode;
|
||||
if (useCameraSettingsStore().isCalibrationMode) return WebsocketPipelineType.Calib3d;
|
||||
return pipelineType.value;
|
||||
},
|
||||
set: (v) => {
|
||||
pipelineType.value = v;
|
||||
}
|
||||
});
|
||||
const confirmChangePipelineType = () => {
|
||||
const type = currentPipelineType.value;
|
||||
if (type === WebsocketPipelineType.DriverMode || type === WebsocketPipelineType.Calib3d) return;
|
||||
useCameraSettingsStore().changeCurrentPipelineType(type);
|
||||
showPipelineTypeChangeDialog.value = false;
|
||||
};
|
||||
const cancelChangePipelineType = () => {
|
||||
pipelineType.value = useCameraSettingsStore().currentWebsocketPipelineType;
|
||||
showPipelineTypeChangeDialog.value = false;
|
||||
};
|
||||
|
||||
// Pipeline duplication'
|
||||
const duplicateCurrentPipeline = () => {
|
||||
useCameraSettingsStore().duplicatePipeline(useCameraSettingsStore().currentCameraSettings.currentPipelineIndex);
|
||||
};
|
||||
|
||||
// Change Props whenever the pipeline settings are changed
|
||||
useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
const currentCameraSettings = state.cameras[useStateStore().currentCameraIndex];
|
||||
|
||||
switch (currentCameraSettings.pipelineSettings.pipelineType) {
|
||||
case PipelineType.Reflective:
|
||||
pipelineType.value = WebsocketPipelineType.Reflective;
|
||||
break;
|
||||
case PipelineType.ColoredShape:
|
||||
pipelineType.value = WebsocketPipelineType.ColoredShape;
|
||||
break;
|
||||
case PipelineType.AprilTag:
|
||||
pipelineType.value = WebsocketPipelineType.AprilTag;
|
||||
break;
|
||||
case PipelineType.Aruco:
|
||||
pipelineType.value = WebsocketPipelineType.Aruco;
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="primary">
|
||||
<v-row style="padding: 12px 12px 0 24px">
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-if="!isCameraNameEdit"
|
||||
v-model="useStateStore().currentCameraIndex"
|
||||
label="Camera"
|
||||
:items="useCameraSettingsStore().cameraNames"
|
||||
@input="changeCurrentCameraIndex"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
v-model="currentCameraName"
|
||||
class="pt-2"
|
||||
:input-cols="12 - 3"
|
||||
:rules="[(v) => checkCameraName(v)]"
|
||||
label="Camera"
|
||||
@onEnter="saveCameraNameEdit"
|
||||
@onEscape="cancelCameraNameEdit"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="2" style="display: flex; align-items: center; justify-content: center">
|
||||
<div v-if="isCameraNameEdit" style="display: flex; gap: 14px">
|
||||
<pv-icon
|
||||
icon-name="mdi-content-save"
|
||||
color="#c5c5c5"
|
||||
:disabled="checkCameraName(currentCameraName) !== true"
|
||||
@click="() => saveCameraNameEdit(currentCameraName)"
|
||||
/>
|
||||
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelCameraNameEdit" />
|
||||
</div>
|
||||
<pv-icon
|
||||
v-else
|
||||
color="#c5c5c5"
|
||||
icon-name="mdi-pencil"
|
||||
tooltip="Edit Camera Name"
|
||||
@click="startCameraNameEdit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="padding: 0 12px 0 24px">
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-if="!isPipelineNameEdit"
|
||||
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
label="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:items="pipelineNamesWrapper"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
/>
|
||||
<pv-input
|
||||
v-else
|
||||
v-model="currentPipelineName"
|
||||
:input-cols="12 - 3"
|
||||
:rules="[(v) => checkPipelineName(v)]"
|
||||
label="Pipeline"
|
||||
@onEnter="(v) => savePipelineNameEdit(v)"
|
||||
@onEscape="cancelPipelineNameEdit"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="2" class="pa-0" style="display: flex; align-items: center; justify-content: center">
|
||||
<div v-if="isPipelineNameEdit" style="display: flex; gap: 14px">
|
||||
<pv-icon
|
||||
icon-name="mdi-content-save"
|
||||
color="#c5c5c5"
|
||||
:disabled="checkPipelineName(currentPipelineName) !== true"
|
||||
@click="() => savePipelineNameEdit(currentPipelineName)"
|
||||
/>
|
||||
<pv-icon icon-name="mdi-cancel" color="red darken-2" @click="cancelPipelineNameEdit" />
|
||||
</div>
|
||||
<v-menu v-else-if="!useCameraSettingsStore().isDriverMode" offset-y nudge-bottom="7" auto>
|
||||
<template #activator="{ on }">
|
||||
<v-icon color="#c5c5c5" v-on="on" @click="cancelPipelineNameEdit"> mdi-menu </v-icon>
|
||||
</template>
|
||||
<v-list dark dense color="primary">
|
||||
<v-list-item @click="startPipelineNameEdit">
|
||||
<v-list-item-title>
|
||||
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-pencil" tooltip="Edit pipeline name" />
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showCreatePipelineDialog">
|
||||
<v-list-item-title>
|
||||
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-plus" tooltip="Add new pipeline" />
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showPipelineDeletionConfirmationDialog = true">
|
||||
<v-list-item-title>
|
||||
<pv-icon color="red darken-2" :right="true" icon-name="mdi-delete" tooltip="Delete pipeline" />
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="duplicateCurrentPipeline">
|
||||
<v-list-item-title>
|
||||
<pv-icon color="#c5c5c5" :right="true" icon-name="mdi-content-copy" tooltip="Duplicate pipeline" />
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<pv-icon
|
||||
v-else-if="useCameraSettingsStore().isDriverMode && useCameraSettingsStore().pipelineNames.length === 0"
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
icon-name="mdi-plus"
|
||||
tooltip="Add new pipeline"
|
||||
@click="showCreatePipelineDialog"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="padding: 0 12px 12px 24px">
|
||||
<v-col cols="10" class="pa-0">
|
||||
<pv-select
|
||||
v-model="currentPipelineType"
|
||||
label="Type"
|
||||
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:items="pipelineTypesWrapper"
|
||||
@input="showPipelineTypeChangeDialog = true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog v-model="showPipelineCreationDialog" dark persistent width="500">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title> Create New Pipeline </v-card-title>
|
||||
<v-card-text>
|
||||
<pv-input
|
||||
v-model="newPipelineName"
|
||||
placeholder="Pipeline Name"
|
||||
:label-cols="3"
|
||||
:input-cols="12 - 3"
|
||||
label="Pipeline Name"
|
||||
:rules="[(v) => checkPipelineName(v)]"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="newPipelineType"
|
||||
:select-cols="12 - 3"
|
||||
label="Tracking Type"
|
||||
tooltip="Pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:items="[
|
||||
{ name: 'Reflective', value: WebsocketPipelineType.Reflective },
|
||||
{ name: 'Colored Shape', value: WebsocketPipelineType.ColoredShape },
|
||||
{ name: 'AprilTag', value: WebsocketPipelineType.AprilTag },
|
||||
{ name: 'Aruco', value: WebsocketPipelineType.Aruco }
|
||||
]"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="#ffd843" :disabled="checkPipelineName(newPipelineName) !== true" @click="createNewPipeline">
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn color="error" @click="cancelPipelineCreation"> Cancel </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showPipelineDeletionConfirmationDialog" dark width="500">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title> Pipeline Deletion Confirmation </v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to delete the pipeline
|
||||
<b style="color: white; font-weight: bold">{{
|
||||
useCameraSettingsStore().currentPipelineSettings.pipelineNickname
|
||||
}}</b
|
||||
>? This cannot be undone.
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="confirmDeleteCurrentPipeline"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" @click="showPipelineDeletionConfirmationDialog = false"> No, take me back </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showPipelineTypeChangeDialog" persistent width="600">
|
||||
<v-card color="primary" dark>
|
||||
<v-card-title>Change Pipeline Type</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to change the current pipeline type? This will cause all the pipeline settings to be
|
||||
overwritten and they will be lost. If this isn't what you want, duplicate this pipeline first or export
|
||||
settings.
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="error" @click="confirmChangePipelineType"> Yes, I'm sure </v-btn>
|
||||
<v-btn color="#ffd843" @click="cancelChangePipelineType"> No, take me back </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card>
|
||||
</template>
|
||||
89
photon-client/src/components/dashboard/CamerasCard.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
||||
|
||||
defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
|
||||
const driverMode = computed<boolean>({
|
||||
get: () => useCameraSettingsStore().isDriverMode,
|
||||
set: (v) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineIndex(
|
||||
v ? -1 : useCameraSettingsStore().currentCameraSettings.lastPipelineIndex || 0,
|
||||
true
|
||||
)
|
||||
});
|
||||
|
||||
const fpsTooLow = computed<boolean>(() => {
|
||||
const currFPS = useStateStore().currentPipelineResults?.fps || 0;
|
||||
const targetFPS = useCameraSettingsStore().currentVideoFormat.fps;
|
||||
const driverMode = useCameraSettingsStore().isDriverMode;
|
||||
const gpuAccel = useSettingsStore().general.gpuAcceleration !== undefined;
|
||||
const isReflective = useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective;
|
||||
|
||||
return currFPS - targetFPS < -5 && currFPS !== 0 && !driverMode && gpuAccel && isReflective;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card color="primary" height="100%" style="display: flex; flex-direction: column" dark>
|
||||
<v-card-title
|
||||
class="pb-0 mb-0 pl-4 pt-1"
|
||||
style="min-height: 50px; justify-content: space-between; align-content: center"
|
||||
>
|
||||
<div class="pt-2">
|
||||
<span class="mr-4">Cameras</span>
|
||||
<v-chip
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
style="font-size: 1rem; padding: 0; margin: 0"
|
||||
>
|
||||
<span class="pr-1">
|
||||
Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
fpsTooLow &&
|
||||
!useCameraSettingsStore().currentPipelineSettings.inputShouldShow &&
|
||||
useCameraSettingsStore().currentPipelineSettings.pipelineType === PipelineType.Reflective
|
||||
"
|
||||
>
|
||||
HSV thresholds are too broad; narrow them for better performance
|
||||
</span>
|
||||
<span v-else-if="fpsTooLow && useCameraSettingsStore().currentPipelineSettings.inputShouldShow">
|
||||
stop viewing the raw stream for better performance
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
<div>
|
||||
<v-switch
|
||||
v-model="driverMode"
|
||||
:disabled="useCameraSettingsStore().isCalibrationMode || useCameraSettingsStore().pipelineNames.length === 0"
|
||||
label="Driver Mode"
|
||||
style="margin-left: auto"
|
||||
color="accent"
|
||||
class="pt-2"
|
||||
/>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-divider style="border-color: white" />
|
||||
<v-row class="pl-3 pr-3 pt-3 pb-3" style="flex-wrap: nowrap; justify-content: center">
|
||||
<v-col v-show="value.includes(0)" style="max-width: 500px; display: flex; align-items: center">
|
||||
<photon-camera-stream id="input-camera-stream" stream-type="Raw" style="width: 100%; height: auto" />
|
||||
</v-col>
|
||||
<v-col v-show="value.includes(1)" style="max-width: 500px; display: flex; align-items: center">
|
||||
<photon-camera-stream id="output-camera-stream" stream-type="Processed" style="width: 100%; height: auto" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
165
photon-client/src/components/dashboard/ConfigOptions.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue";
|
||||
import { computed, getCurrentInstance, onBeforeUpdate, ref } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import InputTab from "@/components/dashboard/tabs/InputTab.vue";
|
||||
import ThresholdTab from "@/components/dashboard/tabs/ThresholdTab.vue";
|
||||
import ContoursTab from "@/components/dashboard/tabs/ContoursTab.vue";
|
||||
import AprilTagTab from "@/components/dashboard/tabs/AprilTagTab.vue";
|
||||
import ArucoTab from "@/components/dashboard/tabs/ArucoTab.vue";
|
||||
import OutputTab from "@/components/dashboard/tabs/OutputTab.vue";
|
||||
import TargetsTab from "@/components/dashboard/tabs/TargetsTab.vue";
|
||||
import PnPTab from "@/components/dashboard/tabs/PnPTab.vue";
|
||||
import Map3DTab from "@/components/dashboard/tabs/Map3DTab.vue";
|
||||
import { WebsocketPipelineType } from "@/types/WebsocketDataTypes";
|
||||
|
||||
interface ConfigOption {
|
||||
tabName: string;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const allTabs = Object.freeze({
|
||||
inputTab: {
|
||||
tabName: "Input",
|
||||
component: InputTab
|
||||
},
|
||||
thresholdTab: {
|
||||
tabName: "Threshold",
|
||||
component: ThresholdTab
|
||||
},
|
||||
contoursTab: {
|
||||
tabName: "Contours",
|
||||
component: ContoursTab
|
||||
},
|
||||
apriltagTab: {
|
||||
tabName: "AprilTag",
|
||||
component: AprilTagTab
|
||||
},
|
||||
arucoTab: {
|
||||
tabName: "Aruco",
|
||||
component: ArucoTab
|
||||
},
|
||||
outputTab: {
|
||||
tabName: "Output",
|
||||
component: OutputTab
|
||||
},
|
||||
targetsTab: {
|
||||
tabName: "Targets",
|
||||
component: TargetsTab
|
||||
},
|
||||
pnpTab: {
|
||||
tabName: "PnP",
|
||||
component: PnPTab
|
||||
},
|
||||
map3dTab: {
|
||||
tabName: "3D",
|
||||
component: Map3DTab
|
||||
}
|
||||
});
|
||||
|
||||
const selectedTabs = ref([0, 0, 0, 0]);
|
||||
const getTabGroups = (): ConfigOption[][] => {
|
||||
const smAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false;
|
||||
const mdAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false;
|
||||
const lgAndDown = getCurrentInstance()?.proxy.$vuetify.breakpoint.lgAndDown || false;
|
||||
const xl = getCurrentInstance()?.proxy.$vuetify.breakpoint.xl || false;
|
||||
|
||||
if (smAndDown || useCameraSettingsStore().isDriverMode || (mdAndDown && !useStateStore().sidebarFolded)) {
|
||||
return [Object.values(allTabs)];
|
||||
} else if (mdAndDown || !useStateStore().sidebarFolded) {
|
||||
return [
|
||||
[
|
||||
allTabs.inputTab,
|
||||
allTabs.thresholdTab,
|
||||
allTabs.contoursTab,
|
||||
allTabs.apriltagTab,
|
||||
allTabs.arucoTab,
|
||||
allTabs.outputTab
|
||||
],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (lgAndDown) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab, allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
} else if (xl) {
|
||||
return [
|
||||
[allTabs.inputTab],
|
||||
[allTabs.thresholdTab],
|
||||
[allTabs.contoursTab, allTabs.apriltagTab, allTabs.arucoTab, allTabs.outputTab],
|
||||
[allTabs.targetsTab, allTabs.pnpTab, allTabs.map3dTab]
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
const tabGroups = computed<ConfigOption[][]>(() => {
|
||||
// Just return the input tab because we know that is always the case in driver mode
|
||||
if (useCameraSettingsStore().isDriverMode) return [[allTabs.inputTab]];
|
||||
|
||||
const allow3d = useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled;
|
||||
const isAprilTag = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.AprilTag;
|
||||
const isAruco = useCameraSettingsStore().currentWebsocketPipelineType === WebsocketPipelineType.Aruco;
|
||||
|
||||
return getTabGroups()
|
||||
.map((tabGroup) =>
|
||||
tabGroup.filter(
|
||||
(tabConfig) =>
|
||||
!(!allow3d && tabConfig.tabName === "3D") && //Filter out 3D tab any time 3D isn't calibrated
|
||||
!((!allow3d || isAprilTag || isAruco) && tabConfig.tabName === "PnP") && //Filter out the PnP config tab if 3D isn't available, or we're doing AprilTags
|
||||
!((isAprilTag || isAruco) && tabConfig.tabName === "Threshold") && //Filter out threshold tab if we're doing AprilTags
|
||||
!((isAprilTag || isAruco) && tabConfig.tabName === "Contours") && //Filter out contours if we're doing AprilTags
|
||||
!(!isAprilTag && tabConfig.tabName === "AprilTag") && //Filter out apriltag unless we actually are doing AprilTags
|
||||
!(!isAruco && tabConfig.tabName === "Aruco") //Filter out aruco unless we actually are doing Aruco
|
||||
)
|
||||
)
|
||||
.filter((it) => it.length); // Remove empty tab groups
|
||||
});
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
// Force the current tab to the input tab on driver mode change
|
||||
if (useCameraSettingsStore().isDriverMode) {
|
||||
selectedTabs.value[0] = 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="tabGroups">
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
>
|
||||
<v-card color="primary" height="100%" class="pr-4 pl-4">
|
||||
<v-tabs
|
||||
v-model="selectedTabs[tabGroupIndex]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-4 pb-2">
|
||||
<KeepAlive>
|
||||
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-slide-group__next--disabled,
|
||||
.v-slide-group__prev--disabled {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
85
photon-client/src/components/dashboard/StreamConfigCard.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const props = defineProps<{
|
||||
// TODO fully update v-model usage in custom components on Vue3 update
|
||||
value: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: number[]): void;
|
||||
}>();
|
||||
|
||||
const localValue = computed({
|
||||
get: () => props.value,
|
||||
set: (v) => emit("input", v)
|
||||
});
|
||||
|
||||
const processingMode = computed<number>({
|
||||
get: () => (useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled ? 1 : 0),
|
||||
set: (v) => {
|
||||
if (useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: v === 1 }, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useStateStore().colorPickingMode"
|
||||
class="mt-3"
|
||||
color="primary"
|
||||
style="height: 100%; display: flex; flex-direction: column"
|
||||
>
|
||||
<v-row align="center" class="pa-3 pb-0">
|
||||
<v-col>
|
||||
<p style="color: white">Processing Mode</p>
|
||||
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
|
||||
<v-btn color="secondary">
|
||||
<v-icon>mdi-square-outline</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated">
|
||||
<v-icon>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row align="center" class="pa-3 pt-0">
|
||||
<v-col>
|
||||
<p style="color: white">Stream Display</p>
|
||||
<v-btn-toggle v-model="localValue" :multiple="true" mandatory dark class="fill">
|
||||
<v-btn color="secondary" class="fill">
|
||||
<v-icon>mdi-import</v-icon>
|
||||
<span>Raw</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary" class="fill">
|
||||
<v-icon>mdi-export</v-icon>
|
||||
<span>Processed</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-btn-toggle.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-btn-toggle.fill > .v-btn {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
91
photon-client/src/components/dashboard/tabs/AprilTagTab.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.AprilTag">
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag 36h11 (6.5in)', 'AprilTag 25h9 (6in)', 'AprilTag 16h5 (6in)']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decimate"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decimate"
|
||||
tooltip="Increases FPS at the expense of range by reducing image resolution initially"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decimate: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.blur"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Blur"
|
||||
tooltip="Gaussian blur added to the image, high FPS cost for slightly decreased noise"
|
||||
:min="0"
|
||||
:max="5"
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ blur: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threads"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Threads"
|
||||
tooltip="Number of threads spawned by the AprilTag detector"
|
||||
:min="1"
|
||||
:max="8"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threads: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.refineEdges"
|
||||
class="pt-2"
|
||||
label="Refine Edges"
|
||||
tooltip="Further refines the AprilTag corner position initial estimate, suggested left on"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ refineEdges: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.decisionMargin"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Decision Margin Cutoff"
|
||||
tooltip="Tags with a 'margin' (decoding quality score) less than this wil be rejected. Increase this to reduce the number of false positive detections"
|
||||
:min="0"
|
||||
:max="250"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ decisionMargin: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.numIterations"
|
||||
class="pt-2 pb-4"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Pose Estimation Iterations"
|
||||
tooltip="Number of iterations the pose estimation algorithm will run, 50-100 is a good starting point"
|
||||
:min="0"
|
||||
:max="500"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ numIterations: value }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
82
photon-client/src/components/dashboard/tabs/ArucoTab.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentPipelineSettings.pipelineType === PipelineType.Aruco">
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.tagFamily"
|
||||
label="Target family"
|
||||
:items="['AprilTag Family 36h11', 'AprilTag Family 25h9', 'AprilTag Family 16h5']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ tagFamily: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.useCornerRefinement"
|
||||
class="pt-2"
|
||||
label="Refine Corners"
|
||||
tooltip="Further refine the initial corners with subpixel accuracy."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ useCornerRefinement: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="currentPipelineSettings.threshWinSizes"
|
||||
label="Thresh Min/Max Size"
|
||||
tooltip="The minimum and maximum adaptive threshold window size. Larger windows tend more towards global thresholding, but small windows can be weak to noise."
|
||||
:min="3"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="2"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshWinSizes: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshStepSize"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Thresh Step Size"
|
||||
tooltip="Smaller values will cause more steps between the min/max sizes. More, varied steps can improve detection robustness to lighting, but may decrease performance."
|
||||
:min="2"
|
||||
:max="128"
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshStepSize: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.threshConstant"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Thresh Constant"
|
||||
tooltip="Affects the threshold window mean value cutoff for all steps. Higher values can improve performance, but may harm detection rate."
|
||||
:min="0"
|
||||
:max="128"
|
||||
:step="1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ threshConstant: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="currentPipelineSettings.debugThreshold"
|
||||
class="pt-2"
|
||||
label="Debug Threshold"
|
||||
tooltip="Display the first threshold step to the color stream."
|
||||
:switch-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ debugThreshold: value }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
231
photon-client/src/components/dashboard/tabs/ContoursTab.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// TODO fix pipeline typing in order to fix this, the store settings call should be able to infer that only valid pipeline type settings are exposed based on pre-checks for the entire config section
|
||||
// Defer reference to store access method
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
// TODO fix pv-range-slider so that store access doesn't need to be deferred
|
||||
const contourArea = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourArea) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourArea = v)
|
||||
});
|
||||
const contourRatio = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourRatio) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourRatio = v)
|
||||
});
|
||||
const contourFullness = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.contourFullness) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.contourFullness = v)
|
||||
});
|
||||
const contourPerimeter = computed<[number, number]>({
|
||||
get: () =>
|
||||
currentPipelineSettings.pipelineType === PipelineType.ColoredShape
|
||||
? (Object.values(currentPipelineSettings.contourPerimeter) as [number, number])
|
||||
: ([0, 0] as [number, number]),
|
||||
set: (v) => {
|
||||
if (currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
|
||||
currentPipelineSettings.contourPerimeter = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
const contourRadius = computed<[number, number]>({
|
||||
get: () =>
|
||||
currentPipelineSettings.pipelineType === PipelineType.ColoredShape
|
||||
? (Object.values(currentPipelineSettings.contourRadius) as [number, number])
|
||||
: ([0, 0] as [number, number]),
|
||||
set: (v) => {
|
||||
if (currentPipelineSettings.pipelineType === PipelineType.ColoredShape) {
|
||||
currentPipelineSettings.contourRadius = v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<pv-range-slider
|
||||
v-model="contourArea"
|
||||
label="Area"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.01"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourArea: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType !== PipelineType.ColoredShape"
|
||||
v-model="contourRatio"
|
||||
label="Ratio (W/H)"
|
||||
tooltip="Min and max ratio between the width and height of a contour's bounding rectangle"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRatio: value }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks, as well as aspect ratio"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourFullness"
|
||||
label="Fullness"
|
||||
tooltip="Min and max ratio between a contour's area and its bounding rectangle"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFullness: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape"
|
||||
v-model="contourPerimeter"
|
||||
label="Perimeter"
|
||||
tooltip="Min and max perimeter of the shape, in pixels"
|
||||
min="0"
|
||||
max="4000"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourPerimeter: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSpecklePercentage"
|
||||
label="Speckle Rejection"
|
||||
tooltip="Rejects contours whose average area is less than the given percentage of the average area of all the other contours"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSpecklePercentage: value }, false)
|
||||
"
|
||||
/>
|
||||
<template v-if="currentPipelineSettings.pipelineType === PipelineType.Reflective">
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeX"
|
||||
label="X Filter Tightness"
|
||||
tooltip="Rejects contours whose center X is further than X standard deviations left/right of the mean X location"
|
||||
:min="0.1"
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeX: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.contourFilterRangeY"
|
||||
label="Y Filter Tightness"
|
||||
tooltip="Rejects contours whose center Y is further than X standard deviations above/below the mean Y location"
|
||||
:min="0.1"
|
||||
:max="4"
|
||||
:step="0.1"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourFilterRangeY: value }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode"
|
||||
label="Target Grouping"
|
||||
tooltip="Whether or not every two targets are paired with each other (good for e.g. 2019 targets)"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Single', 'Dual', 'Two or More']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourGroupingMode: value }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourIntersection"
|
||||
label="Target Intersection"
|
||||
tooltip="If target grouping is in dual mode it will use this dropdown to decide how targets are grouped with adjacent targets"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['None', 'Up', 'Down', 'Left', 'Right']"
|
||||
:disabled="useCameraSettingsStore().currentPipelineSettings.contourGroupingMode === 0"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourIntersection: value }, false)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="currentPipelineSettings.pipelineType === PipelineType.ColoredShape">
|
||||
<v-divider class="mt-3" />
|
||||
<pv-select
|
||||
v-model="currentPipelineSettings.contourShape"
|
||||
label="Target Shape"
|
||||
tooltip="The shape of targets to look for"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Circle', 'Polygon', 'Triangle', 'Quadrilateral']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourShape: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.accuracyPercentage"
|
||||
:disabled="currentPipelineSettings.contourShape < 1"
|
||||
label="Shape Simplification"
|
||||
tooltip="How much we should simply the input contour before checking how many sides it has"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ accuracyPercentage: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.circleDetectThreshold"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle match distance"
|
||||
tooltip="How close the centroid of a contour must be to the center of a circle in order for them to be matched"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleDetectThreshold: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-range-slider
|
||||
v-model="contourRadius"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Radius"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourRadius: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.maxCannyThresh"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Max Canny Threshold"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ maxCannyThresh: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="currentPipelineSettings.circleAccuracy"
|
||||
:disabled="currentPipelineSettings.contourShape !== 0"
|
||||
label="Circle Accuracy"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ circleAccuracy: value }, false)"
|
||||
/>
|
||||
<v-divider class="mt-3" />
|
||||
</template>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourSortMode"
|
||||
label="Target Sort"
|
||||
tooltip="Chooses the sorting mode used to determine the 'best' targets to provide to user code"
|
||||
:select-cols="interactiveCols"
|
||||
:items="['Largest', 'Smallest', 'Highest', 'Lowest', 'Rightmost', 'Leftmost', 'Centermost']"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourSortMode: value }, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
154
photon-client/src/components/dashboard/tabs/InputTab.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
// Due to something with libcamera or something else IDK much about, the 90° rotations need to be disabled if the libcamera drivers are being used.
|
||||
const cameraRotations = computed(() =>
|
||||
["Normal", "90° CW", "180°", "90° CCW"].map((v, i) => ({
|
||||
name: v,
|
||||
value: i,
|
||||
disabled: useSettingsStore().gpuAccelerationEnabled ? [1, 3].includes(i) : false
|
||||
}))
|
||||
);
|
||||
|
||||
const streamDivisors = [1, 2, 4, 6];
|
||||
const getFilteredStreamDivisors = (): number[] => {
|
||||
const currentResolutionWidth = useCameraSettingsStore().currentVideoFormat.resolution.width;
|
||||
return streamDivisors.filter(
|
||||
(x) =>
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
!useSettingsStore().gpuAccelerationEnabled ||
|
||||
currentResolutionWidth / x < 400
|
||||
);
|
||||
};
|
||||
const getNumberOfSkippedDivisors = () => streamDivisors.length - getFilteredStreamDivisors().length;
|
||||
|
||||
const cameraResolutions = computed(() =>
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.map(
|
||||
(f) => `${f.resolution.width} X ${f.resolution.height} at ${f.fps} FPS, ${f.pixelFormat}`
|
||||
)
|
||||
);
|
||||
const handleResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cameraVideoModeIndex: value }, false);
|
||||
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ streamingFrameDivisor: getNumberOfSkippedDivisors() }, false);
|
||||
useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor = 0;
|
||||
|
||||
if (!useCameraSettingsStore().isCurrentVideoFormatCalibrated) {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ solvePNPEnabled: false }, true);
|
||||
}
|
||||
};
|
||||
|
||||
const streamResolutions = computed(() => {
|
||||
const streamDivisors = getFilteredStreamDivisors();
|
||||
const currentResolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
return streamDivisors.map(
|
||||
(x) => `${Math.floor(currentResolution.width / x)} X ${Math.floor(currentResolution.height / x)}`
|
||||
);
|
||||
});
|
||||
const handleStreamResolutionChange = (value: number) => {
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ streamingFrameDivisor: value + getNumberOfSkippedDivisors() },
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraExposure"
|
||||
:disabled="useCameraSettingsStore().currentCameraSettings.pipelineSettings.cameraAutoExposure"
|
||||
label="Exposure"
|
||||
tooltip="Directly controls how much light is allowed to fall onto the sensor, which affects apparent brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
:step="0.1"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraExposure: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBrightness"
|
||||
label="Brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBrightness: args }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraAutoExposure"
|
||||
class="pt-2"
|
||||
label="Auto Exposure"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Enables or Disables camera automatic adjustment for current lighting conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraAutoExposure: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraGain >= 0"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraGain"
|
||||
label="Camera Gain"
|
||||
tooltip="Controls camera gain, similar to brightness"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraRedGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraRedGain"
|
||||
label="Red AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls red automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraRedGain: args }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain !== -1"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraBlueGain"
|
||||
label="Blue AWB Gain"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:slider-cols="interactiveCols"
|
||||
tooltip="Controls blue automatic white balance gain, which affects how the camera captures colors in different conditions"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ cameraBlueGain: args }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode"
|
||||
label="Orientation"
|
||||
tooltip="Rotates the camera stream"
|
||||
:items="cameraRotations"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineSetting({ inputImageRotationMode: args }, false)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cameraVideoModeIndex"
|
||||
label="Resolution"
|
||||
tooltip="Resolution and FPS the camera should directly capture at"
|
||||
:items="cameraResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => handleResolutionChange(args)"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.streamingFrameDivisor"
|
||||
label="Stream Resolution"
|
||||
tooltip="Resolution to which camera frames are downscaled for streaming to the dashboard"
|
||||
:items="streamResolutions"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(args) => handleStreamResolutionChange(args)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
23
photon-client/src/components/dashboard/tabs/Map3DTab.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { PhotonTarget } from "@/types/PhotonTrackingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import Photon3dVisualizer from "@/components/app/photon-3d-visualizer.vue";
|
||||
|
||||
const trackedTargets = computed<PhotonTarget[]>(() => useStateStore().currentPipelineResults?.targets || []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row style="width: 100%">
|
||||
<v-col>
|
||||
<span class="white--text">Target Visualization</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row style="width: 100%">
|
||||
<v-col style="display: flex; align-items: center; justify-content: center">
|
||||
<photon3d-visualizer :targets="trackedTargets" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
221
photon-client/src/components/dashboard/tabs/OutputTab.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType, RobotOffsetPointMode } from "@/types/PipelineTypes";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { RobotOffsetType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const isTagPipeline = computed(
|
||||
() =>
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
|
||||
);
|
||||
|
||||
interface MetricItem {
|
||||
header: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const offsetPoints = computed<MetricItem[]>(() => {
|
||||
switch (useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode) {
|
||||
case RobotOffsetPointMode.Single:
|
||||
const value = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetSinglePoint);
|
||||
return [{ header: "Offset Point", value: `(${value[0].toFixed(2)}°, ${value[1].toFixed(2)}°)` }];
|
||||
case RobotOffsetPointMode.Dual:
|
||||
const firstPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointA);
|
||||
const firstPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointAArea;
|
||||
const secondPoint = Object.values(useCameraSettingsStore().currentPipelineSettings.offsetDualPointB);
|
||||
const secondPointArea = useCameraSettingsStore().currentPipelineSettings.offsetDualPointBArea;
|
||||
return [
|
||||
{ header: "First Offset Point", value: `(${firstPoint[0].toFixed(2)}°, ${firstPoint[1].toFixed(2)}°)` },
|
||||
{ header: "First Offset Point Area", value: `${firstPointArea.toFixed(2)}%` },
|
||||
{ header: "Second Offset Point", value: `(${secondPoint[0].toFixed(2)}°, ${secondPoint[1].toFixed(2)}°)` },
|
||||
{ header: "Second Offset Point Area", value: `${secondPointArea.toFixed(2)}%` }
|
||||
];
|
||||
default:
|
||||
case RobotOffsetPointMode.None:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOffsetPointEdge"
|
||||
label="Target Offset Point"
|
||||
tooltip="Changes where the 'center' of the target is (used for calculating e.g. pitch and yaw)"
|
||||
:items="['Center', 'Top', 'Bottom', 'Left', 'Right']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOffsetPointEdge: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-select
|
||||
v-if="!isTagPipeline"
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.contourTargetOrientation"
|
||||
label="Target Orientation"
|
||||
tooltip="Used to determine how to calculate target landmarks (e.g. the top, left, or bottom of the target)"
|
||||
:items="['Portrait', 'Landscape']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ contourTargetOrientation: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.outputShowMultipleTargets"
|
||||
label="Show Multiple Targets"
|
||||
tooltip="If enabled, up to five targets will be displayed and sent via PhotonLib, instead of just one"
|
||||
:disabled="isTagPipeline"
|
||||
:switch-cols="interactiveCols"
|
||||
@input="
|
||||
(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ outputShowMultipleTargets: value }, false)
|
||||
"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="
|
||||
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
|
||||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
v-model="currentPipelineSettings.doMultiTarget"
|
||||
label="Do Multi-Target Estimation"
|
||||
tooltip="If enabled, all visible fiducial targets will be used to provide a single pose estimate from their combined model."
|
||||
:switch-cols="interactiveCols"
|
||||
:disabled="!isTagPipeline"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doMultiTarget: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-if="
|
||||
(currentPipelineSettings.pipelineType === PipelineType.AprilTag ||
|
||||
currentPipelineSettings.pipelineType === PipelineType.Aruco) &&
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
v-model="currentPipelineSettings.doSingleTargetAlways"
|
||||
label="Always Do Single-Target Estimation"
|
||||
tooltip="If disabled, visible fiducial targets used for multi-target estimation will not also be used for single-target estimation."
|
||||
:switch-cols="interactiveCols"
|
||||
:disabled="!isTagPipeline || !currentPipelineSettings.doMultiTarget"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ doSingleTargetAlways: value }, false)"
|
||||
/>
|
||||
<v-divider />
|
||||
<table
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
class="metrics-table mt-3 mb-3"
|
||||
>
|
||||
<tr>
|
||||
<th v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item metric-item-title">
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td v-for="(item, itemIndex) in offsetPoints" :key="itemIndex" class="metric-item">
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode"
|
||||
label="Robot Offset Mode"
|
||||
tooltip="Used to add an arbitrary offset to the location of the targeting crosshair"
|
||||
:items="['None', 'Single Point', 'Dual Point']"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ offsetRobotOffsetMode: value }, false)"
|
||||
/>
|
||||
<v-row
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode !== RobotOffsetPointMode.None"
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-row
|
||||
v-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Single"
|
||||
>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Single)"
|
||||
>
|
||||
Take Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-else-if="useCameraSettingsStore().currentPipelineSettings.offsetRobotOffsetMode === RobotOffsetPointMode.Dual"
|
||||
>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualFirst)"
|
||||
>
|
||||
Take First Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
class="black--text"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.DualSecond)"
|
||||
>
|
||||
Take Second Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%"
|
||||
@click="useCameraSettingsStore().takeRobotOffsetPoint(RobotOffsetType.Clear)"
|
||||
>
|
||||
Clear All Points
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.metrics-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-radius: 5px;
|
||||
border: 1px solid white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
padding: 1px 15px 1px 10px;
|
||||
border-right: 1px solid;
|
||||
font-weight: normal;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metric-item-title {
|
||||
font-size: 18px;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #ffd843;
|
||||
}
|
||||
</style>
|
||||
48
photon-client/src/components/dashboard/tabs/PnPTab.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { TargetModel } from "@/types/PipelineTypes";
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<pv-select
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.targetModel"
|
||||
label="Target Model"
|
||||
:items="[
|
||||
{ name: '2016 High Goal', value: TargetModel.StrongholdHighGoal },
|
||||
{ name: '2019 Dual Target', value: TargetModel.DeepSpaceDualTarget },
|
||||
{ name: '2020 High Goal Outer', value: TargetModel.InfiniteRechargeHighGoalOuter },
|
||||
{ name: '2020 Power Cell (7in)', value: TargetModel.CircularPowerCell7in },
|
||||
{ name: '2022 Cargo Ball (9.5in)', value: TargetModel.RapidReactCircularCargoBall },
|
||||
{ name: '2023 AprilTag 6in (16h5)', value: TargetModel.AprilTag6in_16h5 },
|
||||
{ name: '2024 AprilTag 6.5in (36h11)', value: TargetModel.AprilTag6p5in_36h11 }
|
||||
]"
|
||||
:select-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ targetModel: value }, false)"
|
||||
/>
|
||||
<pv-slider
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.cornerDetectionAccuracyPercentage"
|
||||
class="pt-2"
|
||||
:slider-cols="interactiveCols"
|
||||
label="Contour simplification Percentage"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="
|
||||
(value) =>
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting({ cornerDetectionAccuracyPercentage: value }, false)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
150
photon-client/src/components/dashboard/tabs/TargetsTab.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PipelineType } from "@/types/PipelineTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const currentPipelineSettings = useCameraSettingsStore().currentPipelineSettings;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-row align="start" class="pb-4" style="height: 300px">
|
||||
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
|
||||
<v-simple-table fixed-header dense dark>
|
||||
<template #default>
|
||||
<thead style="font-size: 1.25rem">
|
||||
<tr>
|
||||
<th
|
||||
v-if="
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
|
||||
"
|
||||
class="text-center"
|
||||
>
|
||||
Fiducial ID
|
||||
</th>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<th class="text-center">Pitch θ°</th>
|
||||
<th class="text-center">Yaw θ°</th>
|
||||
<th class="text-center">Skew θ°</th>
|
||||
<th class="text-center">Area %</th>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th class="text-center">X meters</th>
|
||||
<th class="text-center">Y meters</th>
|
||||
<th class="text-center">Z Angle θ°</th>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
(useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco) &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
>
|
||||
<th class="text-center">Ambiguity %</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(target, index) in useStateStore().currentPipelineResults?.targets" :key="index">
|
||||
<td
|
||||
v-if="
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco
|
||||
"
|
||||
>
|
||||
{{ target.fiducialId }}
|
||||
</td>
|
||||
<template v-if="!useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled">
|
||||
<td>{{ target.pitch.toFixed(2) }}°</td>
|
||||
<td>{{ target.yaw.toFixed(2) }}°</td>
|
||||
<td>{{ target.skew.toFixed(2) }}°</td>
|
||||
<td>{{ target.area.toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td>{{ target.pose?.x.toFixed(2) }} m</td>
|
||||
<td>{{ target.pose?.y.toFixed(2) }} m</td>
|
||||
<td>{{ (((target.pose?.angle_z || 0) * 180.0) / Math.PI).toFixed(2) }}°</td>
|
||||
</template>
|
||||
<template
|
||||
v-if="
|
||||
(useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco) &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
>
|
||||
<td>{{ target.ambiguity >= 0 ? target.ambiguity?.toFixed(2) + "%" : "(In Multi-Target)" }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-row
|
||||
v-if="
|
||||
(useCameraSettingsStore().currentPipelineType === PipelineType.AprilTag ||
|
||||
useCameraSettingsStore().currentPipelineType === PipelineType.Aruco) &&
|
||||
currentPipelineSettings.doMultiTarget &&
|
||||
useCameraSettingsStore().isCurrentVideoFormatCalibrated &&
|
||||
useCameraSettingsStore().currentPipelineSettings.solvePNPEnabled
|
||||
"
|
||||
align="start"
|
||||
class="pb-4 white--text"
|
||||
>
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-4" style="font-size: 16px">Multi-tag pose, field-to-camera</v-card-subtitle>
|
||||
<v-simple-table fixed-header height="100%" dense dark>
|
||||
<thead style="font-size: 1.25rem">
|
||||
<th class="text-center">X meters</th>
|
||||
<th class="text-center">Y meters</th>
|
||||
<th class="text-center">Z Angle θ°</th>
|
||||
<th class="text-center">Tags</th>
|
||||
</thead>
|
||||
<tbody v-show="useStateStore().currentPipelineResults?.multitagResult">
|
||||
<td>{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.x.toFixed(2) }} m</td>
|
||||
<td>{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.y.toFixed(2) }} m</td>
|
||||
<td>{{ useStateStore().currentPipelineResults?.multitagResult?.bestTransform.angle_z.toFixed(2) }}°</td>
|
||||
<td>{{ useStateStore().currentPipelineResults?.multitagResult?.fiducialIDsUsed }}</td>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background-color: #006492 !important;
|
||||
|
||||
th,
|
||||
td {
|
||||
background-color: #006492 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
tbody :hover td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
249
photon-client/src/components/dashboard/tabs/ThresholdTab.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, getCurrentInstance, onBeforeUnmount, onMounted } from "vue";
|
||||
import PvRangeSlider from "@/components/common/pv-range-slider.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { ColorPicker, type HSV } from "@/lib/ColorPicker";
|
||||
|
||||
const averageHue = computed<number>(() => {
|
||||
const isHueInverted = useCameraSettingsStore().currentPipelineSettings.hueInverted;
|
||||
let val = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue).reduce((a, b) => a + b, 0);
|
||||
|
||||
if (isHueInverted) val += 180;
|
||||
if (val > 360) val -= 360;
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
// TODO fix pv-range-slider so that store access doesn't need to be deferred
|
||||
const hsvHue = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvHue = v)
|
||||
});
|
||||
const hsvSaturation = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvSaturation = v)
|
||||
});
|
||||
const hsvValue = computed<[number, number]>({
|
||||
get: () => Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue) as [number, number],
|
||||
set: (v) => (useCameraSettingsStore().currentPipelineSettings.hsvValue = v)
|
||||
});
|
||||
|
||||
let selectedEventMode: 0 | 1 | 2 | 3 = 0;
|
||||
const handleStreamClick = (event: MouseEvent) => {
|
||||
if (!useStateStore().colorPickingMode || selectedEventMode === 0) return;
|
||||
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if (cameraStream === null) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = cameraStream.clientWidth;
|
||||
canvas.height = cameraStream.clientHeight;
|
||||
|
||||
// Get the (x, y) position of the click with (0, 0) in the top left corner
|
||||
const rect = cameraStream.getBoundingClientRect();
|
||||
const x = Math.round(((event.clientX - rect.left) / rect.width) * cameraStream.clientWidth);
|
||||
const y = Math.round(((event.clientY - rect.top) / rect.height) * cameraStream.clientHeight);
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) return;
|
||||
|
||||
context.drawImage(cameraStream as CanvasImageSource, 0, 0, cameraStream.clientWidth, cameraStream.clientHeight);
|
||||
const colorPicker = new ColorPicker(context.getImageData(x, y, 1, 1).data);
|
||||
|
||||
// Calculate HSV values based on the mode
|
||||
let selectedHSVData: [HSV, HSV] = [
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]
|
||||
];
|
||||
if (selectedEventMode === 1) {
|
||||
selectedHSVData = colorPicker.selectedColorRange();
|
||||
} else {
|
||||
const currentHue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvHue);
|
||||
const currentSaturation = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvSaturation);
|
||||
const currentValue = Object.values(useCameraSettingsStore().currentPipelineSettings.hsvValue);
|
||||
|
||||
const currentData: [HSV, HSV] = [
|
||||
[currentHue[0], currentSaturation[0], currentValue[0]],
|
||||
[currentHue[1], currentSaturation[1], currentValue[1]]
|
||||
];
|
||||
|
||||
if (selectedEventMode === 2) {
|
||||
selectedHSVData = colorPicker.expandColorRange(currentData);
|
||||
} else if (selectedEventMode === 3) {
|
||||
selectedHSVData = colorPicker.shrinkColorRange(currentData);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store and backend with the new HSV values
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{
|
||||
hsvHue: [selectedHSVData[0][0], selectedHSVData[1][0]],
|
||||
hsvSaturation: [selectedHSVData[0][1], selectedHSVData[1][1]],
|
||||
hsvValue: [selectedHSVData[0][2], selectedHSVData[1][2]]
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
disableColorPicking();
|
||||
};
|
||||
|
||||
// Put some default values in case color picking was enabled before the enableColorPicking method is called
|
||||
let inputShowing = true;
|
||||
let outputShowing = false;
|
||||
const enableColorPicking = (mode: 1 | 2 | 3) => {
|
||||
useStateStore().colorPickingMode = true;
|
||||
inputShowing = useCameraSettingsStore().currentPipelineSettings.inputShouldShow;
|
||||
outputShowing = useCameraSettingsStore().currentPipelineSettings.outputShouldShow;
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ outputShouldDraw: false, inputShouldShow: true, outputShouldShow: false },
|
||||
true
|
||||
);
|
||||
selectedEventMode = mode;
|
||||
};
|
||||
const disableColorPicking = () => {
|
||||
useStateStore().colorPickingMode = false;
|
||||
useCameraSettingsStore().changeCurrentPipelineSetting(
|
||||
{ outputShouldDraw: true, inputShouldShow: inputShowing, outputShouldShow: outputShowing },
|
||||
true
|
||||
);
|
||||
selectedEventMode = 0;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if (cameraStream === null) return;
|
||||
|
||||
cameraStream.addEventListener("click", handleStreamClick);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
const cameraStream = document.getElementById("input-camera-stream");
|
||||
if (cameraStream === null) return;
|
||||
|
||||
cameraStream.removeEventListener("click", handleStreamClick);
|
||||
});
|
||||
|
||||
const interactiveCols = computed(
|
||||
() =>
|
||||
(getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndDown || false) &&
|
||||
(!useStateStore().sidebarFolded || useCameraSettingsStore().isDriverMode)
|
||||
)
|
||||
? 9
|
||||
: 8;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="threshold-modifiers" :style="{ '--averageHue': averageHue }">
|
||||
<pv-range-slider
|
||||
id="hue-slider"
|
||||
v-model="hsvHue"
|
||||
:class="useCameraSettingsStore().currentPipelineSettings.hueInverted ? 'inverted-slider' : 'normal-slider'"
|
||||
label="Hue"
|
||||
tooltip="Describes color"
|
||||
:min="0"
|
||||
:max="180"
|
||||
:slider-cols="interactiveCols"
|
||||
:inverted="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvHue: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
id="sat-slider"
|
||||
v-model="hsvSaturation"
|
||||
class="normal-slider"
|
||||
label="Saturation"
|
||||
tooltip="Describes colorfulness; the smaller this value the 'whiter' the color becomes"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvSaturation: value }, false)"
|
||||
/>
|
||||
<pv-range-slider
|
||||
id="value-slider"
|
||||
v-model="hsvValue"
|
||||
class="normal-slider"
|
||||
label="Value"
|
||||
tooltip="Describes lightness; the smaller this value the 'blacker' the color becomes"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:slider-cols="interactiveCols"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hsvValue: value }, false)"
|
||||
/>
|
||||
<pv-switch
|
||||
v-model="useCameraSettingsStore().currentPipelineSettings.hueInverted"
|
||||
label="Invert Hue"
|
||||
:switch-cols="interactiveCols"
|
||||
tooltip="Selects the hue range outside of the hue slider bounds instead of inside"
|
||||
@input="(value) => useCameraSettingsStore().changeCurrentPipelineSetting({ hueInverted: value }, false)"
|
||||
/>
|
||||
<v-divider class="mt-3" />
|
||||
<div>
|
||||
<div class="pt-3 white--text">Color Picker</div>
|
||||
<v-row justify="center" class="mt-3 mb-3">
|
||||
<template v-if="!useStateStore().colorPickingMode">
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 2 : 3)"
|
||||
>
|
||||
<v-icon left> mdi-minus </v-icon>
|
||||
Shrink Range
|
||||
</v-btn>
|
||||
<v-btn color="accent" class="ma-2 black--text" small @click="enableColorPicking(1)">
|
||||
<v-icon left> mdi-plus-minus </v-icon>
|
||||
{{ useCameraSettingsStore().currentPipelineSettings.hueInverted ? "Exclude" : "Set to" }} Average
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="ma-2 black--text"
|
||||
small
|
||||
@click="enableColorPicking(useCameraSettingsStore().currentPipelineSettings.hueInverted ? 3 : 2)"
|
||||
>
|
||||
<v-icon left> mdi-plus </v-icon>
|
||||
Expand Range
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn color="accent" class="ma-2 black--text" style="width: 30%" small @click="disableColorPicking">
|
||||
Cancel
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.threshold-modifiers {
|
||||
--averageHue: 0;
|
||||
}
|
||||
#hue-slider >>> .v-slider {
|
||||
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#sat-slider >>> .v-slider {
|
||||
background: linear-gradient(to right, #fff 0%, hsl(var(--averageHue), 100%, 50%) 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
#value-slider >>> .v-slider {
|
||||
background: linear-gradient(to right, #000 0%, hsl(var(--averageHue), 100%, 50%) 100%);
|
||||
border-radius: 10px;
|
||||
/* prettier-ignore */
|
||||
box-shadow: 0 0 5px #333, inset 0 0 3px #333;
|
||||
}
|
||||
>>> .v-slider__thumb {
|
||||
outline: black solid thin;
|
||||
}
|
||||
.normal-slider >>> .v-slider__track-fill {
|
||||
outline: black solid thin;
|
||||
}
|
||||
|
||||
.inverted-slider >>> .v-slider__track-background {
|
||||
outline: black solid thin;
|
||||
}
|
||||
</style>
|
||||
@@ -1,154 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col
|
||||
align="center"
|
||||
cols="12"
|
||||
>
|
||||
<span class="white--text">Target Location</span>
|
||||
<canvas
|
||||
id="canvasId"
|
||||
class="mt-2"
|
||||
width="800"
|
||||
height="800"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import theme from "../../../theme";
|
||||
|
||||
export default {
|
||||
name: "MiniMap",
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
targets: Array,
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
horizontalFOV: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ctx: undefined,
|
||||
canvas: undefined,
|
||||
x: 0,
|
||||
y: 0,
|
||||
targetWidth: 40,
|
||||
targetHeight: 6
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hLen: {
|
||||
get() {
|
||||
return Math.tan(this.horizontalFOV / 2 * Math.PI / 180) * 150;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
targets: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
horizontalFOV() {
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
const canvas = document.getElementById("canvasId"); // getting the canvas element
|
||||
const ctx = canvas.getContext("2d"); // getting the canvas context
|
||||
this.canvas = canvas; // setting the canvas as a vue variable
|
||||
this.ctx = ctx; // setting the canvas context as a vue variable
|
||||
this.grad = this.ctx.createLinearGradient(400, 800, 400, 600);
|
||||
this.grad.addColorStop(0, "rgb(119,119,119)");
|
||||
this.grad.addColorStop(0.05, "rgba(14,92,22,0.96)");
|
||||
this.grad.addColorStop(0.8, 'rgba(43,43,43,0.48)');
|
||||
|
||||
// setting canvas context values for drawing
|
||||
|
||||
|
||||
this.ctx.font = "26px Arial";
|
||||
this.ctx.strokeStyle = "whitesmoke";
|
||||
this.ctx.lineWidth = 2;
|
||||
|
||||
this.$nextTick(function () {
|
||||
this.drawPlayer();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
this.clearBoard();
|
||||
this.drawPlayer();
|
||||
for (let index in this.targets) {
|
||||
this.drawTarget(index, this.targets[index].pose);
|
||||
}
|
||||
},
|
||||
drawTarget(index, target) {
|
||||
// first save the untranslated/unrotated context
|
||||
let x = 800 - (160 * target.x); // getting meters as pixels
|
||||
let y = 400 - (160 * target.y);
|
||||
this.ctx.save();
|
||||
this.ctx.beginPath();
|
||||
// move the rotation point to the center of the rect
|
||||
this.ctx.translate(y + this.targetWidth / 2, x + this.targetHeight / 2); // wpi lib makes x forward and back and y left to right
|
||||
// rotate the rect
|
||||
this.ctx.rotate(target.rot * -1 * Math.PI / 180.0);
|
||||
|
||||
// draw the rect on the transformed context
|
||||
// Note: after transforming [0,0] is visually [x,y]
|
||||
// so the rect needs to be offset accordingly when drawn
|
||||
this.ctx.rect(-this.targetWidth / 2, -this.targetHeight / 2, this.targetWidth, this.targetHeight);
|
||||
|
||||
this.ctx.fillStyle = theme.accent;
|
||||
this.ctx.fill();
|
||||
|
||||
// restore the context to its untranslated/unrotated state
|
||||
this.ctx.restore();
|
||||
this.ctx.fillStyle = "whitesmoke";
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(y + this.targetWidth / 2, x + this.targetHeight / 2, 3, 0, 2 * Math.PI, true);
|
||||
this.ctx.fill();
|
||||
this.ctx.fillText(index, y - 30, x - 5);
|
||||
|
||||
},
|
||||
drawPlayer() {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 + this.hLen, 650);
|
||||
this.ctx.lineTo(400 - this.hLen, 650);
|
||||
this.ctx.closePath();
|
||||
this.ctx.fillStyle = this.grad;
|
||||
this.ctx.fill();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 + this.hLen, 650);
|
||||
this.ctx.stroke();
|
||||
this.ctx.moveTo(400, 820);
|
||||
this.ctx.lineTo(400 - this.hLen, 650);
|
||||
this.ctx.stroke();
|
||||
|
||||
},
|
||||
clearBoard() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // clearing the canvas
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#canvasId {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background-color: #232C37;
|
||||
border-radius: 5px;
|
||||
border: 2px solid grey;
|
||||
box-shadow: 0 0 5px 1px;
|
||||
}
|
||||
|
||||
th {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,375 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row align="center">
|
||||
<v-col
|
||||
cols="10"
|
||||
md="5"
|
||||
lg="10"
|
||||
class="pt-0 pb-0 pl-6"
|
||||
>
|
||||
<CVselect
|
||||
v-if="isCameraNameEdit === false"
|
||||
v-model="currentCameraIndex"
|
||||
name="Camera"
|
||||
:list="$store.getters.cameraList"
|
||||
@input="handleInput('currentCamera',currentCameraIndex)"
|
||||
/>
|
||||
<CVinput
|
||||
v-else
|
||||
v-model="newCameraName"
|
||||
name="Camera"
|
||||
input-cols="9"
|
||||
:error-message="checkCameraName"
|
||||
@Enter="saveCameraNameChange"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="2"
|
||||
md="1"
|
||||
lg="2"
|
||||
>
|
||||
<CVicon
|
||||
v-if="isCameraNameEdit === false"
|
||||
color="#c5c5c5"
|
||||
:hover="true"
|
||||
text="edit"
|
||||
tooltip="Edit camera name"
|
||||
@click="changeCameraName"
|
||||
/>
|
||||
<div v-else>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
style="display: inline-block;"
|
||||
:hover="true"
|
||||
text="save"
|
||||
tooltip="Save Camera Name"
|
||||
@click="saveCameraNameChange"
|
||||
/>
|
||||
<CVicon
|
||||
color="error"
|
||||
style="display: inline-block;"
|
||||
:hover="true"
|
||||
text="close"
|
||||
tooltip="Discard Changes"
|
||||
@click="discardCameraNameChange"
|
||||
/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="10"
|
||||
md="5"
|
||||
lg="10"
|
||||
class="pt-0 pb-0 pl-6"
|
||||
>
|
||||
<CVselect
|
||||
v-model="currentPipelineIndex"
|
||||
name="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="$store.getters.isDriverMode"
|
||||
:list="($store.getters.isDriverMode ? ['Driver Mode'] : []).concat($store.getters.pipelineList)"
|
||||
@input="handleInputWithIndex('currentPipeline', currentPipelineIndex)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="2"
|
||||
md="1"
|
||||
lg="2"
|
||||
>
|
||||
<v-menu
|
||||
v-if="!$store.getters.isDriverMode"
|
||||
offset-y
|
||||
auto
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon
|
||||
color="#c5c5c5"
|
||||
v-on="on"
|
||||
>
|
||||
menu
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list dense>
|
||||
<v-list-item @click="toPipelineNameChange">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="edit"
|
||||
tooltip="Edit pipeline name"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="toCreatePipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="add"
|
||||
tooltip="Add new pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteCurrentPipeline">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="red darken-2"
|
||||
:right="true"
|
||||
text="delete"
|
||||
tooltip="Delete pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="openDuplicateDialog">
|
||||
<v-list-item-title>
|
||||
<CVicon
|
||||
color="#c5c5c5"
|
||||
:right="true"
|
||||
text="mdi-content-copy"
|
||||
tooltip="Duplicate pipeline"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!--pipeline duplicate dialog-->
|
||||
<v-dialog
|
||||
v-model="duplicateDialog"
|
||||
dark
|
||||
width="500"
|
||||
height="357"
|
||||
>
|
||||
<v-card dark>
|
||||
<v-card-title
|
||||
class="headline"
|
||||
primary-title
|
||||
>
|
||||
Duplicate Pipeline
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<CVselect
|
||||
v-model="pipeIndexToDuplicate"
|
||||
name="Pipeline"
|
||||
:list="$store.getters.pipelineList"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
@click="duplicatePipeline"
|
||||
>
|
||||
Duplicate
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="closeDuplicateDialog"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!--pipeline naming dialog-->
|
||||
<v-dialog
|
||||
v-model="namingDialog"
|
||||
dark
|
||||
width="500"
|
||||
height="357"
|
||||
>
|
||||
<v-card dark>
|
||||
<v-card-title
|
||||
class="headline"
|
||||
primary-title
|
||||
>
|
||||
Pipeline Name
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<CVinput
|
||||
v-model="newPipelineName"
|
||||
name="Pipeline"
|
||||
:error-message="checkPipelineName"
|
||||
@Enter="savePipelineNameChange"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="#ffd843"
|
||||
:disabled="checkPipelineName !==''"
|
||||
@click="savePipelineNameChange"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="discardPipelineNameChange"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CVicon from '../common/cv-icon'
|
||||
import CVselect from '../common/cv-select'
|
||||
import CVinput from '../common/cv-input'
|
||||
|
||||
export default {
|
||||
name: "CameraAndPipelineSelect",
|
||||
components: {
|
||||
CVicon,
|
||||
CVselect,
|
||||
CVinput
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
re: RegExp("^[A-Za-z0-9 \\-)(]*[A-Za-z0-9][A-Za-z0-9 \\-)(.]*$"),
|
||||
isCameraNameEdit: false,
|
||||
newCameraName: "",
|
||||
cameraNameError: "",
|
||||
isPipelineNameEdit: false,
|
||||
namingDialog: false,
|
||||
newPipelineName: "",
|
||||
duplicateDialog: false,
|
||||
pipeIndexToDuplicate: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checkCameraName() {
|
||||
if (this.newCameraName !== this.$store.getters.cameraList[this.currentCameraIndex]) {
|
||||
if (this.re.test(this.newCameraName)) {
|
||||
for (let cam in this.cameraList) {
|
||||
if (this.cameraList.hasOwnProperty(cam)) {
|
||||
if (this.newCameraName === this.cameraList[cam]) {
|
||||
return "A camera by that name already Exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "A camera name can only contain letters, numbers and spaces"
|
||||
}
|
||||
}
|
||||
return "";
|
||||
},
|
||||
checkPipelineName() {
|
||||
if (this.newPipelineName !== this.$store.getters.pipelineList[this.currentPipelineIndex - 1] || this.isPipelineNameEdit === false) {
|
||||
if (this.re.test(this.newPipelineName)) {
|
||||
for (let pipe in this.$store.getters.pipelineList) {
|
||||
if (this.$store.getters.pipelineList.hasOwnProperty(pipe)) {
|
||||
if (this.newPipelineName === this.$store.getters.pipelineList[pipe]) {
|
||||
return "A pipeline with this name already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return "A pipeline name can only contain letters, numbers, and spaces"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
},
|
||||
currentCameraIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentCameraIndex;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentCameraIndex', value);
|
||||
}
|
||||
},
|
||||
currentPipelineIndex: {
|
||||
get() {
|
||||
return this.$store.getters.currentPipelineIndex + (this.$store.getters.isDriverMode ? 1 : 0);
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('currentPipelineIndex', value - (this.$store.getters.isDriverMode ? 1 : 0));
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCameraName() {
|
||||
this.newCameraName = this.$store.getters.cameraList[this.currentCameraIndex];
|
||||
this.isCameraNameEdit = true;
|
||||
},
|
||||
saveCameraNameChange() {
|
||||
if (this.checkCameraName === "") {
|
||||
// this.handleInputWithIndex("changeCameraName", this.newCameraName);
|
||||
this.axios.post('http://' + this.$address + '/api/setCameraNickname',
|
||||
{name: this.newCameraName, cameraIndex: this.$store.getters.currentCameraIndex})
|
||||
// eslint-disable-next-line
|
||||
.then(r => {
|
||||
this.$emit('camera-name-changed')
|
||||
})
|
||||
.catch(e => {
|
||||
console.log("HTTP error while changing camera name " + e);
|
||||
this.$emit('camera-name-changed')
|
||||
})
|
||||
this.discardCameraNameChange();
|
||||
}
|
||||
},
|
||||
discardCameraNameChange() {
|
||||
this.isCameraNameEdit = false;
|
||||
this.newCameraName = "";
|
||||
},
|
||||
toPipelineNameChange() {
|
||||
this.newPipelineName = this.$store.getters.pipelineList[this.currentPipelineIndex - 1];
|
||||
this.isPipelineNameEdit = true;
|
||||
this.namingDialog = true;
|
||||
},
|
||||
toCreatePipeline() {
|
||||
this.newPipelineName = "New Pipeline";
|
||||
this.isPipelineNameEdit = false;
|
||||
this.namingDialog = true;
|
||||
},
|
||||
openDuplicateDialog() {
|
||||
this.pipeIndexToDuplicate = this.currentPipelineIndex - 1;
|
||||
this.duplicateDialog = true;
|
||||
},
|
||||
deleteCurrentPipeline() {
|
||||
if (this.$store.getters.pipelineList.length > 1) {
|
||||
this.handleInputWithIndex('deleteCurrentPipeline', {});
|
||||
} else {
|
||||
this.snackbar = true;
|
||||
}
|
||||
},
|
||||
savePipelineNameChange() {
|
||||
if (this.checkPipelineName === "") {
|
||||
if (this.isPipelineNameEdit) {
|
||||
this.handleInputWithIndex("changePipelineName", this.newPipelineName);
|
||||
} else {
|
||||
this.handleInputWithIndex("addNewPipeline", this.newPipelineName);
|
||||
}
|
||||
this.discardPipelineNameChange();
|
||||
}
|
||||
},
|
||||
duplicatePipeline() {
|
||||
// if (!this.anotherCamera) {
|
||||
// this.pipelineDuplicate.camera = -1
|
||||
// }
|
||||
this.handleInputWithIndex("duplicatePipeline", this.pipeIndexToDuplicate);
|
||||
// this.axios.post("http://" + this.$address + "/api/vision/duplicate", this.pipeIndexToDuplicate);
|
||||
|
||||
this.closeDuplicateDialog();
|
||||
},
|
||||
closeDuplicateDialog() {
|
||||
this.duplicateDialog = false;
|
||||
this.pipeIndexToDuplicate = undefined;
|
||||
},
|
||||
discardPipelineNameChange() {
|
||||
this.namingDialog = false;
|
||||
this.isPipelineNameEdit = false;
|
||||
this.newPipelineName = "";
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="takePointA"
|
||||
>
|
||||
Take Point A
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
style="width: 100%;"
|
||||
class="black--text"
|
||||
@click="takePointB"
|
||||
>
|
||||
Take Point B
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%;"
|
||||
@click="clearPoints"
|
||||
>
|
||||
Clear All Points
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DualCalibration",
|
||||
|
||||
methods: {
|
||||
clearPoints() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 0, this.$store.state.currentCameraIndex)
|
||||
},
|
||||
takePointA() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 2, this.$store.state.currentCameraIndex)
|
||||
},
|
||||
takePointB() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 3, this.$store.state.currentCameraIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row
|
||||
align="center"
|
||||
justify="start"
|
||||
>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="accent"
|
||||
class="black--text"
|
||||
style="width: 100%;"
|
||||
@click="takePoint"
|
||||
>
|
||||
Take Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
small
|
||||
color="yellow darken-3"
|
||||
style="width: 100%;"
|
||||
@click="clearPoint"
|
||||
>
|
||||
Clear Point
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SingleCalibration",
|
||||
methods: {
|
||||
clearPoint() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 0, this.$store.state.currentCameraIndex)
|
||||
},
|
||||
takePoint() {
|
||||
this.handleInputWithIndex("robotOffsetPoint", 1, this.$store.state.currentCameraIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { Euler, Quaternion as ThreeQuat } from "three";
|
||||
import type { Quaternion } from "@/types/PhotonTrackingTypes";
|
||||
|
||||
const quaternionToEuler = (rot_quat: Quaternion): { x: number; y: number; z: number } => {
|
||||
const quat = new ThreeQuat(rot_quat.X, rot_quat.Y, rot_quat.Z, rot_quat.W);
|
||||
const euler = new Euler().setFromQuaternion(quat, "ZYX");
|
||||
|
||||
return {
|
||||
x: euler.x * (180.0 / Math.PI),
|
||||
y: euler.y * (180.0 / Math.PI),
|
||||
z: euler.z * (180.0 / Math.PI)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title>AprilTag Field Layout</v-card-title>
|
||||
<div class="ml-5">
|
||||
<p>Field width: {{ useSettingsStore().currentFieldLayout.field.width.toFixed(2) }} meters</p>
|
||||
<p>Field length: {{ useSettingsStore().currentFieldLayout.field.length.toFixed(2) }} meters</p>
|
||||
|
||||
<!-- Simple table height must be set here and in the CSS for the fixed-header to work -->
|
||||
<v-simple-table fixed-header height="100%" dense dark>
|
||||
<template #default>
|
||||
<thead style="font-size: 1.25rem">
|
||||
<tr>
|
||||
<th class="text-center">ID</th>
|
||||
<th class="text-center">X meters</th>
|
||||
<th class="text-center">Y meters</th>
|
||||
<th class="text-center">Z meters</th>
|
||||
<th class="text-center">θ<sub>x</sub>°</th>
|
||||
<th class="text-center">θ<sub>y</sub>°</th>
|
||||
<th class="text-center">θ<sub>z</sub>°</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(tag, index) in useSettingsStore().currentFieldLayout.tags" :key="index">
|
||||
<td>{{ tag.ID }}</td>
|
||||
<td v-for="(val, idx) in Object.values(tag.pose.translation)" :key="idx">{{ val.toFixed(2) }} m</td>
|
||||
<td v-for="(val, idx) in Object.values(quaternionToEuler(tag.pose.rotation.quaternion))" :key="idx + 4">
|
||||
{{ val.toFixed(2) }}°
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background-color: #006492 !important;
|
||||
|
||||
th,
|
||||
td {
|
||||
background-color: #006492 !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
tbody :hover td {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
335
photon-client/src/components/settings/DeviceControlCard.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import axios from "axios";
|
||||
|
||||
const restartProgram = () => {
|
||||
axios
|
||||
.post("/utils/restartProgram")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully sent program restart request",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
// This endpoint always return 204 regardless of outcome
|
||||
if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const restartDevice = () => {
|
||||
axios
|
||||
.post("/utils/restartDevice")
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully dispatched the restart command. It isn't confirmed if a device restart will occur.",
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "The backend is unable to fulfil the request to restart the device.",
|
||||
color: "error"
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Error while trying to process the request! The backend didn't respond.",
|
||||
color: "error"
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "An error occurred while trying to process the request.",
|
||||
color: "error"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const address = inject<string>("backendHost");
|
||||
|
||||
const offlineUpdate = ref();
|
||||
const openOfflineUpdatePrompt = () => {
|
||||
offlineUpdate.value.click();
|
||||
};
|
||||
const handleOfflineUpdate = (payload: Event & { target: (EventTarget & HTMLInputElement) | null }) => {
|
||||
if (payload.target === null || !payload.target.files) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("jarData", payload.target.files[0]);
|
||||
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Progress...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
|
||||
axios
|
||||
.post("/utils/offlineUpdate", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: ({ progress }) => {
|
||||
const uploadPercentage = (progress || 0) * 100.0;
|
||||
if (uploadPercentage < 99.5) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "New Software Upload in Process, " + uploadPercentage.toFixed(2) + "% complete",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Installing uploaded software...",
|
||||
color: "secondary",
|
||||
timeout: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportLogFile = ref();
|
||||
const openExportLogsPrompt = () => {
|
||||
exportLogFile.value.click();
|
||||
};
|
||||
|
||||
const exportSettings = ref();
|
||||
const openExportSettingsPrompt = () => {
|
||||
exportSettings.value.click();
|
||||
};
|
||||
|
||||
enum ImportType {
|
||||
AllSettings,
|
||||
HardwareConfig,
|
||||
HardwareSettings,
|
||||
NetworkConfig,
|
||||
ApriltagFieldLayout
|
||||
}
|
||||
const showImportDialog = ref(false);
|
||||
const importType = ref<ImportType | number>(-1);
|
||||
const importFile = ref<File | null>(null);
|
||||
const handleSettingsImport = () => {
|
||||
if (importType.value === -1 || importFile.value === null) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", importFile.value);
|
||||
|
||||
let settingsEndpoint: string;
|
||||
switch (importType.value) {
|
||||
case ImportType.HardwareConfig:
|
||||
settingsEndpoint = "/hardwareConfig";
|
||||
break;
|
||||
case ImportType.HardwareSettings:
|
||||
settingsEndpoint = "/hardwareSettings";
|
||||
break;
|
||||
case ImportType.NetworkConfig:
|
||||
settingsEndpoint = "/networkConfig";
|
||||
break;
|
||||
case ImportType.ApriltagFieldLayout:
|
||||
settingsEndpoint = "/aprilTagFieldLayout";
|
||||
break;
|
||||
default:
|
||||
case ImportType.AllSettings:
|
||||
settingsEndpoint = "";
|
||||
break;
|
||||
}
|
||||
|
||||
axios
|
||||
.post(`/settings${settingsEndpoint}`, formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" }
|
||||
})
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
showImportDialog.value = false;
|
||||
importType.value = -1;
|
||||
importFile.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title>Device Control</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-row>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn color="red" @click="restartProgram">
|
||||
<v-icon left> mdi-restart </v-icon>
|
||||
Restart PhotonVision
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4" md="6">
|
||||
<v-btn color="red" @click="restartDevice">
|
||||
<v-icon left> mdi-restart-alert </v-icon>
|
||||
Restart Device
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="4">
|
||||
<v-btn color="secondary" @click="openOfflineUpdatePrompt">
|
||||
<v-icon left> mdi-upload </v-icon>
|
||||
Offline Update
|
||||
</v-btn>
|
||||
<input ref="offlineUpdate" type="file" accept=".jar" style="display: none" @change="handleOfflineUpdate" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider style="margin: 12px 0" />
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="() => (showImportDialog = true)">
|
||||
<v-icon left> mdi-import </v-icon>
|
||||
Import Settings
|
||||
</v-btn>
|
||||
<v-dialog
|
||||
v-model="showImportDialog"
|
||||
width="600"
|
||||
@input="
|
||||
() => {
|
||||
importType = -1;
|
||||
importFile = null;
|
||||
}
|
||||
"
|
||||
>
|
||||
<v-card color="primary" dark>
|
||||
<v-card-title>Import Settings</v-card-title>
|
||||
<v-card-text>
|
||||
Upload and apply previously saved or exported PhotonVision settings to this device
|
||||
<v-row class="mt-6 ml-4">
|
||||
<pv-select
|
||||
v-model="importType"
|
||||
label="Type"
|
||||
tooltip="Select the type of settings file you are trying to upload"
|
||||
:items="[
|
||||
'All Settings',
|
||||
'Hardware Config',
|
||||
'Hardware Settings',
|
||||
'Network Config',
|
||||
'Apriltag Layout'
|
||||
]"
|
||||
:select-cols="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row class="mt-6 ml-4 mr-8">
|
||||
<v-file-input
|
||||
v-model="importFile"
|
||||
:disabled="importType === -1"
|
||||
:error-messages="importType === -1 ? 'Settings type not selected' : ''"
|
||||
:accept="importType === ImportType.AllSettings ? '.zip' : '.json'"
|
||||
/>
|
||||
</v-row>
|
||||
<v-row
|
||||
class="mt-12 ml-8 mr-8 mb-1"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
align="center"
|
||||
>
|
||||
<v-btn color="secondary" :disabled="importFile === null" @click="handleSettingsImport">
|
||||
<v-icon left> mdi-import </v-icon>
|
||||
Import Settings
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="openExportSettingsPrompt">
|
||||
<v-icon left> mdi-export </v-icon>
|
||||
Export Settings
|
||||
</v-btn>
|
||||
<a
|
||||
ref="exportSettings"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/settings/photonvision_config.zip`"
|
||||
download="photonvision-settings.zip"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="openExportLogsPrompt">
|
||||
<v-icon left> mdi-download </v-icon>
|
||||
Download Current Log
|
||||
|
||||
<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
|
||||
<a
|
||||
ref="exportLogFile"
|
||||
style="color: black; text-decoration: none; display: none"
|
||||
:href="`http://${address}/api/utils/photonvision-journalctl.txt`"
|
||||
download="photonvision-journalctl.txt"
|
||||
target="_blank"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
|
||||
<v-icon left> mdi-eye </v-icon>
|
||||
Show log viewer
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-divider {
|
||||
border-color: white !important;
|
||||
}
|
||||
.v-btn {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
21
photon-client/src/components/settings/LEDControlCard.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import PvSlider from "@/components/common/pv-slider.vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title>LED Control</v-card-title>
|
||||
<div class="ml-5">
|
||||
<pv-slider
|
||||
v-model="useSettingsStore().lighting.brightness"
|
||||
label="Brightness"
|
||||
class="pt-2"
|
||||
:slider-cols="12"
|
||||
:min="0"
|
||||
:max="100"
|
||||
@input="(args) => useSettingsStore().changeLEDBrightness(args)"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
204
photon-client/src/components/settings/MetricsCard.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { computed, onBeforeMount, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
|
||||
interface MetricItem {
|
||||
header: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const generalMetrics = computed<MetricItem[]>(() => [
|
||||
{
|
||||
header: "Version",
|
||||
value: useSettingsStore().general.version || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Hardware Model",
|
||||
value: useSettingsStore().general.hardwareModel || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Platform",
|
||||
value: useSettingsStore().general.hardwarePlatform || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "GPU Acceleration",
|
||||
value: useSettingsStore().general.gpuAcceleration || "Unknown"
|
||||
}
|
||||
]);
|
||||
const platformMetrics = computed<MetricItem[]>(() => [
|
||||
{
|
||||
header: "CPU Temp",
|
||||
value: useSettingsStore().metrics.cpuTemp === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuTemp}°C`
|
||||
},
|
||||
{
|
||||
header: "CPU Usage",
|
||||
value: useSettingsStore().metrics.cpuUtil === undefined ? "Unknown" : `${useSettingsStore().metrics.cpuUtil}%`
|
||||
},
|
||||
{
|
||||
header: "CPU Memory Usage",
|
||||
value:
|
||||
useSettingsStore().metrics.ramUtil === undefined || useSettingsStore().metrics.cpuMem === undefined
|
||||
? "Unknown"
|
||||
: `${useSettingsStore().metrics.ramUtil || "Unknown"}MB of ${useSettingsStore().metrics.cpuMem}MB`
|
||||
},
|
||||
{
|
||||
header: "GPU Memory Usage",
|
||||
value:
|
||||
useSettingsStore().metrics.gpuMemUtil === undefined || useSettingsStore().metrics.gpuMem === undefined
|
||||
? "Unknown"
|
||||
: `${useSettingsStore().metrics.gpuMemUtil}MB of ${useSettingsStore().metrics.gpuMem}MB`
|
||||
},
|
||||
{
|
||||
header: "CPU Throttling",
|
||||
value: useSettingsStore().metrics.cpuThr || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "CPU Uptime",
|
||||
value: useSettingsStore().metrics.cpuUptime || "Unknown"
|
||||
},
|
||||
{
|
||||
header: "Disk Usage",
|
||||
value: useSettingsStore().metrics.diskUtilPct || "Unknown"
|
||||
}
|
||||
]);
|
||||
|
||||
const metricsLastFetched = ref("Never");
|
||||
const fetchMetrics = () => {
|
||||
useSettingsStore()
|
||||
.requestMetricsUpdate()
|
||||
.catch((error) => {
|
||||
if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Unable to fetch metrics! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to fetch metrics."
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
const pad = (num: number): string => {
|
||||
return String(num).padStart(2, "0");
|
||||
};
|
||||
|
||||
const date = new Date();
|
||||
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
fetchMetrics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title style="display: flex; justify-content: space-between">
|
||||
<span>Stats</span>
|
||||
<pv-icon icon-name="mdi-reload" color="white" tooltip="Reload Metrics" hover @click="fetchMetrics" />
|
||||
</v-card-title>
|
||||
<v-row class="pt-2 pa-4 ma-0 ml-5 pb-1">
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> General Metrics </v-card-subtitle>
|
||||
<v-simple-table class="metrics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item metric-item-title">
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td v-for="(item, itemIndex) in generalMetrics" :key="itemIndex" class="metric-item">
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-row class="pa-4 ma-0 ml-5">
|
||||
<v-card-subtitle class="ma-0 pa-0 pb-2" style="font-size: 16px"> Hardware Metrics </v-card-subtitle>
|
||||
<v-simple-table class="metrics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item metric-item-title">
|
||||
{{ item.header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td v-for="(item, itemIndex) in platformMetrics" :key="itemIndex" class="metric-item">
|
||||
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
|
||||
<span v-else>---</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<div style="text-align: right">
|
||||
<span>Last Fetched: {{ metricsLastFetched }}</span>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.metrics-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
font-size: 16px !important;
|
||||
padding: 1px 15px 1px 10px;
|
||||
border-right: 1px solid;
|
||||
font-weight: normal;
|
||||
color: white !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.metric-item-title {
|
||||
font-size: 18px !important;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #ffd843;
|
||||
}
|
||||
|
||||
.v-data-table {
|
||||
thead,
|
||||
tbody {
|
||||
background-color: #006492;
|
||||
}
|
||||
|
||||
:hover {
|
||||
tbody > tr {
|
||||
background-color: #005281 !important;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0.55em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #ffd843;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
205
photon-client/src/components/settings/NetworkingCard.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { computed, ref } from "vue";
|
||||
import PvInput from "@/components/common/pv-input.vue";
|
||||
import PvRadio from "@/components/common/pv-radio.vue";
|
||||
import PvSwitch from "@/components/common/pv-switch.vue";
|
||||
import PvSelect from "@/components/common/pv-select.vue";
|
||||
import { NetworkConnectionType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
|
||||
const settingsValid = ref(true);
|
||||
const isValidNetworkTablesIP = (v: string | undefined): boolean => {
|
||||
// Check if it is a valid team number between 1-9999
|
||||
const teamNumberRegex = /^[1-9][0-9]{0,3}$/;
|
||||
// Check if it is a team number longer than 5 digits
|
||||
const badTeamNumberRegex = /^[0-9]{5,}$/;
|
||||
|
||||
if (v === undefined) return false;
|
||||
if (teamNumberRegex.test(v)) return true;
|
||||
if (isValidIPv4(v)) return true;
|
||||
// need to check these before the hostname. "0" and "99999" are valid hostnames, but we don't want to allow then
|
||||
if (v === "0") return false;
|
||||
if (badTeamNumberRegex.test(v)) return false;
|
||||
return isValidHostname(v);
|
||||
};
|
||||
const isValidIPv4 = (v: string | undefined) => {
|
||||
// https://stackoverflow.com/a/17871737
|
||||
const ipv4Regex = /^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/;
|
||||
|
||||
if (v === undefined) return false;
|
||||
return ipv4Regex.test(v);
|
||||
};
|
||||
const isValidHostname = (v: string | undefined) => {
|
||||
// https://stackoverflow.com/a/18494710
|
||||
const hostnameRegex = /^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)+(\.([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*))*$/;
|
||||
|
||||
if (v === undefined) return false;
|
||||
return hostnameRegex.test(v);
|
||||
};
|
||||
|
||||
const saveGeneralSettings = () => {
|
||||
const changingStaticIp = useSettingsStore().network.connectionType === NetworkConnectionType.Static;
|
||||
|
||||
useSettingsStore()
|
||||
.saveGeneralSettings()
|
||||
.then((response) => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: response.data.text || response.data,
|
||||
color: "success"
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
if (error.status === 504 || changingStaticIp) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: `Connection lost! Try the new static IP at ${useSettingsStore().network.staticIp}:5800 or ${
|
||||
useSettingsStore().network.hostname
|
||||
}:5800?`
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: error.response.data.text || error.response.data
|
||||
});
|
||||
}
|
||||
} else if (error.request) {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "Error while trying to process the request! The backend didn't respond."
|
||||
});
|
||||
} else {
|
||||
useStateStore().showSnackbarMessage({
|
||||
color: "error",
|
||||
message: "An error occurred while trying to process the request."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const currentNetworkInterfaceIndex = computed<number>({
|
||||
get: () => useSettingsStore().networkInterfaceNames.indexOf(useSettingsStore().network.networkManagerIface || ""),
|
||||
set: (v) => (useSettingsStore().network.networkManagerIface = useSettingsStore().networkInterfaceNames[v])
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
|
||||
<v-card-title>Networking</v-card-title>
|
||||
<div class="ml-5">
|
||||
<v-form ref="form" v-model="settingsValid">
|
||||
<pv-input
|
||||
v-model="useSettingsStore().network.ntServerAddress"
|
||||
label="Team Number/NetworkTables Server Address"
|
||||
tooltip="Enter the Team Number or the IP address of the NetworkTables Server"
|
||||
:label-cols="4"
|
||||
:disabled="useSettingsStore().network.runNTServer"
|
||||
:rules="[
|
||||
(v) =>
|
||||
isValidNetworkTablesIP(v) ||
|
||||
'The NetworkTables Server Address must be a valid Team Number, IP address, or Hostname'
|
||||
]"
|
||||
/>
|
||||
<v-banner
|
||||
v-show="
|
||||
!isValidNetworkTablesIP(useSettingsStore().network.ntServerAddress) &&
|
||||
!useSettingsStore().network.runNTServer
|
||||
"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
style="margin: 10px 0"
|
||||
icon="mdi-alert-circle-outline"
|
||||
>
|
||||
The NetworkTables Server Address is not set or is invalid. NetworkTables is unable to connect.
|
||||
</v-banner>
|
||||
<pv-radio
|
||||
v-model="useSettingsStore().network.connectionType"
|
||||
label="IP Assignment Mode"
|
||||
tooltip="DHCP will make the radio (router) automatically assign an IP address; this may result in an IP address that changes across reboots. Static IP assignment means that you pick the IP address and it won't change."
|
||||
:input-cols="12 - 4"
|
||||
:list="['DHCP', 'Static']"
|
||||
:disabled="!(useSettingsStore().network.shouldManage && useSettingsStore().network.canManage)"
|
||||
/>
|
||||
<pv-input
|
||||
v-if="useSettingsStore().network.connectionType === NetworkConnectionType.Static"
|
||||
v-model="useSettingsStore().network.staticIp"
|
||||
:input-cols="12 - 4"
|
||||
label="Static IP"
|
||||
:rules="[(v) => isValidIPv4(v) || 'Invalid IPv4 address']"
|
||||
:disabled="!(useSettingsStore().network.shouldManage && useSettingsStore().network.canManage)"
|
||||
/>
|
||||
<pv-input
|
||||
v-model="useSettingsStore().network.hostname"
|
||||
label="Hostname"
|
||||
:input-cols="12 - 4"
|
||||
:rules="[(v) => isValidHostname(v) || 'Invalid hostname']"
|
||||
:disabled="!(useSettingsStore().network.shouldManage && useSettingsStore().network.canManage)"
|
||||
/>
|
||||
<v-divider class="pb-3" />
|
||||
<span style="font-weight: 700">Advanced Networking</span>
|
||||
<pv-switch
|
||||
v-model="useSettingsStore().network.shouldManage"
|
||||
:disabled="!useSettingsStore().network.canManage"
|
||||
label="Manage Device Networking"
|
||||
tooltip="If enabled, Photon will manage device hostname and network settings."
|
||||
:label-cols="4"
|
||||
class="pt-2"
|
||||
/>
|
||||
<pv-select
|
||||
v-model="currentNetworkInterfaceIndex"
|
||||
label="NetworkManager interface"
|
||||
:disabled="!(useSettingsStore().network.shouldManage && useSettingsStore().network.canManage)"
|
||||
:select-cols="12 - 4"
|
||||
tooltip="Name of the interface PhotonVision should manage the IP address of"
|
||||
:items="useSettingsStore().networkInterfaceNames"
|
||||
/>
|
||||
<v-banner
|
||||
v-show="
|
||||
!useSettingsStore().networkInterfaceNames.length &&
|
||||
useSettingsStore().network.shouldManage &&
|
||||
useSettingsStore().network.canManage
|
||||
"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
>
|
||||
Photon cannot detect any wired connections! Please send program logs to the developers for help.
|
||||
</v-banner>
|
||||
<pv-switch
|
||||
v-model="useSettingsStore().network.runNTServer"
|
||||
label="Run NetworkTables Server (Debugging Only)"
|
||||
tooltip="If enabled, this device will create a NT server. This is useful for home debugging, but should be disabled on-robot."
|
||||
class="mt-3 mb-3"
|
||||
:label-cols="4"
|
||||
/>
|
||||
<v-banner
|
||||
v-show="useSettingsStore().network.runNTServer"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
>
|
||||
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
|
||||
</v-banner>
|
||||
</v-form>
|
||||
<v-btn
|
||||
color="accent"
|
||||
:class="useSettingsStore().network.runNTServer ? 'mt-3' : ''"
|
||||
style="color: black; width: 100%"
|
||||
:disabled="!settingsValid && !useSettingsStore().network.runNTServer"
|
||||
@click="saveGeneralSettings"
|
||||
>
|
||||
Save
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-banner__wrapper {
|
||||
padding: 6px !important;
|
||||
}
|
||||
</style>
|
||||
97
photon-client/src/lib/AutoReconnectingWebsocket.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
import type { IncomingWebsocketData } from "@/types/WebsocketDataTypes";
|
||||
|
||||
/**
|
||||
* {@link WebSocket} wrapper class that automatically reconnects to the provided host address if the connection was closed by the remote host or a connection failure.
|
||||
* Data sent and received by the Websocket is automatically encoded and decoded using msgpack.
|
||||
*/
|
||||
export class AutoReconnectingWebsocket {
|
||||
private readonly serverAddress: string | URL;
|
||||
private websocket: WebSocket | null | undefined;
|
||||
|
||||
private readonly onConnect: () => void;
|
||||
private readonly onData: (data: IncomingWebsocketData) => void;
|
||||
private readonly onDisconnect: () => void;
|
||||
|
||||
/**
|
||||
* Create an AutoReconnectingWebsocket
|
||||
*
|
||||
* @param serverAddress address of the websocket
|
||||
* @param onConnect action to run on websocket connection (when the websocket changes to the OPEN state)
|
||||
* @param onData decoded websocket message data consumer. The data is automatically decoded by msgpack.
|
||||
* @param onDisconnect action to run on websocket disconnection (when the websocket changes to the CLOSED state)
|
||||
*/
|
||||
constructor(
|
||||
serverAddress: string | URL,
|
||||
onConnect: () => void,
|
||||
onData: (data: IncomingWebsocketData) => void,
|
||||
onDisconnect: () => void
|
||||
) {
|
||||
this.serverAddress = serverAddress;
|
||||
|
||||
this.onConnect = onConnect;
|
||||
this.onData = onData;
|
||||
this.onDisconnect = onDisconnect;
|
||||
|
||||
this.initializeWebsocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data over the websocket. This is a no-op if the websocket is not in the OPEN state.
|
||||
*
|
||||
* @param data data to send
|
||||
* @param encodeData whether or not to encode the data using msgpack (defaults to true)
|
||||
* @see isConnected
|
||||
*
|
||||
*/
|
||||
send(data, encodeData = true) {
|
||||
// Only send data if the websocket is open
|
||||
if (this.isConnected()) {
|
||||
if (encodeData) {
|
||||
this.websocket?.send(encode(data));
|
||||
} else {
|
||||
this.websocket?.send(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WebSocket is OPEN and connected
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.websocket === null || this.websocket === undefined
|
||||
? false
|
||||
: this.websocket.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation of the websocket and the binding of the action consumers.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private initializeWebsocket() {
|
||||
this.websocket = new WebSocket(this.serverAddress);
|
||||
this.websocket.binaryType = "arraybuffer";
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
console.debug("[WebSocket] Websocket Open");
|
||||
this.onConnect();
|
||||
};
|
||||
this.websocket.onmessage = (event: MessageEvent) => {
|
||||
this.onData(decode(event.data) as IncomingWebsocketData);
|
||||
};
|
||||
this.websocket.onclose = (event: CloseEvent) => {
|
||||
this.onDisconnect();
|
||||
|
||||
this.websocket = null;
|
||||
|
||||
console.info("[WebSocket] The WebSocket was closed. Will reattempt in 500 milliseconds.", event.reason);
|
||||
setTimeout(this.initializeWebsocket.bind(this), 500);
|
||||
};
|
||||
this.websocket.onerror = () => {
|
||||
this.websocket?.close();
|
||||
};
|
||||
|
||||
console.debug(`[WebSocket] Attempting to initialize Websocket connection to ${this.serverAddress}`);
|
||||
}
|
||||
}
|
||||
101
photon-client/src/lib/ColorPicker.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export type HSV = [number, number, number];
|
||||
export type RGBA = [number, number, number, number] | Uint8ClampedArray;
|
||||
|
||||
export class ColorPicker {
|
||||
public hsvData: HSV;
|
||||
|
||||
constructor(pixelData: RGBA) {
|
||||
this.hsvData = this.RGBtoHSV(pixelData);
|
||||
}
|
||||
|
||||
public selectedColorRange() {
|
||||
return this.widenRange([[...this.hsvData], [...this.hsvData]]);
|
||||
}
|
||||
|
||||
public expandColorRange(currentRange: [HSV, HSV]) {
|
||||
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
|
||||
return this.createRange(currentRange.concat(widenedHSV));
|
||||
}
|
||||
|
||||
public shrinkColorRange(currentRange: [HSV, HSV]) {
|
||||
const widenedHSV = this.widenRange([[...this.hsvData], [...this.hsvData]]);
|
||||
|
||||
//Tries to shrink the lower part of to widened HSV
|
||||
if (!this.shrinkRange(currentRange, widenedHSV[0])) {
|
||||
//If the prev attempt failed, try to shrink the higher part of to widened HSV
|
||||
this.shrinkRange(currentRange, widenedHSV[1]);
|
||||
}
|
||||
|
||||
return currentRange;
|
||||
}
|
||||
|
||||
private createRange(range: HSV[]): [HSV, HSV] {
|
||||
const newRange: [HSV, HSV] = [
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]
|
||||
];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
newRange[0][i] = range[0][i];
|
||||
newRange[1][i] = range[0][i];
|
||||
for (let j = range.length - 1; j >= 0; j--) {
|
||||
newRange[0][i] = Math.min(range[j][i], newRange[0][i]);
|
||||
newRange[1][i] = Math.max(range[j][i], newRange[1][i]);
|
||||
}
|
||||
}
|
||||
return newRange;
|
||||
}
|
||||
|
||||
private widenRange(range: [HSV, HSV]): [HSV, HSV] {
|
||||
const expanded: [HSV, HSV] = [
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]
|
||||
];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
//Expanding the range by 10
|
||||
expanded[0][i] = Math.max(0, range[0][i] - 10);
|
||||
expanded[1][i] = Math.min(255, range[1][i] + 10);
|
||||
}
|
||||
expanded[1][0] = Math.min(180, expanded[1][0]); //h is up to 180
|
||||
return expanded;
|
||||
}
|
||||
|
||||
private shrinkRange(range: [HSV, HSV], color: HSV): boolean {
|
||||
for (let i = 0; i < color.length; i++) {
|
||||
if (!(range[0][i] <= color[i] && color[i] <= range[1][i])) return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < color.length; i++) {
|
||||
if (color[i] - range[0][i] < range[1][i] - color[i]) {
|
||||
//shrink from min side
|
||||
range[0][i] = Math.min(range[0][i] + 10, range[1][i]);
|
||||
} else {
|
||||
//shrink from max side
|
||||
range[1][i] = Math.max(range[1][i] - 10, range[0][i]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private RGBtoHSV(rgba: RGBA): HSV {
|
||||
// Normalize RGB ranges
|
||||
let r = rgba[0],
|
||||
g = rgba[1],
|
||||
b = rgba[2];
|
||||
r = r / 255;
|
||||
g = g / 255;
|
||||
b = b / 255;
|
||||
|
||||
const minRGB = Math.min(r, Math.min(g, b));
|
||||
const maxRGB = Math.max(r, Math.max(g, b));
|
||||
const d = r === minRGB ? g - b : b === minRGB ? r - g : b - r;
|
||||
const h = r === minRGB ? 3 : b === minRGB ? 1 : 5;
|
||||
let H = 30 * (h - d / (maxRGB - minRGB));
|
||||
let S = (255 * (maxRGB - minRGB)) / maxRGB;
|
||||
let V = 255 * maxRGB;
|
||||
if (isNaN(H)) H = 0;
|
||||
if (isNaN(S)) S = 0;
|
||||
if (isNaN(V)) V = 0;
|
||||
return [Math.round(H), Math.round(S), Math.round(V)];
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store/index'
|
||||
import vuetify from './plugins/vuetify';
|
||||
import msgPack from 'msgpack5';
|
||||
import axios from 'axios';
|
||||
import VueAxios from "vue-axios";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
Vue.prototype.$address = location.host;
|
||||
} else if (process.env.NODE_ENV === "development") {
|
||||
Vue.prototype.$address = location.hostname + ":5800";
|
||||
}
|
||||
|
||||
const wsURL = '//' + Vue.prototype.$address + '/websocket';
|
||||
|
||||
import VueNativeSock from 'vue-native-websocket';
|
||||
|
||||
Vue.use(VueNativeSock, wsURL, {
|
||||
reconnection: true,
|
||||
reconnectionDelay: 100,
|
||||
connectManually: true,
|
||||
format: "arraybuffer",
|
||||
});
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.prototype.$msgPack = msgPack(true);
|
||||
|
||||
import {dataHandleMixin} from './mixins/global/dataHandleMixin'
|
||||
|
||||
Vue.mixin(dataHandleMixin);
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
render: h => h(App)
|
||||
}).$mount('#app');
|
||||