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,
|
useInteractionsAPI,
|
||||||
// useMeasurementsWebsocket,
|
// useMeasurementsWebsocket,
|
||||||
// useMetricAPI,
|
// useMetricAPI,
|
||||||
// useMineAPI,
|
useMineAPI,
|
||||||
usePermissionAPI,
|
usePermissionAPI,
|
||||||
// usePreferenceAPI,
|
// usePreferenceAPI,
|
||||||
// useSecurity,
|
// useSecurity,
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,9 @@ import { makeStyles } from "@mui/styles";
|
||||||
import { green, red } from "@mui/material/colors";
|
import { green, red } from "@mui/material/colors";
|
||||||
import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers";
|
import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers";
|
||||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||||
import dayjs from "dayjs";
|
// import dayjs from "dayjs";
|
||||||
import CancelSubmit from "common/CancelSubmit";
|
import CancelSubmit from "common/CancelSubmit";
|
||||||
|
import { Sensor } from "ventilation/drawable/Sensor";
|
||||||
|
|
||||||
// import { Sensor } from "ventilation/drawable/Sensor";
|
// import { Sensor } from "ventilation/drawable/Sensor";
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ interface Props {
|
||||||
closeDialogCallback: Function;
|
closeDialogCallback: Function;
|
||||||
refreshCallback: Function;
|
refreshCallback: Function;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
// sensor?: Sensor;
|
sensor?: Sensor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InteractionSettings(props: Props) {
|
export default function InteractionSettings(props: Props) {
|
||||||
|
|
@ -126,7 +127,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
closeDialogCallback,
|
closeDialogCallback,
|
||||||
refreshCallback,
|
refreshCallback,
|
||||||
canEdit,
|
canEdit,
|
||||||
// sensor
|
sensor
|
||||||
} = props;
|
} = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { success, error } = useSnackbar();
|
const { success, error } = useSnackbar();
|
||||||
|
|
@ -206,11 +207,11 @@ export default function InteractionSettings(props: Props) {
|
||||||
or(interaction.settings.result, undefined)
|
or(interaction.settings.result, undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
// if (sensor) {
|
if (sensor) {
|
||||||
// interaction.settings.subtype = 2;
|
interaction.settings.subtype = 2;
|
||||||
// interaction.settings.nodeOne = sensor.index;
|
interaction.settings.nodeOne = sensor.index;
|
||||||
// interaction.settings.nodeTwo = sensor.index;
|
interaction.settings.nodeTwo = sensor.index;
|
||||||
// }
|
}
|
||||||
|
|
||||||
setIsRemoveInteractionOpen(false);
|
setIsRemoveInteractionOpen(false);
|
||||||
setNumConditions(numConditions);
|
setNumConditions(numConditions);
|
||||||
|
|
@ -982,15 +983,15 @@ export default function InteractionSettings(props: Props) {
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
{/* {multiNodeSource() && !sensor && (
|
{multiNodeSource() && !sensor && (
|
||||||
<Grid
|
<Grid
|
||||||
key={"nodes"}
|
key={"nodes"}
|
||||||
container
|
container
|
||||||
direction="row"
|
direction="row"
|
||||||
justify="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={2}>
|
spacing={2}>
|
||||||
<Grid item xs={10} sm={4}>
|
<Grid size={{ xs: 10, sm: 4 }}>
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
id="subtype"
|
id="subtype"
|
||||||
|
|
@ -1019,7 +1020,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={10} sm={4}>
|
<Grid size={{ xs: 10, sm: 4 }}>
|
||||||
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
|
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
|
@ -1036,7 +1037,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
</TextField>
|
</TextField>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={10} sm={4}>
|
<Grid size={{ xs: 10, sm: 4 }}>
|
||||||
{subtypeDropdown === 3 && (
|
{subtypeDropdown === 3 && (
|
||||||
<TextField
|
<TextField
|
||||||
select
|
select
|
||||||
|
|
@ -1054,7 +1055,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)} */}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -1564,7 +1565,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
||||||
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
|
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
|
||||||
{/* <Button onClick={close} color="primary">
|
<Button onClick={close} color="primary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
|
|
@ -1574,7 +1575,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
|
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
)} */}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,23 @@ import { lazy, Suspense } from "react";
|
||||||
import LoadingScreen from "../app/LoadingScreen";
|
import LoadingScreen from "../app/LoadingScreen";
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { useAuth0 } from "@auth0/auth0-react";
|
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 Header from "app/Header";
|
||||||
import Logout from "pages/Logout";
|
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 { ErrorBoundary } from "react-error-boundary";
|
||||||
import DeviceComponent from "pages/DeviceComponent";
|
|
||||||
import Bins from "pages/Bins";
|
|
||||||
import { getWhitelabel } from "services/whiteLabel";
|
import { getWhitelabel } from "services/whiteLabel";
|
||||||
import Bin from "pages/Bin";
|
|
||||||
|
|
||||||
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
||||||
const DevicePage = lazy(() => import("pages/Device"));
|
const DevicePage = lazy(() => import("pages/Device"));
|
||||||
|
const Devices = lazy(() => import("pages/Devices"));
|
||||||
const GroupsPage = lazy(() => import("pages/Groups"));
|
const GroupsPage = lazy(() => import("pages/Groups"));
|
||||||
const GroupPage = lazy(() => import("pages/Group"));
|
const GroupPage = lazy(() => import("pages/Group"));
|
||||||
const Teams = lazy(() => import("pages/Teams"));
|
const Teams = lazy(() => import("pages/Teams"));
|
||||||
const Users = lazy(() => import("pages/Users"));
|
const Users = lazy(() => import("pages/Users"));
|
||||||
const TeamPage = lazy(() => import("pages/Team"));
|
const TeamPage = lazy(() => import("pages/Team"));
|
||||||
const BinPage = lazy(() => import("pages/Bin"));
|
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 {
|
interface Props {
|
||||||
toggleTheme: () => void;
|
toggleTheme: () => void;
|
||||||
|
|
@ -48,6 +42,7 @@ export default function Router(props: Props) {
|
||||||
<Route path="devices/*" element={<DevicesRoute/>} />
|
<Route path="devices/*" element={<DevicesRoute/>} />
|
||||||
<Route path="groups/*" element={<GroupsRoute/>} />
|
<Route path="groups/*" element={<GroupsRoute/>} />
|
||||||
<Route path="bins/*" element={<BinsRoute/>} />
|
<Route path="bins/*" element={<BinsRoute/>} />
|
||||||
|
<Route path="mines/*" element={<MinesRoute/>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +107,7 @@ export default function Router(props: Props) {
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/:binID" // "/settings/basic"
|
path="/:binID" // "/settings/basic"
|
||||||
element={<Bin />}
|
element={<BinPage />}
|
||||||
/>
|
/>
|
||||||
{/* <Route
|
{/* <Route
|
||||||
path="/:deviceID/components/:componentID" // "/settings/basic"
|
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 = () => {
|
const GroupsRoute = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,37 @@
|
||||||
import { ChevronRight, People, Person } from "@mui/icons-material";
|
import { ChevronRight, People, Person } from "@mui/icons-material";
|
||||||
import ChevronLeft from "@mui/icons-material/ChevronLeft";
|
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 classNames from "classnames";
|
||||||
import { useWidth } from "../hooks/useWidth";
|
import { useWidth } from "../hooks/useWidth";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import BindaptIcon from "../products/Bindapt/BindaptIcon";
|
import BindaptIcon from "../products/Bindapt/BindaptIcon";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import BinsIcon from "products/Bindapt/BinsIcon";
|
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;
|
const drawerWidth = 230;
|
||||||
|
|
||||||
|
|
@ -80,6 +105,7 @@ export default function SideNavigator(props: Props) {
|
||||||
const width = useWidth();
|
const width = useWidth();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [{ user }] = useGlobalState()
|
||||||
|
|
||||||
const getClasses = (page: string) => {
|
const getClasses = (page: string) => {
|
||||||
if (page === "/device") {
|
if (page === "/device") {
|
||||||
|
|
@ -101,7 +127,7 @@ export default function SideNavigator(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticatedSideMenu = () => {
|
const authenticatedSideMenu = () => {
|
||||||
|
const isMiVent = IsMiVent();
|
||||||
return (
|
return (
|
||||||
<List className={classes.list} component="nav">
|
<List className={classes.list} component="nav">
|
||||||
<Tooltip title="Bins" placement="right">
|
<Tooltip title="Bins" placement="right">
|
||||||
|
|
@ -132,6 +158,22 @@ export default function SideNavigator(props: Props) {
|
||||||
{open && <ListItemText primary="Devices" />}
|
{open && <ListItemText primary="Devices" />}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</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">
|
<Tooltip title="Teams" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-dashboard"
|
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,
|
useUserAPI,
|
||||||
// useStripeAPI,
|
// useStripeAPI,
|
||||||
// useDataDogProxyAPI,
|
// useDataDogProxyAPI,
|
||||||
// useMineAPI,
|
useMineAPI,
|
||||||
// useKeyManagerAPI,
|
// useKeyManagerAPI,
|
||||||
// useTerminalAPI,
|
// useTerminalAPI,
|
||||||
useGateAPI,
|
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 FieldProvider, { useFieldAPI } from "./fieldAPI";
|
||||||
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
|
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
|
||||||
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
|
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
|
||||||
|
import MineProvider, { useMineAPI } from "./mineAPI";
|
||||||
// import NoteProvider from "providers/noteAPI";
|
// import NoteProvider from "providers/noteAPI";
|
||||||
|
|
||||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||||
|
|
@ -56,7 +57,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
||||||
<FieldProvider>
|
<FieldProvider>
|
||||||
<TransactionProvider>
|
<TransactionProvider>
|
||||||
<FileControllerProvider>
|
<FileControllerProvider>
|
||||||
{children}
|
<MineProvider>
|
||||||
|
{children}
|
||||||
|
</MineProvider>
|
||||||
</FileControllerProvider>
|
</FileControllerProvider>
|
||||||
</TransactionProvider>
|
</TransactionProvider>
|
||||||
</FieldProvider>
|
</FieldProvider>
|
||||||
|
|
@ -100,4 +103,5 @@ export {
|
||||||
useFieldAPI,
|
useFieldAPI,
|
||||||
useTransactionAPI,
|
useTransactionAPI,
|
||||||
useFileControllerAPI,
|
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