Skip to content

Commit

Permalink
annotation-toolkit v1.1.1 - Screenshots, layer buttons, instructions …
Browse files Browse the repository at this point in the history
…pane, etc. (#477)

* Fix bug where actions menu header was showing even when there were no gathered actions

* Layers actions menu enhancements

* Add a better json viewer for the DebugPanel. Major enhancements

* prepare v1.1.0 of annotation-toolkit

* Merge in extract-css-plugin fix, add intent prop to layerButtons and videoPlayerProps to VideoPlayer

* Updates

* Add support for screenshots for the VideoPlayer component

* Update layer path

* Remove accidentally added folder

* Have the video player auto-detect an id if none is provided. Content panel now passes down layer info via context (LayerContext)

* release notes fix

* Fix alwaysOnLayers map bugfix
  • Loading branch information
pringshia authored Aug 4, 2021
1 parent 807efef commit 16c693a
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 47 deletions.
2 changes: 1 addition & 1 deletion packages/annotation-toolkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "annotation-toolkit",
"version": "1.1.0",
"version": "1.1.1",
"description": "",
"main": "build/bundle.js",
"scripts": {
Expand Down
27 changes: 23 additions & 4 deletions packages/annotation-toolkit/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
# vNext
- The `layerButtons` object now accepts an `intent` property which can be used to color-code buttons.
- The `<VideoPlayer />` accepts a `videoPlayerProps` object that can be used to forward props to the underlying `react-video-player` object.
- The Layers panel is now scrollable.
# v1.1.1
- **NEW** Support for generating `<VideoPlayer />` screenshots via the requests queue. Usage:
```js
push(requestsPathFor("Video"), {
type: "screenshot",
payload: {
size: [x, y, cropWidth, cropHeight], // size is of original dimensions before videoScale is applied
callback: (info) => {
// info is the base64 encoded image data
},
}
})
```
- **FIX** The `<VideoPlayer />` no longer requires an `id` property. This requirement was introduced as an unintentional constraint in v1.1.0 and wasn't present in v1.0.x. It will automatically detect its `id` via context if none is provided. If one is provided, it will use that instead (however this `id` MUST correspond to an already defined `id` for another layer). In shell-mode, we now print a nice error message when an `id` if no layer is found matching this `id`. In standalone mode, this `id` can be any arbitrarily created `id`.
- **ENHANCEMENT** The `layerButtons` object now accepts an `intent` property which can be used to color-code buttons.
- **ENHANCEMENT** The `<VideoPlayer />` accepts a `videoPlayerProps` object that can be used to forward props to the underlying `react-video-player` object.
- **ENHANCEMENT** The Layers Panel is now scrollable.
- **NEW** `<AppShell />` accepts an `instructionsPane` renderProp to draw out an information panel on the top right hand corner of the screen above the actions pane.
- **FIX** Big performance bump by avoiding unnecessary renders within the LayerPanel.
- **FIX** Fix character encoding of active layer actions indicator.
- **NEW** Add support for `secondaryLabel` which works similar to `secondaryIcon` for `<Layer />` components.
- **NEW** A portal ref component is available via global state at `_unsafe.portalRef` for other BlueprintJS components such as dialogs and modals to use.
- **NEW** `contextHeight` prop for the `<AppShell />` to control the height of the Context Panel. Default: `200px`.

# v1.1.0
- **ENHANCEMENT** A helpful message is shown when `<AppShell />` is missing a `layers={...}` prop.
Expand Down
27 changes: 21 additions & 6 deletions packages/annotation-toolkit/src/AppShell.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import LayersPanel from "./panels/LayersPanel";
import Layer from "./layers/Layer";
import cx from "classnames";
import { Button } from "@blueprintjs/core";
import { useStore } from "global-context-store";

function Window({ title, children, buttons, bodyClassNames = [] }) {
return (
Expand All @@ -21,10 +22,9 @@ function Window({ title, children, buttons, bodyClassNames = [] }) {
key={idx + "-" + button.title}
title={button.title}
onClick={() => button.action && button.action()}
className={
"mosaic-default-control bp3-button bp3-minimal bp3-icon-" +
button.icon
}
icon={button.icon}
minimal={true}
className={"mosaic-default-control"}
></Button>
))}
</div>
Expand Down Expand Up @@ -101,10 +101,22 @@ function AppShell({
layerButtons = [],
showDebugPane = false,
contextPanel: ContextPanel = () => null,
contextHeight = "200px",
instructionPane = null,
}) {
const { set } = useStore();

const portalRef = React.useRef();
React.useEffect(() => {
set("_unsafe.portalRef", portalRef.current);
}, [portalRef.current]);

return (
<div className="full">
<div
style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }}
ref={portalRef}
></div>
{showNavbar ? (
<Navbar className="bp3-dark">
<Navbar.Group align={Alignment.LEFT}>
Expand All @@ -131,7 +143,10 @@ function AppShell({
<LayersPanel layers={layers} showDebugPane={showDebugPane} />
</Window>
</div>
<div className="mosaic-tile" style={{ inset: "0% 0% 200px 300px" }}>
<div
className="mosaic-tile"
style={{ inset: `0% 0% ${contextHeight} 300px` }}
>
<Window
title="Content"
bodyClassNames={["grid-background"]}
Expand All @@ -148,7 +163,7 @@ function AppShell({
</div>
<div
className="mosaic-tile"
style={{ inset: "calc(100% - 200px) 0% 0% 300px" }}
style={{ inset: `calc(100% - ${contextHeight}) 0% 0% 300px` }}
>
<Window title="Context">
<ContextPanel />
Expand Down
4 changes: 2 additions & 2 deletions packages/annotation-toolkit/src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import AppShell from "./AppShell";

import Layer from "./layers/Layer";
import Layer, { LayerContext } from "./layers/Layer";
import BBoxFrame from "./layers/BBoxFrame";
import VideoPlayer from "./layers/VideoPlayer";

import "./react-mosaic-component.css";
import "./index.css";

export { AppShell, Layer, BBoxFrame, VideoPlayer };
export { AppShell, Layer, BBoxFrame, VideoPlayer, LayerContext };
export * from "./helpers";
36 changes: 20 additions & 16 deletions packages/annotation-toolkit/src/layers/Layer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useContext } from "react";
import cx from "classnames";
import { useStore } from "global-context-store";
import { Icon } from "@blueprintjs/core";

const LayerContext = React.createContext({ stack: [] });

function Layer({
displayName,
icon,
secondaryIcon = "",
secondaryLabel = "",
children,
component,
actions = null,
Expand Down Expand Up @@ -39,6 +41,7 @@ function Layer({
id: layerId,
noPointerEvents,
hideActionsIfUnselected,
layerStack,
getData,
onSelect,
});
Expand Down Expand Up @@ -81,25 +84,25 @@ function Layer({
onClick={() => setExpanded(!expanded)}
className={cx(
expanded ? "bp3-tree-node-caret-open" : "",
children ? "bp3-tree-node-caret" : "bp3-tree-node-caret-none",
"bp3-icon-standard"
children ? "bp3-tree-node-caret" : "bp3-tree-node-caret-none"
)}
></span>
{icon ? (
>
<Icon icon={children ? "caret-right" : "empty"} />
</span>
{icon ? <Icon className="bp3-tree-node-icon" icon={icon} /> : null}
<span className="bp3-tree-node-label">{displayName}</span>
{secondaryLabel ? (
<span
className={cx(
"bp3-tree-node-icon bp3-icon-standard",
"bp3-icon-" + icon
)}
></span>
style={{ marginRight: 5, opacity: 0.8, fontSize: 12 }}
className={cx("bp3-tree-node-secondary-label")}
>
{secondaryLabel}
</span>
) : null}
<span className="bp3-tree-node-label">{displayName}</span>
<span
className={cx(
"bp3-tree-node-secondary-label bp3-icon-standard",
"bp3-icon-" + secondaryIcon
)}
></span>
<Icon icon={secondaryIcon} />
{/* <span
className={"bp3-icon-standard bp3-icon-" + secondaryIcon}
></span> */}
</div>
{children && expanded ? (
<ul className="bp3-tree-node-list">{children}</ul>
Expand All @@ -110,3 +113,4 @@ function Layer({
}

export default Layer;
export { LayerContext };
30 changes: 28 additions & 2 deletions packages/annotation-toolkit/src/layers/VideoPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactPlayer from "react-player";
import { useStore } from "global-context-store";
import { Spinner } from "@blueprintjs/core";
import { dataPathBuilderFor, requestsPathFor } from "../helpers";
import { LayerContext } from "./Layer";

export default function VideoPlayer({
id,
Expand All @@ -18,6 +19,9 @@ export default function VideoPlayer({
const vidRef = useRef();
const canvasRef = useRef();

const layerInfo = useContext(LayerContext);
id = id || layerInfo?.id || undefined;

const [detectedSize, setDetectedSize] = React.useState([10, 10]);
const [videoLoaded, setVideoLoaded] = React.useState(false);

Expand All @@ -42,11 +46,33 @@ export default function VideoPlayer({
"seconds"
);
} else if (req.type === "screenshot") {
const [x, y, cropWidth, cropHeight] = req?.payload?.size || [
0,
0,
width,
height,
];
canvasRef.current.height = cropHeight;
canvasRef.current.width = cropWidth;
canvasRef.current
.getContext("2d")
.drawImage(vidRef.current.getInternalPlayer(), 0, 0, width, height);
.drawImage(
vidRef.current.getInternalPlayer(),
x / scale,
y / scale,
cropWidth / scale,
cropHeight / scale,
0,
0,
cropWidth,
cropHeight
);
const screenshotData = canvasRef.current.toDataURL("image/png");
req.payload.callback({ store, data: screenshotData });
req.payload.callback({
store,
size: [x, y, cropWidth, cropHeight],
data: screenshotData,
});
}
},
[vidRef.current]
Expand Down
48 changes: 35 additions & 13 deletions packages/annotation-toolkit/src/panels/ContentPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isFunction } from "../utils";
import mapValues from "lodash.mapvalues";

import { Menu, MenuDivider, Classes, Card } from "@blueprintjs/core";
import { LayerContext } from "../layers/Layer";

function ContentPanel({ instructionPane: InstructionPane }) {
const store = useStore();
Expand All @@ -21,12 +22,19 @@ function ContentPanel({ instructionPane: InstructionPane }) {
let layers = get(["layers"]);
if (!layers) return null;
layers = mapValues(layers, (layer) => layer.config);
const alwaysOnLayers = Object.values(layers).filter((layer) => {
return (
layer.alwaysOn === true ||
(isFunction(layer.alwaysOn) && layer.alwaysOn())
);
});
const alwaysOnLayers = Object.entries(layers)
.filter(([layerName, layer]) => {
if (!layer) {
throw new Error(
`Could not find any Layer registered with id: "${layerName}"`
);
}
return (
layer.alwaysOn === true ||
(isFunction(layer.alwaysOn) && layer.alwaysOn())
);
})
.map(([_, layer]) => layer);
const groupedLayers = Object.values(layers).filter((layer) => {
return (
layer.onWithGroup === true &&
Expand Down Expand Up @@ -83,10 +91,12 @@ function ContentPanel({ instructionPane: InstructionPane }) {
pointerEvents: selectedLayer.noPointerEvents ? "none" : "auto",
}}
>
<SelectedViewComponent
id={selectedLayer.id}
{...selectedLayer.getData({ store })}
/>
<LayerContext.Provider value={{ id: selectedLayer.id }}>
<SelectedViewComponent
id={selectedLayer.id}
{...selectedLayer.getData({ store })}
/>
</LayerContext.Provider>
</div>
) : null}
{[...alwaysOnLayers, ...groupedLayers].map((layer) =>
Expand All @@ -99,7 +109,9 @@ function ContentPanel({ instructionPane: InstructionPane }) {
pointerEvents: layer.noPointerEvents ? "none" : "auto",
}}
>
<layer.component id={layer.id} {...layer.getData({ store })} />
<LayerContext.Provider value={{ id: layer.id }}>
<layer.component id={layer.id} {...layer.getData({ store })} />
</LayerContext.Provider>
</div>
)
)}
Expand Down Expand Up @@ -147,12 +159,22 @@ function ContentPanel({ instructionPane: InstructionPane }) {
{gatheredActions.actions.length === 0 && state.selectedLayer ? (
<MenuDivider
icon={"layer"}
title={state.selectedLayer.join(" / ") + " ⬩"}
title={
state.selectedLayer.join(" / ") +
" " +
String.fromCharCode(11049)
}
/>
) : null}
{gatheredActions.actions.map((action, idx) => (
<React.Fragment key={idx}>
<MenuDivider title={gatheredActions.actionPaths[idx] + " ⬩"} />
<MenuDivider
title={
gatheredActions.actionPaths[idx] +
" " +
String.fromCharCode(11049)
}
/>
{action}
</React.Fragment>
))}
Expand Down
4 changes: 1 addition & 3 deletions packages/annotation-toolkit/src/panels/LayersPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ export default function LayersPanel(props) {
<div style={{ display: "flex", height: "100%", flexDirection: "column" }}>
<div style={{ flex: 1, overflowY: "auto" }}>
<div className="bp3-tree">
<ul className="bp3-tree-node-list bp3-tree-root">
<props.layers />
</ul>
<ul className="bp3-tree-node-list bp3-tree-root">{props.layers()}</ul>
</div>
</div>
{props.showDebugPane ? <DebugPanel /> : null}
Expand Down

0 comments on commit 16c693a

Please sign in to comment.