diff --git a/.gitignore b/.gitignore index 2231fe0333..43a33afd53 100755 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ upgrade_deps.sh # ActiveStorage blobs: storage/* tmp +module_graph.* diff --git a/.madgerc b/.madgerc new file mode 100644 index 0000000000..588d2038a7 --- /dev/null +++ b/.madgerc @@ -0,0 +1,14 @@ +{ + "excludeRegExp": [ + "^(?!.*three_d_garden)", + "[/\\\\]components\\.tsx$", + "[/\\\\]config\\.ts$", + "[/\\\\]helpers\\.ts$", + "[/\\\\]constants\\.ts$", + "[/\\\\]__tests__[/\\\\]" + ], + "fileExtensions": [ + "ts", + "tsx" + ] +} diff --git a/app/mutations/devices/seeders/abstract_express.rb b/app/mutations/devices/seeders/abstract_express.rb index f37161273e..b941a82229 100644 --- a/app/mutations/devices/seeders/abstract_express.rb +++ b/app/mutations/devices/seeders/abstract_express.rb @@ -30,8 +30,8 @@ def settings_change_firmware_config_defaults def tool_slots_slot_1 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -40,8 +40,8 @@ def tool_slots_slot_1 def tool_slots_slot_2 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/abstract_genesis.rb b/app/mutations/devices/seeders/abstract_genesis.rb index ea22973469..c8528582f8 100644 --- a/app/mutations/devices/seeders/abstract_genesis.rb +++ b/app/mutations/devices/seeders/abstract_genesis.rb @@ -27,49 +27,49 @@ def settings_change_firmware_config_defaults def tool_slots_slot_1 add_tool_slot(name: ToolNames::SEEDER, - x: 50, - y: 100, - z: -200, + x: TOOL_X, + y: TOOL_Y + 1 * TOOL_SPACING, + z: TOOL_Z, tool: tools_seeder) end def tool_slots_slot_2 add_tool_slot(name: ToolNames::SEED_BIN, - x: 50, - y: 200, - z: -200, + x: TOOL_X, + y: TOOL_Y + 2 * TOOL_SPACING, + z: TOOL_Z, tool: tools_seed_bin) end def tool_slots_slot_3 add_tool_slot(name: ToolNames::SEED_TRAY, - x: 50, - y: 300, - z: -200, + x: TOOL_X, + y: TOOL_Y + 3 * TOOL_SPACING, + z: TOOL_Z, tool: tools_seed_tray) end def tool_slots_slot_4 add_tool_slot(name: ToolNames::WATERING_NOZZLE, - x: 50, - y: 500, - z: -200, + x: TOOL_X, + y: TOOL_Y + 5 * TOOL_SPACING, + z: TOOL_Z, tool: tools_watering_nozzle) end def tool_slots_slot_5 add_tool_slot(name: ToolNames::SOIL_SENSOR, - x: 50, - y: 600, - z: -200, + x: TOOL_X, + y: TOOL_Y + 6 * TOOL_SPACING, + z: TOOL_Z, tool: tools_soil_sensor) end def tool_slots_slot_6 add_tool_slot(name: ToolNames::WEEDER, - x: 50, - y: 700, - z: -200, + x: TOOL_X, + y: TOOL_Y + 7 * TOOL_SPACING, + z: TOOL_Z, tool: tools_weeder) end @@ -174,7 +174,7 @@ def settings_default_map_size_x end def settings_default_map_size_y - device.web_app_config.update!(map_size_y: 1_400) + device.web_app_config.update!(map_size_y: 1_230) end def pin_bindings_button_1 diff --git a/app/mutations/devices/seeders/abstract_seeder.rb b/app/mutations/devices/seeders/abstract_seeder.rb index 8d689bd726..7b8a5b33cb 100644 --- a/app/mutations/devices/seeders/abstract_seeder.rb +++ b/app/mutations/devices/seeders/abstract_seeder.rb @@ -227,7 +227,7 @@ def settings_change_firmware_config_defaults; end def settings_soil_height; end def settings_soil_height - device.fbos_config.update!(soil_height: -200) + device.fbos_config.update!(soil_height: -500) end def tool_slots_slot_1; end diff --git a/app/mutations/devices/seeders/constants.rb b/app/mutations/devices/seeders/constants.rb index 5830b5378f..f70e39aa88 100644 --- a/app/mutations/devices/seeders/constants.rb +++ b/app/mutations/devices/seeders/constants.rb @@ -4,6 +4,15 @@ module Constants ANALOG = CeleryScriptSettingsBag::ANALOG DIGITAL = CeleryScriptSettingsBag::DIGITAL + TOOL_X = 6 + TOOL_Y = 100 + TOOL_Z = -340 + TOOL_SPACING = 100 + + TROUGH_Y = 0 + TROUGH_Z = -200 + TROUGH_SPACING = 25 + module Names EXPRESS = "FarmBot Express" EXPRESS_XL = "FarmBot Express XL" diff --git a/app/mutations/devices/seeders/genesis_one_five.rb b/app/mutations/devices/seeders/genesis_one_five.rb index 48d358b7ef..69faed8370 100644 --- a/app/mutations/devices/seeders/genesis_one_five.rb +++ b/app/mutations/devices/seeders/genesis_one_five.rb @@ -10,8 +10,8 @@ def settings_firmware def tool_slots_slot_7 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -20,8 +20,8 @@ def tool_slots_slot_7 def tool_slots_slot_8 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/genesis_one_seven.rb b/app/mutations/devices/seeders/genesis_one_seven.rb index e791e74740..18f7392715 100644 --- a/app/mutations/devices/seeders/genesis_one_seven.rb +++ b/app/mutations/devices/seeders/genesis_one_seven.rb @@ -17,17 +17,17 @@ def peripherals_rotary_tool_reverse def tool_slots_slot_6 add_tool_slot(name: ToolNames::ROTARY_TOOL, - x: 50, - y: 700, - z: -200, + x: TOOL_X, + y: TOOL_Y + 7 * TOOL_SPACING, + z: TOOL_Z, tool: tools_rotary) end def tool_slots_slot_7 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -36,8 +36,8 @@ def tool_slots_slot_7 def tool_slots_slot_8 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/genesis_one_six.rb b/app/mutations/devices/seeders/genesis_one_six.rb index f33b0b319b..e3372fed0b 100644 --- a/app/mutations/devices/seeders/genesis_one_six.rb +++ b/app/mutations/devices/seeders/genesis_one_six.rb @@ -17,17 +17,17 @@ def peripherals_rotary_tool_reverse def tool_slots_slot_7 add_tool_slot(name: ToolNames::ROTARY_TOOL, - x: 50, - y: 800, - z: -200, + x: TOOL_X, + y: TOOL_Y + 8 * TOOL_SPACING, + z: TOOL_Z, tool: tools_rotary) end def tool_slots_slot_8 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -36,8 +36,8 @@ def tool_slots_slot_8 def tool_slots_slot_9 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/genesis_xl_one_five.rb b/app/mutations/devices/seeders/genesis_xl_one_five.rb index 15ef0e14ef..276a090702 100644 --- a/app/mutations/devices/seeders/genesis_xl_one_five.rb +++ b/app/mutations/devices/seeders/genesis_xl_one_five.rb @@ -16,14 +16,14 @@ def settings_default_map_size_x end def settings_default_map_size_y - device.web_app_config.update!(map_size_y: 2_900) + device.web_app_config.update!(map_size_y: 2_730) end def tool_slots_slot_7 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -32,8 +32,8 @@ def tool_slots_slot_7 def tool_slots_slot_8 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/genesis_xl_one_four.rb b/app/mutations/devices/seeders/genesis_xl_one_four.rb index aabd146a94..d8ef17c361 100644 --- a/app/mutations/devices/seeders/genesis_xl_one_four.rb +++ b/app/mutations/devices/seeders/genesis_xl_one_four.rb @@ -16,7 +16,7 @@ def settings_default_map_size_x end def settings_default_map_size_y - device.web_app_config.update!(map_size_y: 2_900) + device.web_app_config.update!(map_size_y: 2_730) end end end diff --git a/app/mutations/devices/seeders/genesis_xl_one_seven.rb b/app/mutations/devices/seeders/genesis_xl_one_seven.rb index 1627580b3b..e936beabff 100644 --- a/app/mutations/devices/seeders/genesis_xl_one_seven.rb +++ b/app/mutations/devices/seeders/genesis_xl_one_seven.rb @@ -16,7 +16,7 @@ def settings_default_map_size_x end def settings_default_map_size_y - device.web_app_config.update!(map_size_y: 2_900) + device.web_app_config.update!(map_size_y: 2_730) end def peripherals_rotary_tool @@ -29,17 +29,17 @@ def peripherals_rotary_tool_reverse def tool_slots_slot_6 add_tool_slot(name: ToolNames::ROTARY_TOOL, - x: 50, - y: 700, - z: -200, + x: TOOL_X, + y: TOOL_Y + 7 * TOOL_SPACING, + z: TOOL_Z, tool: tools_rotary) end def tool_slots_slot_7 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -48,8 +48,8 @@ def tool_slots_slot_7 def tool_slots_slot_8 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/genesis_xl_one_six.rb b/app/mutations/devices/seeders/genesis_xl_one_six.rb index 8376a22d49..4292ccce5e 100644 --- a/app/mutations/devices/seeders/genesis_xl_one_six.rb +++ b/app/mutations/devices/seeders/genesis_xl_one_six.rb @@ -16,7 +16,7 @@ def settings_default_map_size_x end def settings_default_map_size_y - device.web_app_config.update!(map_size_y: 2_900) + device.web_app_config.update!(map_size_y: 2_730) end def peripherals_rotary_tool @@ -29,17 +29,17 @@ def peripherals_rotary_tool_reverse def tool_slots_slot_7 add_tool_slot(name: ToolNames::ROTARY_TOOL, - x: 50, - y: 800, - z: -200, + x: TOOL_X, + y: TOOL_Y + 8 * TOOL_SPACING, + z: TOOL_Z, tool: tools_rotary) end def tool_slots_slot_8 add_tool_slot(name: ToolNames::SEED_TROUGH_1, x: 0, - y: 25, - z: -100, + y: TROUGH_Y, + z: TROUGH_Z, tool: tools_seed_trough_1, pullout_direction: ToolSlot::NONE, gantry_mounted: true) @@ -48,8 +48,8 @@ def tool_slots_slot_8 def tool_slots_slot_9 add_tool_slot(name: ToolNames::SEED_TROUGH_2, x: 0, - y: 50, - z: -100, + y: TROUGH_Y + TROUGH_SPACING, + z: TROUGH_Z, tool: tools_seed_trough_2, pullout_direction: ToolSlot::NONE, gantry_mounted: true) diff --git a/app/mutations/devices/seeders/tool_slot_fixtures.yml b/app/mutations/devices/seeders/tool_slot_fixtures.yml deleted file mode 100644 index e815c49c65..0000000000 --- a/app/mutations/devices/seeders/tool_slot_fixtures.yml +++ /dev/null @@ -1,109 +0,0 @@ ---- -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 1 - radius: 25.0 - depth: 0 - x: 50.0 - y: 100.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 1 - radius: 25.0 - depth: 0 - x: 50.0 - y: 500.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 1 - radius: 25.0 - depth: 0 - x: 50.0 - y: 200.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 1 - radius: 25.0 - depth: 0 - x: 50.0 - y: 300.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 1 - radius: 25.0 - depth: 0 - x: 50.0 - y: 600.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 0 - radius: 25.0 - depth: 0 - x: 0.0 - y: 25.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 0 - radius: 25.0 - depth: 0 - x: 0.0 - y: 50.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 1 - radius: 25.0 - depth: 0 - x: 50.0 - y: 700.0 - z: -200.0 -- meta: {} - pointer_type: ToolSlot - name: Tool Slot - openfarm_slug: not-set - plant_stage: planned - planted_at: - pullout_direction: 0 - radius: 25.0 - depth: 0 - x: 0.0 - y: 75.0 - z: -200.0 diff --git a/db/migrate/20250221191831_change_map_size_y_default.rb b/db/migrate/20250221191831_change_map_size_y_default.rb new file mode 100644 index 0000000000..84acb13c2d --- /dev/null +++ b/db/migrate/20250221191831_change_map_size_y_default.rb @@ -0,0 +1,5 @@ +class ChangeMapSizeYDefault < ActiveRecord::Migration[6.1] + def change + change_column_default(:web_app_configs, :map_size_y, from: 1400, to: 1230) + end +end diff --git a/db/structure.sql b/db/structure.sql index c0f963f558..60712aa0db 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,6 +1,7 @@ SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); @@ -2005,7 +2006,7 @@ CREATE TABLE public.web_app_configs ( show_pins boolean DEFAULT false, disable_emergency_unlock_confirmation boolean DEFAULT true, map_size_x integer DEFAULT 2900, - map_size_y integer DEFAULT 1400, + map_size_y integer DEFAULT 1230, expand_step_options boolean DEFAULT false, hide_sensors boolean DEFAULT false, confirm_plant_deletion boolean DEFAULT true, @@ -3979,6 +3980,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240405171128'), ('20240625195838'), ('20241203194030'), -('20241203211516'); +('20241203211516'), +('20250221191831'); diff --git a/frontend/__test_support__/fake_html_events.ts b/frontend/__test_support__/fake_html_events.ts index dd3fa91a56..0584a9230b 100644 --- a/frontend/__test_support__/fake_html_events.ts +++ b/frontend/__test_support__/fake_html_events.ts @@ -12,17 +12,6 @@ export const changeEvent = (value: string): ChangeEvent => { return event as ChangeEvent; }; -type IMGEvent = React.SyntheticEvent; -export const imgEvent = (): IMGEvent => { - const event: DeepPartial = { - currentTarget: { - getAttribute: jest.fn(), - setAttribute: jest.fn(), - } - }; - return event as IMGEvent; -}; - type FormEvent = React.FormEvent; export const formEvent = (): FormEvent => { const event: Partial = { preventDefault: jest.fn() }; diff --git a/frontend/__test_support__/fake_props.ts b/frontend/__test_support__/fake_props.ts index 6020edd1ab..68a4c9dfa2 100644 --- a/frontend/__test_support__/fake_props.ts +++ b/frontend/__test_support__/fake_props.ts @@ -6,7 +6,7 @@ export const fakeAddPlantProps = (plants: TaggedPlant[]): AddPlantProps => ({ gridSize: { x: 1000, y: 2000 }, dispatch: jest.fn(), - getConfigValue: jest.fn(), + getConfigValue: jest.fn(() => true), plants, curves: [], designer: fakeDesignerState(), diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index d3be6a923e..051113a369 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -389,7 +389,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig { default_plant_depth: 5, disable_emergency_unlock_confirmation: false, map_size_x: 2900, - map_size_y: 1400, + map_size_y: 1230, user_interface_read_only_mode: false, ["three_d_garden" as BooleanConfigKey]: false, ["dark_mode" as BooleanConfigKey]: false, diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index ad1a153a8d..a4a59c1ef3 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -9,6 +9,19 @@ import { import * as THREE from "three"; import React, { ReactNode } from "react"; import { TransitionFn, UseSpringProps } from "@react-spring/three"; +import { ThreeElements } from "@react-three/fiber"; + +const GroupForTests = (props: ThreeElements["group"]) => + // @ts-expect-error Property does not exist on type JSX.IntrinsicElements + ; + +jest.mock("../three_d_garden/components", () => ({ + ...jest.requireActual("../three_d_garden/components"), + Group: (props: ThreeElements["group"]) => + props.visible === false + ? <> + : , +})); jest.mock("three/examples/jsm/Addons.js", () => ({ SVGLoader: class { @@ -383,6 +396,12 @@ jest.mock("@react-three/drei", () => { PaletteMaterial001: {} as THREE.MeshStandardMaterial, }, }, + [ASSETS.models.seedTrough]: { + nodes: { Seed_Trough: {} as THREE.Mesh }, + materials: { + [SeedTroughAssemblyMaterial.two]: {} as THREE.MeshStandardMaterial, + }, + }, [ASSETS.models.seedTroughAssembly]: { nodes: { mesh0_mesh: {} as THREE.Mesh, @@ -454,8 +473,19 @@ jest.mock("@react-three/drei", () => { [PartName.toolbay3Logo]: {} as THREE.Mesh, }, }, + [ASSETS.models.toolbay1]: { + nodes: { + [PartName.toolbay1]: {} as THREE.Mesh, + [PartName.toolbay1Logo]: {} as THREE.Mesh, + }, + }, [ASSETS.models.seeder]: { nodes: { [PartName.seeder]: {} as THREE.Mesh }, + materials: { PaletteMaterial001: {} as THREE.MeshStandardMaterial }, + }, + [ASSETS.models.weeder]: { + nodes: { [PartName.weeder]: {} as THREE.Mesh }, + materials: { PaletteMaterial001: {} as THREE.MeshStandardMaterial }, }, [ASSETS.models.seedTray]: { nodes: { [PartName.seedTray]: {} as THREE.Mesh }, @@ -612,8 +642,8 @@ jest.mock("@react-three/drei", () => {
{name}
, Billboard: ({ name, children }: { name: string, children: ReactNode }) =>
{children}
, - Image: ({ name }: { name: string }) => -
{name}
, + Image: ({ name, url }: { name: string, url: string }) => +
{name} {url}
, Clouds: ({ name }: { name: string }) =>
{name}
, Cloud: ({ name }: { name: string }) => diff --git a/frontend/css/farm_designer/three_d_garden.scss b/frontend/css/farm_designer/three_d_garden.scss index 4c31e5f76b..eff5d11360 100644 --- a/frontend/css/farm_designer/three_d_garden.scss +++ b/frontend/css/farm_designer/three_d_garden.scss @@ -158,11 +158,7 @@ white-space: nowrap; color: $off_black; - &.outdoor.active, - &.lab.active, - &.genesis.active, - &.standard.active, - &.mobile.active { + &.active { background: rgba(255, 255, 255, 0.6); } diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts index 49d9d88a9a..72acfc3b4c 100644 --- a/frontend/external_urls.ts +++ b/frontend/external_urls.ts @@ -91,7 +91,9 @@ export namespace ExternalUrl { export const cameraCalibrationCard = `${PRODUCTS}/camera-calibration-card`; export const cameraReplacement = `${PRODUCTS}/genesis-v1-5-express-v1-0-camera-free-replacement`; - export const genesisKit = `${KITS}/farmbot-genesis-v1-7`; - export const genesisXlKit = `${KITS}/farmbot-genesis-xl-v1-7`; + export const genesisKit = (version: string) => + `${KITS}/farmbot-genesis-${version.replace(".", "-")}`; + export const genesisXlKit = (version: string) => + `${KITS}/farmbot-genesis-xl-${version.replace(".", "-")}`; } } diff --git a/frontend/farm_designer/__tests__/index_test.tsx b/frontend/farm_designer/__tests__/index_test.tsx index d2f524ff40..fd7b58761a 100644 --- a/frontend/farm_designer/__tests__/index_test.tsx +++ b/frontend/farm_designer/__tests__/index_test.tsx @@ -5,6 +5,13 @@ jest.mock("../../api/crud", () => ({ jest.mock("../../plants/plant_inventory", () => ({ Plants: () =>
})); +let mockIsMobile = false; +let mockIsDesktop = false; +jest.mock("../../screen_size", () => ({ + isMobile: () => mockIsMobile, + isDesktop: () => mockIsDesktop, +})); + import React from "react"; import { getDefaultAxisLength, getGridSize, RawFarmDesigner as FarmDesigner, @@ -81,7 +88,7 @@ describe("", () => { expect(legendProps.imageAgeInfo).toEqual({ newestDate: "", toOldest: 1 }); const gardenMapProps = wrapper.find(GardenMap).props(); expect(gardenMapProps.mapTransformProps.gridSize.x).toEqual(2900); - expect(gardenMapProps.mapTransformProps.gridSize.y).toEqual(1400); + expect(gardenMapProps.mapTransformProps.gridSize.y).toEqual(1230); }); it("loads image info", () => { @@ -122,13 +129,38 @@ describe("", () => { }); it("renders saved garden indicator", () => { + mockIsMobile = false; + mockIsDesktop = true; + const p = fakeProps(); + p.designer.openedSavedGarden = 1; + p.designer.panelOpen = false; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("viewing saved garden"); + expect(wrapper.html()).not.toContain("three-d-garden"); + }); + + it("renders saved garden indicator on medium screens", () => { + mockIsMobile = false; + mockIsDesktop = false; const p = fakeProps(); p.designer.openedSavedGarden = 1; + p.designer.panelOpen = false; const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("viewing saved garden"); expect(wrapper.html()).not.toContain("three-d-garden"); }); + it("doesn't render saved garden indicator", () => { + mockIsMobile = true; + mockIsDesktop = false; + const p = fakeProps(); + p.designer.openedSavedGarden = 1; + p.designer.panelOpen = false; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("viewing saved garden"); + expect(wrapper.html()).not.toContain("three-d-garden"); + }); + it("toggles setting", () => { const p = fakeProps(); const state = fakeState(); @@ -167,7 +199,7 @@ describe("", () => { describe("getDefaultAxisLength()", () => { it("returns axis lengths", () => { const axes = getDefaultAxisLength(() => false); - expect(axes).toEqual({ x: 2900, y: 1400, z: 400 }); + expect(axes).toEqual({ x: 2900, y: 1230, z: 400 }); }); }); @@ -180,7 +212,7 @@ describe("getGridSize()", () => { y: { value: 200, isDefault: false }, z: { value: 400, isDefault: true }, }); - expect(grid).toEqual({ x: 2900, y: 1400 }); + expect(grid).toEqual({ x: 2900, y: 1230 }); }); it("returns custom grid size", () => { diff --git a/frontend/farm_designer/__tests__/state_to_props_test.ts b/frontend/farm_designer/__tests__/state_to_props_test.ts index bd0be15333..10c1916914 100644 --- a/frontend/farm_designer/__tests__/state_to_props_test.ts +++ b/frontend/farm_designer/__tests__/state_to_props_test.ts @@ -190,7 +190,7 @@ describe("botSize()", () => { const state = fakeState(); expect(botSize(state)).toEqual({ x: { value: 2900, isDefault: true }, - y: { value: 1400, isDefault: true }, + y: { value: 1230, isDefault: true }, z: { value: 400, isDefault: true }, }); }); diff --git a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx index ab47204a77..511adb4a8b 100644 --- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx +++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx @@ -25,6 +25,11 @@ describe("", () => { dispatch: jest.fn(), getWebAppConfigValue: jest.fn(), curves: [], + mapPoints: [], + weeds: [], + botPosition: { x: 1, y: 2, z: 3 }, + negativeZ: false, + mountedToolName: undefined, }); it("converts props", () => { @@ -37,6 +42,9 @@ describe("", () => { expectedConfig.bedWidthOuter = 1660; expectedConfig.botSizeX = 3000; expectedConfig.botSizeY = 1500; + expectedConfig.x = 1; + expectedConfig.y = 2; + expectedConfig.z = 3; expectedConfig.ccSupportSize = 1; expectedConfig.beamLength = 1; expectedConfig.columnLength = 1; @@ -55,6 +63,33 @@ describe("", () => { expect(ThreeDGarden).toHaveBeenCalledWith({ config: expectedConfig, addPlantProps: expect.any(Object), + mapPoints: [], + weeds: [], + }, {}); + }); + + it("converts props: unknown position", () => { + const p = fakeProps(); + p.botPosition = { x: undefined, y: undefined, z: undefined }; + render(); + expect(ThreeDGarden).toHaveBeenCalledWith({ + config: expect.objectContaining({ x: 0, y: 0, z: 0 }), + addPlantProps: expect.any(Object), + mapPoints: [], + weeds: [], + }, {}); + }); + + it("converts props: negative z", () => { + const p = fakeProps(); + p.botPosition = { x: undefined, y: undefined, z: -100 }; + p.negativeZ = true; + render(); + expect(ThreeDGarden).toHaveBeenCalledWith({ + config: expect.objectContaining({ negativeZ: true, x: 0, y: 0, z: -100 }), + addPlantProps: expect.any(Object), + mapPoints: [], + weeds: [], }, {}); }); }); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index cb2194b5d0..5e2d3862ef 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -27,6 +27,7 @@ import { ThreeDGardenMap } from "./three_d_garden_map"; import { Outlet } from "react-router"; import { ErrorBoundary } from "../error_boundary"; import { get3DConfigValueFunction } from "../settings/three_d_settings"; +import { isDesktop, isMobile } from "../screen_size"; export const getDefaultAxisLength = (getConfigValue: GetWebAppConfigValue): Record => { @@ -35,7 +36,7 @@ export const getDefaultAxisLength = if (isFinite(mapSizeX) && isFinite(mapSizeY)) { return { x: mapSizeX, y: mapSizeY, z: 400 }; } - return { x: 2900, y: 1400, z: 400 }; + return { x: 2900, y: 1230, z: 400 }; }; export const getGridSize = ( @@ -211,11 +212,17 @@ export class RawFarmDesigner plants={this.props.plants} get3DConfigValue={get3DConfigValueFunction(this.props.farmwareEnvs)} sourceFbosConfig={this.props.sourceFbosConfig} + negativeZ={!!this.props.botMcuParams.movement_home_up_z} gridOffset={gridOffset} mapTransformProps={this.mapTransformProps} botSize={this.props.botSize} dispatch={this.props.dispatch} curves={this.props.curves} + mapPoints={this.props.genericPoints} + weeds={this.props.weeds} + toolSlots={this.props.toolSlots} + mountedToolName={this.props.mountedToolInfo.name} + botPosition={this.props.botLocationData.position} getWebAppConfigValue={this.props.getConfigValue} /> :
} - {this.props.designer.openedSavedGarden && + {this.props.designer.openedSavedGarden + && !isMobile() + && (isDesktop() || !this.props.designer.panelOpen) && } {!this.props.getConfigValue(BooleanSetting.three_d_garden) && diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx index 9f8bd7fe20..cc1cd77561 100644 --- a/frontend/farm_designer/three_d_garden_map.tsx +++ b/frontend/farm_designer/three_d_garden_map.tsx @@ -5,10 +5,14 @@ import { BotSize, MapTransformProps, AxisNumberProperty, TaggedPlant, } from "./map/interfaces"; import { clone } from "lodash"; -import { SourceFbosConfig } from "../devices/interfaces"; -import { ConfigurationName, TaggedCurve } from "farmbot"; +import { BotPosition, SourceFbosConfig } from "../devices/interfaces"; +import { + ConfigurationName, TaggedCurve, TaggedGenericPointer, TaggedWeedPointer, +} from "farmbot"; import { DesignerState } from "./interfaces"; import { GetWebAppConfigValue } from "../config_storage/actions"; +import { BooleanSetting } from "../session_keys"; +import { SlotWithTool } from "../resources/interfaces"; export interface ThreeDGardenMapProps { botSize: BotSize; @@ -16,11 +20,17 @@ export interface ThreeDGardenMapProps { gridOffset: AxisNumberProperty; get3DConfigValue(key: string): number; sourceFbosConfig: SourceFbosConfig; + negativeZ: boolean; designer: DesignerState; plants: TaggedPlant[]; dispatch: Function; getWebAppConfigValue: GetWebAppConfigValue; curves: TaggedCurve[]; + mapPoints: TaggedGenericPointer[]; + weeds: TaggedWeedPointer[]; + botPosition: BotPosition; + toolSlots?: SlotWithTool[]; + mountedToolName: string | undefined; } export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { @@ -31,6 +41,13 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.bedWidthOuter = gridSize.y + 160; config.bedLengthOuter = gridSize.x + 160; config.zoomBeacons = false; + config.trail = !!props.getWebAppConfigValue(BooleanSetting.display_trail); + + config.negativeZ = props.negativeZ; + + config.x = props.botPosition.x || 0; + config.y = props.botPosition.y || 0; + config.z = props.botPosition.z || 0; const { designer } = props; config.distanceIndicator = designer.distanceIndicator; @@ -59,6 +76,10 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { return ({ initSave: jest.fn(), })); +jest.mock("../../devices/actions", () => ({ + updateConfig: jest.fn(), +})); + import { saveOrEditFarmwareEnv, getEnv, generateFarmwareDictionary, isPendingInstallation, @@ -17,6 +21,7 @@ import { import { edit, initSave, save } from "../../api/crud"; import { fakeFarmware } from "../../__test_support__/fake_farmwares"; import { fakeState } from "../../__test_support__/fake_state"; +import { updateConfig } from "../../devices/actions"; describe("getEnv()", () => { it("returns API farmware env", () => { @@ -86,4 +91,13 @@ describe("saveOrEditFarmwareEnv()", () => { expect(initSave).toHaveBeenCalledWith("FarmwareEnv", { key: "new_key", value: "new_value" }); }); + + it("saves new env var and updates soil height", () => { + const ri = buildResourceIndex([]).index; + saveOrEditFarmwareEnv(ri, true)( + "measure_soil_height_measured_distance", "100")(jest.fn()); + expect(initSave).toHaveBeenCalledWith("FarmwareEnv", + { key: "measure_soil_height_measured_distance", value: "100" }); + expect(updateConfig).toHaveBeenCalledWith({ soil_height: -100 }); + }); }); diff --git a/frontend/farmware/state_to_props.ts b/frontend/farmware/state_to_props.ts index 0aef8d89ad..224b2fae18 100644 --- a/frontend/farmware/state_to_props.ts +++ b/frontend/farmware/state_to_props.ts @@ -8,23 +8,28 @@ import { save, edit, initSave } from "../api/crud"; import { FarmwareManifestInfo, Farmwares, SaveFarmwareEnv } from "./interfaces"; import { manifestInfo, manifestInfoPending } from "./generate_manifest_info"; import { t } from "../i18next_wrapper"; +import { updateConfig } from "../devices/actions"; /** Edit an existing Farmware env variable or add a new one. */ -export const saveOrEditFarmwareEnv = (ri: ResourceIndex): SaveFarmwareEnv => - (key: string, value: string) => (dispatch: Function) => { - const fwEnvLookup: Record = {}; - selectAllFarmwareEnvs(ri) - .map(x => { fwEnvLookup[x.body.key] = x; }); - if (Object.keys(fwEnvLookup).includes(key)) { - const fwEnv = fwEnvLookup[key]; - if (value != fwEnv.body.value) { - dispatch(edit(fwEnv, { value })); - dispatch(save(fwEnv.uuid)); +export const saveOrEditFarmwareEnv = + (ri: ResourceIndex, copyDistance = false): SaveFarmwareEnv => + (key: string, value: string) => (dispatch: Function) => { + const fwEnvLookup: Record = {}; + selectAllFarmwareEnvs(ri) + .map(x => { fwEnvLookup[x.body.key] = x; }); + if (Object.keys(fwEnvLookup).includes(key)) { + const fwEnv = fwEnvLookup[key]; + if (value != fwEnv.body.value) { + dispatch(edit(fwEnv, { value })); + dispatch(save(fwEnv.uuid)); + } + } else { + dispatch(initSave("FarmwareEnv", { key, value })); } - } else { - dispatch(initSave("FarmwareEnv", { key, value })); - } - }; + if (copyDistance && key == "measure_soil_height_measured_distance") { + dispatch(updateConfig({ soil_height: -parseInt(value) })); + } + }; export const isPendingInstallation = (farmware: FarmwareManifestInfo | undefined) => !farmware || farmware.installation_pending; diff --git a/frontend/plants/plant_panel.tsx b/frontend/plants/plant_panel.tsx index 670ee038e2..cb7b89092b 100644 --- a/frontend/plants/plant_panel.tsx +++ b/frontend/plants/plant_panel.tsx @@ -179,7 +179,7 @@ export function PlantPanel(props: PlantPanelProps) { navigate(Path.cropSearch()); }} /> - {(timeSettings && !inSavedGarden) && + {timeSettings && !inSavedGarden &&
@@ -187,9 +187,7 @@ export function PlantPanel(props: PlantPanelProps) { datePlanted={plantedAt} timeSettings={timeSettings} />
- {(!inSavedGarden) - ? - : t(startCase(plantStatus))} +
{daysOldText({ age: daysOld, stage: plantStatus })} diff --git a/frontend/points/__tests__/point_inventory_test.tsx b/frontend/points/__tests__/point_inventory_test.tsx index f02e5e8504..7912a41ffe 100644 --- a/frontend/points/__tests__/point_inventory_test.tsx +++ b/frontend/points/__tests__/point_inventory_test.tsx @@ -136,6 +136,17 @@ describe("", () => { expect(wrapper.text()).not.toContain("point 1"); }); + it("filters point grids", () => { + const p = fakeProps(); + const gridPoint = fakePoint(); + gridPoint.body.meta.gridId = "123"; + gridPoint.body.name = "mesh"; + p.genericPoints = [gridPoint]; + const wrapper = mount(); + wrapper.setState({ searchTerm: "0" }); + expect(wrapper.text()).not.toContain("mesh"); + }); + it("changes sort term", () => { const wrapper = shallow(); const menu = wrapper.find(SearchField).props().customLeftIcon; diff --git a/frontend/points/point_inventory.tsx b/frontend/points/point_inventory.tsx index 72dae8f014..e181c29b98 100644 --- a/frontend/points/point_inventory.tsx +++ b/frontend/points/point_inventory.tsx @@ -198,7 +198,7 @@ export class RawPoints extends React.Component { dispatch(createGroup({ criteria: { ...DEFAULT_CRITERIA, @@ -283,6 +283,7 @@ export class RawPoints extends React.Component { dispatch={dispatch} />)} {gridIds.map(gridId => { const gridPoints = points.filter(p => p.body.meta.gridId == gridId); + if (gridPoints.length == 0) { return
; } const pointName = gridPoints[0].body.name; return = { show_pins: false, disable_emergency_unlock_confirmation: true, map_size_x: 2900, - map_size_y: 1400, + map_size_y: 1230, expand_step_options: false, hide_sensors: false, confirm_plant_deletion: true, diff --git a/frontend/settings/dev/__tests__/dev_settings_test.tsx b/frontend/settings/dev/__tests__/dev_settings_test.tsx index 7247153d0a..f067edeca6 100644 --- a/frontend/settings/dev/__tests__/dev_settings_test.tsx +++ b/frontend/settings/dev/__tests__/dev_settings_test.tsx @@ -9,6 +9,7 @@ import { mount, shallow } from "enzyme"; import { DevWidgetFERow, DevWidgetFBOSRow, DevWidgetDelModeRow, DevWidgetShowInternalEnvsRow, + DevWidget3dCameraRow, } from "../dev_settings"; import { DevSettings } from "../dev_support"; import { setWebAppConfigValue } from "../../../config_storage/actions"; @@ -52,6 +53,40 @@ describe("", () => { }); }); +describe("", () => { + const MOCK_CAMERA_VALUE = "{\"position\": [0, 0, 0], \"target\": [0, 0, 0]}"; + + it("changes dev camera value", () => { + const wrapper = shallow(); + wrapper.find("BlurableInput").simulate("commit", + { currentTarget: { value: MOCK_CAMERA_VALUE } }); + expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", + JSON.stringify({ [DevSettings.CAMERA3D]: MOCK_CAMERA_VALUE })); + wrapper.find(".fa-times").simulate("click"); + expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}"); + delete mockDevSettings[DevSettings.CAMERA3D]; + }); + + it("enables dev camera position", () => { + const wrapper = mount(); + wrapper.find(".fa-angle-double-up").simulate("click"); + expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", + JSON.stringify({ + [DevSettings.CAMERA3D]: JSON.stringify( + { position: [-500, -500, 400], target: [-1500, -200, 200] }) + })); + delete mockDevSettings[DevSettings.CAMERA3D]; + }); + + it("disables dev camera position", () => { + mockDevSettings[DevSettings.CAMERA3D] = MOCK_CAMERA_VALUE; + const wrapper = mount(); + wrapper.find(".fa-times").simulate("click"); + expect(setWebAppConfigValue).toHaveBeenCalledWith("internal_use", "{}"); + delete mockDevSettings[DevSettings.CAMERA3D]; + }); +}); + describe("", () => { it("enables delete mode", () => { const wrapper = mount(); diff --git a/frontend/settings/dev/dev_settings.tsx b/frontend/settings/dev/dev_settings.tsx index bdfd5a13fc..82c56a7c38 100644 --- a/frontend/settings/dev/dev_settings.tsx +++ b/frontend/settings/dev/dev_settings.tsx @@ -32,6 +32,24 @@ export const DevWidgetFBOSRow = () => { ; }; +export const DevWidget3dCameraRow = () => { + return + +
+
+
; +}; + export const DevWidgetDelModeRow = () =>
; }; interface PromoInfoProps { isGenesis: boolean; + kitVersion: string; } const PromoInfo = (props: PromoInfoProps) => { - const { isGenesis } = props; + const { isGenesis, kitVersion } = props; return

Explore our models

{isGenesis @@ -118,7 +122,7 @@ const PromoInfo = (props: PromoInfoProps) => {

FarmBot Genesis is our flagship kit for prosumers and enthusiasts featuring our most advanced technology, features, and options. - Coming 90% pre-assembled in the box, Genesis can be installed on + Coming 95% pre-assembled in the box, Genesis can be installed on an existing raised bed in an afternoon. It is suitable for fixed or mobile raised beds in classrooms, research labs, and backyards.

@@ -139,13 +143,14 @@ const PromoInfo = (props: PromoInfoProps) => { + ? ExternalUrl.Store.genesisKit(kitVersion) + : ExternalUrl.Store.genesisXlKit(kitVersion)}>

Order Genesis

XL

+

{kitVersion}

; }; @@ -284,7 +289,9 @@ export const PrivateOverlay = (props: OverlayProps) => { - + @@ -329,7 +336,7 @@ export const PrivateOverlay = (props: OverlayProps) => { + options={["Outdoor", "Lab", "Greenhouse"]} /> diff --git a/frontend/three_d_garden/constants.ts b/frontend/three_d_garden/constants.ts index 96889e1921..19eff71617 100644 --- a/frontend/three_d_garden/constants.ts +++ b/frontend/three_d_garden/constants.ts @@ -15,6 +15,7 @@ export const ASSETS: Record> = { aluminum: "/3D/textures/aluminum.avif", concrete: "/3D/textures/concrete.avif", screen: "/3D/textures/screen.avif", + bricks: "/3D/textures/bricks.avif", }, shapes: { track: "/3D/shapes/track.svg", @@ -36,10 +37,13 @@ export const ASSETS: Record> = { horizontalMotorHousing: "/3D/models/horizontal_motor_housing.glb", zAxisMotorMount: "/3D/models/z_axis_motor_mount.glb", toolbay3: "/3D/models/toolbay_3.glb", + toolbay1: "/3D/models/toolbay_1.glb", rotaryTool: "/3D/models/rotary_tool.glb", seeder: "/3D/models/seeder.glb", + weeder: "/3D/models/weeder.glb", seedTray: "/3D/models/seed_tray.glb", seedBin: "/3D/models/seed_bin.glb", + seedTrough: "/3D/models/seed_trough.glb", seedTroughAssembly: "/3D/models/seed_trough_assembly.glb", seedTroughHolder: "/3D/models/seed_trough_holder.glb", soilSensor: "/3D/models/soil_sensor.glb", @@ -56,12 +60,18 @@ export const ASSETS: Record> = { }, other: { gear: "/app-resources/img/icons/settings.svg", + weed: "/3D/icons/generic-weed.avif", + plant: "/3D/icons/generic-plant.avif", }, people: { person1: "/3D/people/person_1.avif", person1Flipped: "/3D/people/person_1_flipped.avif", person2: "/3D/people/person_2.avif", person2Flipped: "/3D/people/person_2_flipped.avif", + person3: "/3D/people/person_3.avif", + person3Flipped: "/3D/people/person_3_flipped.avif", + person4: "/3D/people/person_4.avif", + person4Flipped: "/3D/people/person_4_flipped.avif", }, }; @@ -339,11 +349,15 @@ export enum PartName { zAxisMotorMount = "Z-Axis_Motor_Mount", toolbay3 = "mesh0_mesh", toolbay3Logo = "mesh0_mesh_1", + toolbay1 = "mesh0_mesh", + toolbay1Logo = "mesh0_mesh_1", seeder = "Seeder_Brass_Insert", + weeder = "Weeder_Blade_(medium)", vacuumPump = "Lower_Vacuum_Tube", wateringNozzle = "M5_x_30mm_Screw", seedBin = "Seed_Bin", seedTray = "Seed_Tray", + seedTrough = "Seed_Trough", cameraMountHalf = "Camera_Mount_Half", pi = "Raspberry_Pi_4B", farmduino = "Farmduino", diff --git a/frontend/three_d_garden/__tests__/arrow_test.tsx b/frontend/three_d_garden/elements/__tests__/arrow_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/arrow_test.tsx rename to frontend/three_d_garden/elements/__tests__/arrow_test.tsx diff --git a/frontend/three_d_garden/__tests__/button_test.tsx b/frontend/three_d_garden/elements/__tests__/button_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/button_test.tsx rename to frontend/three_d_garden/elements/__tests__/button_test.tsx diff --git a/frontend/three_d_garden/__tests__/distance_indicator_test.tsx b/frontend/three_d_garden/elements/__tests__/distance_indicator_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/distance_indicator_test.tsx rename to frontend/three_d_garden/elements/__tests__/distance_indicator_test.tsx diff --git a/frontend/three_d_garden/elements/__tests__/text_test.tsx b/frontend/three_d_garden/elements/__tests__/text_test.tsx new file mode 100644 index 0000000000..80dad52894 --- /dev/null +++ b/frontend/three_d_garden/elements/__tests__/text_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Text, TextProps } from "../text"; + +describe("", () => { + const fakeProps = (): TextProps => ({ + children: "text", + position: [0, 0, 0], + rotation: [0, 0, 0], + fontSize: 10, + color: "black", + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("text"); + }); +}); diff --git a/frontend/three_d_garden/arrow.tsx b/frontend/three_d_garden/elements/arrow.tsx similarity index 94% rename from frontend/three_d_garden/arrow.tsx rename to frontend/three_d_garden/elements/arrow.tsx index 8040f0af79..570d599a6b 100644 --- a/frontend/three_d_garden/arrow.tsx +++ b/frontend/three_d_garden/elements/arrow.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Extrude } from "@react-three/drei"; import { Shape } from "three"; -import { MeshPhongMaterial } from "./components"; +import { MeshPhongMaterial } from "../components"; export interface ArrowProps { length: number; diff --git a/frontend/three_d_garden/button.tsx b/frontend/three_d_garden/elements/button.tsx similarity index 97% rename from frontend/three_d_garden/button.tsx rename to frontend/three_d_garden/elements/button.tsx index 449d8500c8..48cb250e44 100644 --- a/frontend/three_d_garden/button.tsx +++ b/frontend/three_d_garden/elements/button.tsx @@ -2,7 +2,7 @@ import React from "react"; import * as THREE from "three"; import { Box } from "@react-three/drei"; import { BufferGeometry } from "three"; -import { Group, MeshPhongMaterial } from "./components"; +import { Group, MeshPhongMaterial } from "../components"; import { Text } from "./text"; export interface PresetButtonProps { diff --git a/frontend/three_d_garden/distance_indicator.tsx b/frontend/three_d_garden/elements/distance_indicator.tsx similarity index 97% rename from frontend/three_d_garden/distance_indicator.tsx rename to frontend/three_d_garden/elements/distance_indicator.tsx index 9dc795e374..73e7482210 100644 --- a/frontend/three_d_garden/distance_indicator.tsx +++ b/frontend/three_d_garden/elements/distance_indicator.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Box } from "@react-three/drei"; import { Arrow } from "./arrow"; -import { Group, MeshPhongMaterial } from "./components"; +import { Group, MeshPhongMaterial } from "../components"; import { Text } from "./text"; enum BoxDimension { diff --git a/frontend/three_d_garden/elements/index.ts b/frontend/three_d_garden/elements/index.ts new file mode 100644 index 0000000000..5c72cace48 --- /dev/null +++ b/frontend/three_d_garden/elements/index.ts @@ -0,0 +1,4 @@ +export * from "./arrow"; +export * from "./button"; +export * from "./distance_indicator"; +export * from "./text"; diff --git a/frontend/three_d_garden/text.tsx b/frontend/three_d_garden/elements/text.tsx similarity index 85% rename from frontend/three_d_garden/text.tsx rename to frontend/three_d_garden/elements/text.tsx index 0a5d413667..5380f7457f 100644 --- a/frontend/three_d_garden/text.tsx +++ b/frontend/three_d_garden/elements/text.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Center, Text3D } from "@react-three/drei"; -import { ASSETS } from "./constants"; -import { MeshPhongMaterial } from "./components"; +import { ASSETS } from "../constants"; +import { MeshPhongMaterial } from "../components"; -interface TextProps { +export interface TextProps { children: React.ReactNode; position: [number, number, number]; rotation: [number, number, number]; diff --git a/frontend/three_d_garden/garden/__tests__/clouds_test.tsx b/frontend/three_d_garden/garden/__tests__/clouds_test.tsx new file mode 100644 index 0000000000..d2884a48c1 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/clouds_test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Clouds, CloudsProps } from "../clouds"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): CloudsProps => ({ + config: clone(INITIAL), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("clouds"); + }); +}); diff --git a/frontend/three_d_garden/garden/__tests__/grid_test.tsx b/frontend/three_d_garden/garden/__tests__/grid_test.tsx new file mode 100644 index 0000000000..986d9fc40b --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/grid_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Grid, GridProps } from "../grid"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): GridProps => ({ + config: clone(INITIAL), + }); + + it("renders", () => { + const p = fakeProps(); + p.config.grid = true; + const { container } = render(); + expect(container).toContainHTML("grid"); + }); +}); diff --git a/frontend/three_d_garden/garden/__tests__/ground_test.tsx b/frontend/three_d_garden/garden/__tests__/ground_test.tsx new file mode 100644 index 0000000000..83e4d63ef2 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/ground_test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Ground, GroundProps } from "../ground"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): GroundProps => ({ + config: clone(INITIAL), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("ground"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx similarity index 94% rename from frontend/three_d_garden/__tests__/plants_test.tsx rename to frontend/three_d_garden/garden/__tests__/plants_test.tsx index 32bc15c344..7aed3aadb3 100644 --- a/frontend/three_d_garden/__tests__/plants_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx @@ -1,15 +1,15 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { clone } from "lodash"; -import { fakePlant } from "../../__test_support__/fake_state/resources"; -import { INITIAL } from "../config"; +import { fakePlant } from "../../../__test_support__/fake_state/resources"; +import { INITIAL } from "../../config"; import { calculatePlantPositions, convertPlants, ThreeDPlant, ThreeDPlantProps, } from "../plants"; -import { CROPS } from "../../crops/constants"; +import { CROPS } from "../../../crops/constants"; describe("calculatePlantPositions()", () => { it("calculates plant positions", () => { diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx new file mode 100644 index 0000000000..84dd84b877 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Point, PointProps } from "../point"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; +import { fakePoint } from "../../../__test_support__/fake_state/resources"; + +describe("", () => { + const fakeProps = (): PointProps => ({ + config: clone(INITIAL), + point: fakePoint(), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("cylinder"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/sky_test.tsx b/frontend/three_d_garden/garden/__tests__/sky_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/sky_test.tsx rename to frontend/three_d_garden/garden/__tests__/sky_test.tsx diff --git a/frontend/three_d_garden/__tests__/solar_test.tsx b/frontend/three_d_garden/garden/__tests__/solar_test.tsx similarity index 52% rename from frontend/three_d_garden/__tests__/solar_test.tsx rename to frontend/three_d_garden/garden/__tests__/solar_test.tsx index 901b87e49c..aec76f6bb8 100644 --- a/frontend/three_d_garden/__tests__/solar_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/solar_test.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { mount } from "enzyme"; +import { render } from "@testing-library/react"; import { Solar, SolarProps } from "../solar"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { @@ -11,7 +11,9 @@ describe("", () => { }); it("renders", () => { - const wrapper = mount(); - expect(wrapper.html()).toContain("solar"); + const p = fakeProps(); + p.config.solar = true; + const { container } = render(); + expect(container).toContainHTML("solar"); }); }); diff --git a/frontend/three_d_garden/__tests__/sun_test.tsx b/frontend/three_d_garden/garden/__tests__/sun_test.tsx similarity index 90% rename from frontend/three_d_garden/__tests__/sun_test.tsx rename to frontend/three_d_garden/garden/__tests__/sun_test.tsx index caff85ec11..695ff31e0d 100644 --- a/frontend/three_d_garden/__tests__/sun_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/sun_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { Sun, SunProps } from "../sun"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx new file mode 100644 index 0000000000..d202fd1529 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Weed, WeedProps } from "../weed"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; +import { fakeWeed } from "../../../__test_support__/fake_state/resources"; + +describe("", () => { + const fakeProps = (): WeedProps => ({ + config: clone(INITIAL), + weed: fakeWeed(), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("weed"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/zoom_beacons_test.tsx b/frontend/three_d_garden/garden/__tests__/zoom_beacons_test.tsx similarity index 97% rename from frontend/three_d_garden/__tests__/zoom_beacons_test.tsx rename to frontend/three_d_garden/garden/__tests__/zoom_beacons_test.tsx index a53f42cd48..0f2e3cee7e 100644 --- a/frontend/three_d_garden/__tests__/zoom_beacons_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/zoom_beacons_test.tsx @@ -1,5 +1,5 @@ let mockIsDesktop = true; -jest.mock("../../screen_size", () => ({ +jest.mock("../../../screen_size", () => ({ isDesktop: () => mockIsDesktop, })); @@ -7,7 +7,7 @@ import React from "react"; import { mount } from "enzyme"; import { ZoomBeacons, ZoomBeaconsProps } from "../zoom_beacons"; import { clone } from "lodash"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; describe("", () => { beforeEach(() => { diff --git a/frontend/three_d_garden/garden/clouds.tsx b/frontend/three_d_garden/garden/clouds.tsx new file mode 100644 index 0000000000..4ae51beef3 --- /dev/null +++ b/frontend/three_d_garden/garden/clouds.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Config, seasonProperties } from "../config"; +import { Cloud, Clouds as DreiClouds } from "@react-three/drei"; +import { ASSETS } from "../constants"; + +export interface CloudsProps { + config: Config; +} + +export const Clouds = (props: CloudsProps) => { + const { config } = props; + return + + ; +}; diff --git a/frontend/three_d_garden/garden/grid.tsx b/frontend/three_d_garden/garden/grid.tsx new file mode 100644 index 0000000000..821382507b --- /dev/null +++ b/frontend/three_d_garden/garden/grid.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { Config } from "../config"; +import { Group } from "../components"; +import { Line } from "@react-three/drei"; +import { zero as zeroFunc, extents as extentsFunc } from "../helpers"; +import { range } from "lodash"; + +export interface GridProps { + config: Config; +} + +export const Grid = (props: GridProps) => { + const { config } = props; + const zero = zeroFunc(config); + const gridZ = zero.z - config.soilHeight + 5; + const extents = extentsFunc(config); + return + {range(0, config.botSizeX + 100, 100).map(x => + )} + {range(0, config.botSizeY + 100, 100).map(y => + )} + ; +}; diff --git a/frontend/three_d_garden/garden/ground.tsx b/frontend/three_d_garden/garden/ground.tsx new file mode 100644 index 0000000000..75dd2e14be --- /dev/null +++ b/frontend/three_d_garden/garden/ground.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Config, detailLevels } from "../config"; +import { Circle, Detailed, useTexture } from "@react-three/drei"; +import { MeshPhongMaterial } from "../components"; +import { ASSETS } from "../constants"; +import { RepeatWrapping } from "three"; + +export interface GroundProps { + config: Config; +} + +export const Ground = (props: GroundProps) => { + const { config } = props; + const groundZ = config.bedZOffset + config.bedHeight; + + const grassTexture = useTexture(ASSETS.textures.grass + "?=grass"); + grassTexture.wrapS = RepeatWrapping; + grassTexture.wrapT = RepeatWrapping; + grassTexture.repeat.set(24, 24); + const labFloorTexture = useTexture(ASSETS.textures.concrete + "?=labFloor"); + labFloorTexture.wrapS = RepeatWrapping; + labFloorTexture.wrapT = RepeatWrapping; + labFloorTexture.repeat.set(16, 24); + const brickTexture = useTexture(ASSETS.textures.bricks + "?=bricks"); + brickTexture.wrapS = RepeatWrapping; + brickTexture.wrapT = RepeatWrapping; + brickTexture.repeat.set(30, 30); + + const getGroundProperties = (sceneName: string) => { + switch (sceneName) { + case "Greenhouse": + return { texture: brickTexture, color: "#999", lowDetailColor: "#8c6f64" }; + case "Lab": + return { texture: labFloorTexture, color: "#aaa", lowDetailColor: "gray" }; + default: + return { texture: grassTexture, color: "#ddd", lowDetailColor: "darkgreen" }; + } + }; + + const groundProperties = getGroundProperties(config.scene); + + const GroundWrapper = ({ children }: { children: React.ReactElement }) => + + {children} + ; + + return + + + + + + + ; +}; diff --git a/frontend/three_d_garden/garden/index.ts b/frontend/three_d_garden/garden/index.ts new file mode 100644 index 0000000000..564fcf8827 --- /dev/null +++ b/frontend/three_d_garden/garden/index.ts @@ -0,0 +1,10 @@ +export * from "./clouds"; +export * from "./grid"; +export * from "./ground"; +export * from "./plants"; +export * from "./point"; +export * from "./sky"; +export * from "./solar"; +export * from "./sun"; +export * from "./weed"; +export * from "./zoom_beacons"; diff --git a/frontend/three_d_garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx similarity index 91% rename from frontend/three_d_garden/plants.tsx rename to frontend/three_d_garden/garden/plants.tsx index d251917c50..adcb35667d 100644 --- a/frontend/three_d_garden/plants.tsx +++ b/frontend/three_d_garden/garden/plants.tsx @@ -1,12 +1,12 @@ -import { TaggedPlant } from "../farm_designer/map/interfaces"; -import { Config } from "./config"; -import { GARDENS, PLANTS } from "./constants"; +import { TaggedPlant } from "../../farm_designer/map/interfaces"; +import { Config } from "../config"; +import { GARDENS, PLANTS } from "../constants"; import { Billboard, Image } from "@react-three/drei"; import React from "react"; import { Vector3 } from "three"; -import { threeSpace, zZero as zZeroFunc } from "./helpers"; -import { Text } from "./text"; -import { findIcon } from "../crops/find"; +import { threeSpace, zZero as zZeroFunc } from "../helpers"; +import { Text } from "../elements"; +import { findIcon } from "../../crops/find"; import { kebabCase } from "lodash"; interface Plant { diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx new file mode 100644 index 0000000000..e22e5561fc --- /dev/null +++ b/frontend/three_d_garden/garden/point.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { TaggedGenericPointer } from "farmbot"; +import { Config } from "../config"; +import { Group, MeshPhongMaterial } from "../components"; +import { Cylinder, Sphere } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { zero as zeroFunc, threeSpace } from "../helpers"; + +export interface PointProps { + point: TaggedGenericPointer; + config: Config; +} + +export const Point = (props: PointProps) => { + const { point, config } = props; + const RADIUS = 25; + const HEIGHT = 100; + return + + + + + + + + + + ; +}; diff --git a/frontend/three_d_garden/sky.tsx b/frontend/three_d_garden/garden/sky.tsx similarity index 97% rename from frontend/three_d_garden/sky.tsx rename to frontend/three_d_garden/garden/sky.tsx index b406e47447..c2eb96b49d 100644 --- a/frontend/three_d_garden/sky.tsx +++ b/frontend/three_d_garden/garden/sky.tsx @@ -5,7 +5,7 @@ import { Vector3 as Vector3Type } from "@react-three/fiber"; import { Sky as SkyImpl } from "three-stdlib"; import { Vector3 } from "three"; import { ForwardRefComponent } from "@react-three/drei/helpers/ts-utils"; -import { Primitive } from "./components"; +import { Primitive } from "../components"; export type SkyProps = { distance?: number diff --git a/frontend/three_d_garden/solar.tsx b/frontend/three_d_garden/garden/solar.tsx similarity index 94% rename from frontend/three_d_garden/solar.tsx rename to frontend/three_d_garden/garden/solar.tsx index da647d24d9..be72a50ddf 100644 --- a/frontend/three_d_garden/solar.tsx +++ b/frontend/three_d_garden/garden/solar.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Shape } from "three"; import { Extrude, Line } from "@react-three/drei"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Group, Mesh, BoxGeometry, MeshPhongMaterial } from "./components"; +import { threeSpace } from "../helpers"; +import { Config } from "../config"; +import { Group, Mesh, BoxGeometry, MeshPhongMaterial } from "../components"; export interface SolarProps { config: Config; diff --git a/frontend/three_d_garden/sun.tsx b/frontend/three_d_garden/garden/sun.tsx similarity index 93% rename from frontend/three_d_garden/sun.tsx rename to frontend/three_d_garden/garden/sun.tsx index fed1db4b85..9079fb57fc 100644 --- a/frontend/three_d_garden/sun.tsx +++ b/frontend/three_d_garden/garden/sun.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { Config, seasonProperties } from "./config"; +import { Config, seasonProperties } from "../config"; import { Vector3 } from "three"; import { useSpring, animated } from "@react-spring/three"; -import { Group, PointLight } from "./components"; +import { Group, PointLight } from "../components"; const AnimatedPointLight = animated(PointLight); diff --git a/frontend/three_d_garden/garden/weed.tsx b/frontend/three_d_garden/garden/weed.tsx new file mode 100644 index 0000000000..609ca6c79d --- /dev/null +++ b/frontend/three_d_garden/garden/weed.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { TaggedWeedPointer } from "farmbot"; +import { Config } from "../config"; +import { ASSETS } from "../constants"; +import { Group, MeshPhongMaterial } from "../components"; +import { Image, Billboard, Sphere } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { zero as zeroFunc, threeSpace } from "../helpers"; + +export interface WeedProps { + weed: TaggedWeedPointer; + config: Config; +} + +export const Weed = (props: WeedProps) => { + const { weed, config } = props; + return + + + + + + + ; +}; diff --git a/frontend/three_d_garden/zoom_beacons.tsx b/frontend/three_d_garden/garden/zoom_beacons.tsx similarity index 94% rename from frontend/three_d_garden/zoom_beacons.tsx rename to frontend/three_d_garden/garden/zoom_beacons.tsx index 690e55e05f..98a8a1536d 100644 --- a/frontend/three_d_garden/zoom_beacons.tsx +++ b/frontend/three_d_garden/garden/zoom_beacons.tsx @@ -1,10 +1,10 @@ import { Sphere, Html, Line } from "@react-three/drei"; import React from "react"; -import { Config } from "./config"; -import { FOCI, getCameraOffset, setUrlParam } from "./zoom_beacons_constants"; +import { Config } from "../config"; +import { FOCI, getCameraOffset, setUrlParam } from "../zoom_beacons_constants"; import { useSpring, animated } from "@react-spring/three"; -import { Group, Mesh, MeshPhongMaterial } from "./components"; -import { isDesktop } from "../screen_size"; +import { Group, Mesh, MeshPhongMaterial } from "../components"; +import { isDesktop } from "../../screen_size"; const beaconColor = "#0266b5"; diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden_model.tsx similarity index 53% rename from frontend/three_d_garden/garden.tsx rename to frontend/three_d_garden/garden_model.tsx index aaccede1b6..7bdd7ec347 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden_model.tsx @@ -3,33 +3,30 @@ import { ThreeEvent } from "@react-three/fiber"; import { GizmoHelper, GizmoViewcube, OrbitControls, PerspectiveCamera, - Circle, Stats, Image, Clouds, Cloud, OrthographicCamera, - Detailed, Sphere, - useTexture, - Line, + Stats, Image, OrthographicCamera, + Sphere, } from "@react-three/drei"; -import { RepeatWrapping, BackSide } from "three"; +import { BackSide } from "three"; import { Bot } from "./bot"; import { AddPlantProps, Bed } from "./bed"; -import { zero as zeroFunc, extents as extentsFunc } from "./helpers"; -import { Sky } from "./sky"; -import { Config, detailLevels, seasonProperties } from "./config"; -import { ASSETS } from "./constants"; +import { + Sky, Solar, Sun, sunPosition, ZoomBeacons, + calculatePlantPositions, convertPlants, ThreeDPlant, + Point, Grid, Clouds, Ground, Weed, +} from "./garden"; +import { Config } from "./config"; import { useSpring, animated } from "@react-spring/three"; -import { Solar } from "./solar"; -import { Sun, sunPosition } from "./sun"; -import { Lab } from "./lab"; -import { ZoomBeacons } from "./zoom_beacons"; -import { getCamera, Camera as CameraInterface } from "./zoom_beacons_constants"; +import { Lab, Greenhouse } from "./scenes"; +import { getCamera } from "./zoom_beacons_constants"; import { - AmbientLight, AxesHelper, Group, MeshBasicMaterial, MeshPhongMaterial, + AmbientLight, AxesHelper, Group, MeshBasicMaterial, } from "./components"; -import { isDesktop } from "../screen_size"; -import { isUndefined, range } from "lodash"; -import { - calculatePlantPositions, convertPlants, ThreeDPlant, -} from "./plants"; +import { isUndefined } from "lodash"; import { ICON_URLS } from "../crops/constants"; +import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; +import { BooleanSetting } from "../session_keys"; +import { SlotWithTool } from "../resources/interfaces"; +import { cameraInit } from "./camera"; const AnimatedGroup = animated(Group); @@ -38,12 +35,14 @@ export interface GardenModelProps { activeFocus: string; setActiveFocus(focus: string): void; addPlantProps?: AddPlantProps; + mapPoints?: TaggedGenericPointer[]; + weeds?: TaggedWeedPointer[]; + toolSlots?: SlotWithTool[]; + mountedToolName?: string | undefined; } -// eslint-disable-next-line complexity export const GardenModel = (props: GardenModelProps) => { const { config } = props; - const groundZ = config.bedZOffset + config.bedHeight; const Camera = config.perspective ? PerspectiveCamera : OrthographicCamera; const plants = isUndefined(props.addPlantProps) @@ -74,33 +73,7 @@ export const GardenModel = (props: GardenModelProps) => { }, }); - const grassTexture = useTexture(ASSETS.textures.grass + "?=grass"); - grassTexture.wrapS = RepeatWrapping; - grassTexture.wrapT = RepeatWrapping; - grassTexture.repeat.set(24, 24); - const labFloorTexture = useTexture(ASSETS.textures.concrete + "?=labFloor"); - labFloorTexture.wrapS = RepeatWrapping; - labFloorTexture.wrapT = RepeatWrapping; - labFloorTexture.repeat.set(16, 24); - - const Ground = ({ children }: { children: React.ReactElement }) => - - {children} - ; - - const initCamera: CameraInterface = { - position: isDesktop() ? [2000, -4000, 2500] : [5400, -2500, 3400], - target: [0, 0, 0], - }; - const camera = getCamera(config, props.activeFocus, initCamera); - - const zero = zeroFunc(config); - const gridZ = zero.z - config.soilHeight + 5; - const extents = extentsFunc(config); + const camera = getCamera(config, props.activeFocus, cameraInit()); // eslint-disable-next-line no-null/no-null return { } - - - - - - - - - - - + + - + {(!props.addPlantProps + || !!props.addPlantProps.getConfigValue(BooleanSetting.show_farmbot)) && + } {ICON_URLS.map((url, i) => )} @@ -184,24 +135,11 @@ export const GardenModel = (props: GardenModelProps) => { config={config} hoveredPlant={hoveredPlant} />)} - - {range(0, config.botSizeX + 100, 100).map(x => - )} - {range(0, config.botSizeY + 100, 100).map(y => - )} - + @@ -211,7 +149,18 @@ export const GardenModel = (props: GardenModelProps) => { config={config} hoveredPlant={hoveredPlant} />)} + + {props.mapPoints?.map(point => + )} + + + {props.weeds?.map(weed => + )} + + ; }; diff --git a/frontend/three_d_garden/helpers.ts b/frontend/three_d_garden/helpers.ts index 5890d0ffbb..a2a5643bba 100644 --- a/frontend/three_d_garden/helpers.ts +++ b/frontend/three_d_garden/helpers.ts @@ -22,7 +22,7 @@ export const getColorFromBrightness = (value: number) => { }; return colorMap[value]; }; -export const zDir = -1; +export const zDir = (config: Config) => config.negativeZ ? -1 : 1; export const zero = (config: Config): Record<"x" | "y" | "z", number> => ({ x: threeSpace(config.bedXOffset, config.bedLengthOuter), @@ -33,7 +33,7 @@ export const zero = (config: Config): Record<"x" | "y" | "z", number> => ({ export const extents = (config: Config): Record<"x" | "y" | "z", number> => ({ x: threeSpace(config.bedXOffset + config.botSizeX, config.bedLengthOuter), y: threeSpace(config.bedYOffset + config.botSizeY, config.bedWidthOuter), - z: zZero(config) + zDir * config.botSizeZ, + z: zZero(config) + zDir(config) * config.botSizeZ, }); interface Vector3Array extends Array { diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx index 8c5e90b503..feb0adb033 100644 --- a/frontend/three_d_garden/index.tsx +++ b/frontend/three_d_garden/index.tsx @@ -1,13 +1,19 @@ import { Canvas } from "@react-three/fiber"; import React from "react"; import { Config } from "./config"; -import { GardenModel } from "./garden"; +import { GardenModel } from "./garden_model"; import { noop } from "lodash"; import { AddPlantProps } from "./bed"; +import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; +import { SlotWithTool } from "../resources/interfaces"; export interface ThreeDGardenProps { config: Config; addPlantProps: AddPlantProps; + mapPoints: TaggedGenericPointer[]; + weeds: TaggedWeedPointer[]; + toolSlots?: SlotWithTool[]; + mountedToolName?: string; } export const ThreeDGarden = (props: ThreeDGardenProps) => { @@ -18,6 +24,10 @@ export const ThreeDGarden = (props: ThreeDGardenProps) => { config={props.config} activeFocus={""} setActiveFocus={noop} + mapPoints={props.mapPoints} + weeds={props.weeds} + toolSlots={props.toolSlots} + mountedToolName={props.mountedToolName} addPlantProps={props.addPlantProps} />
diff --git a/frontend/three_d_garden/scenes/__tests__/greenhouse_test.tsx b/frontend/three_d_garden/scenes/__tests__/greenhouse_test.tsx new file mode 100644 index 0000000000..4a834e2e66 --- /dev/null +++ b/frontend/three_d_garden/scenes/__tests__/greenhouse_test.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Greenhouse, GreenhouseProps } from "../greenhouse"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): GreenhouseProps => ({ + config: clone(INITIAL), + activeFocus: "", + }); + + it("renders", () => { + const p = fakeProps(); + p.config.scene = "Greenhouse"; + p.config.people = false; + p.activeFocus = ""; + const { container } = render(); + expect(container).toContainHTML("greenhouse-environment"); + expect(container).not.toContainHTML("people"); + expect(container).toContainHTML("starter-tray-1"); + expect(container).toContainHTML("starter-tray-2"); + expect(container).toContainHTML("left-greenhouse-wall"); + expect(container).toContainHTML("right-greenhouse-wall"); + expect(container).toContainHTML("potted-plant"); + }); + + it("not visible when scene is not greenhouse", () => { + const p = fakeProps(); + p.config.scene = "Lab"; + const { container } = render(); + expect(container).not.toContainHTML("greenhouse-environment"); + }); + + it("renders with people", () => { + const p = fakeProps(); + p.config.scene = "Greenhouse"; + p.config.people = true; + p.activeFocus = ""; + const { container } = render(); + expect(container).toContainHTML("greenhouse-environment"); + expect(container).toContainHTML("people"); + }); + + it("doesn't render people or potted plant when active focus is set", () => { + const p = fakeProps(); + p.config.scene = "Greenhouse"; + p.config.people = true; + p.activeFocus = "foo"; + const { container } = render(); + expect(container).toContainHTML("greenhouse-environment"); + expect(container).not.toContainHTML("people"); + }); +}); diff --git a/frontend/three_d_garden/scenes/__tests__/lab_test.tsx b/frontend/three_d_garden/scenes/__tests__/lab_test.tsx new file mode 100644 index 0000000000..d64521afe1 --- /dev/null +++ b/frontend/three_d_garden/scenes/__tests__/lab_test.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Lab, LabProps } from "../lab"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): LabProps => ({ + config: clone(INITIAL), + activeFocus: "", + }); + + it("renders", () => { + const p = fakeProps(); + p.config.scene = "Lab"; + p.config.people = false; + p.activeFocus = ""; + render(); + const { container } = render(); + expect(container).toContainHTML("shelf"); + expect(container).not.toContainHTML("people"); + }); + + it("not visible when scene is not lab", () => { + const p = fakeProps(); + p.config.scene = "Greenhouse"; + render(); + const { container } = render(); + expect(container).not.toContainHTML("shelf"); + expect(container).not.toContainHTML("people"); + }); + + it("renders with people", () => { + const p = fakeProps(); + p.config.scene = "Lab"; + p.config.people = true; + p.activeFocus = ""; + render(); + const { container } = render(); + expect(container).toContainHTML("shelf"); + expect(container).toContainHTML("people"); + }); +}); diff --git a/frontend/three_d_garden/scenes/greenhouse.tsx b/frontend/three_d_garden/scenes/greenhouse.tsx new file mode 100644 index 0000000000..1d38fc7fcc --- /dev/null +++ b/frontend/three_d_garden/scenes/greenhouse.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import { Box, useTexture } from "@react-three/drei"; +import { DoubleSide, RepeatWrapping } from "three"; +import { ASSETS } from "../constants"; +import { threeSpace } from "../helpers"; +import { Config } from "../config"; +import { Group, MeshPhongMaterial } from "../components"; +import { StarterTray, PottedPlant, GreenhouseWall, People } from "./props"; + +export interface GreenhouseProps { + config: Config; + activeFocus: string; +} + +const wallLength = 10000; +const wallOffset = 2000; +const shelfThickness = 50; +const shelfHeight = 800; +const shelfDepth = 600; + +export const Greenhouse = (props: GreenhouseProps) => { + const { config } = props; + const groundZ = -config.bedZOffset - config.bedHeight; + + const shelfWoodTexture = useTexture(ASSETS.textures.wood + "?=shelf"); + shelfWoodTexture.wrapS = RepeatWrapping; + shelfWoodTexture.wrapT = RepeatWrapping; + shelfWoodTexture.repeat.set(0.3, 0.3); + + return + + + + + + + + + + + + + + + + + + + + + + + + ; +}; diff --git a/frontend/three_d_garden/scenes/index.ts b/frontend/three_d_garden/scenes/index.ts new file mode 100644 index 0000000000..1a1721be73 --- /dev/null +++ b/frontend/three_d_garden/scenes/index.ts @@ -0,0 +1,2 @@ +export * from "./lab"; +export * from "./greenhouse"; diff --git a/frontend/three_d_garden/lab.tsx b/frontend/three_d_garden/scenes/lab.tsx similarity index 62% rename from frontend/three_d_garden/lab.tsx rename to frontend/three_d_garden/scenes/lab.tsx index a41674470f..ea2262eaa7 100644 --- a/frontend/three_d_garden/lab.tsx +++ b/frontend/three_d_garden/scenes/lab.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { Box, Billboard, Image, Extrude, useTexture } from "@react-three/drei"; +import { Box, Extrude, useTexture } from "@react-three/drei"; import { DoubleSide, Shape, RepeatWrapping } from "three"; -import { ASSETS } from "./constants"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Desk } from "./desk"; -import { Group, MeshPhongMaterial } from "./components"; +import { ASSETS } from "../constants"; +import { threeSpace } from "../helpers"; +import { Config } from "../config"; +import { Desk, People } from "./props"; +import { Group, MeshPhongMaterial } from "../components"; export interface LabProps { config: Config; @@ -42,7 +42,7 @@ export const Lab = (props: LabProps) => { shelfWoodTexture.wrapT = RepeatWrapping; shelfWoodTexture.repeat.set(0.3, 0.3); - return + return { ))} - - - - - - - - + ; }; diff --git a/frontend/three_d_garden/__tests__/desk_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/desk_test.tsx similarity index 90% rename from frontend/three_d_garden/__tests__/desk_test.tsx rename to frontend/three_d_garden/scenes/props/__tests__/desk_test.tsx index 4e1f3246f6..bfbd4919be 100644 --- a/frontend/three_d_garden/__tests__/desk_test.tsx +++ b/frontend/three_d_garden/scenes/props/__tests__/desk_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { Desk, DeskProps } from "../desk"; import { clone } from "lodash"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; describe("", () => { const fakeProps = (): DeskProps => ({ diff --git a/frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx new file mode 100644 index 0000000000..3072c5dafc --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { GreenhouseWall } from "../greenhouse_wall"; + +describe("", () => { + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("greenhouse-wall"); + }); +}); diff --git a/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx new file mode 100644 index 0000000000..0494efaba6 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { People, PeopleProps } from "../people"; +import { INITIAL } from "../../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): PeopleProps => ({ + activeFocus: "", + config: clone(INITIAL), + people: [], + }); + + it("renders", () => { + const p = fakeProps(); + p.config.people = true; + const { container } = render(); + expect(container).toContainHTML("people"); + }); +}); diff --git a/frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx new file mode 100644 index 0000000000..d0698f53b6 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { PottedPlant } from "../potted_plant"; + +describe("", () => { + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("pot-with-plant"); + }); +}); diff --git a/frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx new file mode 100644 index 0000000000..12b115fca6 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Desk, DeskProps } from "../desk"; +import { clone } from "lodash"; +import { INITIAL } from "../../../config"; + +describe("", () => { + const fakeProps = (): DeskProps => ({ + config: clone(INITIAL), + activeFocus: "", + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("desk"); + }); +}); diff --git a/frontend/three_d_garden/desk.tsx b/frontend/three_d_garden/scenes/props/desk.tsx similarity index 94% rename from frontend/three_d_garden/desk.tsx rename to frontend/three_d_garden/scenes/props/desk.tsx index fe129c5acf..159caa60ce 100644 --- a/frontend/three_d_garden/desk.tsx +++ b/frontend/three_d_garden/scenes/props/desk.tsx @@ -1,10 +1,10 @@ import React from "react"; import { RepeatWrapping } from "three"; import { Box, useTexture } from "@react-three/drei"; -import { ASSETS } from "./constants"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Group, MeshPhongMaterial } from "./components"; +import { ASSETS } from "../../constants"; +import { threeSpace } from "../../helpers"; +import { Config } from "../../config"; +import { Group, MeshPhongMaterial } from "../../components"; export interface DeskProps { config: Config; diff --git a/frontend/three_d_garden/scenes/props/greenhouse_wall.tsx b/frontend/three_d_garden/scenes/props/greenhouse_wall.tsx new file mode 100644 index 0000000000..ed186f7539 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/greenhouse_wall.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Box } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { Group, MeshPhongMaterial } from "../../components"; +import { range } from "lodash"; + +const wallLength = 10000; +const wallHeight = 2500; +const glassThickness = 10; + +const numWallCols = 8; +const numWallRows = 4; +const wallGap = 20; +const paneWidth = (wallLength - (numWallCols + 1) * wallGap) / numWallCols; +const paneHeight = (wallHeight - (numWallRows + 1) * wallGap) / numWallRows; + +const openPanels = [ + { row: 2, col: 1 }, + { row: 2, col: 2 }, + { row: 2, col: 3 }, +]; + +export const GreenhouseWall = () => { + + return + {range(numWallRows).map(row => + range(numWallCols).map(col => { + const isOpen = openPanels.some(panel => + panel.row === row && + panel.col === col, + ); + return + + ; + }), + )} + {range(numWallCols + 1).map(col => ( + + + + ))} + {range(numWallRows + 1).map(row => ( + + + + ))} + ; +}; diff --git a/frontend/three_d_garden/scenes/props/index.ts b/frontend/three_d_garden/scenes/props/index.ts new file mode 100644 index 0000000000..2dc0930c90 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/index.ts @@ -0,0 +1,5 @@ +export * from "./desk"; +export * from "./greenhouse_wall"; +export * from "./people"; +export * from "./potted_plant"; +export * from "./starter_tray"; diff --git a/frontend/three_d_garden/scenes/props/people.tsx b/frontend/three_d_garden/scenes/props/people.tsx new file mode 100644 index 0000000000..ef53a3c0aa --- /dev/null +++ b/frontend/three_d_garden/scenes/props/people.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { Billboard, Image } from "@react-three/drei"; +import { Group } from "../../components"; +import { Config } from "../../config"; +import { threeSpace } from "../../helpers"; +import { Vector3 } from "three"; +import { ASSETS } from "../../constants"; + +export interface PeopleProps { + config: Config; + activeFocus: string; + people: { url: string, offset: number[] }[]; +} + +export const People = (props: PeopleProps) => { + const { people, config } = props; + const groundZ = -config.bedZOffset - config.bedHeight; + return + {people.map((person, i) => { + const scalingData = SCALING_DATA[person.url]; + const offset = new Vector3(...person.offset); + return + + ; + })} + ; +}; + +interface DataRecord { + scale: [number, number]; + position: number[]; +} + +const SCALING_DATA: Record = { + [ASSETS.people.person1]: { scale: [900, 1800], position: [0, 900, 0] }, + [ASSETS.people.person1Flipped]: { scale: [900, 1800], position: [0, 900, 0] }, + [ASSETS.people.person2]: { scale: [700, 1700], position: [0, 850, 0] }, + [ASSETS.people.person2Flipped]: { scale: [700, 1700], position: [0, 850, 0] }, + [ASSETS.people.person3]: { scale: [875, 1800], position: [0, 900, 0] }, + [ASSETS.people.person3Flipped]: { scale: [875, 1800], position: [0, 900, 0] }, + [ASSETS.people.person4]: { scale: [580, 1700], position: [0, 850, 0] }, + [ASSETS.people.person4Flipped]: { scale: [580, 1700], position: [0, 850, 0] }, +}; diff --git a/frontend/three_d_garden/scenes/props/potted_plant.tsx b/frontend/three_d_garden/scenes/props/potted_plant.tsx new file mode 100644 index 0000000000..0c4efe3712 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/potted_plant.tsx @@ -0,0 +1,46 @@ +import React, { useMemo } from "react"; +import { Billboard, Circle, Image } from "@react-three/drei"; +import * as THREE from "three"; +import { Group, MeshPhongMaterial, Mesh } from "../../components"; + +const potHeight = 400; +const plantHeight = 500; + +export const PottedPlant = () => { + const points = useMemo(() => [ + new THREE.Vector2(0, 0), + new THREE.Vector2(0.3, 0), + new THREE.Vector2(0.35, 0.1), + new THREE.Vector2(0.25, 0.6), + new THREE.Vector2(0.3, 0.8), + new THREE.Vector2(0.4, 1), + new THREE.Vector2(0.35, 1), + new THREE.Vector2(0.2, 0.6), + new THREE.Vector2(0, 0.6), + ], []); + + const geometry = useMemo(() => + new THREE.LatheGeometry(points, 32, 0, Math.PI * 2), [points]); + + return + + + + + + + + + + ; +}; diff --git a/frontend/three_d_garden/scenes/props/starter_tray.tsx b/frontend/three_d_garden/scenes/props/starter_tray.tsx new file mode 100644 index 0000000000..062341b9ed --- /dev/null +++ b/frontend/three_d_garden/scenes/props/starter_tray.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Box, Billboard, Image } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { ASSETS } from "../../constants"; +import { Group, MeshPhongMaterial } from "../../components"; +import { range } from "lodash"; + +const length = 250; +const width = 700; +const height = 50; +const cellSize = 50; +const seedlingSize = 40; + +export const StarterTray = () => { + + return + + + + {range(5).map(row => + range(14).map(col => { + const x = -width / 2 + cellSize / 2 + col * cellSize; + const y = -length / 2 + cellSize / 2 + row * cellSize; + return + + ; + }), + )} + ; +}; diff --git a/frontend/three_d_garden/zoom_beacons_constants.tsx b/frontend/three_d_garden/zoom_beacons_constants.tsx index 8b0ccf3103..175dc33d57 100644 --- a/frontend/three_d_garden/zoom_beacons_constants.tsx +++ b/frontend/three_d_garden/zoom_beacons_constants.tsx @@ -177,7 +177,7 @@ export const FOCI = (config: Config): Focus[] => [ position: [ threeSpace(config.x, config.bedLengthOuter) + config.bedXOffset, threeSpace(config.y + 150, config.bedWidthOuter) + config.bedYOffset, - zZero(config) + zDir * config.z, + zZero(config) - zDir(config) * config.z, ], camera: { narrow: { diff --git a/frontend/tools/__tests__/add_tool_slot_test.tsx b/frontend/tools/__tests__/add_tool_slot_test.tsx index 4d833aa598..a2c428afc3 100644 --- a/frontend/tools/__tests__/add_tool_slot_test.tsx +++ b/frontend/tools/__tests__/add_tool_slot_test.tsx @@ -70,9 +70,11 @@ describe("", () => { it("saves tool slot", () => { const wrapper = shallow(); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; wrapper.find("SaveBtn").simulate("click"); expect(save).toHaveBeenCalled(); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("saves on unmount", () => { diff --git a/frontend/tools/__tests__/add_tool_test.tsx b/frontend/tools/__tests__/add_tool_test.tsx index 41023823e9..ac33be80b2 100644 --- a/frontend/tools/__tests__/add_tool_test.tsx +++ b/frontend/tools/__tests__/add_tool_test.tsx @@ -76,12 +76,14 @@ describe("", () => { p.dispatch = mockDispatch(); const wrapper = shallow(); wrapper.setState({ toolName: "Foo" }); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; await wrapper.find(SaveBtn).simulate("click"); expect(init).toHaveBeenCalledWith("Tool", { name: "Foo", flow_rate_ml_per_s: 0, }); expect(wrapper.state().uuid).toEqual(undefined); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("removes unsaved tool on exit", async () => { @@ -90,12 +92,14 @@ describe("", () => { p.dispatch = mockDispatch(); const wrapper = shallow(); wrapper.setState({ toolName: "Foo" }); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; await wrapper.find(SaveBtn).simulate("click"); expect(init).toHaveBeenCalledWith("Tool", { name: "Foo", flow_rate_ml_per_s: 0, }); expect(wrapper.state().uuid).toEqual("fake uuid"); - expect(mockNavigate).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); wrapper.unmount(); expect(destroy).toHaveBeenCalledWith("fake uuid"); }); @@ -113,20 +117,24 @@ describe("", () => { ])("adds peripherals: %s", (firmware, expectedAdds) => { const p = fakeProps(); p.firmwareHardware = firmware; - const wrapper = mount(); + const wrapper = mount(); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; wrapper.find("button").last().simulate("click"); expect(initSave).toHaveBeenCalledTimes(expectedAdds); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("doesn't add stock tools twice", () => { const p = fakeProps(); p.firmwareHardware = "express_k10"; p.existingToolNames = ["Seed Trough 1"]; - const wrapper = mount(); + const wrapper = mount(); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; wrapper.find("button").last().simulate("click"); expect(initSave).toHaveBeenCalledTimes(2); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("copies a tool name", () => { diff --git a/frontend/tools/add_tool.tsx b/frontend/tools/add_tool.tsx index 8a80050808..0e17b03282 100644 --- a/frontend/tools/add_tool.tsx +++ b/frontend/tools/add_tool.tsx @@ -9,7 +9,6 @@ import { SaveBtn } from "../ui"; import { SpecialStatus } from "farmbot"; import { initSave, destroy, init, save } from "../api/crud"; import { Panel } from "../farm_designer/panel_header"; -import { useNavigate } from "react-router"; import { selectAllTools } from "../resources/selectors"; import { betterCompact } from "../util"; import { @@ -27,6 +26,7 @@ import { reduceToolName, ToolName, } from "../farm_designer/map/tool_graphics/all_tools"; import { WaterFlowRateInput } from "./edit_tool"; +import { NavigationContext } from "../routes_helpers"; export const mapStateToProps = (props: Everything): AddToolProps => ({ dispatch: props.dispatch, @@ -54,9 +54,12 @@ export class RawAddTool extends React.Component { newTool = (name: string) => this.props.dispatch(initSave("Tool", { name })); + static contextType = NavigationContext; + context!: React.ContextType; + navigate = this.context; + back = () => { - const navigate = useNavigate(); - navigate(Path.tools()); + this.navigate(Path.tools()); }; save = () => { @@ -139,7 +142,6 @@ export class RawAddTool extends React.Component { AddStockTools = () => { const add = this.state.toAdd.filter(this.filterExisting); - const navigate = useNavigate(); return