imported mining stuff, can add a mine and list them

This commit is contained in:
Carter 2025-03-13 11:42:50 -06:00
parent 7dad5217e7
commit 5fcb99ef40
36 changed files with 6622 additions and 32 deletions

View file

@ -14,7 +14,7 @@ export {
useInteractionsAPI,
// useMeasurementsWebsocket,
// useMetricAPI,
// useMineAPI,
useMineAPI,
usePermissionAPI,
// usePreferenceAPI,
// useSecurity,

View file

@ -63,8 +63,9 @@ import { makeStyles } from "@mui/styles";
import { green, red } from "@mui/material/colors";
import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import dayjs from "dayjs";
// import dayjs from "dayjs";
import CancelSubmit from "common/CancelSubmit";
import { Sensor } from "ventilation/drawable/Sensor";
// import { Sensor } from "ventilation/drawable/Sensor";
@ -112,7 +113,7 @@ interface Props {
closeDialogCallback: Function;
refreshCallback: Function;
canEdit?: boolean;
// sensor?: Sensor;
sensor?: Sensor;
}
export default function InteractionSettings(props: Props) {
@ -126,7 +127,7 @@ export default function InteractionSettings(props: Props) {
closeDialogCallback,
refreshCallback,
canEdit,
// sensor
sensor
} = props;
const theme = useTheme();
const { success, error } = useSnackbar();
@ -206,11 +207,11 @@ export default function InteractionSettings(props: Props) {
or(interaction.settings.result, undefined)
);
// if (sensor) {
// interaction.settings.subtype = 2;
// interaction.settings.nodeOne = sensor.index;
// interaction.settings.nodeTwo = sensor.index;
// }
if (sensor) {
interaction.settings.subtype = 2;
interaction.settings.nodeOne = sensor.index;
interaction.settings.nodeTwo = sensor.index;
}
setIsRemoveInteractionOpen(false);
setNumConditions(numConditions);
@ -982,15 +983,15 @@ export default function InteractionSettings(props: Props) {
)}
</Grid>
</Grid>
{/* {multiNodeSource() && !sensor && (
{multiNodeSource() && !sensor && (
<Grid
key={"nodes"}
container
direction="row"
justify="center"
justifyContent="center"
alignItems="center"
spacing={2}>
<Grid item xs={10} sm={4}>
<Grid size={{ xs: 10, sm: 4 }}>
<TextField
select
id="subtype"
@ -1019,7 +1020,7 @@ export default function InteractionSettings(props: Props) {
</MenuItem>
</TextField>
</Grid>
<Grid item xs={10} sm={4}>
<Grid size={{ xs: 10, sm: 4 }}>
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
<TextField
select
@ -1036,7 +1037,7 @@ export default function InteractionSettings(props: Props) {
</TextField>
)}
</Grid>
<Grid item xs={10} sm={4}>
<Grid size={{ xs: 10, sm: 4 }}>
{subtypeDropdown === 3 && (
<TextField
select
@ -1054,7 +1055,7 @@ export default function InteractionSettings(props: Props) {
)}
</Grid>
</Grid>
)} */}
)}
</React.Fragment>
);
};
@ -1564,7 +1565,7 @@ export default function InteractionSettings(props: Props) {
</Grid>
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
{/* <Button onClick={close} color="primary">
<Button onClick={close} color="primary">
Cancel
</Button>
{canEdit && (
@ -1574,7 +1575,7 @@ export default function InteractionSettings(props: Props) {
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
Submit
</Button>
)} */}
)}
</Grid>
</Grid>
);

View file

@ -2,29 +2,23 @@ import { lazy, Suspense } from "react";
import LoadingScreen from "../app/LoadingScreen";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { useAuth0 } from "@auth0/auth0-react";
// import Teams from "pages/Teams";
// import Users from "pages/Users";
// import TeamPage from "pages/Team";
import Header from "app/Header";
import Logout from "pages/Logout";
import Devices from "pages/Devices";
// import DevicePage from "pages/Device";
// import GroupsPage from "pages/Groups";
// import GroupPage from "pages/Group";
import { ErrorBoundary } from "react-error-boundary";
import DeviceComponent from "pages/DeviceComponent";
import Bins from "pages/Bins";
import { getWhitelabel } from "services/whiteLabel";
import Bin from "pages/Bin";
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
const DevicePage = lazy(() => import("pages/Device"));
const Devices = lazy(() => import("pages/Devices"));
const GroupsPage = lazy(() => import("pages/Groups"));
const GroupPage = lazy(() => import("pages/Group"));
const Teams = lazy(() => import("pages/Teams"));
const Users = lazy(() => import("pages/Users"));
const TeamPage = lazy(() => import("pages/Team"));
const BinPage = lazy(() => import("pages/Bin"));
const Bins = lazy(() => import("pages/Bins"));
const Mines = lazy(() => import("pages/Mines"));
const DeviceComponent = lazy(() => import("pages/DeviceComponent"));
interface Props {
toggleTheme: () => void;
@ -48,6 +42,7 @@ export default function Router(props: Props) {
<Route path="devices/*" element={<DevicesRoute/>} />
<Route path="groups/*" element={<GroupsRoute/>} />
<Route path="bins/*" element={<BinsRoute/>} />
<Route path="mines/*" element={<MinesRoute/>} />
</Routes>
)
}
@ -112,7 +107,7 @@ export default function Router(props: Props) {
/>
<Route
path="/:binID" // "/settings/basic"
element={<Bin />}
element={<BinPage />}
/>
{/* <Route
path="/:deviceID/components/:componentID" // "/settings/basic"
@ -131,6 +126,36 @@ export default function Router(props: Props) {
);
};
const MinesRoute = () => {
return (
<div>
<Routes>
<Route
key="Mines page route"
path="" // "/settings/basic"
element={<Mines key={"Bins page"} />}
/>
{/* <Route
path="/:binID" // "/settings/basic"
element={<Bin />}
/> */}
{/* <Route
path="/:deviceID/components/:componentID" // "/settings/basic"
element={<DeviceComponent />}
/>
<Route
path="/:deviceID/history" // "/settings/basic"
element={<DeviceHistory />}
/> */}
<Route
path="/:binID/*" // "/settings/basic"
element={<RelativeRoutes />}
/>
</Routes>
</div>
);
};
const GroupsRoute = () => {
return (

View file

@ -1,12 +1,37 @@
import { ChevronRight, People, Person } from "@mui/icons-material";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import { darken, Divider, Grid2 as Grid, IconButton, lighten, List, ListItemButton, ListItemIcon, ListItemText, SwipeableDrawer, Theme, Toolbar, Tooltip, useTheme } from "@mui/material";
import {
darken,
Divider,
Grid2 as Grid,
IconButton,
lighten,
List,
ListItemButton,
ListItemIcon,
ListItemText,
SwipeableDrawer,
Theme,
Toolbar,
Tooltip,
useTheme
} from "@mui/material";
import classNames from "classnames";
import { useWidth } from "../hooks/useWidth";
import { makeStyles } from "@mui/styles";
import BindaptIcon from "../products/Bindapt/BindaptIcon";
import { Link, useLocation } from "react-router-dom";
import BinsIcon from "products/Bindapt/BinsIcon";
import { useGlobalState } from "providers";
import {
hasTutorialPlaylist,
IsAdaptiveAgriculture,
IsAdCon,
isBXT,
IsMiVent,
IsOmniAir
} from "services/whiteLabel";
import MiningIcon from "products/ventilation/MiningIcon";
const drawerWidth = 230;
@ -80,6 +105,7 @@ export default function SideNavigator(props: Props) {
const width = useWidth();
const classes = useStyles();
const location = useLocation();
const [{ user }] = useGlobalState()
const getClasses = (page: string) => {
if (page === "/device") {
@ -101,7 +127,7 @@ export default function SideNavigator(props: Props) {
}
const authenticatedSideMenu = () => {
const isMiVent = IsMiVent();
return (
<List className={classes.list} component="nav">
<Tooltip title="Bins" placement="right">
@ -132,6 +158,22 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Devices" />}
</ListItemButton>
</Tooltip>
{(isMiVent || user.hasFeature("admin")) && (
<Tooltip title="Mines" placement="right">
<ListItemButton
id="tour-dashboard"
component={Link}
to="/mines"
onClick={onClose}
classes={getClasses("/mine")}
>
<ListItemIcon>
<MiningIcon />
</ListItemIcon>
{open && <ListItemText primary="Mines" />}
</ListItemButton>
</Tooltip>
)}
<Tooltip title="Teams" placement="right">
<ListItemButton
id="tour-dashboard"

200
src/pages/Mines.tsx Normal file
View 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
View 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>
);
}

View file

@ -34,7 +34,7 @@ export {
useUserAPI,
// useStripeAPI,
// useDataDogProxyAPI,
// useMineAPI,
useMineAPI,
// useKeyManagerAPI,
// useTerminalAPI,
useGateAPI,

View 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);

View file

@ -20,6 +20,7 @@ import GrainBagProvider, { useGrainBagAPI } from "./grainBagAPI";
import FieldProvider, { useFieldAPI } from "./fieldAPI";
import TransactionProvider, { useTransactionAPI } from "./transactionAPI";
import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAPI";
import MineProvider, { useMineAPI } from "./mineAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -56,7 +57,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<FieldProvider>
<TransactionProvider>
<FileControllerProvider>
<MineProvider>
{children}
</MineProvider>
</FileControllerProvider>
</TransactionProvider>
</FieldProvider>
@ -100,4 +103,5 @@ export {
useFieldAPI,
useTransactionAPI,
useFileControllerAPI,
useMineAPI,
};

View 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>
);
}

View 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
View 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
View 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>
);
}

View 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>
);
}

View 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>;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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>
);
}

View 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;
}*/
}

View 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;
}
}

View 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
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}