Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Open sidebar
Georg Christian Felbinger
annotate
Commits
63404119
Commit
63404119
authored
Nov 16, 2017
by
Georg Felbinger
Browse files
version 0.2.0
- fixed issues #1,#2,#3,#4 - larger preview, positioning in corner with most space
parent
a413b378
Pipeline
#15560
failed with stages
Changes
11
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
392 additions
and
267 deletions
+392
-267
client/src/AppController.tsx
client/src/AppController.tsx
+2
-114
client/src/AppModel.tsx
client/src/AppModel.tsx
+0
-9
client/src/AppView.tsx
client/src/AppView.tsx
+53
-40
client/src/annotation/AnnotationController.tsx
client/src/annotation/AnnotationController.tsx
+119
-11
client/src/annotation/AnnotationModel.tsx
client/src/annotation/AnnotationModel.tsx
+11
-4
client/src/annotation/AnnotationView.tsx
client/src/annotation/AnnotationView.tsx
+113
-70
client/src/annotation/LabelSelectComponent.tsx
client/src/annotation/LabelSelectComponent.tsx
+19
-12
client/src/annotation/PreviewComponent.tsx
client/src/annotation/PreviewComponent.tsx
+10
-5
client/src/util/KeyBinding.tsx
client/src/util/KeyBinding.tsx
+18
-1
client/src/util/components/ConfirmDialog.tsx
client/src/util/components/ConfirmDialog.tsx
+46
-0
server/package.json
server/package.json
+1
-1
No files found.
client/src/AppController.tsx
View file @
63404119
import
axios
from
'
axios
'
;
import
{
Box
,
Label
,
PostImage
}
from
'
./api/Image
'
;
import
{
ReducerFactory
}
from
'
./util/Reducers
'
;
import
{
connect
,
Dispatch
}
from
'
react-redux
'
;
import
{
RootState
}
from
'
./index
'
;
...
...
@@ -8,111 +6,7 @@ import { Component } from './AppView';
const
actions
=
new
ReducerFactory
<
State
,
OwnProps
>
();
const
CreateBox
=
actions
.
createAction
<
Box
>
(
(
state
,
box
)
=>
({
...
state
,
box
:
[...
state
.
box
,
box
.
value
]
})
);
const
DeleteBox
=
actions
.
createAction
<
number
>
(
(
state
,
boxIndex
)
=>
({
...
state
,
box
:
state
.
box
.
filter
((
b
,
i
)
=>
i
!==
boxIndex
.
value
)
})
);
const
LoadLabels
=
actions
.
createVoidAction
(
(
state
,
action
)
=>
{
axios
.
get
(
'
/v1/labels
'
)
.
then
(
response
=>
response
.
data
as
string
[])
.
then
(
labels
=>
UpdateLabels
.
create
(
action
.
dispatch
,
action
.
props
)(
labels
));
return
state
;
});
const
LoadImage
=
actions
.
createVoidAction
(
(
state
,
action
)
=>
{
axios
.
get
(
'
/v1/image
'
)
.
then
(
response
=>
response
.
data
as
Label
)
.
then
(
label
=>
UpdateImage
.
create
(
action
.
dispatch
,
action
.
props
)(
label
));
return
state
;
}
);
const
SaveImage
=
actions
.
createVoidAction
(
(
state
,
action
)
=>
{
const
auth
=
action
.
props
.
authToken
.
map
(
a
=>
({
authToken
:
a
}
as
{}))
.
get
(()
=>
({}));
const
data
:
PostImage
=
{
...
auth
,
label
:
{
url
:
state
.
image
,
boxes
:
state
.
box
.
map
(
b
=>
({
...
b
,
start
:
{
x
:
b
.
start
.
x
*
640
,
y
:
b
.
start
.
y
*
480
},
size
:
{
x
:
b
.
size
.
x
*
640
,
y
:
b
.
size
.
y
*
480
}
}))
}
};
axios
.
post
(
'
/v1/image
'
,
data
).
then
(()
=>
{
Reset
.
create
(
action
.
dispatch
,
action
.
props
)();
LoadImage
.
create
(
action
.
dispatch
,
action
.
props
)();
LoadLabels
.
create
(
action
.
dispatch
,
action
.
props
)();
});
return
state
;
}
);
const
Reset
=
actions
.
createVoidAction
(
()
=>
{
return
initialState
;
}
);
const
UpdateImage
=
actions
.
createAction
<
Label
>
(
(
state
,
action
)
=>
{
return
{
...
state
,
image
:
action
.
value
.
url
,
box
:
action
.
value
.
boxes
.
map
(
b
=>
({
...
b
,
start
:
{
x
:
b
.
start
.
x
/
640
,
y
:
b
.
start
.
y
/
480
},
size
:
{
x
:
b
.
size
.
x
/
640
,
y
:
b
.
size
.
y
/
480
}
}))
};
}
);
const
UpdateLabels
=
actions
.
createAction
<
string
[]
>
(
(
state
,
action
)
=>
({
...
state
,
labels
:
action
.
value
})
);
const
initialState
:
State
=
{
labels
:
[],
image
:
''
,
box
:
[],
};
const
initialState
:
State
=
{};
export
const
AppReducer
=
actions
.
build
(
initialState
);
...
...
@@ -127,12 +21,6 @@ export const App = connect(
};
},
(
dispatch
:
Dispatch
<
{}
>
,
props
:
OwnProps
):
Actions
=>
{
return
{
onCreateBox
:
CreateBox
.
create
(
dispatch
,
props
),
onDeleteBox
:
DeleteBox
.
create
(
dispatch
,
props
),
onSave
:
SaveImage
.
create
(
dispatch
,
props
),
onLoadImage
:
LoadImage
.
create
(
dispatch
,
props
),
onLoadLabels
:
LoadLabels
.
create
(
dispatch
,
props
),
};
return
{};
}
)(
Component
);
client/src/AppModel.tsx
View file @
63404119
import
{
Box
}
from
'
./api/Image
'
;
import
{
Option
}
from
'
./util/Types
'
;
export
interface
OwnProps
{
...
...
@@ -6,17 +5,9 @@ export interface OwnProps {
}
export
interface
State
{
labels
:
string
[];
image
:
string
;
box
:
Box
[];
}
export
interface
Actions
{
onCreateBox
:
(
box
:
Box
)
=>
void
;
onDeleteBox
:
(
boxIndex
:
number
)
=>
void
;
onSave
:
(
v
:
{})
=>
void
;
onLoadImage
:
(
v
:
{})
=>
void
;
onLoadLabels
:
(
v
:
{})
=>
void
;
}
export
interface
Props
extends
OwnProps
{
...
...
client/src/AppView.tsx
View file @
63404119
import
*
as
React
from
'
react
'
;
import
{
AnnotateImage
}
from
'
./annotation/AnnotationController
'
;
import
{
Button
,
FormGroup
,
Grid
,
InputGroup
,
Jumbotron
,
Nav
,
Navbar
,
NavItem
}
from
'
react-bootstrap
'
;
import
{
renderIf
}
from
'
./util/Reacts
'
;
import
{
Col
,
Grid
,
Jumbotron
,
Nav
,
Navbar
,
NavItem
,
Row
,
Table
}
from
'
react-bootstrap
'
;
import
{
Actions
,
Props
}
from
'
./AppModel
'
;
import
{
Lifecycle
}
from
'
./Lifecycle
'
;
import
{
LoginButton
}
from
'
./login/LoginController
'
;
import
{
Score
}
from
'
./scores/ScoreController
'
;
...
...
@@ -25,45 +23,60 @@ export const Component = (props: Props & Actions) => {
</
Navbar
.
Collapse
>
</
Navbar
>
<
Grid
>
<
Lifecycle
componentDidMount
=
{
()
=>
{
props
.
onLoadImage
({});
props
.
onLoadLabels
({});
}
}
/>
<
AnnotateImage
imageUrl
=
{
props
.
state
.
image
}
boxes
=
{
props
.
state
.
box
}
labels
=
{
props
.
state
.
labels
}
onCreateBox
=
{
props
.
onCreateBox
}
authToken
=
{
props
.
authToken
}
/>
<
Jumbotron
>
{
props
.
state
.
box
.
map
((
box
,
i
)
=>
{
return
(
<
FormGroup
key
=
{
i
}
>
<
InputGroup
>
<
InputGroup
.
Addon
>
{
i
}
:
{
box
.
label
}
</
InputGroup
.
Addon
>
<
Button
id
=
{
`d-
${
i
}
`
}
onClick
=
{
()
=>
props
.
onDeleteBox
(
i
)
}
>
x
</
Button
>
</
InputGroup
>
</
FormGroup
>
);
})
}
<
Button
default
=
{
false
}
onClick
=
{
props
.
onSave
}
>
{
renderIf
(
props
.
state
.
box
.
length
===
0
,
()
=>
'
No Labels
'
,
()
=>
'
Send
'
)
}
</
Button
>
</
Jumbotron
>
<
Score
/>
<
Row
>
<
Col
xs
=
{
12
}
md
=
{
12
}
>
<
Score
/>
</
Col
>
</
Row
>
<
Row
>
<
Col
xs
=
{
12
}
md
=
{
12
}
>
<
Jumbotron
>
<
h3
>
Readme
</
h3
>
<
h4
>
Keybindings
</
h4
>
<
Table
>
<
thead
>
<
tr
>
<
th
>
Key
</
th
>
<
th
>
Effect
</
th
>
</
tr
>
</
thead
>
<
tbody
>
<
tr
>
<
td
>
[Shift+]Arrows
</
td
>
<
td
>
Move second control point. (Use Shift for fast mode)
</
td
>
</
tr
>
<
tr
>
<
td
>
Ctrl+[Shift+]Arrows
</
td
>
<
td
>
Move first control point. (Use Shift for fast mode)
</
td
>
</
tr
>
<
tr
>
<
td
>
Numbers
</
td
>
<
td
>
Select the label type
</
td
>
</
tr
>
<
tr
>
<
td
>
Enter
</
td
>
<
td
>
Add current selection to labels
</
td
>
</
tr
>
<
tr
>
<
td
>
Shift+Enter
</
td
>
<
td
>
Send image to server
</
td
>
</
tr
>
</
tbody
>
</
Table
>
<
h4
>
How to label
</
h4
>
<
ul
>
<
li
>
Select the bounding boxes around objects that you can clearly identify.
</
li
>
<
li
>
Surround the objects as close as possible, but completely.
</
li
>
<
li
>
The robots own belly isn't a robot.
</
li
>
</
ul
>
</
Jumbotron
>
</
Col
>
</
Row
>
</
Grid
>
</
div
>
);
...
...
client/src/annotation/AnnotationController.tsx
View file @
63404119
...
...
@@ -5,6 +5,8 @@ import { Point, Points } from '../api/Point';
import
{
connect
,
Dispatch
}
from
'
react-redux
'
;
import
{
RootState
}
from
'
../index
'
;
import
{
Component
}
from
'
./AnnotationView
'
;
import
{
Box
,
Label
,
PostImage
}
from
'
../api/Image
'
;
import
axios
from
'
axios
'
;
const
reducer
=
new
ReducerFactory
<
State
,
Props
>
();
...
...
@@ -133,31 +135,131 @@ const TouchEnd = reducer.createAction<React.TouchEvent<{}>>((s, a) => {
}
});
const
CreateBox
=
reducer
.
createAction
<
Box
>
(
(
state
,
box
)
=>
({
...
state
,
box
:
[...
state
.
box
,
box
.
value
]
})
);
const
DeleteBox
=
reducer
.
createAction
<
number
>
(
(
state
,
boxIndex
)
=>
({
...
state
,
box
:
state
.
box
.
filter
((
b
,
i
)
=>
i
!==
boxIndex
.
value
)
})
);
const
SaveImage
=
reducer
.
createVoidAction
(
(
state
,
action
)
=>
{
const
auth
=
action
.
props
.
authToken
.
map
(
a
=>
({
authToken
:
a
}
as
{}))
.
get
(()
=>
({}));
const
data
:
PostImage
=
{
...
auth
,
label
:
{
url
:
state
.
image
,
boxes
:
state
.
box
.
map
(
b
=>
({
...
b
,
start
:
{
x
:
b
.
start
.
x
*
640
,
y
:
b
.
start
.
y
*
480
},
size
:
{
x
:
b
.
size
.
x
*
640
,
y
:
b
.
size
.
y
*
480
}
}))
}
};
axios
.
post
(
'
/v1/image
'
,
data
).
then
(()
=>
{
Reset
.
create
(
action
.
dispatch
,
action
.
props
)();
LoadImage
.
create
(
action
.
dispatch
,
action
.
props
)();
LoadLabels
.
create
(
action
.
dispatch
,
action
.
props
)();
});
return
state
;
}
);
const
LoadLabels
=
reducer
.
createVoidAction
(
(
state
,
action
)
=>
{
axios
.
get
(
'
/v1/labels
'
)
.
then
(
response
=>
response
.
data
as
string
[])
.
then
(
labels
=>
UpdateLabels
.
create
(
action
.
dispatch
,
action
.
props
)(
labels
));
return
state
;
});
const
LoadImage
=
reducer
.
createVoidAction
(
(
state
,
action
)
=>
{
axios
.
get
(
'
/v1/image
'
)
.
then
(
response
=>
response
.
data
as
Label
)
.
then
(
label
=>
UpdateImage
.
create
(
action
.
dispatch
,
action
.
props
)(
label
));
return
state
;
}
);
const
UpdateImage
=
reducer
.
createAction
<
Label
>
(
(
state
,
action
)
=>
{
return
{
...
state
,
image
:
action
.
value
.
url
,
box
:
action
.
value
.
boxes
.
map
(
b
=>
({
...
b
,
start
:
{
x
:
b
.
start
.
x
/
640
,
y
:
b
.
start
.
y
/
480
},
size
:
{
x
:
b
.
size
.
x
/
640
,
y
:
b
.
size
.
y
/
480
}
}))
};
}
);
const
UpdateLabels
=
reducer
.
createAction
<
string
[]
>
(
(
state
,
action
)
=>
({
...
state
,
label
:
state
.
label
===
''
?
action
.
value
[
0
]
:
state
.
label
,
labels
:
action
.
value
})
);
const
Reset
=
reducer
.
createVoidAction
((
s
)
=>
initialState
);
const
initialState
:
State
=
{
const
ResetSelect
=
reducer
.
createVoidAction
(
s
=>
({
...
s
,
...
initialSelectState
}));
const
initialSelectState
=
{
initial
:
true
,
label
:
''
,
offset
:
{
x
:
0
,
y
:
0
},
p1
:
{
x
:
0
,
y
:
0
},
p2
:
{
x
:
1
,
y
:
1
},
currentMove
:
CurrentMove
.
NONE
currentMove
:
CurrentMove
.
NONE
,
};
const
initialState
:
State
=
{
...
initialSelectState
,
label
:
''
,
labels
:
[],
box
:
[],
image
:
''
};
export
const
Reducer
=
reducer
.
build
(
initialState
);
export
const
AnnotateImage
=
connect
(
(
state
:
RootState
,
ownProps
:
OwnProps
)
=>
{
let
label
=
state
.
annotation
.
label
;
if
(
label
===
''
&&
ownProps
.
labels
.
length
>
0
)
{
label
=
ownProps
.
labels
[
0
];
}
return
({
...
ownProps
,
state
:
{
...
state
.
annotation
,
label
:
label
}
state
:
state
.
annotation
});
},
(
dispatch
:
Dispatch
<
Action
<
{},
{}
>>
,
props
:
Props
):
Actions
=>
({
...
...
@@ -169,5 +271,11 @@ export const AnnotateImage = connect(
onTouchEnd
:
TouchEnd
.
create
(
dispatch
,
props
),
onArrow
:
ArrowMove
.
create
(
dispatch
,
props
),
onChangeLabel
:
ChangeLabel
.
create
(
dispatch
,
props
),
onCreateBox
:
CreateBox
.
create
(
dispatch
,
props
),
onDeleteBox
:
DeleteBox
.
create
(
dispatch
,
props
),
onSave
:
SaveImage
.
create
(
dispatch
,
props
),
onLoadImage
:
LoadImage
.
create
(
dispatch
,
props
),
onLoadLabels
:
LoadLabels
.
create
(
dispatch
,
props
),
onReset
:
Reset
.
create
(
dispatch
,
props
),
onResetSelect
:
ResetSelect
.
create
(
dispatch
,
props
)
}))(
Component
);
client/src/annotation/AnnotationModel.tsx
View file @
63404119
import
{
Point
}
from
'
../api/Point
'
;
import
{
Box
}
from
'
../api/Image
'
;
import
{
Option
}
from
'
../util/Types
'
;
export
enum
CurrentMove
{
P1
,
P2
,
NONE
...
...
@@ -12,13 +13,13 @@ export interface State {
p2
:
Point
;
currentMove
:
CurrentMove
;
label
:
string
;
labels
:
string
[];
image
:
string
;
box
:
Box
[];
}
export
interface
OwnProps
{
imageUrl
:
string
;
boxes
:
Box
[];
labels
:
string
[];
onCreateBox
:
(
box
:
Box
)
=>
void
;
authToken
:
Option
<
string
>
;
}
export
interface
Actions
{
...
...
@@ -30,7 +31,13 @@ export interface Actions {
onTouchEnd
:
(
e
:
React
.
TouchEvent
<
{}
>
)
=>
void
;
onArrow
:
(
a
:
{
e
:
KeyboardEvent
,
d
:
Point
})
=>
void
;
onChangeLabel
:
(
l
:
string
)
=>
void
;
onCreateBox
:
(
box
:
Box
)
=>
void
;
onDeleteBox
:
(
boxIndex
:
number
)
=>
void
;
onSave
:
()
=>
void
;
onLoadImage
:
(
v
:
{})
=>
void
;
onLoadLabels
:
(
v
:
{})
=>
void
;
onReset
:
()
=>
void
;
onResetSelect
:
()
=>
void
;
}
export
interface
Props
extends
OwnProps
{
...
...
client/src/annotation/AnnotationView.tsx
View file @
63404119
import
*
as
React
from
'
react
'
;
import
*
as
ReactDOM
from
'
react-dom
'
;
import
{
Jumbotron
}
from
'
react-bootstrap
'
;
import
{
Button
,
Col
,
ControlLabel
,
FormGroup
,
InputGroup
,
Jumbotron
,
Row
}
from
'
react-bootstrap
'
;
import
{
Actions
,
CurrentMove
,
Props
}
from
'
./AnnotationModel
'
;
import
{
LabelSelect
}
from
'
./LabelSelectComponent
'
;
import
{
KeyBinding
}
from
'
../util/KeyBinding
'
;
...
...
@@ -10,83 +10,126 @@ import './AnnotationView.css';
import
{
renderIf
}
from
'
../util/Reacts
'
;
import
{
Rect
}
from
'
./RectComponent
'
;
import
{
Preview
}
from
'
./PreviewComponent
'
;
import
{
Lifecycle
}
from
'
../Lifecycle
'
;
export
const
Component
=
(
props
:
Props
&
Actions
)
=>
{
const
stateStart
=
Points
.
reduce
(
props
.
state
.
p1
,
props
.
state
.
p2
,
Math
.
min
);
const
stateEnd
=
Points
.
reduce
(
props
.
state
.
p1
,
props
.
state
.
p2
,
Math
.
max
);
const
stateSize
=
Points
.
minus
(
stateEnd
,
stateStart
);
let
movingX
=
props
.
state
.
currentMove
===
CurrentMove
.
P1
?
props
.
state
.
p1
.
x
:
props
.
state
.
p2
.
x
;
const
movingLeft
=
stateStart
.
x
!==
movingX
;
const
posX
=
movingLeft
?
stateStart
.
x
-
.
15
:
stateEnd
.
x
+
.
025
;
const
previewPos
=
{
x
:
`
${
posX
*
100
}
%`
,
y
:
`
${
stateStart
.
y
*
100
}
%`
};
let
ctrlPointSize
=
{
x
:
'
0.2em
'
,
y
:
'
0.2em
'
};
return
(
<
div
>
<
Jumbotron
ref
=
{
e
=>
preventTouchDefaults
(
e
)
}
onMouseDown
=
{
e
=>
props
.
onMouseDown
(
e
)
}
onMouseMove
=
{
e
=>
props
.
onMouseMove
(
e
)
}
onMouseUp
=
{
e
=>
props
.
onMouseUp
(
e
)
}
onTouchStart
=
{
e
=>
props
.
onTouchStart
(
e
)
}
onTouchMove
=
{
e
=>
props
.
onTouchMove
(
e
)
}
onTouchEnd
=
{
e
=>
props
.
onTouchEnd
(
e
)
}
>
<
div
id
=
"labelImage"
className
=
"image draw-panel"
>
<
img
src
=
{
props
.
imageUrl
}
width
=
"100%"
draggable
=
{
false
}
/>
{
renderIf
(
!
(
props
.
state
.
currentMove
===
CurrentMove
.
NONE
),
()
=>
(
<
Preview
imageUrl
=
{
props
.
imageUrl
}
stateStart
=
{
stateStart
}
stateSize
=
{
stateSize
}
position
=
{
previewPos
}
/>
))
}
{
renderIf
(
!
props
.
state
.
initial
,
()
=>
(
<
div
>
<
Rect
start
=
{
ToPercent
(
stateStart
)
}
size
=
{
ToPercent
(
stateSize
)
}
content
=
{
props
.
state
.
label
}
color
=
"pink"
/>
<
Rect
start
=
{
ToPercent
(
props
.
state
.
p1
)
}
size
=
{
ctrlPointSize
}
background
=
"pink"
color
=
"pink"
/>
<
Rect
start
=
{
ToPercent
(
props
.
state
.
p2
)
}
size
=
{
ctrlPointSize
}
background
=
"pink"
color
=
"pink"
<
Row
>
<
Lifecycle
componentDidMount
=
{
()
=>
{
props
.
onLoadImage
({});
props
.
onLoadLabels
({});
}
}
/>
<
KeyBinding
code
=
"ArrowUp"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
0
,
y
:
-
.
001
}})
}
/>
<
KeyBinding
code
=
"ArrowDown"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
0
,
y
:
.
001
}})
}
/>
<
KeyBinding
code
=
"ArrowLeft"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
-
.
001
,
y
:
0
}})
}
/>
<
KeyBinding
code
=
"ArrowRight"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
.
001
,
y
:
0
}})
}
/>
<
KeyBinding
code
=
"Escape"
onKey
=
{
e
=>
props
.
onResetSelect
()
}
/>
<
KeyBinding
code
=
"Shift+Enter"
onKey
=
{
e
=>
window
.
confirm
(
'
Are you sure?
'
)
?
props
.
onSave
()
:
null
}
/>
<
Col
xs
=
{
12
}
md
=
{
8
}
>
<
Jumbotron
ref
=
{
e
=>
preventTouchDefaults
(
e
)
}
onMouseDown
=
{
e
=>
props
.
onMouseDown
(
e
)
}
onMouseMove
=
{
e
=>
props
.
onMouseMove
(
e
)
}
onMouseUp
=
{
e
=>
props
.
onMouseUp
(
e
)
}
onTouchStart
=
{
e
=>
props
.
onTouchStart
(
e
)
}
onTouchMove
=
{
e
=>
props
.
onTouchMove
(
e
)
}
onTouchEnd
=
{
e
=>
props
.
onTouchEnd
(
e
)
}
>
<
div
id
=
"labelImage"
className
=
"image draw-panel"
>
<
img
src
=
{
props
.
state
.
image
}
width
=
"100%"
draggable
=
{
false
}
/>
{
renderIf
(
!
(
props
.
state
.
currentMove
===
CurrentMove
.
NONE
),
()
=>
(
<
Preview
imageUrl
=
{
props
.
state
.
image
}
stateStart
=
{
stateStart
}
stateSize
=
{
stateSize
}
left
=
{
stateStart
.
x
>
(
1
-
stateEnd
.
x
)
?
'
0px
'
:
undefined
}
right
=
{
stateStart
.
x
<=
(
1
-
stateEnd
.
x
)
?
'
0px
'
:
undefined
}
top
=
{
stateStart
.
y
>
(
1
-
stateEnd
.
y
)
?
'
0px
'
:
undefined
}
bottom
=
{
stateStart
.
y
<=
(
1
-
stateEnd
.
y
)
?
'
0px
'
:
undefined
}
/>
</
div
>
))
}
{
props
.
boxes
.
map
((
b
,
i
)
=>
(
<
Rect
start
=
{
ToPercent
(
b
.
start
)
}
size
=
{
ToPercent
(
b
.
size
)
}
content
=
{
`
${
i
}
:
${
b
.
label
}
`
}
/>
))
}
</
div
>
</
Jumbotron
>
<
Jumbotron
>
<
LabelSelect
labels
=
{
props
.
labels
}
label
=
{
props
.
state
.
label
}
onChangeLabel
=
{
props
.
onChangeLabel
}
onSave
=
{
l
=>
{
if
(
!
props
.
state
.
initial
)
{
props
.
onCreateBox
(
makeBox
(
l
,
props
.
state
.
p1
,
props
.
state
.
p2
));
props
.
onReset
();
}
}
}
/>
<
KeyBinding
code
=
"ArrowUp"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
0
,
y
:
-
.
001
}})
}
/>
<
KeyBinding
code
=
"ArrowDown"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
0
,
y
:
.
001
}})
}
/>
<
KeyBinding
code
=
"ArrowLeft"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
-
.
001
,
y
:
0
}})
}
/>
<
KeyBinding
code
=
"ArrowRight"
onKey
=
{
e
=>
props
.
onArrow
({
e
:
e
,
d
:
{
x
:
.
001
,
y
:
0
}})
}
/>
<
KeyBinding
code
=
"Escape"
onKey
=
{
e
=>
props
.
onReset
()
}
/>
</
Jumbotron
>