FarmBot/Farmbot-Web-App

View on GitHub
frontend/farm_designer/map/profile/tools.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
import React from "react";
import { isUndefined } from "lodash";
import {
  ProfilePointProps, ProfileToolProps, ProfileUtmProps, SlotProfileProps,
} from "./interfaces";
import { Color } from "../../../ui";
import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources";
import { isToolFlipped } from "../../../tools/tool_slot_edit_components";
import { withinProfileRange } from "./content";
import { ToolDimensions } from "../tool_graphics/tool";
import { SlotFrontProfile, SlotSideProfile } from "../tool_graphics/slot";
import { troughSize } from "../tool_graphics/seed_trough";
import {
  getToolColor, reduceToolName, ToolImplementProfile, ToolName,
} from "../tool_graphics/all_tools";
import { TaggedToolSlotPointer } from "farmbot";
import { CustomToolProfile } from "../../../tools/custom_tool_graphics";
import { FilePath } from "../../../internal_urls";

export enum UTMDimensions {
  height = 40,
  extrusion = 20,
}

/** Virtual UTM profile. */
// eslint-disable-next-line complexity
export const UTMProfile = (props: ProfileUtmProps) => {
  const { x, y } = props.botPosition;
  const inProfile = !isUndefined(x) && !isUndefined(y) &&
    withinProfileRange({
      axis: props.profileAxis == "x" ? "y" : "x",
      selectionWidth: props.selectionWidth,
      profilePosition: props.position,
      location: { x, y },
    });
  const profileUtmH = props.getX(props.botPosition);
  const profileUtmV = Math.abs(props.botPosition.z || 0);
  const extrusion = UTMDimensions.extrusion;
  if (!inProfile) { return <g id={"utm-not-in-profile"} />; }
  if (!props.expanded) {
    return <g id={"UTM-and-axis"} opacity={0.25}>
      <line id={"z-axis"} strokeWidth={extrusion} stroke={Color.darkGray}
        x1={profileUtmH} y1={0} x2={profileUtmH} y2={profileUtmV} />
      <rect id={"position-indicator"} fill={Color.black}
        x={profileUtmH - 5} y={profileUtmV - 5} width={10} height={10} />
    </g>;
  }
  const extrusionOffset = (extrusion + ToolDimensions.diameter) / 2;
  const toolInfo = props.mountedToolInfo;
  const xProfile = props.profileAxis == "x";
  const yExtrusionX = profileUtmH - (extrusion + extrusion / 2);
  const utmTopY = profileUtmV - UTMDimensions.height;
  const zAxisCenter = profileUtmH + (xProfile ? 0 : extrusionOffset);
  return <g id={"UTM-and-axis"} opacity={0.75}>
    <defs>
      <linearGradient id={"utm-gradient"}>
        <stop offset={"0%"} stopColor={Color.darkGray} stopOpacity={1} />
        <stop offset={"10%"} stopColor={Color.darkGray} stopOpacity={0.75} />
        <stop offset={"90%"} stopColor={Color.darkGray} stopOpacity={0.75} />
        <stop offset={"100%"} stopColor={Color.darkGray} stopOpacity={1} />
      </linearGradient>
    </defs>
    {props.profileWidth &&
      <rect id={"y-axis"}
        x={xProfile ? yExtrusionX : -100}
        y={-props.gantryHeight - extrusion * 3}
        width={xProfile ? extrusion * 2 : props.profileWidth + 200}
        height={extrusion * 3}
        fill={Color.darkGray} fillOpacity={0.5} stroke={"none"} />}
    <line id={"z-axis"}
      strokeWidth={extrusion} stroke={Color.darkGray} opacity={0.5}
      x1={zAxisCenter}
      y1={-props.gantryHeight - (-props.gantryHeight < utmTopY ? 0 : extrusion * 3)}
      x2={zAxisCenter}
      y2={xProfile ? utmTopY : profileUtmV} />
    <line id={"z-axis-separator"}
      strokeWidth={0.5} stroke={Color.darkGray} opacity={0.5}
      x1={profileUtmH + ToolDimensions.diameter / 2}
      y1={Math.min(0, profileUtmV - UTMDimensions.height)}
      x2={profileUtmH + ToolDimensions.diameter / 2}
      y2={profileUtmV} />
    <rect id={"UTM"} fill={"url(#utm-gradient)"} opacity={0.5}
      x={profileUtmH - ToolDimensions.radius}
      y={profileUtmV - UTMDimensions.height}
      width={ToolDimensions.diameter}
      height={UTMDimensions.height} />
    {!props.hidePositionIndicator &&
      <rect id={"position-indicator"} fill={Color.black} opacity={0.5}
        x={profileUtmH - 2} y={profileUtmV - 2}
        width={4} height={4} />}
    <image x={profileUtmH - 25} y={profileUtmV - 35} width={50} height={30}
      xlinkHref={FilePath.image("farmbot")} opacity={1}
      style={{ filter: "invert(1)" }} />
    {toolInfo.name
      ? <ToolProfile toolName={toolInfo.name}
        x={profileUtmH - ToolDimensions.radius} y={profileUtmV}
        width={ToolDimensions.diameter}
        height={ToolDimensions.thickness}
        sideView={props.profileAxis == slotPulloutAxis(toolInfo.pulloutDirection)}
        reversed={props.reversed}
        hidePositionIndicator={props.hidePositionIndicator}
        toolFlipped={getToolDirection(
          toolInfo.pulloutDirection,
          toolInfo.flipped,
          props.reversed)} />
      : <g id={"liquid-ports"}>
        {[-20, 20, 0].map(xP =>
          <rect key={xP} fill={Color.darkGray} opacity={0.5} width={8} height={2}
            x={profileUtmH + xP - 4} y={profileUtmV} />)}
      </g>}
  </g>;
};

/** Determine if tool direction is reversed. */
export const getToolDirection = (
  direction: ToolPulloutDirection | undefined,
  flipped: boolean,
  reversed: boolean,
) => {
  const toolFlipped = flipped ? -1 : 1;
  const mirror = mirrorSlot(direction, reversed) ? -1 : 1;
  return (toolFlipped * mirror) == -1;
};

/** Is tool slot direction negative? */
const negativeDirection = (slotDirection: ToolPulloutDirection | undefined) => [
  ToolPulloutDirection.NEGATIVE_Y,
  ToolPulloutDirection.NEGATIVE_X,
].includes(slotDirection || ToolPulloutDirection.NONE);

/** Determine toolbay slot side profile direction. */
const mirrorSlot = (
  slotDirection: ToolPulloutDirection | undefined,
  reversed: boolean,
) => {
  const negative = negativeDirection(slotDirection);
  return reversed ? !negative : negative;
};

/** Determine toolbay slot view angle (front or side). */
export const slotPulloutAxis =
  (slotDirection: ToolPulloutDirection | undefined) => {
    switch (slotDirection) {
      case ToolPulloutDirection.NEGATIVE_X:
      case ToolPulloutDirection.POSITIVE_X:
      default:
        return "x";
      case ToolPulloutDirection.NEGATIVE_Y:
      case ToolPulloutDirection.POSITIVE_Y:
        return "y";
    }
  };

/** Toolbay slot profile. */
const SlotProfile = (props: SlotProfileProps) => {
  const { x, y, width, height, slotDirection, sideView } = props;
  if (!slotDirection) { return <g id={"no-slot-direction"} />; }
  return sideView
    ? <SlotSideProfile x={x} y={y} width={width} height={height}
      mirror={mirrorSlot(slotDirection, props.reversed)} />
    : <SlotFrontProfile x={x} y={y} width={width} height={height} />;
};

/** SVG tool profile element with color and label. */
export const ToolProfile = (props: ProfileToolProps) => {
  const { toolName, x, y, width, height, sideView } = props;
  const toolType = reduceToolName(toolName);
  const fontColor = toolType == ToolName.seeder
    ? Color.darkGray
    : Color.offWhite;
  const bodyColor = getToolColor(toolName);
  const bodyFill = toolType == ToolName.seedTrough
    ? bodyColor
    : `url(#tool-body-gradient-${toolType})`;
  return <g id={"profile-tool"}>
    <defs>
      <linearGradient id={`tool-body-gradient-${toolType}`}>
        <stop offset={"0%"} stopColor={bodyColor} stopOpacity={1} />
        <stop offset={"10%"} stopColor={bodyColor} stopOpacity={0.75} />
        <stop offset={"90%"} stopColor={bodyColor} stopOpacity={0.75} />
        <stop offset={"100%"} stopColor={bodyColor} stopOpacity={1} />
      </linearGradient>
    </defs>
    <rect id={"tool-body"} opacity={0.75} fill={bodyFill}
      x={x} y={y} width={width} height={height} />
    <line x1={x} y1={y} x2={x + width} y2={y}
      stroke={bodyColor} strokeWidth={0.5} opacity={0.75} />
    <line x1={x} y1={y + height} x2={x + width} y2={y + height}
      stroke={bodyColor} strokeWidth={0.5} opacity={0.5} />
    <SlotProfile sideView={sideView}
      slotDirection={props.slotDirection} reversed={props.reversed}
      x={x} y={y} width={width} height={height} />
    <CustomToolProfile toolName={toolName} sideView={sideView}
      xToolMiddle={x + width / 2} yToolBottom={y + height} />
    <ToolImplementProfile x={x + width / 2} y={y + height} toolName={toolName}
      toolFlipped={props.toolFlipped} sideView={sideView} />
    {!(toolType == ToolName.seedTrough && !sideView) &&
      <text x={x + 5} y={y + height - 2.5} opacity={1}
        textLength={width - 10} lengthAdjust={"spacingAndGlyphs"}
        stroke={"none"} fill={fontColor} fontWeight={"bold"}>
        {toolName}
      </text>}
    {(props.coordinate ?? true) && !props.hidePositionIndicator &&
      <circle id={"point-coordinate-indicator"}
        opacity={0.5} fill={Color.darkGray}
        cx={x + width / 2} cy={y} r={5} />}
  </g>;
};

/** Point -> tool profile with color and label (if applicable). */
export const ToolProfilePoint =
  (props: ProfilePointProps<TaggedToolSlotPointer>) => {
    const { point, tools } = props;
    const { tool_id, gantry_mounted, pullout_direction } = point.body;
    const toolName = tools.filter(tool => tool.body.id == tool_id)[0]?.body.name;
    const trough = reduceToolName(toolName) == ToolName.seedTrough;
    const width = trough
      ? troughSize(props.profileAxis == "y").width
      : ToolDimensions.diameter;
    const slotDirection = gantry_mounted
      ? ToolPulloutDirection.NONE
      : pullout_direction;
    return <ToolProfile toolName={toolName} reversed={props.reversed}
      x={props.getX(point.body) - width / 2} y={Math.abs(point.body.z)}
      width={width} height={ToolDimensions.thickness}
      sideView={props.profileAxis == slotPulloutAxis(slotDirection)}
      slotDirection={slotDirection}
      toolFlipped={getToolDirection(
        pullout_direction,
        isToolFlipped(point.body.meta),
        props.reversed)} />;
  };