imported mining stuff, can add a mine and list them
This commit is contained in:
parent
7dad5217e7
commit
5fcb99ef40
36 changed files with 6622 additions and 32 deletions
|
|
@ -14,7 +14,7 @@ export {
|
|||
useInteractionsAPI,
|
||||
// useMeasurementsWebsocket,
|
||||
// useMetricAPI,
|
||||
// useMineAPI,
|
||||
useMineAPI,
|
||||
usePermissionAPI,
|
||||
// usePreferenceAPI,
|
||||
// useSecurity,
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ import { makeStyles } from "@mui/styles";
|
|||
import { green, red } from "@mui/material/colors";
|
||||
import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers";
|
||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||
import dayjs from "dayjs";
|
||||
// import dayjs from "dayjs";
|
||||
import CancelSubmit from "common/CancelSubmit";
|
||||
import { Sensor } from "ventilation/drawable/Sensor";
|
||||
|
||||
// import { Sensor } from "ventilation/drawable/Sensor";
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ interface Props {
|
|||
closeDialogCallback: Function;
|
||||
refreshCallback: Function;
|
||||
canEdit?: boolean;
|
||||
// sensor?: Sensor;
|
||||
sensor?: Sensor;
|
||||
}
|
||||
|
||||
export default function InteractionSettings(props: Props) {
|
||||
|
|
@ -126,7 +127,7 @@ export default function InteractionSettings(props: Props) {
|
|||
closeDialogCallback,
|
||||
refreshCallback,
|
||||
canEdit,
|
||||
// sensor
|
||||
sensor
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
const { success, error } = useSnackbar();
|
||||
|
|
@ -206,11 +207,11 @@ export default function InteractionSettings(props: Props) {
|
|||
or(interaction.settings.result, undefined)
|
||||
);
|
||||
|
||||
// if (sensor) {
|
||||
// interaction.settings.subtype = 2;
|
||||
// interaction.settings.nodeOne = sensor.index;
|
||||
// interaction.settings.nodeTwo = sensor.index;
|
||||
// }
|
||||
if (sensor) {
|
||||
interaction.settings.subtype = 2;
|
||||
interaction.settings.nodeOne = sensor.index;
|
||||
interaction.settings.nodeTwo = sensor.index;
|
||||
}
|
||||
|
||||
setIsRemoveInteractionOpen(false);
|
||||
setNumConditions(numConditions);
|
||||
|
|
@ -982,15 +983,15 @@ export default function InteractionSettings(props: Props) {
|
|||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
{/* {multiNodeSource() && !sensor && (
|
||||
{multiNodeSource() && !sensor && (
|
||||
<Grid
|
||||
key={"nodes"}
|
||||
container
|
||||
direction="row"
|
||||
justify="center"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
spacing={2}>
|
||||
<Grid item xs={10} sm={4}>
|
||||
<Grid size={{ xs: 10, sm: 4 }}>
|
||||
<TextField
|
||||
select
|
||||
id="subtype"
|
||||
|
|
@ -1019,7 +1020,7 @@ export default function InteractionSettings(props: Props) {
|
|||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={10} sm={4}>
|
||||
<Grid size={{ xs: 10, sm: 4 }}>
|
||||
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
|
||||
<TextField
|
||||
select
|
||||
|
|
@ -1036,7 +1037,7 @@ export default function InteractionSettings(props: Props) {
|
|||
</TextField>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={10} sm={4}>
|
||||
<Grid size={{ xs: 10, sm: 4 }}>
|
||||
{subtypeDropdown === 3 && (
|
||||
<TextField
|
||||
select
|
||||
|
|
@ -1054,7 +1055,7 @@ export default function InteractionSettings(props: Props) {
|
|||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)} */}
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
@ -1564,7 +1565,7 @@ export default function InteractionSettings(props: Props) {
|
|||
</Grid>
|
||||
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
||||
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
|
||||
{/* <Button onClick={close} color="primary">
|
||||
<Button onClick={close} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
{canEdit && (
|
||||
|
|
@ -1574,7 +1575,7 @@ export default function InteractionSettings(props: Props) {
|
|||
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
|
||||
Submit
|
||||
</Button>
|
||||
)} */}
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,29 +2,23 @@ import { lazy, Suspense } from "react";
|
|||
import LoadingScreen from "../app/LoadingScreen";
|
||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
// import Teams from "pages/Teams";
|
||||
// import Users from "pages/Users";
|
||||
// import TeamPage from "pages/Team";
|
||||
import Header from "app/Header";
|
||||
import Logout from "pages/Logout";
|
||||
import Devices from "pages/Devices";
|
||||
// import DevicePage from "pages/Device";
|
||||
// import GroupsPage from "pages/Groups";
|
||||
// import GroupPage from "pages/Group";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import DeviceComponent from "pages/DeviceComponent";
|
||||
import Bins from "pages/Bins";
|
||||
import { getWhitelabel } from "services/whiteLabel";
|
||||
import Bin from "pages/Bin";
|
||||
|
||||
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
||||
const DevicePage = lazy(() => import("pages/Device"));
|
||||
const Devices = lazy(() => import("pages/Devices"));
|
||||
const GroupsPage = lazy(() => import("pages/Groups"));
|
||||
const GroupPage = lazy(() => import("pages/Group"));
|
||||
const Teams = lazy(() => import("pages/Teams"));
|
||||
const Users = lazy(() => import("pages/Users"));
|
||||
const TeamPage = lazy(() => import("pages/Team"));
|
||||
const BinPage = lazy(() => import("pages/Bin"));
|
||||
const Bins = lazy(() => import("pages/Bins"));
|
||||
const Mines = lazy(() => import("pages/Mines"));
|
||||
const DeviceComponent = lazy(() => import("pages/DeviceComponent"));
|
||||
|
||||
interface Props {
|
||||
toggleTheme: () => void;
|
||||
|
|
@ -48,6 +42,7 @@ export default function Router(props: Props) {
|
|||
<Route path="devices/*" element={<DevicesRoute/>} />
|
||||
<Route path="groups/*" element={<GroupsRoute/>} />
|
||||
<Route path="bins/*" element={<BinsRoute/>} />
|
||||
<Route path="mines/*" element={<MinesRoute/>} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
|
@ -112,7 +107,7 @@ export default function Router(props: Props) {
|
|||
/>
|
||||
<Route
|
||||
path="/:binID" // "/settings/basic"
|
||||
element={<Bin />}
|
||||
element={<BinPage />}
|
||||
/>
|
||||
{/* <Route
|
||||
path="/:deviceID/components/:componentID" // "/settings/basic"
|
||||
|
|
@ -131,6 +126,36 @@ export default function Router(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
const MinesRoute = () => {
|
||||
return (
|
||||
<div>
|
||||
<Routes>
|
||||
<Route
|
||||
key="Mines page route"
|
||||
path="" // "/settings/basic"
|
||||
element={<Mines key={"Bins page"} />}
|
||||
/>
|
||||
{/* <Route
|
||||
path="/:binID" // "/settings/basic"
|
||||
element={<Bin />}
|
||||
/> */}
|
||||
{/* <Route
|
||||
path="/:deviceID/components/:componentID" // "/settings/basic"
|
||||
element={<DeviceComponent />}
|
||||
/>
|
||||
<Route
|
||||
path="/:deviceID/history" // "/settings/basic"
|
||||
element={<DeviceHistory />}
|
||||
/> */}
|
||||
<Route
|
||||
path="/:binID/*" // "/settings/basic"
|
||||
element={<RelativeRoutes />}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const GroupsRoute = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,37 @@
|
|||
import { ChevronRight, People, Person } from "@mui/icons-material";
|
||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
||||
import { darken, Divider, Grid2 as Grid, IconButton, lighten, List, ListItemButton, ListItemIcon, ListItemText, SwipeableDrawer, Theme, Toolbar, Tooltip, useTheme } from "@mui/material";
|
||||
import {
|
||||
darken,
|
||||
Divider,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
lighten,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
SwipeableDrawer,
|
||||
Theme,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import classNames from "classnames";
|
||||
import { useWidth } from "../hooks/useWidth";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import BindaptIcon from "../products/Bindapt/BindaptIcon";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import BinsIcon from "products/Bindapt/BinsIcon";
|
||||
import { useGlobalState } from "providers";
|
||||
import {
|
||||
hasTutorialPlaylist,
|
||||
IsAdaptiveAgriculture,
|
||||
IsAdCon,
|
||||
isBXT,
|
||||
IsMiVent,
|
||||
IsOmniAir
|
||||
} from "services/whiteLabel";
|
||||
import MiningIcon from "products/ventilation/MiningIcon";
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
|
|
@ -80,6 +105,7 @@ export default function SideNavigator(props: Props) {
|
|||
const width = useWidth();
|
||||
const classes = useStyles();
|
||||
const location = useLocation();
|
||||
const [{ user }] = useGlobalState()
|
||||
|
||||
const getClasses = (page: string) => {
|
||||
if (page === "/device") {
|
||||
|
|
@ -101,7 +127,7 @@ export default function SideNavigator(props: Props) {
|
|||
}
|
||||
|
||||
const authenticatedSideMenu = () => {
|
||||
|
||||
const isMiVent = IsMiVent();
|
||||
return (
|
||||
<List className={classes.list} component="nav">
|
||||
<Tooltip title="Bins" placement="right">
|
||||
|
|
@ -132,6 +158,22 @@ export default function SideNavigator(props: Props) {
|
|||
{open && <ListItemText primary="Devices" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
{(isMiVent || user.hasFeature("admin")) && (
|
||||
<Tooltip title="Mines" placement="right">
|
||||
<ListItemButton
|
||||
id="tour-dashboard"
|
||||
component={Link}
|
||||
to="/mines"
|
||||
onClick={onClose}
|
||||
classes={getClasses("/mine")}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<MiningIcon />
|
||||
</ListItemIcon>
|
||||
{open && <ListItemText primary="Mines" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Teams" placement="right">
|
||||
<ListItemButton
|
||||
id="tour-dashboard"
|
||||
|
|
|
|||
200
src/pages/Mines.tsx
Normal file
200
src/pages/Mines.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
||||
import classNames from "classnames";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import PageContainer from "./PageContainer";
|
||||
import {
|
||||
Theme,
|
||||
Tooltip,
|
||||
Chip,
|
||||
Box,
|
||||
Grid2 as Grid,
|
||||
useTheme,
|
||||
Typography,
|
||||
Grid2,
|
||||
} from "@mui/material";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useMineAPI, useMobile } from "hooks";
|
||||
// import { useHistory } from "react-router";
|
||||
import AddMine from "ventilation/AddMine";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import SearchBar from "common/SearchBar";
|
||||
import { makeStyles, withStyles } from "@mui/styles";
|
||||
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { or } from "utils";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
gutter: {
|
||||
paddingLeft: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2)
|
||||
},
|
||||
[theme.breakpoints.up("md")]: {
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3)
|
||||
}
|
||||
},
|
||||
actionToolbar: {
|
||||
paddingLeft: 0,
|
||||
paddingRight: theme.spacing(1),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
paddingRight: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
densePaddingY: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1)
|
||||
},
|
||||
mobileContent: {
|
||||
maxWidth: "100%",
|
||||
margin: 0,
|
||||
padding: `${theme.spacing(1)}px ${theme.spacing(1)}px`
|
||||
},
|
||||
tableContainer: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
interface MineRow {
|
||||
mine: pond.Mine;
|
||||
}
|
||||
|
||||
const mineColumns: Column<MineRow>[] = [
|
||||
{
|
||||
title: "Name",
|
||||
cellStyle: { margin: 1 },
|
||||
render: row => {
|
||||
let key = row.mine.settings?.key ? row.mine.settings?.key : "No Key found";
|
||||
let name = row.mine.settings?.name ? row.mine.settings?.name : "Mine";
|
||||
return (
|
||||
<Tooltip title={"ID: " + key}>
|
||||
<Chip variant="outlined" label={name} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default function Mines() {
|
||||
const classes = useStyles();
|
||||
const mineAPI = useMineAPI();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [mineTableData, setMineTableData] = useState<MineRow[]>([]);
|
||||
const [minesTotal, setMinesTotal] = useState<number>(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [mineDialog, setMineDialog] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
|
||||
const ColorAdd = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
color: theme.palette.getContrastText(theme.palette.primary.main),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
borderRadius: "4px",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
cursor: "pointer"
|
||||
},
|
||||
"&:disabled": {
|
||||
backgroundColor: "rgba(0,0,0,0)",
|
||||
color: theme.palette.getContrastText(theme.palette.primary.main)
|
||||
}
|
||||
}
|
||||
}))(Add);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
mineAPI
|
||||
.listMines(pageSize, pageSize * page, "desc", undefined, searchValue, false)
|
||||
.then(resp => {
|
||||
let mineData: MineRow[] = [];
|
||||
resp.data.mines.forEach(mine => {
|
||||
mineData.push({
|
||||
mine: mine
|
||||
});
|
||||
});
|
||||
setMineTableData(mineData);
|
||||
setMinesTotal(resp.data.total);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [pageSize, page, mineAPI, setIsLoading, searchValue]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const handleRowsPerPageChange = (event: any) => {
|
||||
const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value
|
||||
setPageSize(newRowsPerPage);
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
const renderTitle = () => {
|
||||
return (
|
||||
<Grid2 container direction={"row"} spacing={1} alignContent={"center"}>
|
||||
{/* <Grid2 sx={{ border: "1px solid blue"}} alignContent={"center"}> */}
|
||||
<Typography variant="h5">
|
||||
Mines
|
||||
</Typography>
|
||||
<ColorAdd onClick={() => setMineDialog(true)} />
|
||||
</Grid2>
|
||||
)
|
||||
}
|
||||
|
||||
const onClick = (data: MineRow) => {
|
||||
const url = or(data.mine.settings?.key, "")
|
||||
navigate(url)
|
||||
}
|
||||
|
||||
const minesTable = () => {
|
||||
return (
|
||||
<ResponsiveTable<MineRow>
|
||||
title={renderTitle}
|
||||
rows={mineTableData}
|
||||
columns={mineColumns}
|
||||
total={minesTotal}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
setSearchText={setSearchValue}
|
||||
setPage={setPage}
|
||||
handleRowsPerPageChange={handleRowsPerPageChange}
|
||||
hideKeys
|
||||
isLoading={isLoading}
|
||||
onRowClick={onClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tableContent = () => {
|
||||
return <div className={classNames(classes.gutter, classes.tableContainer)}>{minesTable()}</div>;
|
||||
};
|
||||
|
||||
const closeCallback = () => {
|
||||
setMineDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{tableContent()}
|
||||
<AddMine open={mineDialog} closeCallback={closeCallback} refreshCallback={load} />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
202
src/pages/VentEditor.tsx
Normal file
202
src/pages/VentEditor.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { Theme, useTheme } from "@mui/material";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import Editor from "ventilation/Editor";
|
||||
import PageContainer from "./PageContainer";
|
||||
import EditorSidebar from "ventilation/EditorSidebar";
|
||||
import EditorHeader from "ventilation/EditorHeader";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { loadPlaceable, Placeable } from "ventilation/drawable/Placeable";
|
||||
import { Component } from "models";
|
||||
// import { useRouteMatch } from "react-router";
|
||||
// import { MatchParams } from "navigation/Routes";
|
||||
import { useMineAPI } from "hooks";
|
||||
import { Sensor } from "ventilation/drawable/Sensor";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
header: {
|
||||
width: "100%",
|
||||
height: theme.spacing(7),
|
||||
background: "rgba(25, 25, 25, 1)",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid rgb(75, 75, 75)"
|
||||
},
|
||||
middleButtons: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
margin: "auto"
|
||||
},
|
||||
divider: {
|
||||
height: "42px",
|
||||
width: "2px",
|
||||
margin: theme.spacing(1)
|
||||
},
|
||||
editor: {
|
||||
// border: "1px solid red",
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
margin: 0,
|
||||
padding: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
enum Mode {
|
||||
NORMAL,
|
||||
MOVE,
|
||||
ADD,
|
||||
DELETE
|
||||
}
|
||||
|
||||
export default function Ventilation() {
|
||||
const classes = useStyles();
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [mode, setMode] = useState(Mode.NORMAL);
|
||||
// const match = useRouteMatch<MatchParams>();
|
||||
const mineKey = useParams<{ mineKey: string }>()?.mineKey ?? "";
|
||||
// const mineKey = match.params.mineKey;
|
||||
const undoRef = React.useRef(null);
|
||||
const redoRef = React.useRef(null);
|
||||
const saveRef = React.useRef(null);
|
||||
const addBlockRef = React.useRef(null);
|
||||
const theme = useTheme();
|
||||
const [devices, setDevices] = useState<pond.Device[]>([]);
|
||||
const [components, setComponents] = useState<Component[]>([]);
|
||||
const [blocks, setBlocks] = useState<Placeable[]>([]);
|
||||
const [mine, setMine] = useState<pond.MineSettings>(pond.MineSettings.create());
|
||||
const mineAPI = useMineAPI();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||
const [componentPreferences, setComponentPreferences] = useState<
|
||||
Map<string, pond.MineComponentPreferences>
|
||||
>(new Map());
|
||||
const [sensors, setSensors] = useState<Sensor[]>([]);
|
||||
const [componentDevices, setComponentDevices] = useState<Map<string, number>>(new Map());
|
||||
|
||||
const handleResize = (node: any) => {
|
||||
setHeight(node.getBoundingClientRect().height);
|
||||
setWidth(node.getBoundingClientRect().width);
|
||||
};
|
||||
|
||||
const pageContainer = useCallback((node: any) => {
|
||||
if (node) {
|
||||
handleResize(node);
|
||||
window.addEventListener("resize", () => handleResize(node), false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (!mineKey) return;
|
||||
setLoading(true);
|
||||
mineAPI
|
||||
.getMine(mineKey)
|
||||
.then((resp: { data: pond.IGetMineResponse | undefined; }) => {
|
||||
if (resp.data && resp.data.mine && resp.data.mine.settings) {
|
||||
let data = pond.GetMineResponse.create(resp.data);
|
||||
let perms: pond.Permission[] = [];
|
||||
data.permissions.forEach(p => {
|
||||
if (typeof p === "string" && p === "PERMISSION_READ") {
|
||||
perms.push(pond.Permission.PERMISSION_READ);
|
||||
} else if (typeof p === "string" && p === "PERMISSION_WRITE") {
|
||||
perms.push(pond.Permission.PERMISSION_WRITE);
|
||||
} else if (typeof p === "string" && p === "PERMISSION_SHARE") {
|
||||
perms.push(pond.Permission.PERMISSION_SHARE);
|
||||
} else if (typeof p === "string" && p === "PERMISSION_USERS") {
|
||||
perms.push(pond.Permission.PERMISSION_USERS);
|
||||
}
|
||||
});
|
||||
setPermissions(perms);
|
||||
setMine(resp.data.mine.settings);
|
||||
setDevices(data.devices);
|
||||
let newBlocks: Placeable[] = [];
|
||||
if (resp.data.mine?.settings?.placeables)
|
||||
resp.data.mine?.settings?.placeables.forEach(p => {
|
||||
let placeable = loadPlaceable(p);
|
||||
newBlocks.push(placeable as Placeable);
|
||||
});
|
||||
setBlocks(newBlocks);
|
||||
let prefs = new Map(Object.entries(data.componentPreferences));
|
||||
setComponentPreferences(prefs);
|
||||
setComponentDevices(new Map(Object.entries(data.componentDevices)));
|
||||
let components: Component[] = [];
|
||||
if (resp.data.components)
|
||||
resp.data.components.forEach(component => {
|
||||
components.push(Component.any(component));
|
||||
});
|
||||
setComponents(components);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line
|
||||
}, [mineKey, mineAPI]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div style={{ height: "100%", overflow: "hidden" }}>
|
||||
<EditorHeader
|
||||
setMode={setMode}
|
||||
undoRef={undoRef}
|
||||
redoRef={redoRef}
|
||||
saveRef={saveRef}
|
||||
blocks={blocks}
|
||||
setBlocks={setBlocks}
|
||||
devices={devices}
|
||||
setDevices={setDevices}
|
||||
components={components}
|
||||
mine={mine}
|
||||
setMine={setMine}
|
||||
permissions={permissions}
|
||||
setPermissions={setPermissions}
|
||||
componentPreferences={componentPreferences}
|
||||
sensors={sensors}
|
||||
/>
|
||||
{
|
||||
<div style={{ display: "flex", flexDirection: "row", height: "100%" }}>
|
||||
<EditorSidebar
|
||||
addBlockRef={addBlockRef}
|
||||
devices={devices}
|
||||
setDevices={setDevices}
|
||||
components={components}
|
||||
setComponents={setComponents}
|
||||
mine={mine}
|
||||
setMine={setMine}
|
||||
mineComponentPreferences={componentPreferences}
|
||||
setMineComponentPreferences={setComponentPreferences}
|
||||
/>
|
||||
<div ref={pageContainer} className={classes.editor}>
|
||||
<Editor
|
||||
width={width}
|
||||
height={height - parseInt(theme.spacing(7), 10)}
|
||||
mode={mode}
|
||||
undoRef={undoRef}
|
||||
redoRef={redoRef}
|
||||
saveRef={saveRef}
|
||||
addBlockRef={addBlockRef}
|
||||
devices={devices}
|
||||
blocks={blocks}
|
||||
setBlocks={setBlocks}
|
||||
components={components}
|
||||
setComponents={setComponents}
|
||||
componentPreferences={componentPreferences}
|
||||
setComponentPreferences={setComponentPreferences}
|
||||
sensors={sensors}
|
||||
setSensors={setSensors}
|
||||
loading={loading}
|
||||
componentDevices={componentDevices}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ export {
|
|||
useUserAPI,
|
||||
// useStripeAPI,
|
||||
// useDataDogProxyAPI,
|
||||
// useMineAPI,
|
||||
useMineAPI,
|
||||
// useKeyManagerAPI,
|
||||
// useTerminalAPI,
|
||||
useGateAPI,
|
||||
|
|
|
|||
175
src/providers/pond/mineAPI.tsx
Normal file
175
src/providers/pond/mineAPI.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { AxiosResponse } from "axios";
|
||||
import { useHTTP } from "hooks";
|
||||
import { permissionToString } from "pbHelpers/Permission";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useGlobalState } from "providers/StateContainer";
|
||||
import { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { pondURL } from "./pond";
|
||||
|
||||
export interface IMineAPIContext {
|
||||
addMine: (mine: pond.MineSettings) => Promise<AxiosResponse<pond.AddMineResponse>>;
|
||||
getMine: (key: string) => Promise<AxiosResponse<pond.GetMineResponse>>;
|
||||
addDevice: (key: string, deviceID: string, permissions: pond.Permission[]) => Promise<any>;
|
||||
addComponent: (
|
||||
key: string,
|
||||
deviceID: string,
|
||||
componentKey: string,
|
||||
permissions: pond.Permission[]
|
||||
) => Promise<any>;
|
||||
listMines: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
asRoot?: boolean
|
||||
) => Promise<AxiosResponse<pond.ListMinesResponse>>;
|
||||
listMinesSimple: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
asRoot?: boolean
|
||||
) => Promise<AxiosResponse<pond.ListMinesSimpleResponse>>;
|
||||
removeMine: (key: string) => Promise<AxiosResponse<pond.RemoveMineResponse>>;
|
||||
updateMine: (
|
||||
mine: pond.MineSettings,
|
||||
componentPreferences: Map<string, pond.MineComponentPreferences>
|
||||
) => Promise<AxiosResponse<pond.UpdateMineResponse>>;
|
||||
}
|
||||
|
||||
export const MineAPIContext = createContext<IMineAPIContext>({} as IMineAPIContext);
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function MineProvider(props: PropsWithChildren<Props>) {
|
||||
const { children } = props;
|
||||
const { get, del, post, put } = useHTTP();
|
||||
const [{ as }] = useGlobalState();
|
||||
|
||||
const addMine = (mine: pond.MineSettings) => {
|
||||
return post<pond.AddMineResponse>(pondURL("/mines" + (as ? "?as=" + as : "")), mine);
|
||||
};
|
||||
|
||||
const getMine = (key: string) => {
|
||||
return get<pond.GetMineResponse>(pondURL("/mines/" + key + (as ? "?as=" + as : "")));
|
||||
};
|
||||
|
||||
const listMines = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
asRoot?: boolean,
|
||||
keys?: string,
|
||||
types?: string
|
||||
) => {
|
||||
const url = pondURL(
|
||||
"/mines" +
|
||||
"?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + (order ? order : "asc")) +
|
||||
("&by=" + (orderBy ? orderBy : "deviceId")) +
|
||||
(search ? "&search=" + search : "") +
|
||||
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
|
||||
(as ? "&as=" + as : "") +
|
||||
(keys ? "&keys=" + keys : "") +
|
||||
(types ? "&types=" + types : "")
|
||||
);
|
||||
return get<pond.ListMinesResponse>(url);
|
||||
};
|
||||
|
||||
const listMinesSimple = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
asRoot?: boolean
|
||||
) => {
|
||||
const url = pondURL(
|
||||
"/minesSimple" +
|
||||
"?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + (order ? order : "asc")) +
|
||||
("&by=" + (orderBy ? orderBy : "deviceId")) +
|
||||
(search ? "&search=" + search : "") +
|
||||
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
|
||||
(as ? "&as=" + as : "")
|
||||
);
|
||||
return get<pond.ListMinesSimpleResponse>(url);
|
||||
};
|
||||
|
||||
const removeMine = (key: string) => {
|
||||
return del<pond.RemoveMineResponse>(pondURL("/mines/" + key + (as ? "?as=" + as : "")));
|
||||
};
|
||||
|
||||
const updateMine = (
|
||||
mine: pond.MineSettings,
|
||||
componentPreferences: Map<string, pond.MineComponentPreferences>
|
||||
) => {
|
||||
let request = pond.UpdateMineRequest.create();
|
||||
let container = pond.MineComponentPreferencesContainer.create();
|
||||
request.settings = mine;
|
||||
//if (componentPreferences) {
|
||||
componentPreferences.forEach((pref, key) => {
|
||||
container.preferences.push(pref);
|
||||
container.keys.push(key);
|
||||
});
|
||||
request.componentPreferences = container;
|
||||
//}
|
||||
return put<pond.AddMineResponse>(pondURL("/mines"), request);
|
||||
};
|
||||
|
||||
const addDevice = (key: string, deviceID: string, permissions: pond.Permission[]) => {
|
||||
if (as)
|
||||
return post(pondURL(`/mines/` + key + `/addDevice/` + deviceID + `?as=${as}`), {
|
||||
permissions: permissions.map(permission => permissionToString(permission))
|
||||
});
|
||||
return post(pondURL(`/mines/` + key + `/addDevice/` + deviceID), {
|
||||
permissions: permissions.map(permission => permissionToString(permission))
|
||||
});
|
||||
};
|
||||
|
||||
const addComponent = (
|
||||
key: string,
|
||||
deviceID: string,
|
||||
componentKey: string,
|
||||
permissions: pond.Permission[]
|
||||
) => {
|
||||
if (as)
|
||||
return post(
|
||||
pondURL(`/mines/` + key + `/addComponent/` + deviceID + "/" + componentKey + `?as=${as}`),
|
||||
{
|
||||
permissions: permissions.map(permission => permissionToString(permission))
|
||||
}
|
||||
);
|
||||
return post(pondURL(`/mines/` + key + `/addComponent/` + deviceID + "/" + componentKey), {
|
||||
permissions: permissions.map(permission => permissionToString(permission))
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MineAPIContext.Provider
|
||||
value={{
|
||||
addMine,
|
||||
getMine,
|
||||
addDevice,
|
||||
addComponent,
|
||||
listMines,
|
||||
listMinesSimple,
|
||||
removeMine,
|
||||
updateMine
|
||||
}}>
|
||||
{children}
|
||||
</MineAPIContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useMineAPI = () => useContext(MineAPIContext);
|
||||
|
|
@ -20,6 +20,7 @@ import GrainBagProvider, { useGrainBagAPI } from "./grainBagAPI";
|
|||
import FieldProvider, { useFieldAPI } from "./fieldAPI";
|
||||
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
|
||||
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
|
||||
import MineProvider, { useMineAPI } from "./mineAPI";
|
||||
// import NoteProvider from "providers/noteAPI";
|
||||
|
||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||
|
|
@ -56,7 +57,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
<FieldProvider>
|
||||
<TransactionProvider>
|
||||
<FileControllerProvider>
|
||||
{children}
|
||||
<MineProvider>
|
||||
{children}
|
||||
</MineProvider>
|
||||
</FileControllerProvider>
|
||||
</TransactionProvider>
|
||||
</FieldProvider>
|
||||
|
|
@ -100,4 +103,5 @@ export {
|
|||
useFieldAPI,
|
||||
useTransactionAPI,
|
||||
useFileControllerAPI,
|
||||
useMineAPI,
|
||||
};
|
||||
|
|
|
|||
53
src/ventilation/AddMine.tsx
Normal file
53
src/ventilation/AddMine.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField
|
||||
} from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useMineAPI } from "hooks";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
closeCallback: () => void;
|
||||
refreshCallback: () => void;
|
||||
}
|
||||
|
||||
export default function AddMine(props: Props) {
|
||||
const { open, closeCallback, refreshCallback } = props;
|
||||
const [name, setName] = useState("");
|
||||
const mineAPI = useMineAPI();
|
||||
|
||||
const addMine = () => {
|
||||
let newMine = pond.MineSettings.create();
|
||||
newMine.name = name;
|
||||
mineAPI.addMine(newMine).finally(() => {
|
||||
closeCallback();
|
||||
refreshCallback();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onClose={closeCallback}>
|
||||
<DialogTitle>Create new Mine</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeCallback}>Close</Button>
|
||||
<Button onClick={addMine} color={"primary"} disabled={name.length < 1}>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
51
src/ventilation/Canvas.tsx
Normal file
51
src/ventilation/Canvas.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React, { useRef, FC } from "react";
|
||||
interface Props {
|
||||
draw: any;
|
||||
height: number | string;
|
||||
width: number | string;
|
||||
onMouseDown?: any;
|
||||
onMouseUp?: any;
|
||||
onMouseMove?: any;
|
||||
onKeyDown?: any;
|
||||
onKeyUp?: any;
|
||||
onWheel?: any;
|
||||
onContextMenu?: any;
|
||||
}
|
||||
|
||||
const Canvas: FC<Props> = ({
|
||||
draw,
|
||||
height,
|
||||
width,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onMouseMove,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
onWheel,
|
||||
onContextMenu
|
||||
}) => {
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const context = canvas.current?.getContext("2d");
|
||||
draw(context);
|
||||
});
|
||||
|
||||
return (
|
||||
<canvas
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseMove={onMouseMove}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onWheel={onWheel}
|
||||
tabIndex={-1}
|
||||
ref={canvas}
|
||||
width={width}
|
||||
height={height}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Canvas;
|
||||
58
src/ventilation/Drag.tsx
Normal file
58
src/ventilation/Drag.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useState } from "react";
|
||||
import Draggable, { DraggableEvent, PositionOffsetControlPosition } from "react-draggable";
|
||||
import pipeIcon from "../assets/components/pipeT.png";
|
||||
|
||||
interface Props {
|
||||
onStart?: (event: DraggableEvent) => void;
|
||||
onStop?: (event: DraggableEvent) => void;
|
||||
offsetParent?: HTMLElement | Element | undefined;
|
||||
positionOffset?: PositionOffsetControlPosition | undefined;
|
||||
children?: any;
|
||||
transform?: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export default function Drag(props: Props) {
|
||||
const { onStart, onStop, children, selected } = props;
|
||||
const [pointerEvents, setPointerEvents] = useState(true);
|
||||
|
||||
const myOnStart = (event: DraggableEvent) => {
|
||||
if (onStart) onStart(event);
|
||||
setPointerEvents(false);
|
||||
};
|
||||
|
||||
const myOnStop = (event: DraggableEvent) => {
|
||||
if (onStop) onStop(event);
|
||||
setPointerEvents(true);
|
||||
};
|
||||
|
||||
const onDrag = (event: DraggableEvent) => {
|
||||
console.log("drag");
|
||||
};
|
||||
|
||||
const dragHandlers = { onStart: myOnStart, onStop: myOnStop };
|
||||
return (
|
||||
<Draggable {...dragHandlers} grid={[32, 32]} onDrag={onDrag}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 32,
|
||||
height: 32,
|
||||
backgroundColor: "red",
|
||||
pointerEvents: pointerEvents ? "auto" : "none",
|
||||
border: selected ? "1px solid yellow" : "none"
|
||||
//transform: transform,
|
||||
}}>
|
||||
<img
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%"
|
||||
}}
|
||||
src={pipeIcon}
|
||||
alt="pipe"
|
||||
/>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
860
src/ventilation/Editor.tsx
Normal file
860
src/ventilation/Editor.tsx
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import Canvas from "ventilation/Canvas";
|
||||
import Zoom from "ventilation/Zoom";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import {
|
||||
CircularProgress,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ToggleButton,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import { Info, AddCircle, InfoRounded } from "@mui/icons-material";
|
||||
import { VentShaft } from "./drawable/VentShaft";
|
||||
import { VentCorner } from "./drawable/VentCorner";
|
||||
import { Placeable } from "./drawable/Placeable";
|
||||
import VentDialog from "./VentDialog";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteForever";
|
||||
import ClockwiseIcon from "@mui/icons-material/RotateRight";
|
||||
import CounterclockwiseIcon from "@mui/icons-material/RotateLeft";
|
||||
import { useGlobalState, useMineAPI } from "providers";
|
||||
import { Component, Device } from "models";
|
||||
import { Sensor } from "./drawable/Sensor";
|
||||
// import { ToggleButton } from "@material-ui/lab";
|
||||
import { VentTBreak } from "./drawable/VentTBreak";
|
||||
import InteractionSettings from "interactions/InteractionSettings";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
|
||||
interface Props {
|
||||
width?: number;
|
||||
height?: number;
|
||||
mode?: Mode;
|
||||
undoRef: any;
|
||||
redoRef: any;
|
||||
saveRef: any;
|
||||
addBlockRef: any;
|
||||
devices: pond.Device[];
|
||||
blocks: Placeable[];
|
||||
setBlocks: React.Dispatch<React.SetStateAction<Placeable[]>>;
|
||||
components: Component[];
|
||||
setComponents: React.Dispatch<React.SetStateAction<Component[]>>;
|
||||
componentPreferences: Map<string, pond.MineComponentPreferences>;
|
||||
setComponentPreferences: React.Dispatch<
|
||||
React.SetStateAction<Map<string, pond.MineComponentPreferences>>
|
||||
>;
|
||||
componentDevices: Map<string, number>;
|
||||
sensors: Sensor[];
|
||||
setSensors: React.Dispatch<React.SetStateAction<Sensor[]>>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
NORMAL,
|
||||
MOVE,
|
||||
ADD,
|
||||
DELETE
|
||||
}
|
||||
|
||||
enum ViewMode {
|
||||
NORMAL,
|
||||
INFO
|
||||
}
|
||||
|
||||
export default function Editor(props: Props) {
|
||||
const {
|
||||
blocks,
|
||||
setBlocks,
|
||||
devices,
|
||||
components,
|
||||
componentDevices,
|
||||
componentPreferences,
|
||||
sensors,
|
||||
setSensors,
|
||||
setComponentPreferences,
|
||||
loading
|
||||
} = props;
|
||||
const [selected, setSelected] = useState<Placeable[]>([]);
|
||||
const [width, setWidth] = useState<number>(props.width ? props.width : 800);
|
||||
const [height, setHeight] = useState<number>(props.width ? props.width : 600);
|
||||
const [cameraOffsetX, setCameraOffsetX] = useState<number>(0);
|
||||
const [cameraOffsetY, setCameraOffsetY] = useState<number>(0);
|
||||
const [clickX, setClickX] = useState<number>(0);
|
||||
const [clickY, setClickY] = useState<number>(0);
|
||||
const [mode, setMode] = useState<Mode>(props.mode ? props.mode : Mode.NORMAL);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(ViewMode.NORMAL);
|
||||
const [mouseDown, setMouseDown] = useState<boolean>(false);
|
||||
const [undoStates, setUndoStates] = useState<Placeable[][]>([]);
|
||||
const [redoStates, setRedoStates] = useState<Placeable[][]>([]);
|
||||
const [ctrl, setCtrl] = useState<boolean>(false);
|
||||
const [moveOrigin, setMoveOrigin] = useState<Placeable[]>([]);
|
||||
const [mousePosX, setMousePosX] = useState<number>(0);
|
||||
const [mousePosY, setMousePosY] = useState<number>(0);
|
||||
const [hoveredBlock, setHoveredBlock] = useState<number | undefined>(undefined);
|
||||
const [placementBlock, setPlacementBlock] = useState<Placeable | undefined>(undefined);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const [anchorPosX, setAnchorPosX] = useState<number>(0);
|
||||
const [anchorPosY, setAnchorPosY] = useState<number>(0);
|
||||
const [ventType, setVentType] = useState<pond.ShaftType | undefined>(undefined);
|
||||
//const [placeableType, setPlaceableType] = useState<pond.PlaceableType | undefined>(undefined);
|
||||
const [direction, setDirection] = useState<number>(0);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [interactionDevice, setInteractionDevice] = useState<Device>();
|
||||
const [interactionComponent, setInteractionComponent] = useState<Component>();
|
||||
const [interactionSensor, setInteractionSensor] = useState<Sensor>();
|
||||
const [isAddInteractionOpen, setIsAddInteractionOpen] = useState(false);
|
||||
const mineAPI = useMineAPI();
|
||||
const theme = useTheme();
|
||||
|
||||
const [componentMeasurements, setComponentMeasurements] = useState<
|
||||
undefined | Map<string, UnitMeasurement[]>
|
||||
>(undefined);
|
||||
|
||||
const [{ user }] = useGlobalState();
|
||||
|
||||
const save = () => {
|
||||
let settings = pond.MineSettings.create();
|
||||
props.devices.forEach(device => {
|
||||
if (device.settings) settings.devices.push(device.settings?.deviceId);
|
||||
});
|
||||
mineAPI.addMine(settings).then((resp: any) => {
|
||||
console.log(resp);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setWidth(props.width ? props.width : 800);
|
||||
setHeight(props.height ? props.height : 600);
|
||||
}, [props.width, props.height]);
|
||||
|
||||
useEffect(() => {
|
||||
setMode(props.mode ? props.mode : Mode.NORMAL);
|
||||
}, [props.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.length > 0) {
|
||||
selected[0].setDirection(direction);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [direction]);
|
||||
|
||||
useEffect(() => {
|
||||
let compMeasurements: Map<string, UnitMeasurement[]> = new Map();
|
||||
components.forEach(comp => {
|
||||
let measurements: UnitMeasurement[] = [];
|
||||
comp.lastMeasurement.forEach(m => {
|
||||
let um = UnitMeasurement.create(m, user);
|
||||
measurements.push(um);
|
||||
});
|
||||
compMeasurements.set(comp.key(), measurements);
|
||||
});
|
||||
setComponentMeasurements(compMeasurements);
|
||||
}, [components, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
componentMeasurements === undefined ||
|
||||
componentPreferences.size < 1 ||
|
||||
components.length < 1 ||
|
||||
!user ||
|
||||
componentDevices.size < 1 ||
|
||||
componentMeasurements.size < 1
|
||||
)
|
||||
return;
|
||||
let sensors: Sensor[] = [];
|
||||
components.forEach(comp => {
|
||||
if (componentPreferences.get(comp.key())) {
|
||||
let p = componentPreferences.get(comp.key());
|
||||
if (p?.sensors)
|
||||
p?.sensors.forEach((s, i) => {
|
||||
if (s.enabled) {
|
||||
let id = componentDevices.get(comp.key());
|
||||
let device = devices.find(dev => dev.settings?.deviceId === id);
|
||||
let measurements = componentMeasurements.get(comp.key());
|
||||
if (measurements !== undefined)
|
||||
sensors.push(Sensor.create(comp, Device.create(device), s, measurements, i, user));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
setSensors(sensors);
|
||||
}, [
|
||||
components,
|
||||
componentPreferences,
|
||||
setSensors,
|
||||
user,
|
||||
devices,
|
||||
componentDevices,
|
||||
componentMeasurements
|
||||
]);
|
||||
|
||||
const getOffsetX = (e: any): number => {
|
||||
if (e.offsetParent) {
|
||||
return e.offsetLeft + getOffsetX(e.offsetParent);
|
||||
}
|
||||
return e.offsetLeft;
|
||||
};
|
||||
|
||||
const getAbsoluteX = (e: any): number => {
|
||||
return e.pageX - getOffsetX(e.currentTarget);
|
||||
};
|
||||
|
||||
const getX = (e: any): number => {
|
||||
let offset = getOffsetX(e.currentTarget);
|
||||
offset = offset - cameraOffsetX * scale;
|
||||
return (e.pageX - offset) / scale;
|
||||
};
|
||||
|
||||
const getOffsetY = (e: any): number => {
|
||||
if (e.offsetParent) {
|
||||
return e.offsetTop + getOffsetY(e.offsetParent);
|
||||
}
|
||||
return e.offsetTop;
|
||||
};
|
||||
|
||||
const getAbsoluteY = (e: any): number => {
|
||||
return e.pageY - getOffsetY(e.currentTarget);
|
||||
};
|
||||
|
||||
const getY = (e: any): number => {
|
||||
let offset = getOffsetY(e.currentTarget);
|
||||
offset = offset - cameraOffsetY * scale;
|
||||
//offset = offset + mouseOffsetY;
|
||||
return (e.pageY - offset) / scale;
|
||||
};
|
||||
|
||||
const addPlaceable = (p: Placeable) => {
|
||||
let r = blocks;
|
||||
saveState();
|
||||
if (selected.length > 0) {
|
||||
let xy = selected[0].getEndXAndY();
|
||||
p.y = xy.y;
|
||||
p.x = xy.x;
|
||||
p.setAngle(selected[0].getEndAngle(), selected[0].getEndAngle());
|
||||
} else {
|
||||
p.x = clickX;
|
||||
p.y = clickY;
|
||||
}
|
||||
selected[0] = p;
|
||||
r.push(p);
|
||||
setBlocks([...r]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.addBlockRef.current = addPlaceable;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected, addPlaceable, blocks]);
|
||||
|
||||
const add = (x?: number, y?: number) => {
|
||||
let r = blocks;
|
||||
saveState();
|
||||
let p = VentShaft.create();
|
||||
if (x && y) {
|
||||
let v = VentCorner.create(x, y, 32, 0);
|
||||
setBlocks([...r]);
|
||||
return v;
|
||||
} else if (Boolean(anchorEl)) {
|
||||
p.x = anchorPosX + cameraOffsetX;
|
||||
p.y = anchorPosY + cameraOffsetY;
|
||||
p.angle = 0;
|
||||
} else {
|
||||
p.x = p.x + cameraOffsetX;
|
||||
p.y = p.y + cameraOffsetY;
|
||||
}
|
||||
r.push(p);
|
||||
setBlocks([...r]);
|
||||
//return p;
|
||||
};
|
||||
|
||||
const saveState = () => {
|
||||
let u = undoStates;
|
||||
let newBlocks: Placeable[] = [];
|
||||
blocks.forEach(block => {
|
||||
let b = block.clone();
|
||||
newBlocks.push(b);
|
||||
});
|
||||
u.push(newBlocks);
|
||||
setRedoStates([]);
|
||||
};
|
||||
|
||||
const click = (e: any) => {
|
||||
if (hoveredBlock !== undefined) {
|
||||
setMode(hoveredBlock);
|
||||
setMouseDown(false);
|
||||
return;
|
||||
}
|
||||
selected.forEach(s => {
|
||||
s.selected = false;
|
||||
});
|
||||
let hit = false;
|
||||
let r = blocks;
|
||||
let x = getX(e);
|
||||
let y = getY(e);
|
||||
setClickX(x);
|
||||
setClickY(y);
|
||||
let newBlocks: Placeable[] = [];
|
||||
if (mode === Mode.NORMAL) {
|
||||
r.forEach(block => {
|
||||
newBlocks.push(block.clone());
|
||||
if (block.clickCheck(x, y, scale)) {
|
||||
hit = true;
|
||||
setSelected([block]);
|
||||
}
|
||||
});
|
||||
sensors.forEach(sensor => {
|
||||
if (sensor.clickCheck(x, y, scale)) {
|
||||
hit = true;
|
||||
setSelected([sensor]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!hit) setSelected([]);
|
||||
|
||||
if (e.button === 2) return;
|
||||
|
||||
if (hit) setMoveOrigin(newBlocks);
|
||||
|
||||
if (mode === Mode.ADD) {
|
||||
add();
|
||||
} else if (mode === Mode.DELETE) {
|
||||
deleteBlock(x, y, true);
|
||||
}
|
||||
setMouseDown(true);
|
||||
};
|
||||
|
||||
const mouseUp = (e: any) => {
|
||||
setMouseDown(false);
|
||||
if (mode === Mode.NORMAL && selected !== undefined) {
|
||||
let state = [...moveOrigin];
|
||||
let states = [...undoStates];
|
||||
states.push(state);
|
||||
setUndoStates([...states]);
|
||||
setRedoStates([]);
|
||||
selected.forEach(placeable => {
|
||||
if (placeable.type === pond.PlaceableType.PLACEABLE_TYPE_SENSOR) {
|
||||
let s = placeable as Sensor;
|
||||
let prefs = componentPreferences.get(s.getComponent().key());
|
||||
if (prefs?.sensors[s.index]) {
|
||||
prefs.sensors[s.index].x = s.x;
|
||||
prefs.sensors[s.index].y = s.y;
|
||||
componentPreferences.set(s.getComponent().key(), prefs);
|
||||
setComponentPreferences(new Map(componentPreferences));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
//setSelected(undefined);
|
||||
};
|
||||
|
||||
const keyDown = (e: any) => {
|
||||
if (e.key === "1") {
|
||||
setMode(Mode.NORMAL);
|
||||
} else if (e.key === "2") {
|
||||
setMode(Mode.MOVE);
|
||||
} else if (e.key === "3") {
|
||||
setMode(Mode.ADD);
|
||||
} else if (e.key === "4") {
|
||||
setMode(Mode.DELETE);
|
||||
} else if (e.key === "Control") {
|
||||
setCtrl(true);
|
||||
} else if (e.key === "z" && ctrl) {
|
||||
undo();
|
||||
} else if (e.key === "Z" && ctrl) {
|
||||
redo();
|
||||
} else if (e.key === "s" && ctrl) {
|
||||
save();
|
||||
} else if (e.key === "v") {
|
||||
if (viewMode === ViewMode.NORMAL) {
|
||||
setViewMode(ViewMode.INFO);
|
||||
} else {
|
||||
setViewMode(ViewMode.NORMAL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const keyUp = (e: any) => {
|
||||
if (e.key === "Control") {
|
||||
setCtrl(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBlock = (x: number, y: number, save?: boolean) => {
|
||||
if (save) saveState();
|
||||
let b = [...blocks];
|
||||
blocks.forEach((block, index) => {
|
||||
if (block.clickCheck(x + cameraOffsetX, y + cameraOffsetY, scale)) {
|
||||
b.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
});
|
||||
setBlocks(b);
|
||||
};
|
||||
|
||||
const drag = (e: any) => {
|
||||
setHoveredBlock(undefined);
|
||||
let x = getAbsoluteX(e);
|
||||
let y = getAbsoluteY(e);
|
||||
setMousePosX(x);
|
||||
setMousePosY(y);
|
||||
|
||||
if (!mouseDown) {
|
||||
// Set position for placement block
|
||||
if (mode === Mode.ADD && placementBlock) {
|
||||
let p = placementBlock.clone() as Placeable;
|
||||
p.x = x; // - p.width() / 2;
|
||||
p.y = y; // - p.height() / 2;
|
||||
setPlacementBlock(p);
|
||||
}
|
||||
} else {
|
||||
// If the mouse is clicked down
|
||||
if (mode === Mode.MOVE) {
|
||||
setCameraOffsetX(clickX - x / scale);
|
||||
setCameraOffsetY(clickY - y / scale);
|
||||
} else if (selected.length > 0 && mode === Mode.NORMAL) {
|
||||
selected[0].drag(getX(e), getY(e), scale);
|
||||
blocks.forEach(block => {
|
||||
if (block.x !== selected[0].x && block.y !== selected[0].y) {
|
||||
selected[0].snap(block, 16);
|
||||
}
|
||||
});
|
||||
} else if (mode === Mode.DELETE) {
|
||||
deleteBlock(x, y);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const redo = () => {
|
||||
if (redoStates.length < 1) return;
|
||||
let rects = redoStates.pop();
|
||||
let undos = undoStates;
|
||||
if (rects) {
|
||||
undos.push(rects);
|
||||
setUndoStates([...undos]);
|
||||
setBlocks(rects);
|
||||
}
|
||||
};
|
||||
|
||||
const undo = () => {
|
||||
let states = undoStates;
|
||||
let redo = redoStates;
|
||||
let rects = states.pop();
|
||||
if (rects) {
|
||||
redo.push([...rects]);
|
||||
setRedoStates([...redo]);
|
||||
setUndoStates([...states]);
|
||||
setBlocks(rects);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.saveRef.current = save;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [save]);
|
||||
|
||||
useEffect(() => {
|
||||
props.undoRef.current = undo;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [undo]);
|
||||
|
||||
useEffect(() => {
|
||||
props.redoRef.current = redo;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [redo]);
|
||||
|
||||
const wheel = (e: WheelEvent) => {
|
||||
// Scroll down
|
||||
if (e.deltaY > 0) {
|
||||
let newScale = scale - 0.05;
|
||||
if (newScale > 0) {
|
||||
let mouseRatioX = mousePosX / width;
|
||||
let mouseRatioY = mousePosY / height;
|
||||
let newWidth = width / newScale;
|
||||
let dx = (width - newWidth) * mouseRatioX;
|
||||
let olddx = (width - width / scale) * mouseRatioX;
|
||||
let newHeight = height / newScale;
|
||||
let dy = (height - newHeight) * mouseRatioY;
|
||||
let olddy = (height - height / scale) * mouseRatioY;
|
||||
setScale(Math.round(newScale * 1000) / 1000);
|
||||
setCameraOffsetX(cameraOffsetX - olddx + dx);
|
||||
setCameraOffsetY(cameraOffsetY - olddy + dy);
|
||||
}
|
||||
}
|
||||
// Scroll up
|
||||
if (e.deltaY < 0) {
|
||||
let mouseRatioX = mousePosX / width;
|
||||
let mouseRatioY = mousePosY / height;
|
||||
let newScale = scale + 0.05;
|
||||
let newWidth = width / newScale;
|
||||
let dx = (width - newWidth) * mouseRatioX;
|
||||
let olddx = (width - width / scale) * mouseRatioX;
|
||||
let newHeight = height / newScale;
|
||||
let dy = (height - newHeight) * mouseRatioY;
|
||||
let olddy = (height - height / scale) * mouseRatioY;
|
||||
setScale(Math.round(newScale * 1000) / 1000);
|
||||
setCameraOffsetX(cameraOffsetX - olddx + dx);
|
||||
setCameraOffsetY(cameraOffsetY - olddy + dy);
|
||||
}
|
||||
// If the mouse is at the top, scroll mode selection
|
||||
/*if (mousePosY < 100) {
|
||||
// Scroll down
|
||||
if (e.deltaY > 0) {
|
||||
//if (mode === Mode.NORMAL) setMode(Mode.DELETE);
|
||||
//else setMode(mode - 1);
|
||||
|
||||
console.log("scroll down")
|
||||
let newScale = scale - 0.1
|
||||
setScale(newScale)
|
||||
|
||||
}
|
||||
// Scroll up
|
||||
if (e.deltaY < 0) {
|
||||
//if (mode === Mode.DELETE) setMode(Mode.NORMAL);
|
||||
//else setMode(mode + 1);
|
||||
|
||||
console.log("scroll up")
|
||||
let newScale = scale + 0.1
|
||||
setScale(newScale)
|
||||
|
||||
}
|
||||
}*/
|
||||
};
|
||||
|
||||
const drawBlocks = (context: CanvasRenderingContext2D) => {
|
||||
blocks.forEach((block, index) => {
|
||||
if (block.draw) block.draw(context, cameraOffsetX, cameraOffsetY, scale);
|
||||
});
|
||||
selected.forEach(block => {
|
||||
block.highlight(context, cameraOffsetX, cameraOffsetY, scale);
|
||||
});
|
||||
};
|
||||
|
||||
const drawSensors = (context: CanvasRenderingContext2D) => {
|
||||
/*components.forEach(comp => {
|
||||
let x = or(componentPreferences.get(comp.key())?.x, 10)
|
||||
let y = or(componentPreferences.get(comp.key())?.y, 10)
|
||||
context.arc(x - cameraOffsetX, y - cameraOffsetY, 25, 2 * Math.PI, 0, false)
|
||||
context.fill()
|
||||
})*/
|
||||
sensors.forEach(sensor => {
|
||||
sensor.draw(context, cameraOffsetX, cameraOffsetY, scale);
|
||||
if (viewMode === ViewMode.INFO)
|
||||
sensor.highlight(context, cameraOffsetX, cameraOffsetY, scale);
|
||||
});
|
||||
};
|
||||
|
||||
const draw = (context: CanvasRenderingContext2D) => {
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.fillStyle = "rgba(150, 150, 255, 150)";
|
||||
context.strokeStyle = "rgba(247, 202, 24, 1)";
|
||||
|
||||
drawBlocks(context);
|
||||
drawSensors(context);
|
||||
|
||||
// Draw placement block if in add mode
|
||||
if (mode === Mode.ADD) {
|
||||
context.globalAlpha = 0.3;
|
||||
if (placementBlock) placementBlock.draw(context);
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Draw garbage can if in delete mode
|
||||
if (mode === Mode.DELETE) {
|
||||
let i = new Image();
|
||||
i.src = "https://img.icons8.com/material-outlined/48/000000/delete-forever.png";
|
||||
context.globalAlpha = 1;
|
||||
context.drawImage(i, mousePosX - 16, mousePosY - 16, 32, 32);
|
||||
}
|
||||
};
|
||||
|
||||
const contextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setAnchorPosX(getAbsoluteX(e));
|
||||
setAnchorPosY(getAbsoluteY(e));
|
||||
if (!anchorEl) setAnchorEl(e.currentTarget as any);
|
||||
};
|
||||
|
||||
/*const isTBreak = (p: Placeable) => {
|
||||
if (p.type === pond.PlaceableType.PLACEABLE_TYPE_SHAFT) {
|
||||
if (p.subtype === pond.ShaftType.SHAFT_TYPE_T_BREAK) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};*/
|
||||
|
||||
const isDefaultShaft = (p: Placeable) => {
|
||||
if (p.type === pond.PlaceableType.PLACEABLE_TYPE_SHAFT) {
|
||||
if (p.subtype === pond.ShaftType.SHAFT_TYPE_DEFAULT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
cursor:
|
||||
mode === Mode.MOVE
|
||||
? mouseDown
|
||||
? "grabbing"
|
||||
: "grab"
|
||||
: mode === Mode.ADD || mode === Mode.DELETE
|
||||
? "none"
|
||||
: "default"
|
||||
}}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: "rgba(255,255,255,0.1)",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
margin: theme.spacing(2)
|
||||
}}>
|
||||
<Zoom scale={scale} setScale={setScale} />
|
||||
</div>
|
||||
{loading && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
margin: theme.spacing(8)
|
||||
}}>
|
||||
<CircularProgress
|
||||
size={theme.spacing(8)}
|
||||
//style={{ margin: theme.spacing(6) }}
|
||||
/>
|
||||
<Typography>Loading...</Typography>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
//borderRadius: 4,
|
||||
//backgroundColor: "rgba(255,255,255,0.1)",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: theme.spacing(19),
|
||||
margin: theme.spacing(2)
|
||||
}}>
|
||||
<ToggleButton
|
||||
value={viewMode}
|
||||
selected={viewMode === ViewMode.INFO}
|
||||
onChange={() => {
|
||||
if (viewMode === ViewMode.INFO) {
|
||||
setViewMode(ViewMode.NORMAL);
|
||||
} else {
|
||||
setViewMode(ViewMode.INFO);
|
||||
}
|
||||
}}>
|
||||
<InfoRounded />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
<Canvas
|
||||
draw={draw}
|
||||
height={height}
|
||||
width={width}
|
||||
onMouseDown={click}
|
||||
onMouseMove={drag}
|
||||
onMouseUp={mouseUp}
|
||||
onKeyDown={keyDown}
|
||||
onKeyUp={keyUp}
|
||||
onWheel={wheel}
|
||||
onContextMenu={contextMenu}
|
||||
/>
|
||||
</div>
|
||||
<VentDialog type={ventType} setType={setVentType} setDirection={setDirection} />
|
||||
{interactionDevice && interactionComponent && (
|
||||
<InteractionSettings
|
||||
device={interactionDevice}
|
||||
components={components}
|
||||
initialComponent={interactionComponent}
|
||||
mode="add"
|
||||
isDialogOpen={isAddInteractionOpen}
|
||||
closeDialogCallback={() => setIsAddInteractionOpen(false)}
|
||||
refreshCallback={() => {}}
|
||||
canEdit={true}
|
||||
sensor={interactionSensor}
|
||||
/>
|
||||
)}
|
||||
<Menu
|
||||
id="groupMenu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
// getContentAnchorEl={null}
|
||||
anchorOrigin={{ vertical: anchorPosY, horizontal: anchorPosX }}
|
||||
keepMounted
|
||||
onContextMenu={(e: any) => {
|
||||
e.preventDefault();
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
disableAutoFocusItem>
|
||||
{selected.length > 0 && selected[0].type === pond.PlaceableType.PLACEABLE_TYPE_SHAFT && (
|
||||
<MenuItem
|
||||
dense
|
||||
divider
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
setVentType(selected[0].subtype);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Info />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Edit Shaft" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{selected.length > 0 && selected[0].type === pond.PlaceableType.PLACEABLE_TYPE_SENSOR && (
|
||||
<MenuItem
|
||||
dense
|
||||
divider
|
||||
onClick={() => {
|
||||
let s = selected[0] as Sensor;
|
||||
setAnchorEl(null);
|
||||
//setVentType(selected[0].subtype);
|
||||
setInteractionComponent(s.getComponent());
|
||||
setInteractionDevice(s.getDevice());
|
||||
setInteractionSensor(s);
|
||||
setIsAddInteractionOpen(true);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AddCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={"Add Interaction"} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{!(selected.length > 0 && isDefaultShaft(selected[0])) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
let v = VentShaft.create();
|
||||
addPlaceable(v);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AddCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add Straight Shaft" />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
let v = VentCorner.create();
|
||||
addPlaceable(v);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AddCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add Corner" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
let v = VentTBreak.create();
|
||||
addPlaceable(v);
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AddCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Add T-Break" />
|
||||
</MenuItem>
|
||||
{selected.length > 0 &&
|
||||
(selected[0].subtype === pond.ShaftType.SHAFT_TYPE_CORNER ||
|
||||
selected[0].subtype === pond.ShaftType.SHAFT_TYPE_T_BREAK) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
selected[0].setDirection(0);
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ClockwiseIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Clockwise" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{selected.length > 0 &&
|
||||
(selected[0].subtype === pond.ShaftType.SHAFT_TYPE_CORNER ||
|
||||
selected[0].subtype === pond.ShaftType.SHAFT_TYPE_T_BREAK) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
selected[0].setDirection(1);
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<CounterclockwiseIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Counterclockwise" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{selected.length > 0 && (
|
||||
<MenuItem
|
||||
style={{ color: "red" }}
|
||||
dense
|
||||
onClick={() => {
|
||||
saveState();
|
||||
let b = [...blocks];
|
||||
blocks.forEach((block, index) => {
|
||||
if (
|
||||
selected[0].x === block.x &&
|
||||
selected[0].y === block.y &&
|
||||
block.angle === selected[0].angle &&
|
||||
block.type === selected[0].type
|
||||
) {
|
||||
b.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
});
|
||||
setAnchorEl(null);
|
||||
setSelected([]);
|
||||
setBlocks(b);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Delete" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{selected.length > 0 && false && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setAnchorEl(null);
|
||||
let xy = selected[0].getEndXAndY();
|
||||
let x = xy.x;
|
||||
let y = xy.y;
|
||||
//let startBlock = selected[0].clone();
|
||||
let block = add(x, y);
|
||||
if (block) {
|
||||
//block.circle.selected = true;
|
||||
//block.startSnap = startBlock;
|
||||
//setSelected([block]);
|
||||
}
|
||||
//setMouseDown(true);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AddCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Extend" />
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
116
src/ventilation/EditorActions.tsx
Normal file
116
src/ventilation/EditorActions.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from "@material-ui/core";
|
||||
import { AccountCircle, MoreVert, Settings } from "@material-ui/icons";
|
||||
import { isOffline } from "utils/environment";
|
||||
import { mineScope } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import React, { useState } from "react";
|
||||
import ObjectUsers from "user/ObjectUsers";
|
||||
import { or } from "utils";
|
||||
import EditorSettings from "./EditorSettings";
|
||||
import ObjectTeams from "teams/ObjectTeams";
|
||||
import ObjectTeamsIcon from "@material-ui/icons/SupervisedUserCircle";
|
||||
import { useGlobalState } from "providers";
|
||||
|
||||
interface Props {
|
||||
mine: pond.MineSettings;
|
||||
setMine: React.Dispatch<React.SetStateAction<pond.MineSettings>>;
|
||||
permissions: any;
|
||||
refreshCallback: () => void;
|
||||
}
|
||||
|
||||
export default function EditorActions(props: Props) {
|
||||
const { mine, setMine, permissions, refreshCallback } = props;
|
||||
const [settingsDialog, setSettingsDialog] = useState(false);
|
||||
const [userDialog, setUserDialog] = useState(false);
|
||||
const [teamDialog, setTeamDialog] = useState(false);
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
|
||||
const [{ user }] = useGlobalState();
|
||||
|
||||
const menu = () => {
|
||||
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
|
||||
const canManageTeams = canManageUsers && user.hasFeature("teams");
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={menuAnchorEl ? menuAnchorEl : null}
|
||||
open={menuAnchorEl !== null}
|
||||
onClose={() => setMenuAnchorEl(null)}
|
||||
disableAutoFocusItem>
|
||||
{/* Users menu item */}
|
||||
{!isOffline() && canManageUsers && (
|
||||
<MenuItem onClick={() => setUserDialog(true)} button dense>
|
||||
<ListItemIcon>
|
||||
<AccountCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Users" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isOffline() && canManageTeams && (
|
||||
<MenuItem onClick={() => setTeamDialog(true)} button dense>
|
||||
<ListItemIcon>
|
||||
<ObjectTeamsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Teams" />
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const users = () => {
|
||||
return (
|
||||
<ObjectUsers
|
||||
scope={mineScope(or(mine.key, ""))}
|
||||
label={or(mine.name, "No Name")}
|
||||
permissions={or(permissions, [])}
|
||||
isDialogOpen={or(userDialog, false)}
|
||||
closeDialogCallback={() => setUserDialog(false)}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const buttons = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tooltip title="Settings">
|
||||
<IconButton onClick={() => setSettingsDialog(true)}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={mine.key ? "More" : "Save or load mine first"}>
|
||||
<span>
|
||||
<IconButton
|
||||
aria-owns={"deviceMenu"}
|
||||
aria-haspopup="true"
|
||||
disabled={mine.key === undefined || mine.key === ""}
|
||||
onClick={event => setMenuAnchorEl(event.currentTarget)}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{buttons()}
|
||||
{menu()}
|
||||
{users()}
|
||||
<EditorSettings
|
||||
open={settingsDialog}
|
||||
setOpen={setSettingsDialog}
|
||||
mine={mine}
|
||||
setMine={setMine}
|
||||
/>
|
||||
<ObjectTeams
|
||||
scope={mineScope(mine.key)}
|
||||
label="Teams"
|
||||
permissions={permissions}
|
||||
isDialogOpen={teamDialog}
|
||||
refreshCallback={refreshCallback}
|
||||
closeDialogCallback={() => setTeamDialog(false)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
43
src/ventilation/EditorDrawer.tsx
Normal file
43
src/ventilation/EditorDrawer.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { createStyles, makeStyles, Theme } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
const drawer = {
|
||||
"&": {
|
||||
bottom: 0,
|
||||
margin: 0,
|
||||
//padding: 0,
|
||||
//maxHeight: "auto",
|
||||
backgroundColor: "rgba(35, 35, 35, 1)",
|
||||
overflow: "hidden",
|
||||
overflowY: "auto"
|
||||
}
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
drawer: {
|
||||
...drawer,
|
||||
height: "calc(100% - " + theme.spacing(7) + "px)",
|
||||
minWidth: theme.spacing(42),
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
closed: {
|
||||
...drawer,
|
||||
height: "calc(100% - " + theme.spacing(7) + "px)",
|
||||
width: theme.spacing(0)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export default function EditorDrawer(props: Props) {
|
||||
//const isMobile = useMobile();
|
||||
const { isOpen, children } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
return <div className={isOpen ? classes.drawer : classes.closed}>{children}</div>;
|
||||
}
|
||||
220
src/ventilation/EditorHeader.tsx
Normal file
220
src/ventilation/EditorHeader.tsx
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import {
|
||||
createStyles,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@material-ui/core";
|
||||
import React, { useState } from "react";
|
||||
import UndoIcon from "assets/editor/undo.png";
|
||||
import RedoIcon from "assets/editor/redo.png";
|
||||
import MoveIcon from "assets/editor/move.png";
|
||||
import CursorIcon from "assets/editor/cursor.png";
|
||||
import DeleteIcon from "assets/editor/delete.png";
|
||||
import { ImgIcon } from "common/ImgIcon";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useMineAPI, useSnackbar } from "providers";
|
||||
import { Save, CloudDownload } from "@material-ui/icons";
|
||||
import { Placeable } from "./drawable/Placeable";
|
||||
import { Component } from "models";
|
||||
import EditorActions from "./EditorActions";
|
||||
import LoadMine from "./LoadMine";
|
||||
import { Sensor } from "./drawable/Sensor";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
header: {
|
||||
width: "100%",
|
||||
height: theme.spacing(7),
|
||||
background: "rgba(25, 25, 25, 1)",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid rgb(75, 75, 75)"
|
||||
},
|
||||
middleButtons: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
margin: "auto"
|
||||
},
|
||||
leftButtons: {
|
||||
display: "flex",
|
||||
alignItems: "left",
|
||||
marginLeft: theme.spacing(1),
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto"
|
||||
},
|
||||
rightButtons: {
|
||||
display: "flex",
|
||||
alignItems: "right",
|
||||
justifyItems: "flex-en",
|
||||
marginRight: theme.spacing(1),
|
||||
marginLeft: "auto"
|
||||
},
|
||||
divider: {
|
||||
height: "42px",
|
||||
width: "2px",
|
||||
margin: theme.spacing(1)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
enum Mode {
|
||||
NORMAL,
|
||||
MOVE,
|
||||
ADD,
|
||||
DELETE
|
||||
}
|
||||
|
||||
interface Props {
|
||||
setMode: any;
|
||||
undoRef: any;
|
||||
redoRef: any;
|
||||
saveRef: any;
|
||||
blocks: Placeable[];
|
||||
devices: pond.Device[];
|
||||
components: Component[];
|
||||
setBlocks: React.Dispatch<React.SetStateAction<Placeable[]>>;
|
||||
setDevices: React.Dispatch<React.SetStateAction<pond.Device[]>>;
|
||||
mine: pond.MineSettings;
|
||||
setMine: React.Dispatch<React.SetStateAction<pond.MineSettings>>;
|
||||
permissions: pond.Permission[];
|
||||
setPermissions: React.Dispatch<React.SetStateAction<pond.Permission[]>>;
|
||||
componentPreferences: Map<string, pond.MineComponentPreferences>;
|
||||
sensors: Sensor[];
|
||||
}
|
||||
|
||||
export default function EditorHeader(props: Props) {
|
||||
const classes = useStyles();
|
||||
const {
|
||||
setMode,
|
||||
undoRef,
|
||||
redoRef,
|
||||
blocks,
|
||||
setBlocks,
|
||||
setDevices,
|
||||
mine,
|
||||
setMine,
|
||||
permissions,
|
||||
setPermissions,
|
||||
componentPreferences
|
||||
} = props;
|
||||
|
||||
const mineAPI = useMineAPI();
|
||||
const snackbar = useSnackbar();
|
||||
const theme = useTheme();
|
||||
const [loadDialog, setLoadDialog] = useState(false);
|
||||
|
||||
const save = () => {
|
||||
let settings = pond.MineSettings.create();
|
||||
blocks.forEach(block => {
|
||||
let shaft = pond.Placeable.create();
|
||||
shaft.angle = block.angle;
|
||||
shaft.direction = block.direction;
|
||||
shaft.radius = block.radius();
|
||||
shaft.type = block.type.valueOf();
|
||||
shaft.subtype = block.subtype;
|
||||
shaft.width = 64;
|
||||
shaft.x = block.x;
|
||||
shaft.y = block.y;
|
||||
shaft.magnitude = block.magnitude();
|
||||
settings.placeables.push(shaft);
|
||||
});
|
||||
|
||||
settings.name = mine.name;
|
||||
|
||||
if (mine.key) {
|
||||
settings.key = mine.key;
|
||||
let prefs = componentPreferences;
|
||||
mineAPI
|
||||
.updateMine(settings, prefs)
|
||||
.then(resp => {
|
||||
if (resp.status === 200) {
|
||||
snackbar.success("Changes successfully saved!");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
snackbar.error("Error saving changes");
|
||||
});
|
||||
} else {
|
||||
mineAPI
|
||||
.addMine(settings)
|
||||
.then(resp => {
|
||||
if (resp.status === 200) {
|
||||
snackbar.success("New mine successfully saved!");
|
||||
settings.key = resp.data.key;
|
||||
setMine(settings);
|
||||
setPermissions([
|
||||
pond.Permission.PERMISSION_READ,
|
||||
pond.Permission.PERMISSION_SHARE,
|
||||
pond.Permission.PERMISSION_USERS,
|
||||
pond.Permission.PERMISSION_WRITE
|
||||
]);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
snackbar.error("Error saving new mine");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container className={classes.header}>
|
||||
<Grid item xs className={classes.leftButtons}>
|
||||
<Typography className={classes.leftButtons} variant="h6">
|
||||
{mine.name ? mine.name : "No Name"}
|
||||
</Typography>
|
||||
<IconButton onClick={save} style={{ marginLeft: theme.spacing(2) }}>
|
||||
<Save />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => setLoadDialog(true)}>
|
||||
<CloudDownload />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs className={classes.middleButtons}>
|
||||
<div className={classes.middleButtons}>
|
||||
<IconButton onClick={undoRef.current ? (undoRef.current as any) : undefined}>
|
||||
<ImgIcon src={UndoIcon} />
|
||||
</IconButton>
|
||||
<IconButton onClick={redoRef.current ? (redoRef.current as any) : undefined}>
|
||||
<ImgIcon src={RedoIcon} />
|
||||
</IconButton>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
|
||||
<IconButton onClick={() => setMode(Mode.NORMAL)}>
|
||||
<ImgIcon src={CursorIcon} />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => setMode(Mode.MOVE)}>
|
||||
<ImgIcon src={MoveIcon} />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => setMode(Mode.DELETE)}>
|
||||
<ImgIcon src={DeleteIcon} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs className={classes.rightButtons}>
|
||||
<div className={classes.rightButtons}>
|
||||
<EditorActions
|
||||
mine={mine}
|
||||
setMine={setMine}
|
||||
refreshCallback={() => {}}
|
||||
permissions={permissions}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
<LoadMine
|
||||
setBlocks={setBlocks}
|
||||
setDevices={setDevices}
|
||||
setMine={setMine}
|
||||
setPermissions={setPermissions}
|
||||
open={loadDialog}
|
||||
setOpen={setLoadDialog}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
130
src/ventilation/EditorSettings.tsx
Normal file
130
src/ventilation/EditorSettings.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import {
|
||||
Button,
|
||||
createStyles,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
makeStyles,
|
||||
TextField,
|
||||
Theme,
|
||||
Typography,
|
||||
Tooltip
|
||||
} from "@material-ui/core";
|
||||
import DeleteButton from "common/DeleteButton";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useMineAPI, useSnackbar } from "providers";
|
||||
import React, { useState } from "react";
|
||||
import { useHistory } from "react-router";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
dialogContent: {
|
||||
minHeight: "25vh"
|
||||
},
|
||||
removeDialog: {
|
||||
zIndex: theme.zIndex.modal + 1
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
mine: pond.MineSettings;
|
||||
setMine: React.Dispatch<React.SetStateAction<pond.MineSettings>>;
|
||||
}
|
||||
|
||||
export default function EditorSettings(props: Props) {
|
||||
const { open, setOpen, mine, setMine } = props;
|
||||
const [nameInput, setNameInput] = useState<string>("");
|
||||
const [removeDialog, setRemoveDialog] = useState<boolean>(false);
|
||||
|
||||
const classes = useStyles();
|
||||
const mineAPI = useMineAPI();
|
||||
const snackbar = useSnackbar();
|
||||
const history = useHistory();
|
||||
|
||||
const updateName = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
setNameInput(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const apply = () => {
|
||||
let m = cloneDeep(mine);
|
||||
m.name = nameInput;
|
||||
setMine(m);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
mineAPI
|
||||
.removeMine(mine.key)
|
||||
.then(resp => {
|
||||
if (resp.status === 200) snackbar.success("Mine successfully removed");
|
||||
setRemoveDialog(false);
|
||||
setOpen(false);
|
||||
setMine(pond.MineSettings.create());
|
||||
history.push("/mines");
|
||||
})
|
||||
.catch(err => {
|
||||
snackbar.error("Could not delete the mine");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<DialogTitle>Mine Settings</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField label="Name" defaultValue={mine.name} onChange={e => updateName(e)} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Tooltip
|
||||
title={
|
||||
mine.key === undefined || mine.key === ""
|
||||
? "Can only delete a saved mine"
|
||||
: "Delete this mine"
|
||||
}>
|
||||
<span>
|
||||
<DeleteButton
|
||||
style={{ marginRight: "auto" }}
|
||||
onClick={() => setRemoveDialog(true)}
|
||||
disabled={mine.key === undefined || mine.key === ""}>
|
||||
Delete
|
||||
</DeleteButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<Button color="primary" onClick={apply}>
|
||||
Apply
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={removeDialog}
|
||||
onClose={() => setRemoveDialog(false)}
|
||||
aria-labelledby="confirm-remove-device-label"
|
||||
aria-describedby="confirm-remove-device-description"
|
||||
className={classes.removeDialog}>
|
||||
<DialogTitle id="confirm-remove-device-title">Delete {mine.name}?</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle1" color="error" align="left">
|
||||
WARNING:
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary" align="left">
|
||||
Clicking 'Accept' will remove the device from all users and groups associated with it.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setRemoveDialog(false)} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={remove} color="primary">
|
||||
Accept
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
245
src/ventilation/EditorSidebar.tsx
Normal file
245
src/ventilation/EditorSidebar.tsx
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import { createStyles, makeStyles, Theme, Tooltip } from "@material-ui/core";
|
||||
import React, { useState } from "react";
|
||||
import FansIcon from "assets/editor/fans.png";
|
||||
import FansYellowIcon from "assets/editor/fansYellow.png";
|
||||
import DevicesIcon from "assets/editor/devices.png";
|
||||
import DuctingIcon from "assets/editor/ducting.png";
|
||||
import SensorsIcon from "assets/editor/sensors.png";
|
||||
import SensorsYellowIcon from "assets/editor/sensorsYellow.png";
|
||||
import DuctingYellowIcon from "assets/editor/ductingYellow.png";
|
||||
import EditorDrawer from "./EditorDrawer";
|
||||
import FansDrawer from "./drawers/FansDrawer";
|
||||
import DuctingDrawer from "./drawers/DuctingDrawer";
|
||||
import DeviceDrawer from "./drawers/DeviceDrawer";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import SensorDrawer from "./drawers/SensorDrawer";
|
||||
import { Component } from "models";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
sidebar: {
|
||||
maxHeight: "auto",
|
||||
width: theme.spacing(10),
|
||||
background: "rgba(25, 25, 25, 1)",
|
||||
margin: 0,
|
||||
padding: 0
|
||||
},
|
||||
sideButtonInactive: {
|
||||
width: theme.spacing(10),
|
||||
height: theme.spacing(10),
|
||||
padding: theme.spacing(2),
|
||||
opacity: 0.4,
|
||||
"&:hover": {
|
||||
cursor: "default"
|
||||
}
|
||||
},
|
||||
sideButton: {
|
||||
width: theme.spacing(10),
|
||||
height: theme.spacing(10),
|
||||
padding: theme.spacing(2),
|
||||
"&:hover": {
|
||||
background: "rgba(200,200,200,0.15)",
|
||||
cursor: "pointer"
|
||||
}
|
||||
},
|
||||
sideButtonSelected: {
|
||||
width: theme.spacing(10),
|
||||
height: theme.spacing(10),
|
||||
padding: theme.spacing(2),
|
||||
zIndex: 40,
|
||||
overflow: "visible",
|
||||
background: "rgba(35, 35, 35, 1)",
|
||||
//backgroundImage:
|
||||
// "radial-gradient(circle at 38px 80px, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0) 40px, blue 40px)",
|
||||
color: "yellow",
|
||||
"&:hover": {
|
||||
background: "rgba(200,200,200,0.15)"
|
||||
},
|
||||
"&:before": {
|
||||
width: "80px",
|
||||
height: "80px",
|
||||
borderRadius: "0 40px 40px 0",
|
||||
backgroundColor: "#FFF",
|
||||
display: "inline-block",
|
||||
verticalAlign: "middle",
|
||||
marginRight: "10px",
|
||||
content: ""
|
||||
}
|
||||
},
|
||||
sideButtonImage: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
marginTop: theme.spacing(-1),
|
||||
color: "yellow"
|
||||
},
|
||||
sideButtonText: {
|
||||
textAlign: "center",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
marginTop: theme.spacing(-1)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
enum Selection {
|
||||
NONE,
|
||||
FANS,
|
||||
DUCTING,
|
||||
DEVICES,
|
||||
SENSORS
|
||||
}
|
||||
|
||||
interface Props {
|
||||
addBlockRef: any;
|
||||
setDevices: any;
|
||||
devices: pond.Device[];
|
||||
components: Component[];
|
||||
setComponents: React.Dispatch<React.SetStateAction<Component[]>>;
|
||||
mine: pond.MineSettings;
|
||||
setMine: React.Dispatch<React.SetStateAction<pond.MineSettings>>;
|
||||
mineComponentPreferences: Map<string, pond.MineComponentPreferences>;
|
||||
setMineComponentPreferences: React.Dispatch<
|
||||
React.SetStateAction<Map<string, pond.MineComponentPreferences>>
|
||||
>;
|
||||
}
|
||||
|
||||
export default function EditorSidebar(props: Props) {
|
||||
const {
|
||||
addBlockRef,
|
||||
setDevices,
|
||||
devices,
|
||||
setComponents,
|
||||
mine,
|
||||
setMine,
|
||||
components,
|
||||
mineComponentPreferences,
|
||||
setMineComponentPreferences
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [selected, setSelected] = useState<Selection>(Selection.NONE);
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
|
||||
const select = (s: Selection) => {
|
||||
if (s === selected) {
|
||||
setSelected(Selection.NONE);
|
||||
setExpanded(false);
|
||||
} else {
|
||||
setSelected(s);
|
||||
setExpanded(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.sidebar}>
|
||||
<div
|
||||
className={
|
||||
mine.key.length < 1
|
||||
? classes.sideButtonInactive
|
||||
: selected === Selection.FANS
|
||||
? classes.sideButtonSelected
|
||||
: classes.sideButton
|
||||
}
|
||||
onClick={() => select(Selection.FANS)}>
|
||||
<img
|
||||
className={classes.sideButtonImage}
|
||||
alt={"Fan"}
|
||||
src={selected === Selection.FANS ? FansYellowIcon : FansIcon}
|
||||
/>
|
||||
<div className={classes.sideButtonText}>Fans</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mine.key.length < 1
|
||||
? classes.sideButtonInactive
|
||||
: selected === Selection.DUCTING
|
||||
? classes.sideButtonSelected
|
||||
: classes.sideButton
|
||||
}
|
||||
onClick={() => select(Selection.DUCTING)}>
|
||||
<img
|
||||
className={classes.sideButtonImage}
|
||||
alt={"Ducting"}
|
||||
src={selected === Selection.DUCTING ? DuctingYellowIcon : DuctingIcon}
|
||||
/>
|
||||
<div className={classes.sideButtonText}>Ducting</div>
|
||||
</div>
|
||||
<Tooltip
|
||||
title={mine.key.length < 1 ? "Save current configuration or load a mine first" : ""}>
|
||||
<div
|
||||
className={
|
||||
mine.key.length < 1
|
||||
? classes.sideButtonInactive
|
||||
: selected === Selection.DEVICES
|
||||
? classes.sideButtonSelected
|
||||
: classes.sideButton
|
||||
}
|
||||
onClick={() => {
|
||||
if (mine.key.length < 1) return;
|
||||
select(Selection.DEVICES);
|
||||
}}>
|
||||
<img
|
||||
className={classes.sideButtonImage}
|
||||
alt={"Devices"}
|
||||
src={selected === Selection.DEVICES ? DevicesIcon : DevicesIcon}
|
||||
/>
|
||||
<div className={classes.sideButtonText}>Devices</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={mine.key.length < 1 ? "Save current configuration or load a mine first" : ""}>
|
||||
<div
|
||||
className={
|
||||
mine.key.length < 1
|
||||
? classes.sideButtonInactive
|
||||
: selected === Selection.SENSORS
|
||||
? classes.sideButtonSelected
|
||||
: classes.sideButton
|
||||
}
|
||||
onClick={() => {
|
||||
if (mine.key.length < 1) return;
|
||||
select(Selection.SENSORS);
|
||||
}}>
|
||||
<img
|
||||
className={classes.sideButtonImage}
|
||||
alt={"Sensors"}
|
||||
src={selected === Selection.SENSORS ? SensorsYellowIcon : SensorsIcon}
|
||||
/>
|
||||
<div className={classes.sideButtonText}>Sensors</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<EditorDrawer isOpen={expanded}>
|
||||
{selected === Selection.FANS ? (
|
||||
<FansDrawer addBlockRef={addBlockRef} />
|
||||
) : selected === Selection.DUCTING ? (
|
||||
<DuctingDrawer addBlockRef={addBlockRef} />
|
||||
) : selected === Selection.DEVICES ? (
|
||||
<DeviceDrawer
|
||||
addBlockRef={addBlockRef}
|
||||
selectedDevices={devices}
|
||||
setSelectedDevices={setDevices}
|
||||
mine={mine}
|
||||
setMine={setMine}
|
||||
/>
|
||||
) : selected === Selection.SENSORS ? (
|
||||
<SensorDrawer
|
||||
addBlockRef={addBlockRef}
|
||||
selectedDevices={devices}
|
||||
setSelectedDevices={setDevices}
|
||||
setMineComponents={setComponents}
|
||||
mineComponents={components}
|
||||
mineKey={mine.key}
|
||||
mineComponentPreferences={mineComponentPreferences}
|
||||
setMineComponentPreferences={setMineComponentPreferences}
|
||||
/>
|
||||
) : (
|
||||
"Coming soon :)"
|
||||
)}
|
||||
</EditorDrawer>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
206
src/ventilation/LoadMine.tsx
Normal file
206
src/ventilation/LoadMine.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import {
|
||||
Button,
|
||||
Card,
|
||||
CircularProgress,
|
||||
createStyles,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
Select,
|
||||
Theme,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useMineAPI, useSnackbar } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { or } from "utils";
|
||||
import { loadPlaceable, Placeable } from "./drawable/Placeable";
|
||||
import LeftIcon from "@material-ui/icons/KeyboardArrowLeft";
|
||||
import RightIcon from "@material-ui/icons/KeyboardArrowRight";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useMobile } from "hooks";
|
||||
|
||||
interface Props {
|
||||
setMine: (value: React.SetStateAction<pond.MineSettings>) => void;
|
||||
setBlocks: (value: React.SetStateAction<Placeable[]>) => void;
|
||||
setDevices: (value: React.SetStateAction<pond.Device[]>) => void;
|
||||
setPermissions: (value: React.SetStateAction<pond.Permission[]>) => void;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
dialog: {
|
||||
//width: theme.spacing(32)
|
||||
},
|
||||
mineCard: {
|
||||
margin: theme.spacing(1),
|
||||
padding: theme.spacing(1),
|
||||
background: theme.palette.background.default,
|
||||
"&:hover": {
|
||||
cursor: "pointer"
|
||||
}
|
||||
},
|
||||
highlight: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0)",
|
||||
zIndex: 10,
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)"
|
||||
}
|
||||
},
|
||||
bottomButtons: {
|
||||
marginLeft: "auto",
|
||||
justifyContent: "center",
|
||||
alignContent: "center"
|
||||
},
|
||||
bottom: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingRight: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(1),
|
||||
marginTop: theme.spacing(1)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default function LoadMine(props: Props) {
|
||||
const { setMine, setBlocks, setDevices, setPermissions, open, setOpen } = props;
|
||||
|
||||
const mineAPI = useMineAPI();
|
||||
const snackbar = useSnackbar();
|
||||
const [mines, setMines] = useState<pond.MineSimple[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [limit, setLimit] = useState(5);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const classes = useStyles();
|
||||
const isMobile = useMobile();
|
||||
|
||||
const load = (key: string) => {
|
||||
setLoading(true);
|
||||
mineAPI
|
||||
.getMine(key)
|
||||
.then(resp => {
|
||||
let newBlocks: Placeable[] = [];
|
||||
if (resp.data.mine?.settings?.placeables)
|
||||
resp.data.mine?.settings?.placeables.forEach(p => {
|
||||
let placeable = loadPlaceable(p);
|
||||
newBlocks.push(placeable as Placeable);
|
||||
});
|
||||
setMine(
|
||||
pond.MineSettings.fromObject(or(resp.data.mine?.settings, pond.MineSettings.create()))
|
||||
);
|
||||
setBlocks(newBlocks);
|
||||
setDevices(or(resp.data.devices, []));
|
||||
setMine(or(resp.data.mine?.settings, undefined));
|
||||
let d = pond.GetMineResponse.fromObject(resp.data);
|
||||
setPermissions(d.permissions);
|
||||
if (resp.status === 200) {
|
||||
snackbar.success("Mine successfully loaded");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
snackbar.error("Error loading mine");
|
||||
})
|
||||
.finally(() => {
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Load list of mines when the tab is opened
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setLoading(true);
|
||||
mineAPI.listMinesSimple(limit, offset).then(resp => {
|
||||
setMines(resp.data.mines);
|
||||
setLoading(false);
|
||||
setTotal(resp.data.total);
|
||||
});
|
||||
}
|
||||
}, [mineAPI, open, limit, offset]);
|
||||
|
||||
const back = () => {
|
||||
if (offset - limit < 0) {
|
||||
setOffset(0);
|
||||
} else {
|
||||
setOffset(offset - limit);
|
||||
}
|
||||
};
|
||||
|
||||
const forward = () => {
|
||||
if (offset + limit > total) {
|
||||
setOffset(total - limit);
|
||||
} else {
|
||||
setOffset(offset + limit);
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = () => {
|
||||
return (
|
||||
<div className={classes.bottom}>
|
||||
<Typography>
|
||||
{offset + 1} - {offset + limit > total ? total : offset + limit} of {total}
|
||||
</Typography>
|
||||
<div className={classes.bottomButtons}>
|
||||
<IconButton onClick={back} size="small">
|
||||
<LeftIcon />
|
||||
</IconButton>
|
||||
<Select value={limit} onChange={event => setLimit(event.target.value as number)}>
|
||||
<MenuItem value={5}>5</MenuItem>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={15}>15</MenuItem>
|
||||
</Select>
|
||||
<IconButton onClick={forward} size="small">
|
||||
<RightIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
className={classes.dialog}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
fullScreen={isMobile}>
|
||||
<DialogTitle>Load a Mine</DialogTitle>
|
||||
<DialogContent>
|
||||
{loading ? (
|
||||
<CircularProgress style={{ margin: "auto" }} />
|
||||
) : mines.length < 1 ? (
|
||||
<Typography>No mines</Typography>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{mines.map((mine, index) => {
|
||||
return (
|
||||
<div key={index} className={classes.highlight}>
|
||||
<Card className={classes.mineCard} onClick={() => load(mine.key)}>
|
||||
<Typography>{mine.name === undefined ? "No Name" : mine.name}</Typography>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{/* Set offsets */}
|
||||
<Divider />
|
||||
{paginator()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
164
src/ventilation/VentDialog.tsx
Normal file
164
src/ventilation/VentDialog.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
IconButton,
|
||||
Grid,
|
||||
Slider,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
useTheme
|
||||
} from "@material-ui/core";
|
||||
import { Close } from "@material-ui/icons";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useMobile } from "hooks";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
|
||||
interface Props {
|
||||
type: pond.ShaftType | undefined;
|
||||
setType: any;
|
||||
setDirection: any;
|
||||
}
|
||||
|
||||
export default function VentDialog(props: Props) {
|
||||
const { type, setType, setDirection } = props;
|
||||
const [ventType, setVentType] = useState<string>("Layflat");
|
||||
const [ventSubType, setVentSubType] = useState<string>("Round");
|
||||
|
||||
const isMobile = useMobile();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
maxWidth={"xs"}
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
open={Boolean(type)}
|
||||
scroll="paper"
|
||||
onClose={() => {
|
||||
setType(undefined);
|
||||
}}>
|
||||
<AppBar position="relative">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" style={{ flex: 1 }}>
|
||||
Editing Vent Shaft
|
||||
</Typography>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => setType(undefined)}
|
||||
aria-label="close">
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
{type === pond.ShaftType.SHAFT_TYPE_CORNER && (
|
||||
<React.Fragment>
|
||||
<div style={{ margin: theme.spacing(2) }}>
|
||||
<Typography variant="subtitle1" style={{ marginBottom: theme.spacing(1) }}>
|
||||
Direction
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
aria-labelledby="demo-radio-buttons-group-label"
|
||||
defaultValue={"clockwise"}
|
||||
name="radio-buttons-group">
|
||||
<FormControlLabel
|
||||
value={"clockwise"}
|
||||
control={<Radio />}
|
||||
label="Clockwise"
|
||||
onClick={() => setDirection(0)}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={"counterclockwise"}
|
||||
control={<Radio />}
|
||||
label="Counter Clockwise"
|
||||
onClick={() => setDirection(1)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Grid container style={{ padding: theme.spacing(2) }}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="subtitle1" style={{ marginBottom: theme.spacing(1) }}>
|
||||
Specifications (coming soon)
|
||||
</Typography>
|
||||
<Grid item xs>
|
||||
<Typography variant="body2">Length:</Typography>
|
||||
<Slider defaultValue={50} aria-label="Default" valueLabelDisplay="auto" />
|
||||
</Grid>
|
||||
<Typography variant="body2">Diameter:</Typography>
|
||||
<Slider defaultValue={50} aria-label="Default" valueLabelDisplay="auto" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider />
|
||||
<Grid container style={{ padding: theme.spacing(2) }}>
|
||||
<FormControl>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Ventilation Type</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="row-radio-buttons-group"
|
||||
value={ventType}>
|
||||
<FormControlLabel
|
||||
value="Layflat"
|
||||
control={<Radio />}
|
||||
label="Layflat"
|
||||
onClick={() => {
|
||||
setVentType("Layflat");
|
||||
setVentSubType("Round");
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="Rigid"
|
||||
control={<Radio />}
|
||||
label="Rigid"
|
||||
onClick={() => {
|
||||
setVentType("Rigid");
|
||||
setVentSubType("Hardline");
|
||||
}}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{ventType === "Layflat" ? (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="demo-simple-select-label">Subtype</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={ventSubType}
|
||||
label="Ventilation Subtybe"
|
||||
onChange={event => setVentSubType(event.target.value as string)}>
|
||||
<MenuItem value={"Round"}>Round</MenuItem>
|
||||
<MenuItem value={"Twin"}>Twin</MenuItem>
|
||||
<MenuItem value={"M"}>M</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="demo-simple-select-label">Subtype</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={ventSubType}
|
||||
label="Ventilation Subtybe"
|
||||
onChange={event => setVentSubType(event.target.value as string)}>
|
||||
<MenuItem value={"Hardline"}>Hardline</MenuItem>
|
||||
<MenuItem value={"Steel"}>Steel</MenuItem>
|
||||
<MenuItem value={"Fiberglass"}>Fiberglass</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Grid>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
52
src/ventilation/Zoom.tsx
Normal file
52
src/ventilation/Zoom.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { IconButton, InputAdornment, TextField, useTheme } from "@material-ui/core";
|
||||
import { Add, Remove } from "@material-ui/icons";
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
scale: number;
|
||||
setScale: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
export default function Zoom(props: Props) {
|
||||
const { scale, setScale } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const onTextInput = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
let value = parseInt(e.currentTarget.value);
|
||||
if (value < 1000 && value > 0) {
|
||||
setScale(value / 100);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
if (scale > 0.1) setScale(scale - 0.05);
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
if (scale < 5) setScale(scale + 0.05);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton onClick={zoomOut}>
|
||||
<Remove />
|
||||
</IconButton>
|
||||
|
||||
<TextField
|
||||
style={{
|
||||
paddingTop: theme.spacing(1),
|
||||
width: "48px"
|
||||
}}
|
||||
value={Math.round(scale * 100)}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">%</InputAdornment>
|
||||
}}
|
||||
onChange={e => onTextInput(e)}
|
||||
/>
|
||||
|
||||
<IconButton onClick={zoomIn}>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
261
src/ventilation/drawable/Block.ts
Normal file
261
src/ventilation/drawable/Block.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import StandardDuctIcon from "assets/mining/standardDuct.png";
|
||||
import ConnectionDuctIcon from "assets/mining/connectionDuct.png";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { or } from "utils";
|
||||
import { Circle } from "./Circle";
|
||||
|
||||
export class Block {
|
||||
public settings: pond.BlockSettings = pond.BlockSettings.create();
|
||||
public img: HTMLImageElement | undefined = undefined;
|
||||
public selected = false;
|
||||
public mouseOffsetX = 0;
|
||||
public mouseOffsetY = 0;
|
||||
public circle = Circle.create(0, 0, 0);
|
||||
public startSnap: Block | undefined = undefined;
|
||||
public endSnap: Block | undefined = undefined;
|
||||
|
||||
public ctrl = false;
|
||||
public shift = false;
|
||||
|
||||
public static create(pb?: pond.Block, img?: HTMLImageElement): Block {
|
||||
let my = new Block();
|
||||
my.img = new Image();
|
||||
if (pb) {
|
||||
my.settings = pond.BlockSettings.fromObject(cloneDeep(or(pb.settings, {})));
|
||||
// For some reason, negative x and y values don't clone correctly
|
||||
if (pb.settings) {
|
||||
my.settings.x = pb.settings.x;
|
||||
my.settings.y = pb.settings.y;
|
||||
}
|
||||
if (!img) {
|
||||
switch (my.settings.type) {
|
||||
case pond.BlockType.BLOCK_TYPE_VENT:
|
||||
my.img.src = StandardDuctIcon;
|
||||
break;
|
||||
case pond.BlockType.BLOCK_TYPE_VENT_CORNER:
|
||||
my.img.src = ConnectionDuctIcon;
|
||||
break;
|
||||
default:
|
||||
my.img = undefined;
|
||||
}
|
||||
} else {
|
||||
my.img = img;
|
||||
}
|
||||
}
|
||||
my.ctrl = false;
|
||||
my.shift = false;
|
||||
return my;
|
||||
}
|
||||
|
||||
public static any(data: any): Block {
|
||||
return Block.create(pond.Block.fromObject(cloneDeep(data)));
|
||||
}
|
||||
|
||||
public draw(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number) {
|
||||
let x = this.settings.x;
|
||||
let y = this.settings.y;
|
||||
let width = this.settings.width;
|
||||
let height = this.settings.height;
|
||||
|
||||
if (this.img) {
|
||||
switch (this.settings.type) {
|
||||
case pond.BlockType.BLOCK_TYPE_NONE:
|
||||
context.fillStyle = "rgba(150, 150, 255, 150)";
|
||||
context.fillRect(x - (offsetX ? offsetX : 0), y - (offsetY ? offsetY : 0), width, height);
|
||||
break;
|
||||
default:
|
||||
context.fillStyle = "rgba(255, 255, 255, 255)";
|
||||
context.drawImage(
|
||||
this.img,
|
||||
x - (offsetX ? offsetX : 0),
|
||||
y - (offsetY ? offsetY : 0),
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
} else {
|
||||
context.fillStyle = "rgba(150, 150, 255, 150)";
|
||||
context.fillRect(x - (offsetX ? offsetX : 0), y - (offsetY ? offsetY : 0), width, height);
|
||||
}
|
||||
}
|
||||
|
||||
public highlight(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number) {
|
||||
let x = this.settings.x;
|
||||
let y = this.settings.y;
|
||||
let width = this.settings.width;
|
||||
let height = this.settings.height;
|
||||
|
||||
context.strokeStyle = "rgba(247, 202, 24, 1)";
|
||||
context.strokeRect(x - (offsetX ? offsetX : 0), y - (offsetY ? offsetY : 0), width, height);
|
||||
}
|
||||
|
||||
public static clone(other?: Block): Block {
|
||||
if (other) {
|
||||
let b = Block.create(
|
||||
pond.Block.fromObject({
|
||||
settings: cloneDeep(other.settings),
|
||||
img: new Image()
|
||||
})
|
||||
);
|
||||
if (b.img)
|
||||
switch (b.settings.type) {
|
||||
case pond.BlockType.BLOCK_TYPE_VENT:
|
||||
b.img.src = StandardDuctIcon;
|
||||
break;
|
||||
case pond.BlockType.BLOCK_TYPE_VENT_CORNER:
|
||||
b.img.src = ConnectionDuctIcon;
|
||||
break;
|
||||
default:
|
||||
b.img = undefined;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
return Block.create();
|
||||
}
|
||||
|
||||
public clone() {
|
||||
return Block.create(
|
||||
pond.Block.fromObject({
|
||||
settings: cloneDeep(this.settings),
|
||||
img: undefined
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public clickCheck(x: number, y: number, extend?: number) {
|
||||
extend = extend ? extend : 0;
|
||||
if (
|
||||
x > this.x() - extend &&
|
||||
x < this.x() + this.width() + extend * 2 &&
|
||||
y > this.y() - extend &&
|
||||
y < this.y() + this.height() + extend * 2
|
||||
) {
|
||||
this.selected = true;
|
||||
this.mouseOffsetX = x - this.x();
|
||||
this.mouseOffsetY = y - this.y();
|
||||
return true;
|
||||
}
|
||||
this.selected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
public x(): number {
|
||||
return this.settings.x;
|
||||
}
|
||||
|
||||
public endX(): number {
|
||||
return this.settings.x + this.settings.width;
|
||||
}
|
||||
|
||||
public y(): number {
|
||||
return this.settings.y;
|
||||
}
|
||||
|
||||
public endY(): number {
|
||||
return this.settings.y + this.settings.height;
|
||||
}
|
||||
|
||||
public width(): number {
|
||||
return this.settings.width;
|
||||
}
|
||||
|
||||
public height(): number {
|
||||
return this.settings.height;
|
||||
}
|
||||
|
||||
public getCtrl(): boolean {
|
||||
return this.ctrl;
|
||||
}
|
||||
|
||||
public setCtrl(c: boolean) {
|
||||
this.ctrl = c;
|
||||
}
|
||||
|
||||
public getShift(): boolean {
|
||||
return this.shift;
|
||||
}
|
||||
|
||||
public setShift(s: boolean) {
|
||||
this.shift = s;
|
||||
}
|
||||
|
||||
public setSelected(s: boolean) {
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
public radians(ang?: number) {
|
||||
let angle = ang ? ang : this.settings.angle;
|
||||
return angle * (Math.PI / 180);
|
||||
}
|
||||
|
||||
public distance(x1: number, x2: number, y1: number, y2: number) {
|
||||
let a = x1 - x2;
|
||||
let b = y1 - y2;
|
||||
//console.log(a)
|
||||
//console.log(b)
|
||||
//console.log(x1)
|
||||
//console.log(x2)
|
||||
return Math.sqrt(a * a + b * b);
|
||||
}
|
||||
|
||||
public getSnap(rect: Block, snapDistance?: number) {
|
||||
snapDistance = snapDistance ? snapDistance : 6;
|
||||
let rect1 = this;
|
||||
let rect2 = rect;
|
||||
|
||||
// Check top right corner of moving square with top left of square 2
|
||||
let x2 = rect2.x();
|
||||
let y2 = rect2.y();
|
||||
let x = rect1.x();
|
||||
let y = rect1.y();
|
||||
let d = this.distance(rect1.x() + rect1.width(), rect2.x(), rect1.y(), y2);
|
||||
if (d < snapDistance) {
|
||||
return {
|
||||
x: x2 - rect1.width(),
|
||||
y: y2
|
||||
};
|
||||
}
|
||||
|
||||
// Top left corner of moving square vs top right corner
|
||||
d = this.distance(x, x2 + rect2.width(), y, y2);
|
||||
if (d < snapDistance) {
|
||||
return {
|
||||
x: x2 + rect2.width(),
|
||||
y: y2
|
||||
};
|
||||
}
|
||||
|
||||
// Bottom left corner of moving square vs top left corner
|
||||
d = this.distance(x, x2, y, y2 - rect2.height());
|
||||
if (d < snapDistance) {
|
||||
return {
|
||||
x: x2,
|
||||
y: y2 - rect2.height()
|
||||
};
|
||||
}
|
||||
|
||||
// Top left corner of moving square vs bottom left corner
|
||||
d = this.distance(x, x2, y, y2 + rect2.height());
|
||||
if (d < snapDistance) {
|
||||
return {
|
||||
x: x2,
|
||||
y: y2 + rect2.height()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public drag = (x: number, y: number) => {
|
||||
this.settings.x = x - this.mouseOffsetX;
|
||||
this.settings.y = y - this.mouseOffsetY;
|
||||
};
|
||||
|
||||
/*public drag = (e: any, offsetX?: number, offsetY?: number) => {
|
||||
let x = e.pageX - (e.currentTarget.offsetLeft + e.currentTarget.clientLeft) - mouseOffsetX;
|
||||
x = x - getOffsetX(e.currentTarget.offsetParent) + cameraOffsetX;
|
||||
selected[0].settings.x = x;
|
||||
let y = e.pageY - (e.currentTarget.offsetTop + e.currentTarget.clientTop) - mouseOffsetY;
|
||||
y = y - getOffsetY(e.currentTarget.offsetParent) + cameraOffsetY;
|
||||
selected[0].settings.y = y;
|
||||
}*/
|
||||
}
|
||||
41
src/ventilation/drawable/Circle.ts
Normal file
41
src/ventilation/drawable/Circle.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export class Circle {
|
||||
public radius = 1;
|
||||
public x = 0;
|
||||
public y = 0;
|
||||
public selected = true;
|
||||
public img: HTMLImageElement | undefined = undefined;
|
||||
|
||||
public static create(x: number, y: number, radius: number) {
|
||||
let c = new Circle();
|
||||
c.x = x;
|
||||
c.y = y;
|
||||
c.radius = radius;
|
||||
return c;
|
||||
}
|
||||
|
||||
public draw(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number) {
|
||||
offsetX = offsetX ? offsetX : 0;
|
||||
offsetY = offsetY ? offsetY : 0;
|
||||
let oldStyle = context.fillStyle;
|
||||
context.beginPath();
|
||||
context.arc(this.x + offsetX, this.y + offsetY, this.radius, 0, 2 * Math.PI);
|
||||
if (this.selected) {
|
||||
context.fillStyle = "grey";
|
||||
context.fill();
|
||||
}
|
||||
context.stroke();
|
||||
context.fillStyle = oldStyle;
|
||||
}
|
||||
|
||||
public clickCheck(x: number, y: number, scale = 1) {
|
||||
let dx = Math.abs(x - this.x);
|
||||
let dy = Math.abs(y - this.y);
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance <= this.radius) {
|
||||
this.selected = true;
|
||||
return true;
|
||||
}
|
||||
this.selected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
207
src/ventilation/drawable/Fan.ts
Normal file
207
src/ventilation/drawable/Fan.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Placeable } from "./Placeable";
|
||||
import Fan1Icon from "assets/editor/fan1.png";
|
||||
import Fan2Icon from "assets/editor/fan2.png";
|
||||
import Fan3Icon from "assets/editor/fan3.png";
|
||||
import { or } from "utils";
|
||||
import { SnapPoint } from "./geometry";
|
||||
|
||||
export class Fan implements Placeable {
|
||||
public angle: number = 0;
|
||||
public ctrl: boolean = false;
|
||||
public shift: boolean = false;
|
||||
public direction = 0;
|
||||
//public snapPoints = []
|
||||
private mouseOffsetX = 0;
|
||||
private mouseOffsetY = 0;
|
||||
type: pond.PlaceableType = pond.PlaceableType.PLACEABLE_TYPE_FAN;
|
||||
public subtype = 0;
|
||||
|
||||
magnitude(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
radius(): number {
|
||||
return this.fan_radius;
|
||||
}
|
||||
|
||||
highlight(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale = 1
|
||||
): void {
|
||||
offsetX = offsetX ? offsetX : 0;
|
||||
offsetY = offsetY ? offsetY : 0;
|
||||
|
||||
let x = (this.x - offsetX) * scale;
|
||||
let y = (this.y - offsetY) * scale;
|
||||
|
||||
context.beginPath();
|
||||
context.strokeStyle = "yellow";
|
||||
context.arc(x, y, this.radius() * scale, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = "white";
|
||||
}
|
||||
|
||||
public drag = (x: number, y: number) => {
|
||||
this.x = x - this.mouseOffsetX;
|
||||
this.y = y - this.mouseOffsetY;
|
||||
};
|
||||
|
||||
public getEndXAndY = (x = this.x, y = this.y, angle = this.angle, length = this.magnitude()) => {
|
||||
return { x: x, y: y };
|
||||
};
|
||||
|
||||
getSnapPoints() {
|
||||
let snaps: SnapPoint[] = [];
|
||||
snaps.push({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
angle: this.angle,
|
||||
startPoint: false,
|
||||
occupant: undefined
|
||||
} as SnapPoint);
|
||||
return snaps;
|
||||
}
|
||||
|
||||
snap(p: Placeable, d: number): void {
|
||||
d = d ? d : 12;
|
||||
p.getSnapPoints().forEach(point => {
|
||||
if (this.distance(this.x, point.x, this.y, point.y) < d) {
|
||||
this.x = point.x;
|
||||
this.y = point.y;
|
||||
//this.angle = point.angle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fan_radius = 1;
|
||||
public x = 0;
|
||||
public y = 0;
|
||||
public selected = true;
|
||||
public img: HTMLImageElement | undefined = undefined;
|
||||
|
||||
public static create(x: number, y: number, radius: number, subtype: number): Fan {
|
||||
let c = new Fan();
|
||||
let i = new Image();
|
||||
i.src = Fan1Icon;
|
||||
switch (subtype) {
|
||||
case pond.FanType.FAN_TYPE_CENTRIFUGAL_INLINE:
|
||||
i.src = Fan1Icon;
|
||||
break;
|
||||
case pond.FanType.FAN_TYPE_CENTRIFUGAL_HIGH_SPEED:
|
||||
i.src = Fan2Icon;
|
||||
break;
|
||||
case pond.FanType.FAN_TYPE_CENTRIFUGAL_LOW_SPEED:
|
||||
i.src = Fan3Icon;
|
||||
break;
|
||||
}
|
||||
c.img = i;
|
||||
c.x = x;
|
||||
c.y = y;
|
||||
c.fan_radius = radius;
|
||||
c.subtype = subtype;
|
||||
return c;
|
||||
}
|
||||
|
||||
public static fromPond(placeable: pond.Placeable) {
|
||||
let f = new Fan();
|
||||
let i = new Image();
|
||||
i.src = Fan1Icon;
|
||||
if (placeable.subtype) {
|
||||
switch (placeable.subtype) {
|
||||
case pond.FanType.FAN_TYPE_CENTRIFUGAL_INLINE:
|
||||
i.src = Fan1Icon;
|
||||
break;
|
||||
case pond.FanType.FAN_TYPE_CENTRIFUGAL_HIGH_SPEED:
|
||||
i.src = Fan2Icon;
|
||||
break;
|
||||
case pond.FanType.FAN_TYPE_CENTRIFUGAL_LOW_SPEED:
|
||||
i.src = Fan3Icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
f.img = i;
|
||||
f.x = placeable.x;
|
||||
f.y = placeable.y;
|
||||
f.fan_radius = or(placeable.radius, placeable.width / 2);
|
||||
f.subtype = placeable.subtype;
|
||||
return f;
|
||||
}
|
||||
|
||||
public draw(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number, scale = 1) {
|
||||
offsetX = offsetX ? offsetX : 0;
|
||||
offsetY = offsetY ? offsetY : 0;
|
||||
let oldStyle = context.fillStyle;
|
||||
if (this.img === undefined) {
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
(this.x + offsetX) * scale,
|
||||
(this.y + offsetY) * scale,
|
||||
this.radius(),
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
if (this.selected) {
|
||||
context.fillStyle = "grey";
|
||||
context.fill();
|
||||
}
|
||||
context.stroke();
|
||||
context.fillStyle = oldStyle;
|
||||
} else {
|
||||
context.fillStyle = "rgba(255, 255, 255, 255)";
|
||||
context.drawImage(
|
||||
this.img,
|
||||
(this.x - (offsetX ? offsetX : 0) - this.radius()) * scale,
|
||||
(this.y - (offsetY ? offsetY : 0) - this.radius()) * scale,
|
||||
this.radius() * 2 * scale,
|
||||
this.radius() * 2 * scale
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public distance(x1: number, x2: number, y1: number, y2: number) {
|
||||
let a = x1 - x2;
|
||||
let b = y1 - y2;
|
||||
return Math.sqrt(a * a + b * b);
|
||||
}
|
||||
|
||||
public clickCheck(x: number, y: number) {
|
||||
let dx = Math.abs(x - this.x);
|
||||
let dy = Math.abs(y - this.y);
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance <= this.radius()) {
|
||||
this.selected = true;
|
||||
this.mouseOffsetX = x - this.x;
|
||||
this.mouseOffsetY = y - this.y;
|
||||
return true;
|
||||
}
|
||||
this.selected = false;
|
||||
this.mouseOffsetX = 0;
|
||||
this.mouseOffsetY = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public clone() {
|
||||
let f = new Fan();
|
||||
f.img = this.img;
|
||||
f.radius = this.radius;
|
||||
f.selected = false;
|
||||
f.x = this.x;
|
||||
f.y = this.y;
|
||||
return f;
|
||||
}
|
||||
|
||||
public getEndAngle(): number {
|
||||
return this.angle;
|
||||
}
|
||||
|
||||
setDirection(direction: number): void {}
|
||||
|
||||
setAngle(angle: number, oldAngle?: number) {
|
||||
this.angle = angle;
|
||||
//if (oldAngle) this.oldAngle = oldAngle
|
||||
}
|
||||
}
|
||||
75
src/ventilation/drawable/Placeable.ts
Normal file
75
src/ventilation/drawable/Placeable.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Fan } from "./Fan";
|
||||
import { VentCorner } from "./VentCorner";
|
||||
import { VentShaft } from "./VentShaft";
|
||||
import { VentTBreak } from "./VentTBreak";
|
||||
import { SnapPoint } from "./geometry";
|
||||
|
||||
export enum Direction {
|
||||
CLOCKWISE,
|
||||
COUNTERCLOCKWISE
|
||||
}
|
||||
|
||||
export interface Placeable {
|
||||
x: number;
|
||||
y: number;
|
||||
angle: number;
|
||||
ctrl: boolean;
|
||||
shift: boolean;
|
||||
selected: boolean;
|
||||
type: pond.PlaceableType;
|
||||
subtype: number;
|
||||
direction: number;
|
||||
magnitude(): number;
|
||||
radius(): number;
|
||||
clone(): any;
|
||||
clickCheck(x: number, y: number, scale?: number): boolean;
|
||||
highlight(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale?: number
|
||||
): void;
|
||||
drag(x: number, y: number, scale?: number): void;
|
||||
getEndXAndY(x?: number, y?: number, angle?: number, length?: number): { x: number; y: number };
|
||||
getEndAngle(): number;
|
||||
getSnapPoints(): SnapPoint[];
|
||||
snap(p: Placeable, d?: number): void;
|
||||
draw(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number, scale?: number): void;
|
||||
setDirection(direction: number | Direction): void;
|
||||
setAngle(angle: number, oldAngle?: number): void;
|
||||
}
|
||||
|
||||
export function loadPlaceable(shaft: pond.Placeable) {
|
||||
if (shaft.direction === undefined) shaft.direction = 0;
|
||||
if (shaft.angle === undefined) shaft.angle = 0;
|
||||
|
||||
// The pond may store the type as a string or number
|
||||
let type = pond.PlaceableType.PLACEABLE_TYPE_DEFAULT.valueOf();
|
||||
if (shaft.type !== undefined) {
|
||||
if (typeof shaft.type === "string") {
|
||||
type = (pond.PlaceableType[shaft.type] as unknown) as number;
|
||||
} else {
|
||||
type = +pond.PlaceableType[shaft.type];
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case pond.PlaceableType.PLACEABLE_TYPE_SHAFT:
|
||||
if (shaft.subtype) {
|
||||
switch (shaft.subtype) {
|
||||
case pond.ShaftType.SHAFT_TYPE_DEFAULT:
|
||||
return VentShaft.fromPond(shaft);
|
||||
case pond.ShaftType.SHAFT_TYPE_CORNER:
|
||||
return VentCorner.fromPond(shaft);
|
||||
case pond.ShaftType.SHAFT_TYPE_T_BREAK:
|
||||
return VentTBreak.fromPond(shaft);
|
||||
}
|
||||
}
|
||||
return VentShaft.fromPond(shaft);
|
||||
case pond.PlaceableType.PLACEABLE_TYPE_FAN:
|
||||
return Fan.fromPond(shaft);
|
||||
default:
|
||||
return VentShaft.fromPond(shaft);
|
||||
}
|
||||
}
|
||||
300
src/ventilation/drawable/Sensor.ts
Normal file
300
src/ventilation/drawable/Sensor.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Placeable } from "./Placeable";
|
||||
import { Component, Device, User } from "models";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import { SnapPoint } from "./geometry";
|
||||
|
||||
export class Sensor implements Placeable {
|
||||
public debug = false;
|
||||
public x = 0;
|
||||
public y = 0;
|
||||
public angle = 0;
|
||||
public ctrl = false;
|
||||
public shift = false;
|
||||
public selected = false;
|
||||
public type = pond.PlaceableType.PLACEABLE_TYPE_SENSOR;
|
||||
public direction = 0;
|
||||
public subtype = pond.ShaftType.SHAFT_TYPE_DEFAULT;
|
||||
public index = 0;
|
||||
|
||||
private sensor_radius = 8;
|
||||
|
||||
//private circle = Circle.create(0, 0, 0);
|
||||
private mouseOffsetX = 0;
|
||||
private mouseOffsetY = 0;
|
||||
private measurements: UnitMeasurement[] = [];
|
||||
|
||||
private component: Component = new Component();
|
||||
private device: Device = new Device();
|
||||
private preferences: pond.MineSensorPreferences = new pond.MineSensorPreferences();
|
||||
|
||||
public getComponent() {
|
||||
return this.component;
|
||||
}
|
||||
|
||||
public name() {
|
||||
return this.preferences.nickname;
|
||||
}
|
||||
|
||||
public getDevice() {
|
||||
return this.device;
|
||||
}
|
||||
|
||||
public getMeasurements() {
|
||||
return this.measurements;
|
||||
}
|
||||
|
||||
public getPreferences() {
|
||||
return this.preferences;
|
||||
}
|
||||
|
||||
public magnitude(): number {
|
||||
return this.sensor_radius;
|
||||
}
|
||||
|
||||
public radius(): number {
|
||||
return this.sensor_radius;
|
||||
}
|
||||
|
||||
public static fromPond(shaft: pond.Placeable) {
|
||||
let v = new Sensor();
|
||||
v.x = shaft.x;
|
||||
v.y = shaft.y;
|
||||
v.angle = shaft.angle;
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static create(
|
||||
component: Component,
|
||||
device: Device,
|
||||
preferences: pond.MineSensorPreferences,
|
||||
measurements: UnitMeasurement[],
|
||||
index: number,
|
||||
user?: User
|
||||
): Sensor {
|
||||
let v = new Sensor();
|
||||
|
||||
v.x = preferences.x;
|
||||
v.y = preferences.y;
|
||||
v.index = index;
|
||||
|
||||
v.device = device;
|
||||
v.component = component;
|
||||
v.preferences = preferences;
|
||||
v.measurements = measurements;
|
||||
|
||||
/*component.lastMeasurement.forEach(m => {
|
||||
let um = UnitMeasurement.create(m, user);
|
||||
v.measurements.push(um);
|
||||
});*/
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public draw(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number, scale = 1) {
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
let r = this.radius() * scale;
|
||||
|
||||
context.fillStyle = "rgba(0, 200, 0, 255)";
|
||||
context.beginPath();
|
||||
context.arc(x * scale, y * scale, r + 0.1, 2 * Math.PI, 0, false);
|
||||
context.closePath();
|
||||
context.fill();
|
||||
}
|
||||
|
||||
public getEndXAndY = () => {
|
||||
return { x: this.endX(), y: this.endY() };
|
||||
};
|
||||
|
||||
public clickCheck = (x: number, y: number, scale = 1) => {
|
||||
let dx = Math.abs(x - this.x);
|
||||
let dy = Math.abs(y - this.y);
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance <= this.radius()) {
|
||||
this.selected = true;
|
||||
return true;
|
||||
}
|
||||
this.selected = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
public highlight(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale = 1
|
||||
) {
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
let radius = this.radius() * scale;
|
||||
|
||||
// Highlight circle
|
||||
context.beginPath();
|
||||
context.arc(x + 0.5, y + 0.5, radius + 2, 0, 2 * Math.PI, false);
|
||||
context.closePath();
|
||||
context.strokeStyle = "rgb(255, 255, 255)";
|
||||
context.stroke();
|
||||
|
||||
// Line going up
|
||||
context.beginPath();
|
||||
context.moveTo(x, y - (radius + 2 * scale));
|
||||
context.lineTo(x, y - (radius + 12));
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
|
||||
// Frame
|
||||
let padding = 8;
|
||||
let width = 0;
|
||||
this.measurements.forEach((m, i) => {
|
||||
if (m.values[0].values[this.index]) {
|
||||
context.font = "18px Times New Roman";
|
||||
width = width + context.measureText(m.values[0].values[this.index].toFixed(1)).width;
|
||||
context.font = "14px Times New Roman";
|
||||
width = width + context.measureText(m.unit).width;
|
||||
}
|
||||
if (i < this.measurements.length - 1) width = width + context.measureText(", ").width;
|
||||
});
|
||||
context.font = "18px Times New Roman";
|
||||
width = Math.max(width, context.measureText(this.preferences.nickname).width) + 0.1;
|
||||
context.moveTo(x, y - (radius + 12));
|
||||
context.clearRect(x, y - (radius + 42 + padding * 2), width + padding * 2, 46);
|
||||
context.lineTo(x + width + 1 + padding * 2, y - (radius + 12));
|
||||
context.lineTo(x + width + 1 + padding * 2, y - (radius + 42 + padding * 2));
|
||||
context.lineTo(x, y - (radius + 42 + padding * 2));
|
||||
context.lineTo(x, y - (radius + 12));
|
||||
context.stroke();
|
||||
|
||||
// Reading
|
||||
let valueString = "";
|
||||
let newx = x + padding;
|
||||
this.measurements.forEach((m, i) => {
|
||||
newx = context.measureText(valueString).width + newx;
|
||||
valueString = "";
|
||||
if (!m.values[0].values[this.index]) {
|
||||
valueString = valueString + "NaN";
|
||||
} else {
|
||||
valueString = valueString + m.values[0].values[this.index].toFixed(1);
|
||||
}
|
||||
valueString = valueString + m.unit;
|
||||
if (i < this.measurements.length - 1) valueString = valueString + ", ";
|
||||
context.fillStyle = m.colour;
|
||||
context.font = "14px Times New Roman";
|
||||
context.fillText(valueString, newx, y - (radius + 12) - padding);
|
||||
});
|
||||
|
||||
// Label
|
||||
context.fillStyle = "white";
|
||||
context.font = "18px Times New Roman, sans-serif";
|
||||
context.fillText(this.preferences.nickname, x + padding, y - (radius + 30 + padding));
|
||||
}
|
||||
|
||||
public clone() {
|
||||
let v = new Sensor();
|
||||
v.x = this.x;
|
||||
v.y = this.y;
|
||||
v.angle = this.angle;
|
||||
//v.circle = this.circle;
|
||||
v.ctrl = this.ctrl;
|
||||
v.shift = this.shift;
|
||||
v.radius = this.radius;
|
||||
return v;
|
||||
}
|
||||
|
||||
public distance(x1: number, x2: number, y1: number, y2: number) {
|
||||
let a = x1 - x2;
|
||||
let b = y1 - y2;
|
||||
return Math.sqrt(a * a + b * b);
|
||||
}
|
||||
|
||||
getSnapPoints() {
|
||||
let snaps: SnapPoint[] = [];
|
||||
snaps.push({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
angle: this.angle,
|
||||
startPoint: false,
|
||||
occupant: undefined
|
||||
} as SnapPoint);
|
||||
snaps.push({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
angle: this.angle + 180,
|
||||
startPoint: true,
|
||||
occupant: undefined
|
||||
} as SnapPoint);
|
||||
if (snaps[1].angle > 360) snaps[1].angle = snaps[1].angle - 360;
|
||||
return snaps;
|
||||
}
|
||||
|
||||
public snap = (p: Placeable, d: number): void => {
|
||||
d = d ? d : 12;
|
||||
p.getSnapPoints().forEach(point => {
|
||||
if (this.distance(this.x, point.x, this.y, point.y) < d) {
|
||||
this.x = point.x;
|
||||
this.y = point.y;
|
||||
this.angle = point.angle;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public drag = (x: number, y: number, scale = 1) => {
|
||||
this.x = x - this.mouseOffsetX;
|
||||
this.y = y - this.mouseOffsetY;
|
||||
};
|
||||
|
||||
public endX(): number {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public endY(): number {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
public getEndAngle(): number {
|
||||
return this.angle;
|
||||
}
|
||||
|
||||
setDirection(direction: number): void {}
|
||||
|
||||
setAngle(angle: number) {
|
||||
this.angle = angle;
|
||||
}
|
||||
}
|
||||
328
src/ventilation/drawable/VentBezier.ts
Normal file
328
src/ventilation/drawable/VentBezier.ts
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { or } from "utils";
|
||||
import { Block } from "./Block";
|
||||
import { Circle } from "./Circle";
|
||||
import {} from "../Editor";
|
||||
|
||||
const remap = (value: number, sourceMin: number, sourceMax: number, destMin = 0.5, destMax = 1) => {
|
||||
let sourceRange = sourceMax - sourceMin;
|
||||
let destRange = destMax - destMin;
|
||||
let newValue = ((value - sourceMin) * destRange) / sourceRange + destMin;
|
||||
//console.log("Value: " + value + "\nmin: " + sourceMin + "\nmax: " + sourceMax)
|
||||
return newValue;
|
||||
};
|
||||
export class VentBezier extends Block {
|
||||
public circle = Circle.create(0, 0, 6);
|
||||
public settings: pond.BlockSettings = pond.BlockSettings.create();
|
||||
public selected = false;
|
||||
public img = undefined;
|
||||
public mouseOffsetX = 0;
|
||||
public mouseOffsetY = 0;
|
||||
public startSnap: Block | undefined = undefined;
|
||||
public endSnap: Block | undefined = undefined;
|
||||
|
||||
public ctrl = false;
|
||||
public shift = false;
|
||||
|
||||
public static create(pb?: pond.Block): VentBezier {
|
||||
let my = new VentBezier();
|
||||
if (pb) {
|
||||
my.settings = pond.BlockSettings.fromObject(cloneDeep(or(pb.settings, {})));
|
||||
// For some reason, negative x and y values don't clone correctly
|
||||
if (pb.settings) {
|
||||
my.settings.x = pb.settings.x;
|
||||
my.settings.y = pb.settings.y;
|
||||
my.settings.width = pb.settings.width;
|
||||
my.settings.height = pb.settings.height;
|
||||
//my.circle.radius = 6
|
||||
}
|
||||
}
|
||||
my.ctrl = false;
|
||||
my.shift = false;
|
||||
return my;
|
||||
}
|
||||
|
||||
public magnitude(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public clone() {
|
||||
return new Block();
|
||||
}
|
||||
|
||||
private drawBezier(
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
endx: number,
|
||||
endy: number,
|
||||
offsetx: number,
|
||||
offsety: number
|
||||
) {
|
||||
if (this.startSnap === undefined) return;
|
||||
|
||||
if (this.startSnap?.settings.angle === 0) {
|
||||
this.startSnap.settings.angle = 360;
|
||||
}
|
||||
|
||||
let h = this.distance(x, endx, y, endy);
|
||||
let theta1 = Math.asin((endy - y) / h);
|
||||
|
||||
if (endx - x < 0 && this.settings.angle < 180 && this.settings.angle >= 0) {
|
||||
if (theta1 < 0) {
|
||||
let diff = 90 - Math.abs(theta1 / (Math.PI / 180));
|
||||
theta1 = (theta1 / (Math.PI / 180) - diff * 2) * (Math.PI / 180);
|
||||
} else {
|
||||
let diff = 90 - Math.abs(theta1 / (Math.PI / 180));
|
||||
theta1 = (theta1 / (Math.PI / 180) + diff * 2) * (Math.PI / 180);
|
||||
}
|
||||
}
|
||||
|
||||
let angleRatio = remap(
|
||||
Math.abs(theta1),
|
||||
this.startSnap?.radians(),
|
||||
this.startSnap?.radians() - 45 * (Math.PI / 180)
|
||||
);
|
||||
if (this.startSnap.settings.angle > 180 && this.startSnap.settings.angle <= 270) {
|
||||
x = remap(
|
||||
Math.abs(theta1),
|
||||
this.startSnap.radians() - 135 * (Math.PI / 180),
|
||||
this.startSnap.radians() - 225 * (Math.PI / 180)
|
||||
);
|
||||
}
|
||||
|
||||
angleRatio = Math.abs(angleRatio);
|
||||
if (angleRatio < 0.5) {
|
||||
angleRatio = 0.5 + (0.5 - angleRatio);
|
||||
}
|
||||
if (angleRatio > 1) {
|
||||
angleRatio = 1;
|
||||
}
|
||||
|
||||
let theta = theta1 + this.startSnap.radians();
|
||||
let a = Math.cos(theta);
|
||||
a = a * h;
|
||||
a = a * angleRatio;
|
||||
let dx = Math.cos(this.radians(this.startSnap.settings.angle)) * a;
|
||||
let dy = Math.sin(this.radians(this.startSnap.settings.angle)) * a;
|
||||
|
||||
if (this.endSnap) {
|
||||
}
|
||||
|
||||
// Bezier curve offset points
|
||||
/*context.beginPath();
|
||||
context.moveTo(x - offsetx, y - offsety);
|
||||
context.lineTo(x + dx - offsetx, y - dy - offsety);
|
||||
context.stroke();*/
|
||||
|
||||
let dx1 = x + dx * 0.8;
|
||||
let dy1 = y - dy * 0.8;
|
||||
|
||||
/*context.beginPath();
|
||||
context.arc(dx1 - offsetx, dy1 - offsety, 6, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(x + dx - offsetx, y - dy - offsety);
|
||||
context.lineTo(endx - offsetx, endy - offsety);
|
||||
context.stroke();*/
|
||||
|
||||
let dx2 = endx - (x + dx);
|
||||
dx2 = x + dx + dx2 * 0.2;
|
||||
let dy2 = endy - (y - dy);
|
||||
dy2 = y - dy + dy2 * 0.2;
|
||||
|
||||
/*context.beginPath();
|
||||
context.arc(0 - offsetx, 0 - offsety, 0, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.arc(dx2 - offsetx, dy2 - offsety, 6, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.arc(x + dx - offsetx, y - dy - offsety, 6, 0, 2 * Math.PI);
|
||||
context.stroke();*/
|
||||
|
||||
context.moveTo(x - offsetx, y - offsety);
|
||||
context.strokeStyle = "rgb(255, 255, 255)";
|
||||
context.bezierCurveTo(
|
||||
dx1 - offsetx,
|
||||
dy1 - offsety,
|
||||
dx2 - offsetx,
|
||||
dy2 - offsety,
|
||||
endx - offsetx,
|
||||
endy - offsety
|
||||
);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
public draw(context: CanvasRenderingContext2D, offsetX?: number, offsetY?: number) {
|
||||
let ox = offsetX ? offsetX : 0;
|
||||
let oy = offsetY ? offsetY : 0;
|
||||
|
||||
if (this.startSnap) {
|
||||
if (this.startSnap.settings.angle === 0) {
|
||||
this.startSnap.settings.angle = 360;
|
||||
}
|
||||
|
||||
if (this.circle.selected) {
|
||||
this.drawBezier(context, this.x(), this.y(), this.width(), this.height(), ox, oy);
|
||||
} else if (this.startSnap) {
|
||||
let radius = this.startSnap.height() / 2;
|
||||
let dy1 = Math.cos(this.startSnap.radians()) * radius;
|
||||
let dx1 = Math.sin(this.startSnap.radians()) * radius;
|
||||
if (this.endSnap) {
|
||||
let dy2 = Math.cos(this.endSnap.radians()) * radius;
|
||||
let dx2 = Math.sin(this.endSnap.radians()) * radius;
|
||||
this.drawBezier(
|
||||
context,
|
||||
this.x() - dx1,
|
||||
this.y() - dy1,
|
||||
this.width() - dx2,
|
||||
this.height() - dy2,
|
||||
ox,
|
||||
oy
|
||||
);
|
||||
this.drawBezier(
|
||||
context,
|
||||
this.x() + dx1,
|
||||
this.y() + dy1,
|
||||
this.width() + dx2,
|
||||
this.height() + dy2,
|
||||
ox,
|
||||
oy
|
||||
);
|
||||
} else {
|
||||
this.drawBezier(
|
||||
context,
|
||||
this.x() - dx1,
|
||||
this.y() - dy1,
|
||||
this.width(),
|
||||
this.height(),
|
||||
ox,
|
||||
oy
|
||||
);
|
||||
}
|
||||
/*console.log(dx1 + ", " + dy1);
|
||||
context.strokeStyle = "rgb(255, 255, 255)";
|
||||
context.beginPath();
|
||||
context.strokeStyle = "rgb(255, 255, 255)";
|
||||
context.arc(this.x() - dx1 - ox, this.y() - dy1 - oy, 6, 0, 2 * Math.PI);
|
||||
context.stroke();*/
|
||||
}
|
||||
}
|
||||
|
||||
/*context.strokeStyle = "rgb(255, 255, 255)";
|
||||
context.beginPath();
|
||||
context.strokeStyle = "rgb(255, 255, 255)";
|
||||
context.arc(this.x() - ox, this.y() - oy, 6, 0, 2 * Math.PI);
|
||||
context.stroke();*/
|
||||
|
||||
context.beginPath();
|
||||
context.arc(this.width() - ox, this.height() - oy, 6, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
public highlight(context: CanvasRenderingContext2D, ox: number, oy: number) {
|
||||
context.fillStyle = "rgb(255, 255, 255)";
|
||||
context.globalAlpha = 0.3;
|
||||
context.beginPath();
|
||||
context.arc(this.width() - ox, this.height() - oy, 6, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
|
||||
public clickCheck(x: number, y: number, extend?: number) {
|
||||
if (this.distance(x, this.endX(), y, this.endY()) <= this.circle.radius) {
|
||||
this.circle.selected = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getSnap(rect: Block, snapDistance: number = 6) {
|
||||
if (rect.x() === this.startSnap?.x() && rect.y() === this.startSnap?.y()) return undefined;
|
||||
if (
|
||||
Math.abs(this.endX() - rect.x()) < snapDistance &&
|
||||
Math.abs(this.endY() - rect.y()) < snapDistance
|
||||
) {
|
||||
this.settings.width = rect.x();
|
||||
this.settings.height = rect.y();
|
||||
this.endSnap = rect;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public drag = (x: number, y: number) => {
|
||||
if (this.circle.selected) {
|
||||
this.settings.width = x;
|
||||
this.settings.height = y;
|
||||
let dx = this.settings.width - this.settings.x;
|
||||
let dy = this.settings.height - this.settings.y;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
dy = dx;
|
||||
} else {
|
||||
dx = dy;
|
||||
}
|
||||
if (this.endSnap) {
|
||||
this.endSnap.settings.x = x;
|
||||
this.endSnap.settings.y = y;
|
||||
}
|
||||
this.settings.angle = Math.abs((Math.atan(dy / dx) * 180) / Math.PI);
|
||||
} /*else {
|
||||
let dx = this.settings.width - this.settings.x;
|
||||
let dy = this.settings.height - this.settings.y;
|
||||
this.settings.x = x;
|
||||
this.settings.y = y;
|
||||
this.settings.width = dx;
|
||||
this.settings.height = dy;
|
||||
}*/
|
||||
};
|
||||
|
||||
public x(): number {
|
||||
return this.settings.x;
|
||||
}
|
||||
|
||||
public y(): number {
|
||||
return this.settings.y;
|
||||
}
|
||||
|
||||
public endX(): number {
|
||||
return this.settings.width;
|
||||
}
|
||||
|
||||
public endY(): number {
|
||||
return this.settings.height;
|
||||
}
|
||||
|
||||
public width(): number {
|
||||
return this.settings.width;
|
||||
}
|
||||
|
||||
public height(): number {
|
||||
return this.settings.height;
|
||||
}
|
||||
|
||||
public getCtrl(): boolean {
|
||||
return this.ctrl;
|
||||
}
|
||||
|
||||
public getShift(): boolean {
|
||||
return this.shift;
|
||||
}
|
||||
|
||||
public setCtrl(ctrl: boolean) {
|
||||
this.ctrl = ctrl;
|
||||
}
|
||||
|
||||
public setShift(shift: boolean) {
|
||||
this.shift = shift;
|
||||
}
|
||||
|
||||
public setSelected(s: boolean) {
|
||||
this.selected = s;
|
||||
this.circle.selected = s;
|
||||
}
|
||||
}
|
||||
351
src/ventilation/drawable/VentCorner.ts
Normal file
351
src/ventilation/drawable/VentCorner.ts
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Direction, Placeable } from "./Placeable";
|
||||
import { SnapPoint } from "./geometry";
|
||||
|
||||
export class VentCorner implements Placeable {
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
direction = Direction.CLOCKWISE;
|
||||
angle: number = 0;
|
||||
ctrl: boolean = false;
|
||||
shift: boolean = false;
|
||||
selected: boolean = false;
|
||||
type: pond.PlaceableType = pond.PlaceableType.PLACEABLE_TYPE_SHAFT;
|
||||
subtype = pond.ShaftType.SHAFT_TYPE_CORNER;
|
||||
|
||||
// Unique to VentCorner
|
||||
private corner_radius: number = 128;
|
||||
private width: number = 64;
|
||||
private mouseOffsetX = 0;
|
||||
private mouseOffsetY = 0;
|
||||
private oldAngle = 0;
|
||||
private snapped = false;
|
||||
|
||||
public static create = (x?: number, y?: number, radius?: number, startAngle?: number) => {
|
||||
let v = new VentCorner();
|
||||
v.x = x ? x : 0;
|
||||
v.y = y ? y : 0;
|
||||
v.angle = startAngle ? startAngle : 0;
|
||||
v.oldAngle = startAngle ? startAngle : 0;
|
||||
v.corner_radius = radius ? radius : 128;
|
||||
return v;
|
||||
};
|
||||
|
||||
public static fromPond(shaft: pond.Placeable) {
|
||||
let v = new VentCorner();
|
||||
v.x = shaft.x;
|
||||
v.y = shaft.y;
|
||||
v.angle = shaft.angle;
|
||||
v.corner_radius = shaft.magnitude;
|
||||
v.direction = shaft.direction;
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public magnitude = (): number => {
|
||||
return this.radius();
|
||||
};
|
||||
|
||||
public radius = (): number => {
|
||||
return this.corner_radius;
|
||||
};
|
||||
|
||||
public clone = () => {
|
||||
let v = new VentCorner();
|
||||
v.x = this.x;
|
||||
v.y = this.y;
|
||||
v.angle = this.angle;
|
||||
v.radius = this.radius;
|
||||
return v;
|
||||
};
|
||||
|
||||
private distance(x: number, y: number, x2?: number, y2?: number) {
|
||||
let a = this.x - x;
|
||||
let b = this.y - y;
|
||||
if (x2) {
|
||||
a = x2 - x;
|
||||
}
|
||||
if (y2) {
|
||||
b = y2 - y;
|
||||
}
|
||||
return Math.sqrt(a * a + b * b);
|
||||
}
|
||||
|
||||
public clickCheck(x: number, y: number, scale = 1): boolean {
|
||||
// Check angle of click relative to the radius
|
||||
let radians = this.angle * (Math.PI / 180);
|
||||
|
||||
//scale it
|
||||
let radius = this.radius();
|
||||
let width = this.width;
|
||||
|
||||
let centerx = 0;
|
||||
let centery = 0;
|
||||
if (this.direction === Direction.CLOCKWISE) {
|
||||
centerx = this.x + Math.cos(radians - 0.5 * Math.PI) * radius;
|
||||
centery = this.y - Math.sin(radians - 0.5 * Math.PI) * radius;
|
||||
} else {
|
||||
centerx = this.x + Math.cos(radians - 1.5 * Math.PI) * radius;
|
||||
centery = this.y - Math.sin(radians - 1.5 * Math.PI) * radius;
|
||||
}
|
||||
|
||||
let { x: endx, y: endy } = this.getEndXAndY(scale);
|
||||
|
||||
// Check that distance from radial center is greater than
|
||||
// inner radius and less than outer radius
|
||||
let d = this.distance(centerx, centery, x, y);
|
||||
if (d > radius - width / 2 && d < radius + width / 2) {
|
||||
let dx = x - centerx;
|
||||
let dy = y - centery;
|
||||
let radians = Math.atan2(dy, dx);
|
||||
if (radians < 0) {
|
||||
radians = radians + 2 * Math.PI;
|
||||
}
|
||||
let ratio = (2 * (radians / (Math.PI / 180))) / 360;
|
||||
dx = this.x - centerx;
|
||||
dy = this.y - centery;
|
||||
radians = Math.atan2(dy, dx);
|
||||
if (radians < 0) {
|
||||
radians = radians + 2 * Math.PI;
|
||||
}
|
||||
let ratioLow = (2 * (radians / (Math.PI / 180))) / 360;
|
||||
dx = endx - centerx;
|
||||
dy = endy - centery;
|
||||
radians = Math.atan2(dy, dx);
|
||||
if (radians < 0) {
|
||||
radians = radians + 2 * Math.PI;
|
||||
}
|
||||
let ratioHigh = (2 * (radians / (Math.PI / 180))) / 360;
|
||||
if (this.direction === Direction.COUNTERCLOCKWISE) {
|
||||
if (ratioHigh > ratioLow) {
|
||||
ratioLow = ratioLow + 2;
|
||||
if (ratio < ratioHigh) ratio = ratio + 2;
|
||||
}
|
||||
if (ratio > ratioHigh && ratio < ratioLow) {
|
||||
this.mouseOffsetX = x - this.x;
|
||||
this.mouseOffsetY = y - this.y;
|
||||
this.selected = true;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ratioHigh < ratioLow) {
|
||||
ratioHigh = ratioHigh + 2;
|
||||
if (ratio < ratioLow) ratio = ratio + 2;
|
||||
}
|
||||
|
||||
// If the angle is within the start and end of the arc relativ eto the radial center
|
||||
if (ratio > ratioLow && ratio < ratioHigh) {
|
||||
this.mouseOffsetX = x - this.x;
|
||||
this.mouseOffsetY = y - this.y;
|
||||
this.selected = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.selected = false;
|
||||
if (this.snapped) {
|
||||
this.oldAngle = this.angle;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public draw(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale = 1
|
||||
): void {
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
let radius = this.radius() * scale;
|
||||
let width = this.width * scale;
|
||||
|
||||
let radians = this.angle * (Math.PI / 180);
|
||||
let ratio = (2 * this.angle) / 360;
|
||||
ratio = 2 - ratio;
|
||||
|
||||
let angleOffset = 0.5;
|
||||
let c = false;
|
||||
if (this.direction === Direction.COUNTERCLOCKWISE) {
|
||||
x = x - Math.cos(radians - 0.5 * Math.PI) * radius;
|
||||
y = y + Math.sin(radians - 0.5 * Math.PI) * radius;
|
||||
angleOffset = 1.5;
|
||||
c = true;
|
||||
} else {
|
||||
x = x + Math.cos(radians - 0.5 * Math.PI) * radius;
|
||||
y = y - Math.sin(radians - 0.5 * Math.PI) * radius;
|
||||
}
|
||||
|
||||
context.strokeStyle = "white";
|
||||
context.beginPath();
|
||||
context.arc(x, y, radius - width / 2, (ratio - angleOffset) * Math.PI, ratio * Math.PI, c);
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.arc(x, y, radius + width / 2, (ratio - angleOffset) * Math.PI, ratio * Math.PI, c);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
highlight(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale = 1
|
||||
): void {
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
let radians = this.angle * (Math.PI / 180);
|
||||
let ratio = (2 * this.angle) / 360;
|
||||
ratio = 2 - ratio;
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
let radius = this.radius() * scale;
|
||||
let width = this.width * scale;
|
||||
|
||||
context.strokeStyle = "yellow";
|
||||
context.beginPath();
|
||||
context.arc(x, y, 6, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
let angleOffset = 0.5;
|
||||
let c = false;
|
||||
if (this.direction === Direction.COUNTERCLOCKWISE) {
|
||||
x = x - Math.cos(radians - 0.5 * Math.PI) * radius;
|
||||
y = y + Math.sin(radians - 0.5 * Math.PI) * radius;
|
||||
angleOffset = 1.5;
|
||||
c = true;
|
||||
} else {
|
||||
x = x + Math.cos(radians - 0.5 * Math.PI) * radius;
|
||||
y = y - Math.sin(radians - 0.5 * Math.PI) * radius;
|
||||
}
|
||||
|
||||
//x = x + Math.cos(radians - 0.5 * Math.PI) * this.radius;
|
||||
//y = y - Math.sin(radians - 0.5 * Math.PI) * this.radius;
|
||||
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
x,
|
||||
y,
|
||||
radius - (width + 2) / 2,
|
||||
(ratio - angleOffset) * Math.PI,
|
||||
ratio * Math.PI,
|
||||
c
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
x,
|
||||
y,
|
||||
radius + (width + 2) / 2,
|
||||
(ratio - angleOffset) * Math.PI,
|
||||
ratio * Math.PI,
|
||||
c
|
||||
);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
drag(x: number, y: number, scale = 1): void {
|
||||
this.x = x - this.mouseOffsetX;
|
||||
this.y = y - this.mouseOffsetY;
|
||||
}
|
||||
|
||||
getEndXAndY(scale = 1): { x: number; y: number } {
|
||||
let ratio = (2 * this.angle) / 360;
|
||||
|
||||
//scale it
|
||||
let x = this.x;
|
||||
let y = this.y;
|
||||
let radius = this.radius();
|
||||
//let width = this.width*scale
|
||||
|
||||
if (this.direction === Direction.COUNTERCLOCKWISE) {
|
||||
x = x - Math.cos((ratio - 0.5) * Math.PI) * radius;
|
||||
y = y + Math.sin((ratio - 0.5) * Math.PI) * radius;
|
||||
x = x + Math.sin((ratio - 1.5) * Math.PI) * radius;
|
||||
y = y + Math.cos((ratio - 1.5) * Math.PI) * radius;
|
||||
return { x: x, y: y };
|
||||
}
|
||||
x = x + Math.cos((ratio - 0.5) * Math.PI) * radius;
|
||||
y = y - Math.sin((ratio - 0.5) * Math.PI) * radius;
|
||||
x = x - Math.sin((ratio - 0.5) * Math.PI) * radius;
|
||||
y = y - Math.cos((ratio - 0.5) * Math.PI) * radius;
|
||||
return { x: x, y: y };
|
||||
}
|
||||
|
||||
getSnapPoints() {
|
||||
let snaps: SnapPoint[] = [];
|
||||
|
||||
let { x: endx, y: endy } = this.getEndXAndY();
|
||||
snaps.push({
|
||||
x: endx,
|
||||
y: endy,
|
||||
angle: this.getEndAngle(),
|
||||
occupant: undefined,
|
||||
startPoint: false
|
||||
} as SnapPoint);
|
||||
|
||||
snaps.push({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
angle: this.angle + 180,
|
||||
occupant: undefined,
|
||||
startPoint: true
|
||||
} as SnapPoint);
|
||||
if (snaps[1].angle > 360) snaps[0].angle = snaps[0].angle - 360;
|
||||
|
||||
return snaps;
|
||||
}
|
||||
|
||||
snap(p: Placeable, d?: number): void {
|
||||
if (!d) d = 12;
|
||||
p.getSnapPoints().forEach(point => {
|
||||
if (!point.startPoint && this.distance(this.x, this.y, point.x, point.y) < d!) {
|
||||
this.x = point.x;
|
||||
this.y = point.y;
|
||||
this.angle = point.angle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getEndAngle(): number {
|
||||
if (this.direction === Direction.COUNTERCLOCKWISE) {
|
||||
return this.angle + 90;
|
||||
}
|
||||
let ang = this.angle - 90;
|
||||
return ang;
|
||||
}
|
||||
|
||||
setDirection(direction: number | Direction): void {
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
setAngle(angle: number, oldAngle?: number) {
|
||||
this.angle = angle;
|
||||
if (oldAngle) this.oldAngle = oldAngle;
|
||||
}
|
||||
}
|
||||
438
src/ventilation/drawable/VentShaft.ts
Normal file
438
src/ventilation/drawable/VentShaft.ts
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Placeable } from "./Placeable";
|
||||
import { Circle } from "./Circle";
|
||||
import { SnapPoint } from "./geometry";
|
||||
|
||||
export class VentShaft implements Placeable {
|
||||
public debug = false;
|
||||
public x = 0;
|
||||
public y = 0;
|
||||
public angle = 0;
|
||||
public ctrl = false;
|
||||
public shift = false;
|
||||
public selected = false;
|
||||
public type = pond.PlaceableType.PLACEABLE_TYPE_SHAFT;
|
||||
public direction = 0;
|
||||
public subtype = pond.ShaftType.SHAFT_TYPE_DEFAULT;
|
||||
|
||||
private length = 128;
|
||||
private diameter = 64;
|
||||
|
||||
private circle = Circle.create(0, 0, 0);
|
||||
private mouseOffsetX = 0;
|
||||
private mouseOffsetY = 0;
|
||||
private oldAngle = 0;
|
||||
private snapped = false;
|
||||
|
||||
public magnitude(): number {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public radius(): number {
|
||||
return this.diameter / 2;
|
||||
}
|
||||
|
||||
public static fromPond(shaft: pond.Placeable) {
|
||||
let v = new VentShaft();
|
||||
v.x = shaft.x;
|
||||
v.y = shaft.y;
|
||||
v.angle = shaft.angle;
|
||||
v.length = shaft.magnitude;
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static create(pb?: pond.Block, img?: HTMLImageElement): VentShaft {
|
||||
let v = new VentShaft();
|
||||
//v.settings = pond.BlockSettings.fromObject(cloneDeep(or(pb?.settings, {})));
|
||||
|
||||
// For some reason, negative x and y values don't clone correctly
|
||||
// Same goes for decimal values in angle
|
||||
if (pb?.settings) {
|
||||
v.x = pb.settings.x;
|
||||
v.y = pb.settings.y;
|
||||
v.angle = pb.settings.angle;
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public draw(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale?: number
|
||||
) {
|
||||
if (!scale) scale = 1;
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
let width = this.length;
|
||||
let height = this.diameter;
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
width = width * scale;
|
||||
height = height * scale;
|
||||
|
||||
let cos = Math.cos;
|
||||
let sin = Math.sin;
|
||||
|
||||
let ang = this.angle;
|
||||
let rad = ang * (Math.PI / 180);
|
||||
|
||||
y = y - cos(rad) * (height / 2);
|
||||
x = x - sin(rad) * (height / 2);
|
||||
|
||||
let x2 = x + cos(rad) * width;
|
||||
let y2 = y - sin(rad) * width;
|
||||
|
||||
if (this.debug) {
|
||||
context.beginPath();
|
||||
context.arc(x, y, 6, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = "rgb(0, 0, 255)";
|
||||
context.beginPath();
|
||||
context.arc(x2, y2, 6, 0, 2 * Math.PI);
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = "rgb(0, 255, 0)";
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
x + cos((ang - 90) * (Math.PI / 180)) * height,
|
||||
y - sin((ang - 90) * (Math.PI / 180)) * height,
|
||||
6,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = "rgb(255, 0, 0)";
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
x2 + cos((ang - 90) * (Math.PI / 180)) * height,
|
||||
y2 - sin((ang - 90) * (Math.PI / 180)) * height,
|
||||
6,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
context.stroke();
|
||||
}
|
||||
context.beginPath();
|
||||
context.moveTo(x, y);
|
||||
context.lineTo(x2, y2);
|
||||
context.closePath();
|
||||
context.moveTo(
|
||||
x + cos((ang - 90) * (Math.PI / 180)) * height,
|
||||
y - sin((ang - 90) * (Math.PI / 180)) * height
|
||||
);
|
||||
context.lineTo(
|
||||
x2 + cos((ang - 90) * (Math.PI / 180)) * height,
|
||||
y2 - sin((ang - 90) * (Math.PI / 180)) * height
|
||||
);
|
||||
context.closePath();
|
||||
context.strokeStyle = "rgba(255, 255, 255, 1)";
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
/*public static clone(p: Placeable): VentShaft {
|
||||
if (p) {
|
||||
let b = VentShaft.create(
|
||||
pond.Block.fromObject({
|
||||
settings: cloneDeep(other.settings),
|
||||
img: new Image()
|
||||
})
|
||||
);
|
||||
let v = new VentShaft()
|
||||
v.x = p.x;
|
||||
v.y = p.y;
|
||||
v.angle = p.angle;
|
||||
v.circle = Circle.create(0, 0, 0);
|
||||
v.ctrl = p.ctrl;
|
||||
v.shift = p.shift;
|
||||
v.diameter = p.diameter;
|
||||
v.length = p.length;
|
||||
if (b.img)
|
||||
switch (b.settings.type) {
|
||||
case pond.BlockType.BLOCK_TYPE_VENT:
|
||||
b.img.src = StandardDuctIcon;
|
||||
break;
|
||||
case pond.BlockType.BLOCK_TYPE_VENT_CORNER:
|
||||
b.img.src = ConnectionDuctIcon;
|
||||
break;
|
||||
default:
|
||||
b.img = undefined;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
return VentShaft.create();
|
||||
}*/
|
||||
|
||||
public getEndXAndY = () => {
|
||||
return { x: this.endX(), y: this.endY() };
|
||||
};
|
||||
|
||||
public clickCheck = (x: number, y: number, scale = 1) => {
|
||||
let x1 = this.x;
|
||||
let y1 = this.y;
|
||||
let width = this.length;
|
||||
let height = this.diameter;
|
||||
|
||||
let cos = Math.cos;
|
||||
let sin = Math.sin;
|
||||
|
||||
let ang = this.angle;
|
||||
let rad = ang * (Math.PI / 180);
|
||||
|
||||
let cx = x1 + Math.cos(rad) * width;
|
||||
let cy = y1 - Math.sin(rad) * width;
|
||||
this.circle.x = cx;
|
||||
this.circle.y = cy;
|
||||
this.circle.radius = height / 4;
|
||||
if (this.circle.clickCheck(x, y, scale)) return true;
|
||||
|
||||
y1 = y1 - cos(rad) * (height / 2);
|
||||
x1 = x1 - sin(rad) * (height / 2);
|
||||
|
||||
let x2 = x1 + cos(rad) * width;
|
||||
let y2 = y1 - sin(rad) * width;
|
||||
|
||||
let x3 = x1 + cos((ang - 90) * (Math.PI / 180)) * height;
|
||||
let y3 = y1 - sin((ang - 90) * (Math.PI / 180)) * height;
|
||||
|
||||
let x4 = x2 + cos((ang - 90) * (Math.PI / 180)) * height;
|
||||
let y4 = y2 - sin((ang - 90) * (Math.PI / 180)) * height;
|
||||
|
||||
let D1 = (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1);
|
||||
let D2 = (x4 - x2) * (y - y2) - (x - x2) * (y4 - y2);
|
||||
let D3 = (x3 - x4) * (y - y4) - (x - x4) * (y3 - y4);
|
||||
let D4 = (x1 - x3) * (y - y3) - (x - x3) * (y1 - y3);
|
||||
|
||||
if (D1 > 0 && D2 > 0 && D3 > 0 && D4 > 0) {
|
||||
this.selected = true;
|
||||
this.mouseOffsetX = x - this.x;
|
||||
this.mouseOffsetY = y - this.y;
|
||||
return true;
|
||||
}
|
||||
this.selected = false;
|
||||
if (this.snapped) {
|
||||
this.oldAngle = this.angle;
|
||||
this.snapped = false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
public highlight(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale?: number
|
||||
) {
|
||||
if (!scale) scale = 1;
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
let width = this.length;
|
||||
let height = this.diameter;
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
width = width * scale;
|
||||
height = height * scale;
|
||||
|
||||
let ang = this.angle;
|
||||
let rad = ang * (Math.PI / 180);
|
||||
|
||||
// Draw selection circle
|
||||
let cx = x + Math.cos(rad) * width;
|
||||
let cy = y - Math.sin(rad) * width;
|
||||
this.circle.x = cx;
|
||||
this.circle.y = cy;
|
||||
this.circle.radius = height / 4;
|
||||
this.circle.draw(context);
|
||||
|
||||
let x1 = x - Math.sin(rad) * ((height + 3) / 2);
|
||||
let y1 = y - Math.cos(rad) * ((height + 3) / 2);
|
||||
|
||||
context.strokeStyle = "rgba(247, 202, 24, 1)";
|
||||
|
||||
// Make the point of rotation the objects x and y before making its rotation
|
||||
context.translate(x1, y1);
|
||||
context.rotate(-rad);
|
||||
context.translate(-x1, -y1);
|
||||
|
||||
// Draw the rectangle and then rotate back to original
|
||||
context.strokeRect(x1, y1, width, height + 3);
|
||||
context.translate(x1, y1);
|
||||
context.rotate(rad);
|
||||
context.translate(-x1, -y1);
|
||||
}
|
||||
|
||||
public clone() {
|
||||
let v = new VentShaft();
|
||||
v.x = this.x;
|
||||
v.y = this.y;
|
||||
v.angle = this.angle;
|
||||
v.circle = this.circle;
|
||||
v.ctrl = this.ctrl;
|
||||
v.shift = this.shift;
|
||||
v.diameter = this.diameter;
|
||||
v.length = this.length;
|
||||
return v;
|
||||
}
|
||||
|
||||
public distance(x1: number, x2: number, y1: number, y2: number) {
|
||||
let a = x1 - x2;
|
||||
let b = y1 - y2;
|
||||
return Math.sqrt(a * a + b * b);
|
||||
}
|
||||
|
||||
public getSnapPoints() {
|
||||
let snaps: SnapPoint[] = [];
|
||||
snaps.push({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
angle: this.angle + 180,
|
||||
occupant: undefined,
|
||||
startPoint: true
|
||||
});
|
||||
if (snaps[0].angle > 360) snaps[0].angle = snaps[0].angle - 360;
|
||||
|
||||
let { x: endx, y: endy } = this.getEndXAndY();
|
||||
snaps.push({
|
||||
x: endx,
|
||||
y: endy,
|
||||
angle: this.angle,
|
||||
occupant: undefined,
|
||||
startPoint: false
|
||||
});
|
||||
return snaps;
|
||||
}
|
||||
|
||||
public snap = (p: Placeable, snapDistance: number): void => {
|
||||
snapDistance = snapDistance ? snapDistance : 12;
|
||||
|
||||
for (let i = 0; i < p.getSnapPoints().length; i++) {
|
||||
let point = p.getSnapPoints()[i];
|
||||
if (point.startPoint === false) {
|
||||
let d = this.distance(this.x, point.x, this.y, point.y);
|
||||
if (d < snapDistance) {
|
||||
this.x = point.x;
|
||||
this.y = point.y;
|
||||
this.angle = point.angle;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private calcAngle = (x: number, y: number) => {
|
||||
var dx = this.x - x;
|
||||
var dy = this.y - y;
|
||||
var theta = Math.atan2(dy, -dx); // [0, Ⲡ] then [-Ⲡ, 0]; clockwise; 0° = east
|
||||
theta *= 180 / Math.PI; // [0, 180] then [-180, 0]; clockwise; 0° = east
|
||||
if (theta < 0) theta += 360; // [0, 360]; clockwise; 0° = east
|
||||
return theta;
|
||||
};
|
||||
|
||||
public drag = (x: number, y: number, scale = 1) => {
|
||||
if (this.circle.selected) {
|
||||
if (this.shift) {
|
||||
let dx = x - this.x;
|
||||
let dy = y - this.y;
|
||||
this.length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (!this.ctrl) this.length = Math.round(this.length / 16) * 16;
|
||||
|
||||
// Rotate 180 instead of having a negative length
|
||||
if (dx < 0) {
|
||||
if (dy > 0) {
|
||||
if (!(this.angle > 180 && this.angle < 270)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
} else if (!(this.angle > 90 && this.angle < 270)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
} else {
|
||||
if (dy > 0) {
|
||||
if (!(this.angle < 360 && this.angle > 180)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
} else if (!(this.angle > 0 && this.angle < 90)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.angle = this.calcAngle(x, y);
|
||||
let dx = x - this.x;
|
||||
let dy = y - this.y;
|
||||
this.length = Math.sqrt(dx * dx + dy * dy);
|
||||
if (!this.ctrl) {
|
||||
this.angle = Math.round(this.angle / 11.25) * 11.25;
|
||||
this.length = Math.round(this.length / 16) * 16;
|
||||
}
|
||||
}
|
||||
this.oldAngle = this.angle;
|
||||
} else {
|
||||
this.x = x - this.mouseOffsetX;
|
||||
this.y = y - this.mouseOffsetY;
|
||||
}
|
||||
};
|
||||
|
||||
public endX(): number {
|
||||
let rad = this.angle * (Math.PI / 180);
|
||||
return this.x + this.length * Math.cos(rad);
|
||||
}
|
||||
|
||||
public endY(): number {
|
||||
let rad = this.angle * (Math.PI / 180);
|
||||
return this.y - this.length * Math.sin(rad);
|
||||
}
|
||||
|
||||
public getEndAngle(): number {
|
||||
return this.angle;
|
||||
}
|
||||
|
||||
setDirection(direction: number): void {}
|
||||
|
||||
setAngle(angle: number, oldAngle?: number) {
|
||||
this.angle = angle;
|
||||
if (oldAngle) this.oldAngle = oldAngle;
|
||||
}
|
||||
}
|
||||
483
src/ventilation/drawable/VentTBreak.ts
Normal file
483
src/ventilation/drawable/VentTBreak.ts
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { Placeable } from "./Placeable";
|
||||
import { Circle } from "./Circle";
|
||||
import { SnapPoint, getRectangularPoints, isPointInRectangle } from "./geometry";
|
||||
|
||||
export class VentTBreak implements Placeable {
|
||||
public debug = false;
|
||||
public x = 0;
|
||||
public y = 0;
|
||||
public angle = 0;
|
||||
public ctrl = false;
|
||||
public shift = false;
|
||||
public selected = false;
|
||||
public type = pond.PlaceableType.PLACEABLE_TYPE_SHAFT;
|
||||
public direction = 0;
|
||||
public subtype = pond.ShaftType.SHAFT_TYPE_T_BREAK;
|
||||
|
||||
private length = 192;
|
||||
private diameter = 64;
|
||||
|
||||
private circle = Circle.create(0, 0, 0);
|
||||
private break_circle = Circle.create(0, 0, 0);
|
||||
private mouseOffsetX = 0;
|
||||
private mouseOffsetY = 0;
|
||||
private oldAngle = 0;
|
||||
private snapped = false;
|
||||
|
||||
private break_length = 64;
|
||||
|
||||
public magnitude(): number {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public radius(): number {
|
||||
return this.break_length;
|
||||
}
|
||||
|
||||
public static fromPond(shaft: pond.Placeable) {
|
||||
let v = new VentTBreak();
|
||||
v.x = shaft.x;
|
||||
v.y = shaft.y;
|
||||
v.angle = shaft.angle;
|
||||
v.length = shaft.magnitude;
|
||||
v.direction = shaft.direction;
|
||||
v.break_circle.selected = false;
|
||||
v.break_length = shaft.radius;
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public static create(pb?: pond.Block, img?: HTMLImageElement): VentTBreak {
|
||||
let v = new VentTBreak();
|
||||
//v.settings = pond.BlockSettings.fromObject(cloneDeep(or(pb?.settings, {})));
|
||||
|
||||
// For some reason, negative x and y values don't clone correctly
|
||||
// Same goes for decimal values in angle
|
||||
if (pb?.settings) {
|
||||
v.x = pb.settings.x;
|
||||
v.y = pb.settings.y;
|
||||
v.angle = pb.settings.angle;
|
||||
}
|
||||
|
||||
document.body.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = true;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = true;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener("keyup", function(event) {
|
||||
if (event.key === "Control") {
|
||||
v.ctrl = false;
|
||||
} else if (event.key === "Shift") {
|
||||
v.shift = false;
|
||||
}
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public draw(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale?: number
|
||||
) {
|
||||
if (!scale) scale = 1;
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
let width = this.length;
|
||||
let height = this.diameter;
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
width = width * scale;
|
||||
height = height * scale;
|
||||
let break_length = this.break_length * scale;
|
||||
|
||||
let { x1, y1, x2, y2, x3, y3, x4, y4 } = getRectangularPoints(x, y, width, height, this.angle);
|
||||
|
||||
context.beginPath();
|
||||
|
||||
let { x: bx, y: by } = this.getBreakOrigin(scale, offsetX, offsetY);
|
||||
//context.arc(bx, by, 6, 0, Math.PI * 360)
|
||||
|
||||
let break_angle = this.angle + 90;
|
||||
if (this.direction === 1) break_angle = this.angle - 90;
|
||||
let { x1: x5, y1: y5, x2: x6, y2: y6, x3: x7, y3: y7, x4: x8, y4: y8 } = getRectangularPoints(
|
||||
bx,
|
||||
by,
|
||||
break_length,
|
||||
height,
|
||||
break_angle
|
||||
);
|
||||
|
||||
if (this.direction === 0) {
|
||||
context.moveTo(x1, y1);
|
||||
context.lineTo(x2, y2);
|
||||
context.moveTo(x4, y4);
|
||||
context.lineTo(x8, y8);
|
||||
context.lineTo(x7, y7);
|
||||
context.moveTo(x6, y6);
|
||||
context.lineTo(x5, y5);
|
||||
context.lineTo(x3, y3);
|
||||
} else if (this.direction === 1) {
|
||||
context.moveTo(x3, y3);
|
||||
context.lineTo(x4, y4);
|
||||
context.moveTo(x1, y1);
|
||||
context.lineTo(x5, y5);
|
||||
context.lineTo(x6, y6);
|
||||
context.moveTo(x7, y7);
|
||||
context.lineTo(x8, y8);
|
||||
context.lineTo(x2, y2);
|
||||
}
|
||||
|
||||
context.strokeStyle = "rgba(255, 255, 255, 1)";
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
public getEndXAndY = (scale = 1, offsetX = 0, offsetY = 0) => {
|
||||
let x = this.endX(this.x, this.angle, this.length);
|
||||
let y = this.endY(this.y, this.angle, this.length);
|
||||
return { x: (x - offsetX) * scale, y: (y - offsetY) * scale };
|
||||
};
|
||||
|
||||
public clickCheck = (x: number, y: number, scale = 1) => {
|
||||
let width = this.length;
|
||||
let height = this.diameter;
|
||||
|
||||
// Check end circle
|
||||
({ x: this.circle.x, y: this.circle.y } = this.getEndXAndY());
|
||||
this.circle.radius = height / 4;
|
||||
if (this.circle.clickCheck(x, y, scale)) {
|
||||
this.break_circle.selected = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check break end circle
|
||||
({ x: this.break_circle.x, y: this.break_circle.y } = this.getBreakEnd());
|
||||
this.break_circle.radius = height / 4;
|
||||
if (this.break_circle.clickCheck(x, y, scale)) {
|
||||
this.circle.selected = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check main rectangle
|
||||
let p = getRectangularPoints(this.x, this.y, width, height, this.angle);
|
||||
if (isPointInRectangle(x, y, p.x1, p.y1, p.x2, p.y2, p.x3, p.y3, p.x4, p.y4)) {
|
||||
this.selected = true;
|
||||
this.mouseOffsetX = x - this.x;
|
||||
this.mouseOffsetY = y - this.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check extension triangle
|
||||
let { x: bx, y: by } = this.getBreakOrigin();
|
||||
//bx = bx * scale
|
||||
//by = by * scale
|
||||
let break_angle = this.angle + 90;
|
||||
if (this.direction === 1) break_angle = this.angle - 90;
|
||||
p = getRectangularPoints(bx, by, this.break_length, height, break_angle);
|
||||
if (isPointInRectangle(x, y, p.x1, p.y1, p.x2, p.y2, p.x3, p.y3, p.x4, p.y4)) {
|
||||
this.selected = true;
|
||||
this.mouseOffsetX = x - this.x;
|
||||
this.mouseOffsetY = y - this.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
this.selected = false;
|
||||
if (this.snapped) {
|
||||
this.oldAngle = this.angle;
|
||||
this.snapped = false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
public highlight(
|
||||
context: CanvasRenderingContext2D,
|
||||
offsetX?: number,
|
||||
offsetY?: number,
|
||||
scale?: number
|
||||
) {
|
||||
if (!scale) scale = 1;
|
||||
let x = this.x - (offsetX ? offsetX : 0);
|
||||
let y = this.y - (offsetY ? offsetY : 0);
|
||||
let width = this.length;
|
||||
let height = this.diameter;
|
||||
|
||||
//scale it
|
||||
x = x * scale;
|
||||
y = y * scale;
|
||||
width = width * scale;
|
||||
height = (height + 3) * scale;
|
||||
let break_length = this.break_length * scale;
|
||||
|
||||
let { x1, y1, x2, y2, x3, y3, x4, y4 } = getRectangularPoints(x, y, width, height, this.angle);
|
||||
|
||||
context.beginPath();
|
||||
|
||||
let { x: bx, y: by } = this.getBreakOrigin(scale, offsetX, offsetY);
|
||||
//context.arc(bx, by, 6, 0, Math.PI * 360)
|
||||
|
||||
let break_angle = this.angle + 90;
|
||||
if (this.direction === 1) break_angle = this.angle - 90;
|
||||
let { x1: x5, y1: y5, x2: x6, y2: y6, x3: x7, y3: y7, x4: x8, y4: y8 } = getRectangularPoints(
|
||||
bx,
|
||||
by,
|
||||
break_length,
|
||||
height,
|
||||
break_angle
|
||||
);
|
||||
|
||||
if (this.direction === 0) {
|
||||
context.lineTo(x1, y1);
|
||||
context.lineTo(x2, y2);
|
||||
context.moveTo(x4, y4);
|
||||
context.lineTo(x8, y8);
|
||||
context.lineTo(x7, y7);
|
||||
context.lineTo(x6, y6);
|
||||
context.lineTo(x5, y5);
|
||||
context.lineTo(x3, y3);
|
||||
context.moveTo(x1, y1);
|
||||
context.lineTo(x2, y2);
|
||||
context.lineTo(x3, y3);
|
||||
context.moveTo(x1, y1);
|
||||
context.lineTo(x4, y4);
|
||||
} else if (this.direction === 1) {
|
||||
context.lineTo(x3, y3);
|
||||
context.lineTo(x4, y4);
|
||||
context.lineTo(x1, y1);
|
||||
context.lineTo(x5, y5);
|
||||
context.lineTo(x6, y6);
|
||||
context.lineTo(x7, y7);
|
||||
context.lineTo(x8, y8);
|
||||
context.lineTo(x2, y2);
|
||||
context.lineTo(x3, y3);
|
||||
}
|
||||
|
||||
context.strokeStyle = "rgba(247, 202, 24, 1)";
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = "rgba(225, 225, 225, 1)";
|
||||
let { x: endx, y: endy } = this.getEndXAndY(scale, offsetX, offsetY);
|
||||
this.circle.x = endx;
|
||||
this.circle.y = endy;
|
||||
this.circle.radius = height / 4;
|
||||
this.circle.draw(context);
|
||||
|
||||
let { x: cx, y: cy } = this.getBreakEnd(scale, offsetX, offsetY);
|
||||
this.break_circle.x = cx;
|
||||
this.break_circle.y = cy;
|
||||
this.break_circle.radius = height / 4;
|
||||
this.break_circle.draw(context);
|
||||
}
|
||||
|
||||
public clone() {
|
||||
let v = new VentTBreak();
|
||||
v.x = this.x;
|
||||
v.y = this.y;
|
||||
v.angle = this.angle;
|
||||
v.circle = this.circle;
|
||||
v.break_circle = this.break_circle;
|
||||
v.break_length = this.break_length;
|
||||
v.ctrl = this.ctrl;
|
||||
v.shift = this.shift;
|
||||
v.diameter = this.diameter;
|
||||
v.length = this.length;
|
||||
return v;
|
||||
}
|
||||
|
||||
public distance(x1: number, x2: number, y1: number, y2: number) {
|
||||
let a = x1 - x2;
|
||||
let b = y1 - y2;
|
||||
return Math.sqrt(a * a + b * b);
|
||||
}
|
||||
|
||||
public getSnapPoints = () => {
|
||||
let snaps: SnapPoint[] = [];
|
||||
snaps.push({
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
angle: this.angle + 180,
|
||||
startPoint: true,
|
||||
occupant: undefined
|
||||
});
|
||||
if (snaps[0].angle > 360) snaps[0].angle = snaps[0].angle - 360;
|
||||
|
||||
let { x: endx, y: endy } = this.getEndXAndY();
|
||||
snaps.push({
|
||||
x: endx,
|
||||
y: endy,
|
||||
angle: this.getEndAngle(),
|
||||
occupant: undefined,
|
||||
startPoint: false
|
||||
});
|
||||
|
||||
let { x: bx, y: by } = this.getBreakEnd();
|
||||
let ang = this.angle + 90;
|
||||
if (this.direction === 1) ang = this.angle - 90;
|
||||
snaps.push({
|
||||
x: bx,
|
||||
y: by,
|
||||
angle: ang,
|
||||
occupant: undefined,
|
||||
startPoint: false
|
||||
});
|
||||
if (snaps[2].angle < 0) snaps[2].angle = snaps[2].angle + 360;
|
||||
|
||||
return snaps;
|
||||
};
|
||||
|
||||
public snap = (p: Placeable, snapDistance: number): void => {
|
||||
snapDistance = snapDistance ? snapDistance : 12;
|
||||
p.getSnapPoints().forEach(point => {
|
||||
if (!point.startPoint && this.distance(this.x, point.x, this.y, point.y) < snapDistance) {
|
||||
this.x = point.x;
|
||||
this.y = point.y;
|
||||
this.angle = point.angle;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private calcAngle = (x: number, y: number) => {
|
||||
var dx = this.x - x;
|
||||
var dy = this.y - y;
|
||||
var theta = Math.atan2(dy, -dx); // [0, Ⲡ] then [-Ⲡ, 0]; clockwise; 0° = east
|
||||
theta *= 180 / Math.PI; // [0, 180] then [-180, 0]; clockwise; 0° = east
|
||||
if (theta < 0) theta += 360; // [0, 360]; clockwise; 0° = east
|
||||
return theta;
|
||||
};
|
||||
|
||||
public drag = (x: number, y: number, scale = 1) => {
|
||||
if (this.circle.selected) {
|
||||
if (this.shift) {
|
||||
let dx = x - this.x;
|
||||
let dy = y - this.y;
|
||||
this.length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (!this.ctrl) this.length = Math.round(this.length / 16) * 16;
|
||||
|
||||
// Rotate 180 instead of having a negative length
|
||||
if (dx < 0) {
|
||||
if (dy > 0) {
|
||||
if (!(this.angle > 180 && this.angle < 270)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
} else if (!(this.angle > 90 && this.angle < 270)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
} else {
|
||||
if (dy > 0) {
|
||||
if (!(this.angle < 360 && this.angle > 180)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
} else if (!(this.angle > 0 && this.angle < 90)) {
|
||||
this.angle += 180;
|
||||
if (this.angle > 360) this.angle -= 360;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.angle = this.calcAngle(x, y);
|
||||
let dx = x - this.x;
|
||||
let dy = y - this.y;
|
||||
this.length = Math.sqrt(dx * dx + dy * dy);
|
||||
if (!this.ctrl) {
|
||||
this.angle = Math.round(this.angle / 11.25) * 11.25;
|
||||
this.length = Math.round(this.length / 16) * 16;
|
||||
}
|
||||
}
|
||||
this.oldAngle = this.angle;
|
||||
} else if (this.break_circle.selected) {
|
||||
let ang = this.angle;
|
||||
|
||||
let { x: ox, y: oy } = this.getBreakOrigin();
|
||||
|
||||
let a = x - ox;
|
||||
let b = y - oy;
|
||||
let h = Math.sqrt(a * a + b * b);
|
||||
|
||||
this.break_length = h;
|
||||
if (!this.ctrl) this.break_length = Math.round(this.break_length / 16) * 16;
|
||||
|
||||
let ang2 = Math.atan2(y - oy, x - ox) * (180 / Math.PI);
|
||||
ang2 = (ang2 + 360) % 360;
|
||||
ang2 = (360 - ang2) % 360;
|
||||
let diff = (ang - ang2 + 360) % 360;
|
||||
|
||||
// Set break length to 0 if you're not extending outward
|
||||
if ((this.direction === 0 && diff < 180) || (this.direction === 1 && diff > 180)) {
|
||||
this.break_length = 0;
|
||||
}
|
||||
} else {
|
||||
this.x = x - this.mouseOffsetX;
|
||||
this.y = y - this.mouseOffsetY;
|
||||
}
|
||||
};
|
||||
|
||||
public endX(x = this.x, angle = this.angle, length = this.length): number {
|
||||
let rad = angle * (Math.PI / 180);
|
||||
return x + length * Math.cos(rad);
|
||||
}
|
||||
|
||||
public endY(y = this.y, angle = this.angle, length = this.length): number {
|
||||
let rad = angle * (Math.PI / 180);
|
||||
return y - length * Math.sin(rad);
|
||||
}
|
||||
|
||||
private getBreakEnd(scale = 1, offsetX = 0, offsetY = 0) {
|
||||
let rad = this.angle * (Math.PI / 180);
|
||||
let rad2 = (this.angle + 90) * (Math.PI / 180);
|
||||
if (this.direction === 1) rad2 = (this.angle - 90) * (Math.PI / 180);
|
||||
let cx = this.x + (Math.cos(rad) * this.length) / 2;
|
||||
let cy = this.y - (Math.sin(rad) * this.length) / 2;
|
||||
cx = cx + Math.cos(rad2) * (this.break_length + this.diameter / 2);
|
||||
cy = cy - Math.sin(rad2) * (this.break_length + this.diameter / 2);
|
||||
return { x: (cx - offsetX) * scale, y: (cy - offsetY) * scale };
|
||||
}
|
||||
|
||||
private getBreakOrigin(scale = 1, offsetX = 0, offsetY = 0) {
|
||||
let rad = this.angle * (Math.PI / 180);
|
||||
let rad2 = (this.angle + 90) * (Math.PI / 180);
|
||||
if (this.direction === 1) rad2 = (this.angle - 90) * (Math.PI / 180);
|
||||
let cx = this.x + (Math.cos(rad) * this.length) / 2;
|
||||
let cy = this.y - (Math.sin(rad) * this.length) / 2;
|
||||
cx = cx + Math.cos(rad2) * (this.diameter / 2);
|
||||
cy = cy - Math.sin(rad2) * (this.diameter / 2);
|
||||
return { x: (cx - offsetX) * scale, y: (cy - offsetY) * scale };
|
||||
}
|
||||
|
||||
public getEndAngle(): number {
|
||||
return this.angle;
|
||||
}
|
||||
|
||||
setDirection(direction: number): void {
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
setAngle(angle: number, oldAngle?: number) {
|
||||
this.angle = angle;
|
||||
if (oldAngle) this.oldAngle = oldAngle;
|
||||
}
|
||||
}
|
||||
63
src/ventilation/drawable/geometry.ts
Normal file
63
src/ventilation/drawable/geometry.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Placeable } from "./Placeable";
|
||||
|
||||
export function isPointInRectangle(
|
||||
x: number,
|
||||
y: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
x3: number,
|
||||
y3: number,
|
||||
x4: number,
|
||||
y4: number
|
||||
) {
|
||||
const minX = Math.min(x1, x2, x3, x4);
|
||||
const maxX = Math.max(x1, x2, x3, x4);
|
||||
const minY = Math.min(y1, y2, y3, y4);
|
||||
const maxY = Math.max(y1, y2, y3, y4);
|
||||
|
||||
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
|
||||
const d1 = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1);
|
||||
const d2 = (x - x2) * (y3 - y2) - (y - y2) * (x3 - x2);
|
||||
const d3 = (x - x3) * (y4 - y3) - (y - y3) * (x4 - x3);
|
||||
const d4 = (x - x4) * (y1 - y4) - (y - y4) * (x1 - x4);
|
||||
|
||||
return (d1 >= 0 && d2 >= 0 && d3 >= 0 && d4 >= 0) || (d1 <= 0 && d2 <= 0 && d3 <= 0 && d4 <= 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getRectangularPoints(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
angle: number
|
||||
) {
|
||||
let x1 = x + (Math.cos((angle - 90) * (Math.PI / 180)) * height) / 2;
|
||||
let y1 = y - (Math.sin((angle - 90) * (Math.PI / 180)) * height) / 2;
|
||||
let x2 = x1 + Math.cos(angle * (Math.PI / 180)) * width;
|
||||
let y2 = y1 - Math.sin(angle * (Math.PI / 180)) * width;
|
||||
let x3 = x2 + Math.cos((angle + 90) * (Math.PI / 180)) * height;
|
||||
let y3 = y2 - Math.sin((angle + 90) * (Math.PI / 180)) * height;
|
||||
let x4 = x3 + Math.cos((angle - 180) * (Math.PI / 180)) * width;
|
||||
let y4 = y3 - Math.sin((angle - 180) * (Math.PI / 180)) * width;
|
||||
/*console.log((angle - 90) * (Math.PI / 180))
|
||||
console.log((angle) * (Math.PI / 180))
|
||||
console.log((angle + 90) * (Math.PI / 180))
|
||||
console.log("(" + x1 + ", " + y1 + ")")
|
||||
console.log("(" + x2 + ", " + y2 + ")")
|
||||
console.log("(" + x3 + ", " + y3 + ")")
|
||||
console.log("(" + x4 + ", " + y4 + ")")*/
|
||||
return { x1, y1, x2, y2, x3, y3, x4, y4 };
|
||||
}
|
||||
|
||||
export interface SnapPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
angle: number;
|
||||
startPoint: boolean;
|
||||
occupant: Placeable | undefined;
|
||||
}
|
||||
364
src/ventilation/drawers/ComponentCards.tsx
Normal file
364
src/ventilation/drawers/ComponentCards.tsx
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
import { useComponentAPI, useMineAPI, useThemeType } from "hooks";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Component } from "models";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
createStyles,
|
||||
Grid,
|
||||
makeStyles,
|
||||
Theme,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@material-ui/core";
|
||||
import CardHeader from "@material-ui/core/CardHeader";
|
||||
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
||||
import EventBlocker from "common/EventBlocker";
|
||||
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import { useGlobalState } from "providers";
|
||||
import DelayedTextField from "common/DelayedTextInput";
|
||||
import { useHistory } from "react-router";
|
||||
|
||||
interface Props {
|
||||
deviceId: number;
|
||||
showMobile?: boolean;
|
||||
mineComponents: Component[];
|
||||
setMineComponents: React.Dispatch<React.SetStateAction<Component[]>>;
|
||||
mineComponentPreferences: Map<string, pond.MineComponentPreferences>;
|
||||
setMineComponentPreferences: React.Dispatch<
|
||||
React.SetStateAction<Map<string, pond.MineComponentPreferences>>
|
||||
>;
|
||||
keys?: string[];
|
||||
types?: string[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
avatarIcon: {
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3)
|
||||
},
|
||||
card: {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "visible",
|
||||
width: "100%"
|
||||
},
|
||||
cardContent: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(1),
|
||||
paddingTop: 0
|
||||
},
|
||||
onHover: {
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer"
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export default function ComponentCards(props: Props) {
|
||||
const {
|
||||
deviceId,
|
||||
showMobile,
|
||||
setMineComponents,
|
||||
mineComponents,
|
||||
mineComponentPreferences,
|
||||
setMineComponentPreferences
|
||||
} = props;
|
||||
const componentAPI = useComponentAPI();
|
||||
const mineAPI = useMineAPI();
|
||||
const [cards, setCards] = useState<JSX.Element[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [firstLoad, setFirstLoad] = useState(true);
|
||||
const themeType = useThemeType();
|
||||
const classes = useStyles();
|
||||
const [{ user }] = useGlobalState();
|
||||
const theme = useTheme();
|
||||
const history = useHistory();
|
||||
|
||||
const keys = props.keys ? props.keys : getContextKeys();
|
||||
const types = props.types ? props.types : getContextTypes();
|
||||
|
||||
const removeComponent = (device: string, component: Component) => {
|
||||
let mine = "";
|
||||
types.forEach((type, index) => {
|
||||
if (type === "mine") mine = keys[index];
|
||||
});
|
||||
mineAPI.addComponent(mine, device, component.key(), [pond.Permission.PERMISSION_INVALID]);
|
||||
};
|
||||
|
||||
const addComponent = (device: string, component: Component) => {
|
||||
let mine = "";
|
||||
types.forEach((type, index) => {
|
||||
if (type === "mine") mine = keys[index];
|
||||
});
|
||||
mineAPI
|
||||
.addComponent(mine, deviceId.toString(), component.key(), [
|
||||
pond.Permission.PERMISSION_READ,
|
||||
pond.Permission.PERMISSION_SHARE,
|
||||
pond.Permission.PERMISSION_WRITE,
|
||||
pond.Permission.PERMISSION_USERS
|
||||
])
|
||||
.then(() => {
|
||||
let prefs = getPreferences(component);
|
||||
mineComponentPreferences.set(component.key(), prefs);
|
||||
setMineComponentPreferences(new Map(mineComponentPreferences));
|
||||
let c = mineComponents;
|
||||
c.push(component);
|
||||
setMineComponents([...c]);
|
||||
});
|
||||
};
|
||||
|
||||
const getPreferences = (component: Component) => {
|
||||
let exists = mineComponents.find(comp => comp.key() === component.key());
|
||||
let prefs = mineComponentPreferences.get(component.key());
|
||||
if ((!prefs?.sensors || prefs.sensors.length < 1) && exists) {
|
||||
prefs = pond.MineComponentPreferences.create();
|
||||
prefs.sensors = [];
|
||||
if (component.lastMeasurement.length > 0) {
|
||||
if (component.lastMeasurement[0].values)
|
||||
component.lastMeasurement[0].values[0].values.forEach((v, i) => {
|
||||
let sensor = pond.MineSensorPreferences.create();
|
||||
sensor.enabled = false;
|
||||
sensor.x = 20;
|
||||
sensor.y = 20;
|
||||
sensor.nickname = component.name();
|
||||
if (component.lastMeasurement[0].values[0].values.length > 1) {
|
||||
sensor.nickname = "Sensor " + i;
|
||||
}
|
||||
prefs!.sensors.push(sensor);
|
||||
});
|
||||
mineComponentPreferences.set(component.key(), prefs);
|
||||
setMineComponentPreferences(new Map(mineComponentPreferences));
|
||||
}
|
||||
}
|
||||
return prefs!;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let k = [...keys];
|
||||
k.push(deviceId.toString());
|
||||
let t = [...types];
|
||||
t.push("device");
|
||||
setLoading(true);
|
||||
componentAPI
|
||||
.list(deviceId, false, k, t, true)
|
||||
.then(resp => {
|
||||
if (loading) return;
|
||||
if (!resp.data.components) return;
|
||||
setLoading(true);
|
||||
let components: Component[] = [];
|
||||
let cards: JSX.Element[] = [];
|
||||
|
||||
resp.data.components.forEach((comp, index) => {
|
||||
if (!comp) return;
|
||||
let component = Component.create(comp);
|
||||
|
||||
// Ignore component without data
|
||||
if (!component.lastMeasurement) return;
|
||||
let measurement: UnitMeasurement[] = [];
|
||||
if (component.lastMeasurement.forEach) {
|
||||
component.lastMeasurement.forEach(m => {
|
||||
measurement.push(UnitMeasurement.any(m, user));
|
||||
});
|
||||
}
|
||||
components.push(component);
|
||||
const exists = mineComponents.find(comp => comp.key() === component.key());
|
||||
const componentIcon = GetComponentIcon(component.type(), component.subType(), themeType);
|
||||
let prefs = getPreferences(component);
|
||||
cards.push(
|
||||
<div key={cards.length + 1} style={{ padding: 6 }}>
|
||||
<Card raised className={classes.card}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
componentIcon ? (
|
||||
<Avatar
|
||||
variant="square"
|
||||
src={componentIcon}
|
||||
className={classes.avatarIcon}
|
||||
alt={component.name()}
|
||||
/>
|
||||
) : (
|
||||
<Box className={classes.avatarIcon} />
|
||||
)
|
||||
}
|
||||
title={
|
||||
component.name() ? (
|
||||
<Typography
|
||||
className={classes.onHover}
|
||||
onClick={() => {
|
||||
history.replace(
|
||||
history.location.pathname +
|
||||
"/devices/" +
|
||||
deviceId +
|
||||
"/components/" +
|
||||
component.key()
|
||||
);
|
||||
}}>
|
||||
{component.name()}
|
||||
</Typography>
|
||||
) : (
|
||||
"No Name"
|
||||
)
|
||||
}
|
||||
subheader={
|
||||
<UnitMeasurementSummary
|
||||
component={component}
|
||||
reading={UnitMeasurement.convertLastMeasurement(measurement)}
|
||||
dense
|
||||
/>
|
||||
}
|
||||
action={
|
||||
<EventBlocker>
|
||||
<Checkbox
|
||||
checked={!!exists}
|
||||
disabled={loading}
|
||||
onChange={() => {
|
||||
if (!exists) {
|
||||
addComponent(deviceId.toString(), component);
|
||||
} else {
|
||||
removeComponent(deviceId.toString(), component);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EventBlocker>
|
||||
}
|
||||
/>
|
||||
{exists && (
|
||||
<CardContent className={classes.cardContent}>
|
||||
<EventBlocker>
|
||||
{prefs?.sensors.map((sensor, i) => {
|
||||
let reading = UnitMeasurement.convertLastMeasurementByNode(measurement, i);
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
key={"grid-" + i}
|
||||
direction="row"
|
||||
style={{
|
||||
boxShadow: "inset 0px 0px 12px rgba(0,0,0,0.5)",
|
||||
marginTop: i > 0 ? 10 : 0
|
||||
}}>
|
||||
<Grid item xs={10}>
|
||||
<Grid container direction="column">
|
||||
<DelayedTextField
|
||||
value={sensor.nickname}
|
||||
variant="outlined"
|
||||
title="Nickname"
|
||||
onChange={e => {
|
||||
let prefs = mineComponentPreferences.get(component.key());
|
||||
if (prefs) {
|
||||
prefs.sensors[i].nickname = e.toString();
|
||||
mineComponentPreferences.set(component.key(), prefs);
|
||||
setMineComponentPreferences(
|
||||
new Map(mineComponentPreferences)
|
||||
);
|
||||
}
|
||||
}}
|
||||
delayMS={2000}
|
||||
size="small"
|
||||
inputProps={{
|
||||
style: { fontSize: 12 }
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item style={{ marginLeft: 6 }}>
|
||||
<UnitMeasurementSummary
|
||||
component={component}
|
||||
reading={reading}
|
||||
dense
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant={"caption"} style={{ fontSize: 12 }}>
|
||||
{"(" + sensor.x + ", " + sensor.y + ")"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
minHeight="100%"
|
||||
maxHeight="100%">
|
||||
<Checkbox
|
||||
checked={sensor.enabled}
|
||||
size="small"
|
||||
onChange={() => {
|
||||
let prefs = mineComponentPreferences.get(component.key());
|
||||
if (sensor.enabled) {
|
||||
if (prefs) {
|
||||
prefs.sensors[i].enabled = false;
|
||||
sensor.enabled = false;
|
||||
mineComponentPreferences.set(component.key(), prefs);
|
||||
setMineComponentPreferences(
|
||||
new Map(mineComponentPreferences)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (prefs) {
|
||||
sensor.enabled = true;
|
||||
mineComponentPreferences.set(component.key(), prefs);
|
||||
setMineComponentPreferences(
|
||||
new Map(mineComponentPreferences)
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</EventBlocker>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
setCards(cards);
|
||||
//setComponents(components);
|
||||
})
|
||||
.finally(() => {
|
||||
setFirstLoad(false);
|
||||
setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [deviceId, componentAPI, showMobile, keys, types]);
|
||||
|
||||
return (
|
||||
<Grid container direction="row" justify="flex-start" spacing={2}>
|
||||
{firstLoad ? (
|
||||
<div
|
||||
style={{
|
||||
margin: theme.spacing(2),
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto"
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
) : (
|
||||
cards
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
295
src/ventilation/drawers/DeviceDrawer.tsx
Normal file
295
src/ventilation/drawers/DeviceDrawer.tsx
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import {
|
||||
Box,
|
||||
Card,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
createStyles,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
Select,
|
||||
Tab,
|
||||
Tabs,
|
||||
Theme,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import DeviceIcon from "assets/editor/device.png";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useDeviceAPI, useMineAPI } from "hooks";
|
||||
import LeftIcon from "@material-ui/icons/KeyboardArrowLeft";
|
||||
import RightIcon from "@material-ui/icons/KeyboardArrowRight";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
topText: {
|
||||
//textAlign: "center",
|
||||
//alignContent: "center"
|
||||
},
|
||||
bottom: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingRight: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(1),
|
||||
marginTop: theme.spacing(1)
|
||||
},
|
||||
bottomButtons: {
|
||||
marginLeft: "auto",
|
||||
justifyContent: "center",
|
||||
alignContent: "center"
|
||||
},
|
||||
divider: {
|
||||
margin: theme.spacing(2)
|
||||
},
|
||||
topTextContainer: {
|
||||
width: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0
|
||||
},
|
||||
deviceCard: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
height: theme.spacing(8)
|
||||
},
|
||||
skeletonCard: {
|
||||
marginTop: theme.spacing(1),
|
||||
height: theme.spacing(8),
|
||||
borderRadius: "6px"
|
||||
},
|
||||
fanIcon: {
|
||||
height: "100%",
|
||||
objectFit: "contain"
|
||||
},
|
||||
iconContainer: {
|
||||
height: "100%",
|
||||
width: theme.spacing(8)
|
||||
},
|
||||
deviceInfo: {
|
||||
display: "block",
|
||||
margin: "auto",
|
||||
marginLeft: theme.spacing(1),
|
||||
padding: 0
|
||||
},
|
||||
checkbox: {
|
||||
marginLeft: "auto",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto"
|
||||
},
|
||||
tab: {
|
||||
width: "50%",
|
||||
maxWidth: "50%"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
addBlockRef: any;
|
||||
selectedDevices: pond.Device[];
|
||||
setSelectedDevices: any;
|
||||
mine: pond.MineSettings;
|
||||
setMine: React.Dispatch<React.SetStateAction<pond.MineSettings>>;
|
||||
}
|
||||
|
||||
export default function DeviceDrawer(props: Props) {
|
||||
//const isMobile = useMobile();
|
||||
const { selectedDevices, setSelectedDevices, mine } = props;
|
||||
const classes = useStyles();
|
||||
//const addBlockRef = props.addBlockRef;
|
||||
const [devices, setDevices] = useState<pond.Device[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [offset, setOffset] = useState<number>(0);
|
||||
const [limit, setLimit] = useState<number>(5);
|
||||
const [selectedIDs, setSelectedIDs] = useState<number[]>([]);
|
||||
const deviceAPI = useDeviceAPI();
|
||||
const mineAPI = useMineAPI();
|
||||
const [tab, setTab] = React.useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let ids: number[] = [];
|
||||
selectedDevices.forEach(dev => {
|
||||
if (dev.settings) {
|
||||
ids.push(dev.settings.deviceId);
|
||||
}
|
||||
});
|
||||
setSelectedIDs([...ids]);
|
||||
}, [selectedDevices]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
deviceAPI.list(limit, offset).then(resp => {
|
||||
setDevices(resp.data.devices);
|
||||
setTotal(resp.data.total);
|
||||
setLoading(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [limit, offset, deviceAPI]);
|
||||
|
||||
const back = () => {
|
||||
if (offset - limit < 0) {
|
||||
setOffset(0);
|
||||
} else {
|
||||
setOffset(offset - limit);
|
||||
}
|
||||
};
|
||||
|
||||
const forward = () => {
|
||||
if (offset + limit > total) {
|
||||
setOffset(total - limit);
|
||||
} else {
|
||||
setOffset(offset + limit);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDevice = (device: pond.Device) => {
|
||||
let id = device.settings ? device.settings.deviceId : 0;
|
||||
const index = selectedIDs.indexOf(id);
|
||||
if (index > -1) {
|
||||
if (device.settings && mine.name !== "") {
|
||||
mineAPI.addDevice(mine.key, device.settings.deviceId.toString(), []).then(resp => {
|
||||
selectedIDs.splice(index, 1);
|
||||
let d = selectedDevices;
|
||||
let deviceIndex = 0;
|
||||
d.forEach((device, i) => {
|
||||
let key = device.settings ? device.settings.deviceId : 0;
|
||||
if (key === id) {
|
||||
deviceIndex = i;
|
||||
}
|
||||
});
|
||||
d.splice(deviceIndex, 1);
|
||||
setSelectedDevices([...d]);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (device.settings && mine) {
|
||||
mineAPI
|
||||
.addDevice(mine.key, device.settings.deviceId.toString(), [
|
||||
pond.Permission.PERMISSION_READ,
|
||||
pond.Permission.PERMISSION_SHARE,
|
||||
pond.Permission.PERMISSION_WRITE,
|
||||
pond.Permission.PERMISSION_USERS
|
||||
])
|
||||
.then(resp => {
|
||||
selectedIDs.push(id);
|
||||
let d = selectedDevices;
|
||||
d.push(device);
|
||||
setSelectedDevices([...d]);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const yourDevices = () => {
|
||||
if (loading) return <CircularProgress />;
|
||||
|
||||
return devices.map((device: pond.Device, i) => {
|
||||
let name = device.settings?.name
|
||||
? device.settings?.name
|
||||
: "Device " + device.settings?.deviceId;
|
||||
return (
|
||||
<Card key={i} className={classes.deviceCard}>
|
||||
<div className={classes.iconContainer}>
|
||||
<img src={DeviceIcon} alt={device.settings?.name} className={classes.fanIcon} />
|
||||
</div>
|
||||
<div className={classes.deviceInfo}>
|
||||
<Typography display="block" variant="body2">
|
||||
{name}
|
||||
</Typography>
|
||||
</div>
|
||||
<Checkbox
|
||||
className={classes.checkbox}
|
||||
checked={device.settings ? selectedIDs.indexOf(device.settings!.deviceId) > -1 : false}
|
||||
onChange={() => device.settings && toggleDevice(device)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const attachedDevices = () => {
|
||||
if (loading) return <CircularProgress />;
|
||||
|
||||
return selectedDevices.map((device: pond.Device) => {
|
||||
let name = device.settings?.name
|
||||
? device.settings?.name
|
||||
: "Device " + device.settings?.deviceId;
|
||||
return (
|
||||
<Card key={device.settings?.name} className={classes.deviceCard}>
|
||||
<div className={classes.iconContainer}>
|
||||
<img src={DeviceIcon} alt={device.settings?.name} className={classes.fanIcon} />
|
||||
</div>
|
||||
<div className={classes.deviceInfo}>
|
||||
<Typography display="block" variant="body2">
|
||||
{name}
|
||||
</Typography>
|
||||
</div>
|
||||
<Checkbox
|
||||
className={classes.checkbox}
|
||||
checked={device.settings ? selectedIDs.indexOf(device.settings!.deviceId) > -1 : false}
|
||||
onChange={() => device.settings && toggleDevice(device)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const bottom = () => {
|
||||
return (
|
||||
<div className={classes.bottom}>
|
||||
{offset + 1} - {devices.length + offset} of {total}
|
||||
<div className={classes.bottomButtons}>
|
||||
<Grid container justify="center">
|
||||
<IconButton onClick={back} size="small">
|
||||
<LeftIcon />
|
||||
</IconButton>
|
||||
<Select value={limit} onChange={event => setLimit(event.target.value as number)}>
|
||||
<MenuItem value={5}>5</MenuItem>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={15}>15</MenuItem>
|
||||
</Select>
|
||||
<IconButton onClick={forward} size="small">
|
||||
<RightIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setTab(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.topTextContainer}>
|
||||
<Typography className={classes.topText} color="textSecondary" variant="caption">
|
||||
Devices you have access to are listed below. Click the check box to gain access to the
|
||||
device's sensor in the editor. Devices that you and others have added can be seen in the
|
||||
second tab.
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className={classes.divider} />
|
||||
<Box component="span">
|
||||
<Tabs
|
||||
style={{ width: "100%", maxWidth: "100%" }}
|
||||
value={tab}
|
||||
onChange={handleTabChange}
|
||||
aria-label="basic tabs example">
|
||||
<Tab label="Devices" classes={{ root: classes.tab }} />
|
||||
<Tab label="Attached" classes={{ root: classes.tab }} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
{tab === 0 ? yourDevices() : attachedDevices()}
|
||||
{bottom()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
181
src/ventilation/drawers/DuctingDrawer.tsx
Normal file
181
src/ventilation/drawers/DuctingDrawer.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { Card, createStyles, Divider, makeStyles, Theme, Typography } from "@material-ui/core";
|
||||
import InlineIcon from "assets/editor/inlineCentrifugal.png";
|
||||
import FullIcon from "assets/editor/fullCentrifugal.png";
|
||||
import LowIcon from "assets/editor/lowCentrifugal.png";
|
||||
import Fan1Icon from "assets/editor/fan1.png";
|
||||
import Fan2Icon from "assets/editor/fan2.png";
|
||||
import Fan3Icon from "assets/editor/fan3.png";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import React from "react";
|
||||
import { Block } from "ventilation/drawable/Block";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { VentShaft } from "ventilation/drawable/VentShaft";
|
||||
import { VentCorner } from "ventilation/drawable/VentCorner";
|
||||
import { VentTBreak } from "ventilation/drawable/VentTBreak";
|
||||
|
||||
const ducts = [
|
||||
{
|
||||
name: "Duct Line",
|
||||
img: InlineIcon,
|
||||
editorImg: Fan1Icon,
|
||||
beta: false
|
||||
},
|
||||
{
|
||||
name: "Corner",
|
||||
img: FullIcon,
|
||||
editorImg: Fan2Icon,
|
||||
beta: false
|
||||
},
|
||||
{
|
||||
name: "T-Break",
|
||||
img: LowIcon,
|
||||
editorImg: Fan3Icon,
|
||||
beta: false
|
||||
},
|
||||
{
|
||||
name: "Y-Break (coming soon)",
|
||||
img: LowIcon,
|
||||
editorImg: Fan3Icon,
|
||||
beta: true
|
||||
}
|
||||
];
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
topText: {
|
||||
//textAlign: "center",
|
||||
//alignContent: "center"
|
||||
},
|
||||
divider: {
|
||||
margin: theme.spacing(2)
|
||||
},
|
||||
topTextContainer: {
|
||||
width: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0
|
||||
},
|
||||
fanCard: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
height: theme.spacing(10)
|
||||
},
|
||||
fanIcon: {
|
||||
height: "100%",
|
||||
objectFit: "contain"
|
||||
},
|
||||
fanIconContainer: {
|
||||
height: "100%",
|
||||
width: theme.spacing(8)
|
||||
},
|
||||
ductInfo: {
|
||||
display: "block",
|
||||
marginLeft: theme.spacing(1),
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto"
|
||||
},
|
||||
addIcon: {
|
||||
//justifySelf: "flex-end",
|
||||
marginLeft: "auto",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
borderRadius: theme.spacing(1),
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
background: "rgba(200,200,200, 0.25)"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
addBlockRef: any;
|
||||
}
|
||||
|
||||
export default function DuctingDrawer(props: Props) {
|
||||
//const isMobile = useMobile();
|
||||
//const { isOpen, children } = props;
|
||||
const classes = useStyles();
|
||||
const addBlockRef = props.addBlockRef;
|
||||
|
||||
const addDuct = () => {
|
||||
let block = VentShaft.create({
|
||||
settings: {
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 64,
|
||||
height: 64,
|
||||
angle: 0
|
||||
}
|
||||
} as pond.Block);
|
||||
addBlockRef.current(block);
|
||||
};
|
||||
|
||||
const addTBreak = () => {
|
||||
let block = VentTBreak.create({
|
||||
settings: {
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 64,
|
||||
height: 64,
|
||||
angle: 0
|
||||
}
|
||||
} as pond.Block);
|
||||
addBlockRef.current(block);
|
||||
};
|
||||
|
||||
const addCorner = () => {
|
||||
let block = VentCorner.create(64, 64, 128, 1 * Math.PI);
|
||||
addBlockRef.current(block);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.topTextContainer}>
|
||||
<Typography className={classes.topText} color="textSecondary" variant="caption">
|
||||
Place a duct into the editor. If a duct or fan is selected in the editor, the new duct
|
||||
will be placed in line with it.
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className={classes.divider} />
|
||||
{ducts.map(duct => {
|
||||
return (
|
||||
<Card key={duct.name} className={classes.fanCard}>
|
||||
<div className={classes.fanIconContainer}>
|
||||
<img src={duct.img} alt={duct.name} className={classes.fanIcon} />
|
||||
</div>
|
||||
<div className={classes.ductInfo}>
|
||||
<Typography display="block" variant="body2">
|
||||
{duct.name}
|
||||
</Typography>
|
||||
</div>
|
||||
{!duct.beta && duct.name === "Duct Line" && (
|
||||
<AddIcon
|
||||
className={classes.addIcon}
|
||||
onClick={() => (addBlockRef.current ? addDuct() : undefined)}
|
||||
/>
|
||||
)}
|
||||
{!duct.beta && duct.name === "Corner" && (
|
||||
<AddIcon
|
||||
className={classes.addIcon}
|
||||
onClick={() => (addBlockRef.current ? addCorner() : undefined)}
|
||||
/>
|
||||
)}
|
||||
{!duct.beta && duct.name === "T-Break" && (
|
||||
<AddIcon
|
||||
className={classes.addIcon}
|
||||
onClick={() => (addBlockRef.current ? addTBreak() : undefined)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
141
src/ventilation/drawers/FansDrawer.tsx
Normal file
141
src/ventilation/drawers/FansDrawer.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { Card, createStyles, Divider, makeStyles, Theme, Typography } from "@material-ui/core";
|
||||
import InlineIcon from "assets/editor/inlineCentrifugal.png";
|
||||
import FullIcon from "assets/editor/fullCentrifugal.png";
|
||||
import LowIcon from "assets/editor/lowCentrifugal.png";
|
||||
import Fan1Icon from "assets/editor/fan1.png";
|
||||
import Fan2Icon from "assets/editor/fan2.png";
|
||||
import Fan3Icon from "assets/editor/fan3.png";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import React from "react";
|
||||
import { Fan } from "ventilation/drawable/Fan";
|
||||
|
||||
const fans = [
|
||||
{
|
||||
name: "In-Line Centrifugal",
|
||||
img: InlineIcon,
|
||||
mfg: "",
|
||||
model: "Unknown",
|
||||
hp: "500HP",
|
||||
editorImg: Fan1Icon
|
||||
},
|
||||
{
|
||||
name: "Full Centrifugal",
|
||||
img: FullIcon,
|
||||
mfg: "",
|
||||
model: "Unknown",
|
||||
hp: "1000HP",
|
||||
editorImg: Fan2Icon
|
||||
},
|
||||
{
|
||||
name: "Low Speed Centrifugal",
|
||||
img: LowIcon,
|
||||
mfg: "",
|
||||
model: "Unknown",
|
||||
hp: "250HP",
|
||||
editorImg: Fan3Icon
|
||||
}
|
||||
];
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
topText: {
|
||||
//textAlign: "center",
|
||||
//alignContent: "center"
|
||||
},
|
||||
divider: {
|
||||
margin: theme.spacing(2)
|
||||
},
|
||||
topTextContainer: {
|
||||
width: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0
|
||||
},
|
||||
fanCard: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
height: theme.spacing(10)
|
||||
},
|
||||
fanIcon: {
|
||||
height: "100%",
|
||||
objectFit: "contain"
|
||||
},
|
||||
fanIconContainer: {
|
||||
height: "100%",
|
||||
width: theme.spacing(8)
|
||||
},
|
||||
fanInfo: {
|
||||
display: "block",
|
||||
marginLeft: theme.spacing(1)
|
||||
},
|
||||
addIcon: {
|
||||
//justifySelf: "flex-end",
|
||||
marginLeft: "auto",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
borderRadius: theme.spacing(1),
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
background: "rgba(200,200,200, 0.25)"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
addBlockRef: any;
|
||||
}
|
||||
|
||||
export default function FansDrawer(props: Props) {
|
||||
//const isMobile = useMobile();
|
||||
//const { isOpen, children } = props;
|
||||
const classes = useStyles();
|
||||
const addBlockRef = props.addBlockRef;
|
||||
|
||||
const addFan = (subtype: number) => {
|
||||
let fan = Fan.create(0, 0, 32, subtype);
|
||||
addBlockRef.current(fan);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.topTextContainer}>
|
||||
<Typography className={classes.topText} color="textSecondary" variant="caption">
|
||||
Select a fan from the list below to place it in the Ventilation Editor
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className={classes.divider} />
|
||||
{fans.map((fan, index) => {
|
||||
return (
|
||||
<Card key={fan.name} className={classes.fanCard}>
|
||||
<div className={classes.fanIconContainer}>
|
||||
<img src={fan.img} alt={fan.name} className={classes.fanIcon} />
|
||||
</div>
|
||||
<div className={classes.fanInfo}>
|
||||
<Typography display="block" variant="body2">
|
||||
{fan.name}
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Typography display="block" variant="caption">
|
||||
Model: {fan.model}
|
||||
</Typography>
|
||||
<Typography display="block" variant="caption">
|
||||
Power: {fan.hp}
|
||||
</Typography>
|
||||
</div>
|
||||
<AddIcon
|
||||
className={classes.addIcon}
|
||||
onClick={() => (addBlockRef.current ? addFan(index + 1) : undefined)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
215
src/ventilation/drawers/SensorDrawer.tsx
Normal file
215
src/ventilation/drawers/SensorDrawer.tsx
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import {
|
||||
Collapse,
|
||||
createStyles,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
MenuItem,
|
||||
Select,
|
||||
Theme,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@material-ui/core";
|
||||
import React, { useState } from "react";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import LeftIcon from "@material-ui/icons/KeyboardArrowLeft";
|
||||
import RightIcon from "@material-ui/icons/KeyboardArrowRight";
|
||||
import { ExpandLess, ExpandMore } from "@material-ui/icons";
|
||||
import DeviceIcon from "assets/editor/device.png";
|
||||
import { ImgIcon } from "common/ImgIcon";
|
||||
import ComponentCards from "./ComponentCards";
|
||||
import { Component } from "models";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return createStyles({
|
||||
topText: {
|
||||
//textAlign: "center",
|
||||
//alignContent: "center"
|
||||
},
|
||||
bottom: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingRight: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(1),
|
||||
marginTop: theme.spacing(1)
|
||||
},
|
||||
bottomButtons: {
|
||||
marginLeft: "auto",
|
||||
justifyContent: "center",
|
||||
alignContent: "center"
|
||||
},
|
||||
divider: {
|
||||
margin: theme.spacing(2)
|
||||
},
|
||||
topTextContainer: {
|
||||
width: "100%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0
|
||||
},
|
||||
deviceCard: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(1),
|
||||
height: theme.spacing(8)
|
||||
},
|
||||
skeletonCard: {
|
||||
marginTop: theme.spacing(1),
|
||||
height: theme.spacing(8),
|
||||
borderRadius: "6px"
|
||||
},
|
||||
fanIcon: {
|
||||
height: "100%",
|
||||
objectFit: "contain"
|
||||
},
|
||||
iconContainer: {
|
||||
height: "100%",
|
||||
width: theme.spacing(8)
|
||||
},
|
||||
deviceInfo: {
|
||||
display: "block",
|
||||
margin: "auto",
|
||||
marginLeft: theme.spacing(1),
|
||||
padding: 0
|
||||
},
|
||||
checkbox: {
|
||||
marginLeft: "auto",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface Props {
|
||||
addBlockRef: any;
|
||||
selectedDevices: pond.Device[];
|
||||
setSelectedDevices: any;
|
||||
mineComponents: Component[];
|
||||
setMineComponents: React.Dispatch<React.SetStateAction<Component[]>>;
|
||||
mineComponentPreferences: Map<string, pond.MineComponentPreferences>;
|
||||
setMineComponentPreferences: React.Dispatch<
|
||||
React.SetStateAction<Map<string, pond.MineComponentPreferences>>
|
||||
>;
|
||||
mineKey: string;
|
||||
}
|
||||
|
||||
export default function SensorDrawer(props: Props) {
|
||||
//const isMobile = useMobile();
|
||||
const {
|
||||
selectedDevices,
|
||||
mineComponents,
|
||||
setMineComponents,
|
||||
mineKey,
|
||||
mineComponentPreferences,
|
||||
setMineComponentPreferences
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [offset, setOffset] = useState<number>(0);
|
||||
const [limit, setLimit] = useState<number>(5);
|
||||
const [expanded, setExpanded] = useState<number>(0);
|
||||
const theme = useTheme();
|
||||
|
||||
const back = () => {
|
||||
if (offset - limit < 0) {
|
||||
setOffset(0);
|
||||
} else {
|
||||
setOffset(offset - limit);
|
||||
}
|
||||
};
|
||||
|
||||
const forward = () => {
|
||||
if (offset + limit > selectedDevices.length) {
|
||||
setOffset(selectedDevices.length - limit);
|
||||
} else {
|
||||
setOffset(offset + limit);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.topTextContainer}>
|
||||
<Typography className={classes.topText} color="textSecondary" variant="caption">
|
||||
Devices selected for this vent system are listed below. Click them to view attached
|
||||
sensors and components.
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className={classes.divider} />
|
||||
<List disablePadding>
|
||||
<Divider />
|
||||
{selectedDevices.map((device: pond.Device) => {
|
||||
return (
|
||||
<React.Fragment key={device.settings?.deviceId}>
|
||||
<ListItem
|
||||
button
|
||||
onClick={() => {
|
||||
if (device.settings?.deviceId === expanded) {
|
||||
setExpanded(0);
|
||||
} else {
|
||||
setExpanded(device.settings ? device.settings.deviceId : 0);
|
||||
}
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<ImgIcon src={DeviceIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
device.settings && device.settings.name
|
||||
? device.settings.name
|
||||
: "Device " + device.settings?.deviceId
|
||||
}
|
||||
/>
|
||||
{device.settings?.deviceId === expanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={device.settings?.deviceId === expanded} timeout="auto" unmountOnExit>
|
||||
{device.settings?.deviceId === expanded && (
|
||||
<React.Fragment>
|
||||
<div style={{ marginTop: theme.spacing(1) }} />
|
||||
<ComponentCards
|
||||
deviceId={device.settings ? device.settings.deviceId : 0}
|
||||
showMobile={true}
|
||||
mineComponents={mineComponents}
|
||||
setMineComponents={setMineComponents}
|
||||
mineComponentPreferences={mineComponentPreferences}
|
||||
setMineComponentPreferences={setMineComponentPreferences}
|
||||
keys={[mineKey]}
|
||||
types={["mine"]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Collapse>
|
||||
<div style={{ marginTop: theme.spacing(1) }} />
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<div className={classes.bottom}>
|
||||
{offset + 1} - {limit + offset} of {selectedDevices.length}
|
||||
<div className={classes.bottomButtons}>
|
||||
<Grid container justify="center">
|
||||
<IconButton onClick={back} size="small">
|
||||
<LeftIcon />
|
||||
</IconButton>
|
||||
<Select value={limit} onChange={event => setLimit(event.target.value as number)}>
|
||||
<MenuItem value={5}>5</MenuItem>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
<MenuItem value={15}>15</MenuItem>
|
||||
</Select>
|
||||
<IconButton onClick={forward} size="small">
|
||||
<RightIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue