From 8939faab9a452c1be13730bf31e67273ab7823b4 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Fri, 21 Feb 2025 22:20:48 -0800 Subject: [PATCH 01/19] connect tubes to solenoid --- frontend/three_d_garden/bot/bot.tsx | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index 84f104e9b..60ad87df2 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -817,6 +817,52 @@ export const Bot = (props: FarmbotModelProps) => { scale={1000} geometry={solenoid.nodes[PartName.solenoid].geometry} material={solenoid.materials.PaletteMaterial001} /> + + + + + + Date: Fri, 21 Feb 2025 22:33:07 -0800 Subject: [PATCH 02/19] water flow animation --- .../three_d_garden/bot/x_axis_water_tube.tsx | 76 +++++++++++------- frontend/three_d_garden/config.ts | 6 +- frontend/three_d_garden/config_overlays.tsx | 3 +- frontend/three_d_garden/constants.ts | 1 + public/3D/textures/water.avif | Bin 0 -> 1988 bytes 5 files changed, 57 insertions(+), 29 deletions(-) create mode 100644 public/3D/textures/water.avif diff --git a/frontend/three_d_garden/bot/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/x_axis_water_tube.tsx index 551cfc1f0..6177c74e4 100644 --- a/frontend/three_d_garden/bot/x_axis_water_tube.tsx +++ b/frontend/three_d_garden/bot/x_axis_water_tube.tsx @@ -1,8 +1,11 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useFrame } from "@react-three/fiber"; +import { TextureLoader, RepeatWrapping } from "three"; import { Cylinder, Tube } from "@react-three/drei"; import { Config } from "../config"; import { threeSpace, easyCubicBezierCurve3 } from "../helpers"; import { Group, MeshPhongMaterial } from "../components"; +import { ASSETS } from "../constants"; export interface XAxisWaterTubeProps { config: Config; @@ -25,30 +28,49 @@ export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { [barbX, barbY, barbZ], ); - return - - - - - - - - - - ; + const [waterTexture, setWaterTexture] = useState(() => { + const texture = new TextureLoader().load(ASSETS.textures.water); + texture.wrapS = texture.wrapT = RepeatWrapping; + return texture; + }); + + useEffect(() => { + const newTexture = new TextureLoader().load(ASSETS.textures.water); + newTexture.wrapS = newTexture.wrapT = RepeatWrapping; + setWaterTexture(newTexture); + }, [config]); + + useFrame((_, delta) => { + if (config.waterFlow && waterTexture) { + waterTexture.offset.x += delta * 0.5; + } + }); + + return ( + + + + + + + + + + + + ); }; diff --git a/frontend/three_d_garden/config.ts b/frontend/three_d_garden/config.ts index ce7e7c47c..f6e23b553 100644 --- a/frontend/three_d_garden/config.ts +++ b/frontend/three_d_garden/config.ts @@ -70,6 +70,7 @@ export interface Config { distanceIndicator: string; kitVersion: string; negativeZ: boolean; + waterFlow: boolean; } export const INITIAL: Config = { @@ -144,6 +145,7 @@ export const INITIAL: Config = { distanceIndicator: "", kitVersion: "v1.7", negativeZ: false, + waterFlow: false, }; export const STRING_KEYS = [ @@ -166,6 +168,7 @@ export const BOOLEAN_KEYS = [ "xyDimensions", "zDimension", "promoInfo", "settingsBar", "zoomBeacons", "solar", "utilitiesPost", "packaging", "lab", "people", "lowDetail", "eventDebug", "cableDebug", "zoomBeaconDebug", "animate", "negativeZ", + "waterFlow", ]; export const PRESETS: Record = { @@ -336,6 +339,7 @@ export const PRESETS: Record = { zoomBeaconDebug: true, animate: true, distanceIndicator: "", + waterFlow: true, }, }; @@ -355,7 +359,7 @@ const OTHER_CONFIG_KEYS: (keyof Config)[] = [ "threeAxes", "xyDimensions", "zDimension", "labelsOnHover", "promoInfo", "settingsBar", "zoomBeacons", "pan", "solar", "utilitiesPost", "packaging", "lab", "people", "scene", "lowDetail", "eventDebug", "cableDebug", "zoomBeaconDebug", - "animate", "distanceIndicator", "kitVersion", "negativeZ", + "animate", "distanceIndicator", "kitVersion", "negativeZ", "waterFlow", ]; export const modifyConfig = (config: Config, update: Partial) => { diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx index 3a801816b..426f1c2b3 100644 --- a/frontend/three_d_garden/config_overlays.tsx +++ b/frontend/three_d_garden/config_overlays.tsx @@ -285,7 +285,7 @@ export const PrivateOverlay = (props: OverlayProps) => { options={["Standard", "Mobile"]} /> - + @@ -294,6 +294,7 @@ export const PrivateOverlay = (props: OverlayProps) => { "seeder", "None"]} /> + diff --git a/frontend/three_d_garden/constants.ts b/frontend/three_d_garden/constants.ts index 19eff7161..bb548a7b4 100644 --- a/frontend/three_d_garden/constants.ts +++ b/frontend/three_d_garden/constants.ts @@ -16,6 +16,7 @@ export const ASSETS: Record> = { concrete: "/3D/textures/concrete.avif", screen: "/3D/textures/screen.avif", bricks: "/3D/textures/bricks.avif", + water: "/3D/textures/water.avif", }, shapes: { track: "/3D/shapes/track.svg", diff --git a/public/3D/textures/water.avif b/public/3D/textures/water.avif new file mode 100644 index 0000000000000000000000000000000000000000..16cf7a9863cc7cb2fb563a98743659e657644a61 GIT binary patch literal 1988 zcmXv|2|N@08=o_CH?%okuj#|&jkY0YLkuZOxr#6|ZP{kd*AY#}BxmvxE4L9Q(?LvF zDRU|~>eE<7KnnDOA{q_X-S(QKx#O`<20Du5L z_A@|02>=i+=hs>U0{Qpu-;NT$!3pjU^Jg&OaD-JLG4S`ws{~3gg|D9`P_Th~mM4Tz zaQlONWb&UM|H)Da5rlm~NK8zOPm-8$gjF0sa$l0e1`>nGSYiY~kRPH{z62&vNSJ+( z5B_1o0)jV%`B&AD{4k*)fH)BHM@dk%1vRb!nIi>S6vNOCT6|rx*F~NytUd1BY#zA~ zpJ%Z4Ae2c8kI>zLq&!Uav2k=edUX32gJb|L8F{wU=<=`Pk?-qJ?^G z_Gbrn4JedpX^ZH}YoFjue;mGV-P_J{GXJg_T2vRe#PqHageAxAEq0}Mbd<4mvNI7u^1)>lBk|3YjP@d zR=-g4`9Rk&Ws|XVgzh_7k?#Fu7;8#XgE8Q%p2(QFhA*b%(w;zuxV7@1W(l{9-C7~8 z<5PnM_vURoye))VbK%sUjE@6arb-=mymG`y=JJbR~(2hH_ixI zDf@K-f-Gs}afpX!1j|+Xp6=i79V_v52MQEqm|uwb6?6NE@P?!yDfDFsH=IqTHBA7CP5ZM|`0{uS$ zZcmUpVLtw)ruka4KCW$!L!gFhY2UoG+JWGZF^QJt1j_RckGVrau(?4k*tkvS?1vxf zrSsBNDr@-mw+bKMx%|Dk^pDqr6P6uBI_NJ%&H(CXv>glJ9dIWmfwsV+{dyl8dQN#y z#Z6WDRG-hy2crfrNZ?3={x5^p6z?%gw!pnk$J%jvOM;!?hxEW&`~_lO&MJhu^YT&0 zRN?b*nUS|3ESQYWOqTv$K*u;mbG(Hf^!rC&TesQ4F<#_}BtON+nIuRYK)L6kucWh^ zuj^PGOD0A-+&Qo0Vm_+lQm_9_JqCgM+yv*ML+srAxEUUGo{zwHGzdydeG{Rg_xSv zbw7&a|C+6wzj#S}?HEiB_2lcfH-0%CXD)k+w6OK`8<*PKy!r;D5`l6z3X6d_wjK@O zX++4LywqZyayeB5s3sGhri8%h@pzWQ!k$K*kxA9bdn3&wlP=$??Y@w{UTpzu>(f+9 zxHKhNr<2xC=M^cp;l-=D9m}I0nvYIjDbgrrJd{K}vN!zX+N7Z?BpV6W+(6hxX=mNt zTI6OpK_;(_qg|GN(t_|g27UFP=D9R*Wl2Di?yK;gNGlpJRo!VLzdl)v83xJb!FvXj zEK~Q!oqTKpsuu&oZD(SqwP+N7^7jt*Wh|5wVKp)}a9&2xYl}MVU}Ws=a0_;8SK?5E zZ|(J++xOFt;RnW2#~dm>x>~!R4)L@YN<9HSX><{4jzq0xYsaA6Kdc1tco#@uk~8o(^?g8LK;&_*k1GHDL&^>S;Y*EhKV;B~~tX z`jy=qqc_!6?^1kpbjulLSh9pCeUS6I(m|M3I4aP5Anm=%(99mn;}=BytHMHMSP=Dp Do&Jf8 literal 0 HcmV?d00001 From a936b4366aaad9305bd7649be62401ea0f403f3a Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Fri, 21 Feb 2025 22:44:20 -0800 Subject: [PATCH 03/19] breakout water flow texture and apply to solenoid tubes --- frontend/three_d_garden/bot/bot.tsx | 116 +++++++++--------- .../three_d_garden/bot/water_flow_texture.tsx | 26 ++++ .../three_d_garden/bot/x_axis_water_tube.tsx | 30 +---- 3 files changed, 92 insertions(+), 80 deletions(-) create mode 100644 frontend/three_d_garden/bot/water_flow_texture.tsx diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index 60ad87df2..ac88196df 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -27,6 +27,7 @@ import { Tools } from "./components/tools"; import { ElectronicsBox } from "./components/electronics_box"; import { Bounds } from "./components/bounds"; import { SlotWithTool } from "../../resources/interfaces"; +import { useWaterFlowTexture } from "./water_flow_texture"; const extrusionWidth = 20; const utmRadius = 35; @@ -165,6 +166,7 @@ export const Bot = (props: FarmbotModelProps) => { const [beamShape, setBeamShape] = useState(); const [columnShape, setColumnShape] = useState(); const [zAxisShape, setZAxisShape] = useState(); + const waterTexture = useWaterFlowTexture(config.waterFlow); useEffect(() => { if (!(trackShape && beamShape && columnShape && zAxisShape)) { const loader = new SVGLoader(); @@ -807,62 +809,64 @@ export const Bot = (props: FarmbotModelProps) => { geometry={beltClip.nodes[PartName.beltClip].geometry}> - - - - - - - + + + + + + + + + { + const [waterTexture, setWaterTexture] = useState(() => { + const texture = new TextureLoader().load(ASSETS.textures.water); + texture.wrapS = texture.wrapT = RepeatWrapping; + return texture; + }); + + useEffect(() => { + const newTexture = new TextureLoader().load(ASSETS.textures.water); + newTexture.wrapS = newTexture.wrapT = RepeatWrapping; + setWaterTexture(newTexture); + }, [waterFlow]); + + useFrame((_, delta) => { + if (waterFlow && waterTexture) { + waterTexture.offset.x -= delta * 0.5; + } + }); + + return waterTexture; +}; diff --git a/frontend/three_d_garden/bot/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/x_axis_water_tube.tsx index 6177c74e4..e83f88864 100644 --- a/frontend/three_d_garden/bot/x_axis_water_tube.tsx +++ b/frontend/three_d_garden/bot/x_axis_water_tube.tsx @@ -1,11 +1,9 @@ -import React, { useEffect, useState } from "react"; -import { useFrame } from "@react-three/fiber"; -import { TextureLoader, RepeatWrapping } from "three"; +import React from "react"; import { Cylinder, Tube } from "@react-three/drei"; import { Config } from "../config"; import { threeSpace, easyCubicBezierCurve3 } from "../helpers"; import { Group, MeshPhongMaterial } from "../components"; -import { ASSETS } from "../constants"; +import { useWaterFlowTexture } from "./water_flow_texture"; export interface XAxisWaterTubeProps { config: Config; @@ -18,33 +16,17 @@ export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { const barbY = threeSpace(-50, config.bedWidthOuter); const barbZ = groundZ + 20; const tubePath = easyCubicBezierCurve3( + [barbX, barbY, barbZ], + [-300, 0, 0], + [300, 0, 0], [ threeSpace(config.bedLengthOuter / 2 - 20, config.bedLengthOuter), threeSpace(-30, config.bedWidthOuter), -140, ], - [300, 0, 0], - [-300, 0, 0], - [barbX, barbY, barbZ], ); - const [waterTexture, setWaterTexture] = useState(() => { - const texture = new TextureLoader().load(ASSETS.textures.water); - texture.wrapS = texture.wrapT = RepeatWrapping; - return texture; - }); - - useEffect(() => { - const newTexture = new TextureLoader().load(ASSETS.textures.water); - newTexture.wrapS = newTexture.wrapT = RepeatWrapping; - setWaterTexture(newTexture); - }, [config]); - - useFrame((_, delta) => { - if (config.waterFlow && waterTexture) { - waterTexture.offset.x += delta * 0.5; - } - }); + const waterTexture = useWaterFlowTexture(config.waterFlow); return ( From 8b25e00a4a377076e7770d80300900fd35c36212 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Fri, 21 Feb 2025 23:09:52 -0800 Subject: [PATCH 04/19] fix tube coordinates --- frontend/three_d_garden/bot/bot.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index ac88196df..75c4e14a7 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -826,15 +826,15 @@ export const Bot = (props: FarmbotModelProps) => { args={[easyCubicBezierCurve3( [ threeSpace(x - 45, bedLengthOuter) + bedXOffset, - threeSpace(-45, bedWidthOuter) + bedYOffset, + threeSpace(-25, bedWidthOuter), -49, ], [200, -55, 25], [5, 10, -250], [ threeSpace(x - 104.75, bedLengthOuter) + bedXOffset, - threeSpace(0, bedWidthOuter) + bedYOffset, - 283, + threeSpace(20, bedWidthOuter), + columnLength - 217, ], ), 40, 5, 8]}> { args={[easyCubicBezierCurve3( [ threeSpace(x - 104.25, bedLengthOuter) + bedXOffset, - threeSpace(0, bedWidthOuter) + bedYOffset, - 400, + threeSpace(20, bedWidthOuter), + columnLength - 98, ], [0, 0, 100], [0, -75, 5], [ threeSpace(x - 70, bedLengthOuter) + bedXOffset, threeSpace(35, bedWidthOuter) + bedYOffset, - 590, + columnLength + 90, ], ), 20, 5, 8]}> Date: Fri, 21 Feb 2025 23:35:13 -0800 Subject: [PATCH 05/19] add y-z and utm tubes --- frontend/three_d_garden/bot/bot.tsx | 70 ++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index 75c4e14a7..e05bca3f8 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -810,17 +810,7 @@ export const Bot = (props: FarmbotModelProps) => { - - { opacity={0.75} /> - + { opacity={0.75} /> + + + + + + Date: Fri, 21 Feb 2025 23:57:46 -0800 Subject: [PATCH 06/19] lint fix --- frontend/three_d_garden/bot/bot.tsx | 8 ++++---- frontend/three_d_garden/bot/x_axis_water_tube.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index e05bca3f8..23fdd61a8 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -828,7 +828,7 @@ export const Bot = (props: FarmbotModelProps) => { ], ), 40, 5, 8]}> @@ -861,7 +861,7 @@ export const Bot = (props: FarmbotModelProps) => { ], ), 20, 5, 8]}> @@ -884,7 +884,7 @@ export const Bot = (props: FarmbotModelProps) => { ], ), 20, 5, 8]}> @@ -907,7 +907,7 @@ export const Bot = (props: FarmbotModelProps) => { ], ), 20, 5, 8]}> diff --git a/frontend/three_d_garden/bot/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/x_axis_water_tube.tsx index e83f88864..19db86912 100644 --- a/frontend/three_d_garden/bot/x_axis_water_tube.tsx +++ b/frontend/three_d_garden/bot/x_axis_water_tube.tsx @@ -35,7 +35,7 @@ export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { receiveShadow={true} args={[tubePath, 20, 5, 8]}> From fc10a8e71e9e8628ede096750d9e61d013fe65c9 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Sat, 22 Feb 2025 00:46:40 -0800 Subject: [PATCH 07/19] use useMemo instead + test --- .../bot/__tests__/water_flow_texture_test.tsx | 37 +++++++++++++++++++ .../three_d_garden/bot/water_flow_texture.tsx | 14 ++----- 2 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx diff --git a/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx b/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx new file mode 100644 index 000000000..8585ffddf --- /dev/null +++ b/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx @@ -0,0 +1,37 @@ +import { renderHook } from "@testing-library/react"; +import { useWaterFlowTexture } from "../water_flow_texture"; +import { RepeatWrapping } from "three"; + +jest.mock("@react-three/fiber", () => ({ + useFrame: jest.fn(callback => callback({}, 0.1)), // Simulate some time passing +})); + +jest.mock("three", () => ({ + ...jest.requireActual("three"), + TextureLoader: jest.fn().mockImplementation(() => ({ + load: jest.fn(() => ({ + wrapS: undefined, + wrapT: undefined, + offset: { x: 0 }, + })), + })), + RepeatWrapping: "RepeatWrapping", +})); + +describe("useWaterFlowTexture", () => { + it("initializes texture with repeat wrapping", () => { + const { result } = renderHook(() => useWaterFlowTexture(false)); + expect(result.current.wrapS).toBe(RepeatWrapping); + expect(result.current.wrapT).toBe(RepeatWrapping); + }); + + it("does not update texture offset when waterFlow is false", () => { + const { result } = renderHook(() => useWaterFlowTexture(false)); + expect(result.current.offset.x).toBe(0); + }); + + it("updates texture offset when waterFlow is true", () => { + const { result } = renderHook(() => useWaterFlowTexture(true)); + expect(result.current.offset.x).toBeLessThan(0); + }); +}); \ No newline at end of file diff --git a/frontend/three_d_garden/bot/water_flow_texture.tsx b/frontend/three_d_garden/bot/water_flow_texture.tsx index f011f59ea..8dbf5f3e2 100644 --- a/frontend/three_d_garden/bot/water_flow_texture.tsx +++ b/frontend/three_d_garden/bot/water_flow_texture.tsx @@ -1,23 +1,17 @@ -import { useEffect, useState } from "react"; +import { useMemo } from "react"; import { useFrame } from "@react-three/fiber"; import { TextureLoader, RepeatWrapping } from "three"; import { ASSETS } from "../constants"; export const useWaterFlowTexture = (waterFlow: boolean) => { - const [waterTexture, setWaterTexture] = useState(() => { + const waterTexture = useMemo(() => { const texture = new TextureLoader().load(ASSETS.textures.water); texture.wrapS = texture.wrapT = RepeatWrapping; return texture; - }); - - useEffect(() => { - const newTexture = new TextureLoader().load(ASSETS.textures.water); - newTexture.wrapS = newTexture.wrapT = RepeatWrapping; - setWaterTexture(newTexture); - }, [waterFlow]); + }, []); useFrame((_, delta) => { - if (waterFlow && waterTexture) { + if (waterFlow) { waterTexture.offset.x -= delta * 0.5; } }); From efcc61fe354edb37a5ff2f482d491f0d7a43386c Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Sat, 22 Feb 2025 00:54:09 -0800 Subject: [PATCH 08/19] newline --- .../three_d_garden/bot/__tests__/water_flow_texture_test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx b/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx index 8585ffddf..eccbc2abb 100644 --- a/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx +++ b/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx @@ -34,4 +34,4 @@ describe("useWaterFlowTexture", () => { const { result } = renderHook(() => useWaterFlowTexture(true)); expect(result.current.offset.x).toBeLessThan(0); }); -}); \ No newline at end of file +}); From 8561cfc9ebbb5c3512195f8f71b515fdf9bc2ad1 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Tue, 25 Feb 2025 23:08:16 -0800 Subject: [PATCH 09/19] add WaterTube component --- .../bot/__tests__/water_flow_texture_test.tsx | 37 ------------ .../bot/__tests__/x_axis_water_tube_test.tsx | 7 ++- frontend/three_d_garden/bot/bot.tsx | 57 +++++-------------- .../components/__tests__/water_tube_test.tsx | 47 +++++++++++++++ .../bot/components/water_tube.tsx | 47 +++++++++++++++ .../three_d_garden/bot/water_flow_texture.tsx | 20 ------- .../three_d_garden/bot/x_axis_water_tube.tsx | 20 ++----- 7 files changed, 118 insertions(+), 117 deletions(-) delete mode 100644 frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx create mode 100644 frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx create mode 100644 frontend/three_d_garden/bot/components/water_tube.tsx delete mode 100644 frontend/three_d_garden/bot/water_flow_texture.tsx diff --git a/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx b/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx deleted file mode 100644 index eccbc2abb..000000000 --- a/frontend/three_d_garden/bot/__tests__/water_flow_texture_test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { renderHook } from "@testing-library/react"; -import { useWaterFlowTexture } from "../water_flow_texture"; -import { RepeatWrapping } from "three"; - -jest.mock("@react-three/fiber", () => ({ - useFrame: jest.fn(callback => callback({}, 0.1)), // Simulate some time passing -})); - -jest.mock("three", () => ({ - ...jest.requireActual("three"), - TextureLoader: jest.fn().mockImplementation(() => ({ - load: jest.fn(() => ({ - wrapS: undefined, - wrapT: undefined, - offset: { x: 0 }, - })), - })), - RepeatWrapping: "RepeatWrapping", -})); - -describe("useWaterFlowTexture", () => { - it("initializes texture with repeat wrapping", () => { - const { result } = renderHook(() => useWaterFlowTexture(false)); - expect(result.current.wrapS).toBe(RepeatWrapping); - expect(result.current.wrapT).toBe(RepeatWrapping); - }); - - it("does not update texture offset when waterFlow is false", () => { - const { result } = renderHook(() => useWaterFlowTexture(false)); - expect(result.current.offset.x).toBe(0); - }); - - it("updates texture offset when waterFlow is true", () => { - const { result } = renderHook(() => useWaterFlowTexture(true)); - expect(result.current.offset.x).toBeLessThan(0); - }); -}); diff --git a/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx b/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx index 95d8e064c..3938bfa39 100644 --- a/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx +++ b/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { mount } from "enzyme"; +import { render } from "@testing-library/react"; import { XAxisWaterTubeProps, XAxisWaterTube } from "../x_axis_water_tube"; import { clone } from "lodash"; import { INITIAL } from "../../config"; @@ -10,7 +10,8 @@ describe("", () => { }); it("renders", () => { - const wrapper = mount(); - expect(wrapper.html()).toContain("x-axis-water-tube"); + const p = fakeProps(); + const { container } = render(); + expect(container).toContainHTML("x-axis-water-tube"); }); }); diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index 23fdd61a8..cfe0289e8 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -27,7 +27,7 @@ import { Tools } from "./components/tools"; import { ElectronicsBox } from "./components/electronics_box"; import { Bounds } from "./components/bounds"; import { SlotWithTool } from "../../resources/interfaces"; -import { useWaterFlowTexture } from "./water_flow_texture"; +import { WaterTube } from "./components/water_tube"; const extrusionWidth = 20; const utmRadius = 35; @@ -166,7 +166,6 @@ export const Bot = (props: FarmbotModelProps) => { const [beamShape, setBeamShape] = useState(); const [columnShape, setColumnShape] = useState(); const [zAxisShape, setZAxisShape] = useState(); - const waterTexture = useWaterFlowTexture(config.waterFlow); useEffect(() => { if (!(trackShape && beamShape && columnShape && zAxisShape)) { const loader = new SVGLoader(); @@ -809,10 +808,9 @@ export const Bot = (props: FarmbotModelProps) => { geometry={beltClip.nodes[PartName.beltClip].geometry}> - - + { threeSpace(20, bedWidthOuter), columnLength - 217, ], - ), 40, 5, 8]}> - - + ), 40, 5, 8]} /> { scale={1000} geometry={solenoid.nodes[PartName.solenoid].geometry} material={solenoid.materials.PaletteMaterial001} /> - { threeSpace(35, bedWidthOuter) + bedYOffset, columnLength + 90, ], - ), 20, 5, 8]}> - - - + { threeSpace(y - 10, bedWidthOuter) + bedYOffset, columnLength + 180, ], - ), 20, 5, 8]}> - - - + { threeSpace(y + 15, bedWidthOuter) + bedYOffset, columnLength - z - zGantryOffset + 75, ], - ), 20, 5, 8]}> - - + ), 20, 5, 8]} /> void; +jest.mock("@react-three/fiber", () => ({ + useFrame: jest.fn((callback) => { + frameCallback = callback; + }), +})); + +describe("", () => { + it("renders static", () => { + const wrapper = render(); + const material = wrapper.container.querySelector("meshphongmaterial"); + expect(material).not.toHaveAttribute("map"); + expect(material).toHaveAttribute("opacity", "0.5"); + }); + + it("renders flowing", () => { + const wrapper = render(); + const material = wrapper.container.querySelector("meshphongmaterial"); + expect(material).toHaveAttribute("map"); + expect(material).toHaveAttribute("opacity", "0.75"); + }); +}); + +describe("useWaterFlowTexture", () => { + it("returns undefined texture when static", () => { + const { result } = renderHook(() => useWaterFlowTexture(false)); + expect(result.current).toBeUndefined(); + }); + + it("offsets texture when flowing", () => { + const { result } = renderHook(() => useWaterFlowTexture(true)); + const initialOffset = result.current!.offset.x; + const delta = 1; + frameCallback({}, delta); + expect(result.current!.offset.x).toBe(initialOffset - delta * 0.25); + }); +}); diff --git a/frontend/three_d_garden/bot/components/water_tube.tsx b/frontend/three_d_garden/bot/components/water_tube.tsx new file mode 100644 index 000000000..1815d9b16 --- /dev/null +++ b/frontend/three_d_garden/bot/components/water_tube.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from "react"; +import { Tube } from "@react-three/drei"; +import { MeshPhongMaterial } from "../../components"; +import { TextureLoader, RepeatWrapping, Texture } from "three"; +import { useFrame } from "@react-three/fiber"; +import { ASSETS } from "../../constants"; + +const waterTexture = new TextureLoader().load(ASSETS.textures.water); +waterTexture.wrapS = waterTexture.wrapT = RepeatWrapping; + +export interface WaterTubeProps { + name: string; + args: Parameters[0]["args"]; + waterFlow: boolean; +} + +export const useWaterFlowTexture = (waterFlow: boolean): Texture | undefined => { + const texture = useMemo(() => waterFlow ? waterTexture : undefined, [waterFlow]); + + useFrame((_, delta) => { + if (waterFlow) { + waterTexture.offset.x -= delta * 0.25; + } + }); + + return texture; +}; + +export const WaterTube = (props: WaterTubeProps) => { + const { name, args, waterFlow } = props; + const waterTexture = useWaterFlowTexture(waterFlow); + + return + {waterFlow + ? + : } + ; +}; diff --git a/frontend/three_d_garden/bot/water_flow_texture.tsx b/frontend/three_d_garden/bot/water_flow_texture.tsx deleted file mode 100644 index 8dbf5f3e2..000000000 --- a/frontend/three_d_garden/bot/water_flow_texture.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useMemo } from "react"; -import { useFrame } from "@react-three/fiber"; -import { TextureLoader, RepeatWrapping } from "three"; -import { ASSETS } from "../constants"; - -export const useWaterFlowTexture = (waterFlow: boolean) => { - const waterTexture = useMemo(() => { - const texture = new TextureLoader().load(ASSETS.textures.water); - texture.wrapS = texture.wrapT = RepeatWrapping; - return texture; - }, []); - - useFrame((_, delta) => { - if (waterFlow) { - waterTexture.offset.x -= delta * 0.5; - } - }); - - return waterTexture; -}; diff --git a/frontend/three_d_garden/bot/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/x_axis_water_tube.tsx index 19db86912..432cfd43f 100644 --- a/frontend/three_d_garden/bot/x_axis_water_tube.tsx +++ b/frontend/three_d_garden/bot/x_axis_water_tube.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { Cylinder, Tube } from "@react-three/drei"; +import { Cylinder } from "@react-three/drei"; import { Config } from "../config"; import { threeSpace, easyCubicBezierCurve3 } from "../helpers"; import { Group, MeshPhongMaterial } from "../components"; -import { useWaterFlowTexture } from "./water_flow_texture"; +import { WaterTube } from "./components/water_tube"; export interface XAxisWaterTubeProps { config: Config; @@ -26,19 +26,11 @@ export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { ], ); - const waterTexture = useWaterFlowTexture(config.waterFlow); - return ( - - - - + + Date: Wed, 26 Feb 2025 08:02:54 -0800 Subject: [PATCH 10/19] more 3D component organization --- frontend/three_d_garden/bot/bot.tsx | 91 +--------------- .../components/__tests__/solenoid_test.tsx | 16 +++ .../__tests__/x_axis_water_tube_test.tsx | 2 +- .../three_d_garden/bot/components/index.ts | 6 + .../bot/components/solenoid.tsx | 103 ++++++++++++++++++ .../bot/components/x_axis_water_tube.tsx | 48 ++++++++ frontend/three_d_garden/bot/index.ts | 1 - .../three_d_garden/bot/x_axis_water_tube.tsx | 50 --------- 8 files changed, 178 insertions(+), 139 deletions(-) create mode 100644 frontend/three_d_garden/bot/components/__tests__/solenoid_test.tsx rename frontend/three_d_garden/bot/{ => components}/__tests__/x_axis_water_tube_test.tsx (91%) create mode 100644 frontend/three_d_garden/bot/components/index.ts create mode 100644 frontend/three_d_garden/bot/components/solenoid.tsx create mode 100644 frontend/three_d_garden/bot/components/x_axis_water_tube.tsx delete mode 100644 frontend/three_d_garden/bot/x_axis_water_tube.tsx diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index cfe0289e8..0f6df1a84 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -21,13 +21,11 @@ import { VacuumPumpCover, VacuumPumpCoverFull, } from "./parts"; import { PowerSupply } from "./power_supply"; -import { XAxisWaterTube } from "./x_axis_water_tube"; import { Group, Mesh, MeshPhongMaterial } from "../components"; -import { Tools } from "./components/tools"; -import { ElectronicsBox } from "./components/electronics_box"; -import { Bounds } from "./components/bounds"; +import { + ElectronicsBox, Bounds, Tools, Solenoid, XAxisWaterTube, +} from "./components"; import { SlotWithTool } from "../../resources/interfaces"; -import { WaterTube } from "./components/water_tube"; const extrusionWidth = 20; const utmRadius = 35; @@ -79,11 +77,6 @@ type CameraMountHalf = GLTF & { nodes: { [PartName.cameraMountHalf]: THREE.Mesh }; materials: never; } - -type Solenoid = GLTF & { - nodes: { [PartName.solenoid]: THREE.Mesh }; - materials: { PaletteMaterial001: THREE.MeshStandardMaterial }; -} type XAxisCCMount = GLTF & { nodes: { [PartName.xAxisCCMount]: THREE.Mesh }; materials: never; @@ -160,7 +153,6 @@ export const Bot = (props: FarmbotModelProps) => { const VacuumPumpCoverComponent = VacuumPumpCover(vacuumPumpCover); const cameraMountHalf = useGLTF( ASSETS.models.cameraMountHalf, LIB_DIR) as CameraMountHalf; - const solenoid = useGLTF(ASSETS.models.solenoid, LIB_DIR) as Solenoid; const xAxisCCMount = useGLTF(ASSETS.models.xAxisCCMount, LIB_DIR) as XAxisCCMount; const [trackShape, setTrackShape] = useState(); const [beamShape, setBeamShape] = useState(); @@ -808,82 +800,7 @@ export const Bot = (props: FarmbotModelProps) => { geometry={beltClip.nodes[PartName.beltClip].geometry}> - - - - - - - + ", () => { + const fakeProps = (): SolenoidProps => ({ + config: clone(INITIAL), + }); + + it("renders solenoid", () => { + const { container } = render(); + expect(container).toContainHTML("solenoid"); + }); +}); diff --git a/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx b/frontend/three_d_garden/bot/components/__tests__/x_axis_water_tube_test.tsx similarity index 91% rename from frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx rename to frontend/three_d_garden/bot/components/__tests__/x_axis_water_tube_test.tsx index 3938bfa39..8b62ef71e 100644 --- a/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/x_axis_water_tube_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { XAxisWaterTubeProps, XAxisWaterTube } from "../x_axis_water_tube"; import { clone } from "lodash"; -import { INITIAL } from "../../config"; +import { INITIAL } from "../../../config"; describe("", () => { const fakeProps = (): XAxisWaterTubeProps => ({ diff --git a/frontend/three_d_garden/bot/components/index.ts b/frontend/three_d_garden/bot/components/index.ts new file mode 100644 index 000000000..91b83999a --- /dev/null +++ b/frontend/three_d_garden/bot/components/index.ts @@ -0,0 +1,6 @@ +export * from "./bounds"; +export * from "./electronics_box"; +export * from "./solenoid"; +export * from "./tools"; +export * from "./water_tube"; +export * from "./x_axis_water_tube"; diff --git a/frontend/three_d_garden/bot/components/solenoid.tsx b/frontend/three_d_garden/bot/components/solenoid.tsx new file mode 100644 index 000000000..ea8d99a64 --- /dev/null +++ b/frontend/three_d_garden/bot/components/solenoid.tsx @@ -0,0 +1,103 @@ +import React from "react"; +import * as THREE from "three"; +import { Config } from "../../config"; +import { Group, Mesh } from "../../components"; +import { WaterTube } from "./water_tube"; +import { easyCubicBezierCurve3, threeSpace } from "../../helpers"; +import { GLTF } from "three-stdlib"; +import { useGLTF } from "@react-three/drei"; +import { ASSETS, LIB_DIR, PartName } from "../../constants"; + +type SolenoidPart = GLTF & { + nodes: { [PartName.solenoid]: THREE.Mesh }; + materials: { PaletteMaterial001: THREE.MeshStandardMaterial }; +} + +export interface SolenoidProps { + config: Config; +} + +export const Solenoid = (props: SolenoidProps) => { + const { config } = props; + const { + x, y, z, bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset, + columnLength, zGantryOffset, + } = config; + const solenoid = useGLTF(ASSETS.models.solenoid, LIB_DIR) as SolenoidPart; + return + + + + + + ; +}; diff --git a/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx new file mode 100644 index 000000000..e7b9b28e8 --- /dev/null +++ b/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Cylinder } from "@react-three/drei"; +import { Config } from "../../config"; +import { threeSpace, easyCubicBezierCurve3 } from "../../helpers"; +import { Group, MeshPhongMaterial } from "../../components"; +import { WaterTube } from "./water_tube"; + +export interface XAxisWaterTubeProps { + config: Config; +} + +export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { + const { config } = props; + const groundZ = -config.bedHeight - config.bedZOffset; + const barbX = threeSpace(config.bedLengthOuter / 2 + 400, config.bedLengthOuter); + const barbY = threeSpace(-50, config.bedWidthOuter); + const barbZ = groundZ + 20; + const tubePath = easyCubicBezierCurve3( + [barbX, barbY, barbZ], + [-300, 0, 0], + [300, 0, 0], + [ + threeSpace(config.bedLengthOuter / 2 - 20, config.bedLengthOuter), + threeSpace(-30, config.bedWidthOuter), + -140, + ], + ); + + return + + + + + + + + ; +}; diff --git a/frontend/three_d_garden/bot/index.ts b/frontend/three_d_garden/bot/index.ts index 7877d4480..1192d9585 100644 --- a/frontend/three_d_garden/bot/index.ts +++ b/frontend/three_d_garden/bot/index.ts @@ -1,3 +1,2 @@ export * from "./bot"; export * from "./power_supply"; -export * from "./x_axis_water_tube"; diff --git a/frontend/three_d_garden/bot/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/x_axis_water_tube.tsx deleted file mode 100644 index 432cfd43f..000000000 --- a/frontend/three_d_garden/bot/x_axis_water_tube.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import { Cylinder } from "@react-three/drei"; -import { Config } from "../config"; -import { threeSpace, easyCubicBezierCurve3 } from "../helpers"; -import { Group, MeshPhongMaterial } from "../components"; -import { WaterTube } from "./components/water_tube"; - -export interface XAxisWaterTubeProps { - config: Config; -} - -export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { - const { config } = props; - const groundZ = -config.bedHeight - config.bedZOffset; - const barbX = threeSpace(config.bedLengthOuter / 2 + 400, config.bedLengthOuter); - const barbY = threeSpace(-50, config.bedWidthOuter); - const barbZ = groundZ + 20; - const tubePath = easyCubicBezierCurve3( - [barbX, barbY, barbZ], - [-300, 0, 0], - [300, 0, 0], - [ - threeSpace(config.bedLengthOuter / 2 - 20, config.bedLengthOuter), - threeSpace(-30, config.bedWidthOuter), - -140, - ], - ); - - return ( - - - - - - - - - - ); -}; From f153099f849fa006823a5ab713a154302bfc810a Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 26 Feb 2025 16:41:31 -0800 Subject: [PATCH 11/19] fix 3D grid bug --- .../__tests__/map_size_setting_test.tsx | 24 ++++++++++------- frontend/farm_designer/map_size_setting.tsx | 6 ++--- .../garden/__tests__/grid_test.tsx | 10 ++++++- frontend/three_d_garden/garden/grid.tsx | 26 ++++++++++++------- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/frontend/farm_designer/__tests__/map_size_setting_test.tsx b/frontend/farm_designer/__tests__/map_size_setting_test.tsx index afc3a1f8b..8f8d9f74e 100644 --- a/frontend/farm_designer/__tests__/map_size_setting_test.tsx +++ b/frontend/farm_designer/__tests__/map_size_setting_test.tsx @@ -5,21 +5,27 @@ jest.mock("../../config_storage/actions", () => ({ import React from "react"; import { MapSizeInputs, MapSizeInputsProps } from "../map_size_setting"; -import { mount } from "enzyme"; +import { render, screen } from "@testing-library/react"; import { setWebAppConfigValue } from "../../config_storage/actions"; import { NumericSetting } from "../../session_keys"; +import { fakeWebAppConfig } from "../../__test_support__/fake_state/resources"; +import { WebAppConfig } from "farmbot/dist/resources/configs/web_app"; +import { changeBlurableInputRTL } from "../../__test_support__/helpers"; describe("", () => { - const fakeProps = (): MapSizeInputsProps => ({ - getConfigValue: () => 100, - dispatch: jest.fn(), - }); + const fakeProps = (config: WebAppConfig): MapSizeInputsProps => { + return { + getConfigValue: key => config[key], + dispatch: jest.fn(), + }; + }; it("changes value", () => { - const wrapper = mount(); - wrapper.find("input").last().simulate("change"), { - currentTarget: { value: 100 } - }; + const config = fakeWebAppConfig(); + const p = fakeProps(config.body); + render(); + const input = screen.getByDisplayValue("" + config.body.map_size_y); + changeBlurableInputRTL(input, "100"); expect(setWebAppConfigValue).toHaveBeenCalledWith( NumericSetting.map_size_y, "100"); }); diff --git a/frontend/farm_designer/map_size_setting.tsx b/frontend/farm_designer/map_size_setting.tsx index 3b107f52d..8623dbe7d 100644 --- a/frontend/farm_designer/map_size_setting.tsx +++ b/frontend/farm_designer/map_size_setting.tsx @@ -3,7 +3,7 @@ import { GetWebAppConfigValue, setWebAppConfigValue, } from "../config_storage/actions"; import { t } from "../i18next_wrapper"; -import { Row } from "../ui"; +import { BlurableInput, Row } from "../ui"; import { NumericSetting } from "../session_keys"; import { NumberConfigKey as WebAppNumberConfigKey, @@ -20,12 +20,12 @@ interface LengthInputProps { const LengthInput = (props: LengthInputProps) => - props.dispatch(setWebAppConfigValue( + onCommit={e => props.dispatch(setWebAppConfigValue( props.setting, e.currentTarget.value))} /> ; diff --git a/frontend/three_d_garden/garden/__tests__/grid_test.tsx b/frontend/three_d_garden/garden/__tests__/grid_test.tsx index 986d9fc40..d99637231 100644 --- a/frontend/three_d_garden/garden/__tests__/grid_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/grid_test.tsx @@ -1,9 +1,17 @@ import React from "react"; import { render } from "@testing-library/react"; -import { Grid, GridProps } from "../grid"; +import { Grid, gridLineOffsets, GridProps } from "../grid"; import { INITIAL } from "../../config"; import { clone } from "lodash"; +describe("gridLineOffsets()", () => { + it("calculates offsets", () => { + expect(gridLineOffsets(50)).toEqual([0, 50]); + expect(gridLineOffsets(200)).toEqual([0, 100, 200]); + expect(gridLineOffsets(510)).toEqual([0, 100, 200, 300, 400, 500, 510]); + }); +}); + describe("", () => { const fakeProps = (): GridProps => ({ config: clone(INITIAL), diff --git a/frontend/three_d_garden/garden/grid.tsx b/frontend/three_d_garden/garden/grid.tsx index 821382507..98085f001 100644 --- a/frontend/three_d_garden/garden/grid.tsx +++ b/frontend/three_d_garden/garden/grid.tsx @@ -3,7 +3,15 @@ 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"; +import { chain, floor, range } from "lodash"; + +export const gridLineOffsets = (botDimension: number): number[] => { + const lastRegularOffset = floor(botDimension, -2); + return chain(range(0, lastRegularOffset + 100, 100)) + .concat(botDimension) + .uniq() + .value(); +}; export interface GridProps { config: Config; @@ -15,19 +23,19 @@ export const Grid = (props: GridProps) => { 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 => - + )} ; }; From 073b4062373deb0fc47c7ad545c0357a11ec9fca Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Thu, 27 Feb 2025 10:26:53 -0800 Subject: [PATCH 12/19] add watering animations --- .../bot/components/__tests__/tools_test.tsx | 39 +++++++++++ .../__tests__/water_stream_test.tsx | 35 ++++++++++ .../components/__tests__/water_tube_test.tsx | 44 +++---------- .../__tests__/watering_animations_test.tsx | 66 +++++++++++++++++++ .../three_d_garden/bot/components/tools.tsx | 19 ++++-- .../bot/components/water_stream.tsx | 40 +++++++++++ .../bot/components/water_tube.tsx | 52 +++++---------- .../bot/components/watering_animations.tsx | 63 ++++++++++++++++++ 8 files changed, 284 insertions(+), 74 deletions(-) create mode 100644 frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx create mode 100644 frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx create mode 100644 frontend/three_d_garden/bot/components/water_stream.tsx create mode 100644 frontend/three_d_garden/bot/components/watering_animations.tsx diff --git a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx index 21dbf78f0..dac2b44dc 100644 --- a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx @@ -34,6 +34,11 @@ import { fakeTool, fakeToolSlot, } from "../../../../__test_support__/fake_state/resources"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; +import { WateringAnimations } from "../watering_animations"; + +jest.mock("../watering_animations", () => ({ + WateringAnimations: jest.fn(() =>
WateringAnimations
), +})); describe("", () => { const fakeProps = (): ToolsProps => ({ @@ -97,4 +102,38 @@ describe("", () => { const { container } = render(); expect(container).not.toContainHTML("toolbay3"); }); + + it("renders watering animations when not in toolbay and water flowing", () => { + const p = fakeProps(); + p.config.waterFlow = true; + const tool = fakeTool(); + tool.body.name = "watering nozzle"; + p.toolSlots = []; + p.mountedToolName = "watering nozzle"; + render(); + expect(WateringAnimations).toHaveBeenCalled(); + }); + + it ("doesn't render watering animations when water not flowing", () => { + const p = fakeProps(); + p.config.waterFlow = false; + const tool = fakeTool(); + tool.body.name = "watering nozzle"; + p.toolSlots = []; + p.mountedToolName = "watering nozzle"; + render(); + expect(WateringAnimations).not.toHaveBeenCalled(); + }); + + it("doesn't render watering animations when in toolbay", () => { + const p = fakeProps(); + p.config.waterFlow = true; + const tool = fakeTool(); + tool.body.name = "watering nozzle"; + const toolSlot = fakeToolSlot(); + toolSlot.body.tool_id = tool.body.id; + p.toolSlots = [{ toolSlot, tool }]; + render(); + expect(WateringAnimations).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx b/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx new file mode 100644 index 000000000..a1504fc3c --- /dev/null +++ b/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { render, renderHook } from "@testing-library/react"; +import { WaterStream, useWaterFlowTexture } from "../water_stream"; + +let frameCallback: (state: unknown, delta: number) => void; +jest.mock("@react-three/fiber", () => ({ + useFrame: jest.fn((callback) => { + frameCallback = callback; + }), +})); + +describe("", () => { + it("renders", () => { + const wrapper = render(); + expect(wrapper.container).toContainHTML("mock-water-stream"); + }); +}); + +describe("useWaterFlowTexture", () => { + it("returns undefined texture when static", () => { + const { result } = renderHook(() => useWaterFlowTexture(false)); + expect(result.current).toBeUndefined(); + }); + + it("offsets texture when flowing", () => { + const { result } = renderHook(() => useWaterFlowTexture(true)); + const initialOffset = result.current!.offset.x; + const delta = 1; + frameCallback({}, delta); + expect(result.current!.offset.x).toBe(initialOffset - delta * 0.05); + }); +}); diff --git a/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx b/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx index e55d594a2..53fcb4c3c 100644 --- a/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx @@ -1,47 +1,23 @@ import React from "react"; -import { render, renderHook } from "@testing-library/react"; -import { WaterTube, useWaterFlowTexture } from "../water_tube"; - -let frameCallback: (state: unknown, delta: number) => void; -jest.mock("@react-three/fiber", () => ({ - useFrame: jest.fn((callback) => { - frameCallback = callback; - }), -})); +import { render } from "@testing-library/react"; +import { WaterTube } from "../water_tube"; describe("", () => { - it("renders static", () => { + it("renders", () => { const wrapper = render(); - const material = wrapper.container.querySelector("meshphongmaterial"); - expect(material).not.toHaveAttribute("map"); - expect(material).toHaveAttribute("opacity", "0.5"); + expect(wrapper.container).toContainHTML("mock-tube-tube"); + expect(wrapper.container).toContainHTML("mock-tube-water-stream"); }); - it("renders flowing", () => { + it("handles undefined args", () => { const wrapper = render(); - const material = wrapper.container.querySelector("meshphongmaterial"); - expect(material).toHaveAttribute("map"); - expect(material).toHaveAttribute("opacity", "0.75"); - }); -}); - -describe("useWaterFlowTexture", () => { - it("returns undefined texture when static", () => { - const { result } = renderHook(() => useWaterFlowTexture(false)); - expect(result.current).toBeUndefined(); - }); - - it("offsets texture when flowing", () => { - const { result } = renderHook(() => useWaterFlowTexture(true)); - const initialOffset = result.current!.offset.x; - const delta = 1; - frameCallback({}, delta); - expect(result.current!.offset.x).toBe(initialOffset - delta * 0.25); + args={undefined} + waterFlow={false} />); + expect(wrapper.container).toContainHTML("mock-tube-tube"); + expect(wrapper.container).toContainHTML("mock-tube-water-stream"); }); }); diff --git a/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx b/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx new file mode 100644 index 000000000..f83b62c04 --- /dev/null +++ b/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { WateringAnimations } from "../watering_animations"; + +jest.mock("@react-three/drei", () => ({ + Cloud: ({ position }: { position: number[] }) => ( +
+ ), + Clouds: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +jest.mock("../water_stream", () => ({ + WaterStream: ({ name }: { name: string }) => ( +
+ ), +})); + +jest.mock("../../components", () => ({ + Group: ({ + children, + visible, + name, + ...rest + }: { + children: React.ReactNode; + visible: boolean; + name: string; + }) => ( +
+ {children} +
+ ), +})); + +describe("", () => { + const fakeProps = { + waterFlow: true, + botPositionZ: 100, + soilHeight: 0, + }; + + it("calculates correct nozzle to soil distance", () => { + const props = { + ...fakeProps, + botPositionZ: 200, + soilHeight: 50, + }; + const { container } = render(); + const clouds = container.querySelectorAll('[data-testid="mock-cloud"]'); + + expect(clouds[0]).toHaveAttribute("data-test-position", "0,0,35"); + expect(clouds[1]).toHaveAttribute("data-test-position", "0,0,190"); + }); + + it("renders water streams with correct names", () => { + const { container } = render(); + const waterStreams = container.querySelectorAll( + '[data-testid="mock-water-stream"]' + ); + + expect(waterStreams[0]).toHaveAttribute("data-name", "water-stream-0"); + expect(waterStreams[15]).toHaveAttribute("data-name", "water-stream-15"); + }); +}); diff --git a/frontend/three_d_garden/bot/components/tools.tsx b/frontend/three_d_garden/bot/components/tools.tsx index a0013430f..c1510444f 100644 --- a/frontend/three_d_garden/bot/components/tools.tsx +++ b/frontend/three_d_garden/bot/components/tools.tsx @@ -22,6 +22,7 @@ import { } from "../../../farm_designer/map/tool_graphics/all_tools"; import { Xyz } from "farmbot"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; +import { WateringAnimations } from "./watering_animations"; type Toolbay3 = GLTF & { nodes: { @@ -108,6 +109,8 @@ export const Tools = (props: ToolsProps) => { : reduceToolName(props.mountedToolName); const zZero = zZeroFunc(props.config); const zDir = zDirFunc(props.config); + const waterFlow = props.config.waterFlow; + const soilHeight = props.config.soilHeight; const toolbay3 = useGLTF(ASSETS.models.toolbay3, LIB_DIR) as Toolbay3; const toolbay1 = useGLTF(ASSETS.models.toolbay1, LIB_DIR) as Toolbay1; @@ -180,11 +183,12 @@ export const Tools = (props: ToolsProps) => { const Tool = (toolProps: ToolProps) => { const { toolPulloutDirection } = toolProps; - const mounted = toolProps.inToolbay && toolProps.toolName == mountedToolName; + const inToolbay = toolProps.inToolbay; + const mounted = inToolbay && toolProps.toolName == mountedToolName; const position = { x: threeSpace(toolProps.x, bedLengthOuter) + bedXOffset, y: threeSpace(toolProps.y, bedWidthOuter) + bedYOffset, - z: zZero - zDir * toolProps.z + (toolProps.inToolbay ? 0 : (utmHeight / 2 - 15)), + z: zZero - zDir * toolProps.z + (inToolbay ? 0 : (utmHeight / 2 - 15)), }; const common = { mounted, position, toolPulloutDirection }; switch (toolProps.toolName) { @@ -203,14 +207,19 @@ export const Tools = (props: ToolsProps) => { return + {!inToolbay && waterFlow && + } ; case ToolName.seedBin: return diff --git a/frontend/three_d_garden/bot/components/water_stream.tsx b/frontend/three_d_garden/bot/components/water_stream.tsx new file mode 100644 index 000000000..1c8b22c40 --- /dev/null +++ b/frontend/three_d_garden/bot/components/water_stream.tsx @@ -0,0 +1,40 @@ +import React, { useMemo } from "react"; +import { Tube } from "@react-three/drei"; +import { MeshPhongMaterial } from "../../components"; +import { TextureLoader, RepeatWrapping, Texture } from "three"; +import { useFrame } from "@react-three/fiber"; +import { ASSETS } from "../../constants"; + +const waterTexture = new TextureLoader().load(ASSETS.textures.water); +waterTexture.wrapS = waterTexture.wrapT = RepeatWrapping; + +export interface WaterStreamProps { + name: string; + args: Parameters[0]["args"]; + waterFlow: boolean; +} + +export const useWaterFlowTexture = (waterFlow: boolean): Texture | undefined => { + const texture = useMemo(() => waterFlow ? waterTexture : undefined, [waterFlow]); + + useFrame((_, delta) => { + if (waterFlow) { + waterTexture.offset.x -= delta * 0.05; + } + }); + + return texture; +}; + +export const WaterStream = (props: WaterStreamProps) => { + const { name, args, waterFlow } = props; + const waterTexture = useWaterFlowTexture(waterFlow); + + return + + ; +}; diff --git a/frontend/three_d_garden/bot/components/water_tube.tsx b/frontend/three_d_garden/bot/components/water_tube.tsx index 1815d9b16..52adeaaed 100644 --- a/frontend/three_d_garden/bot/components/water_tube.tsx +++ b/frontend/three_d_garden/bot/components/water_tube.tsx @@ -1,12 +1,7 @@ -import React, { useMemo } from "react"; +import React from "react"; import { Tube } from "@react-three/drei"; -import { MeshPhongMaterial } from "../../components"; -import { TextureLoader, RepeatWrapping, Texture } from "three"; -import { useFrame } from "@react-three/fiber"; -import { ASSETS } from "../../constants"; - -const waterTexture = new TextureLoader().load(ASSETS.textures.water); -waterTexture.wrapS = waterTexture.wrapT = RepeatWrapping; +import { MeshPhongMaterial, Group } from "../../components"; +import { WaterStream } from "./water_stream"; export interface WaterTubeProps { name: string; @@ -14,34 +9,21 @@ export interface WaterTubeProps { waterFlow: boolean; } -export const useWaterFlowTexture = (waterFlow: boolean): Texture | undefined => { - const texture = useMemo(() => waterFlow ? waterTexture : undefined, [waterFlow]); - - useFrame((_, delta) => { - if (waterFlow) { - waterTexture.offset.x -= delta * 0.25; - } - }); - - return texture; -}; - export const WaterTube = (props: WaterTubeProps) => { const { name, args, waterFlow } = props; - const waterTexture = useWaterFlowTexture(waterFlow); + const [tubePath, tubularSegments, radius = 5, radialSegments] = args || []; - return - {waterFlow - ? - : } - ; + return + + + + + ; }; diff --git a/frontend/three_d_garden/bot/components/watering_animations.tsx b/frontend/three_d_garden/bot/components/watering_animations.tsx new file mode 100644 index 000000000..7fab6e572 --- /dev/null +++ b/frontend/three_d_garden/bot/components/watering_animations.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { range } from "lodash"; +import { Group } from "../../components"; +import { ASSETS } from "../../constants"; +import { Cloud, Clouds as DreiClouds } from "@react-three/drei"; +import { WaterStream } from "./water_stream"; +import { easyCubicBezierCurve3 } from "../../helpers"; + +interface WateringAnimationsProps { + waterFlow: boolean; + botPositionZ: number; + soilHeight: number; +} + +export const WateringAnimations = (props: WateringAnimationsProps) => { + const { waterFlow, botPositionZ, soilHeight } = props; + const nozzleToSoil = botPositionZ - soilHeight; + + return + {range(16).map(i => { + const angle = (i * Math.PI * 2) / 16; + return ; + })} + + + + + + + ; +}; From 555b5ef7fb73732f1692c02ea9a198262e361948 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Thu, 27 Feb 2025 10:32:17 -0800 Subject: [PATCH 13/19] lint --- frontend/three_d_garden/bot/components/__tests__/tools_test.tsx | 2 +- .../bot/components/__tests__/watering_animations_test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx index dac2b44dc..4492f15ca 100644 --- a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx @@ -114,7 +114,7 @@ describe("", () => { expect(WateringAnimations).toHaveBeenCalled(); }); - it ("doesn't render watering animations when water not flowing", () => { + it("doesn't render watering animations when water not flowing", () => { const p = fakeProps(); p.config.waterFlow = false; const tool = fakeTool(); diff --git a/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx b/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx index f83b62c04..a69bd977b 100644 --- a/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx @@ -57,7 +57,7 @@ describe("", () => { it("renders water streams with correct names", () => { const { container } = render(); const waterStreams = container.querySelectorAll( - '[data-testid="mock-water-stream"]' + '[data-testid="mock-water-stream"]', ); expect(waterStreams[0]).toHaveAttribute("data-name", "water-stream-0"); From 20c3b27575a482107ee812ea5850febc3259654f Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 27 Feb 2025 15:41:07 -0800 Subject: [PATCH 14/19] refactor 3D water tubes --- frontend/__test_support__/three_d_mocks.tsx | 16 +++-- .../bot/components/__tests__/tools_test.tsx | 2 +- .../__tests__/water_stream_test.tsx | 15 +++-- .../components/__tests__/water_tube_test.tsx | 25 ++++--- .../__tests__/watering_animations_test.tsx | 67 ++++--------------- .../bot/components/solenoid.tsx | 36 ++++++---- .../three_d_garden/bot/components/tools.tsx | 11 ++- .../bot/components/water_stream.tsx | 10 ++- .../bot/components/water_tube.tsx | 21 +++--- .../bot/components/watering_animations.tsx | 34 +++++----- .../bot/components/x_axis_water_tube.tsx | 9 ++- 11 files changed, 115 insertions(+), 131 deletions(-) diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index a4a59c1ef..79386996b 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -10,6 +10,7 @@ import * as THREE from "three"; import React, { ReactNode } from "react"; import { TransitionFn, UseSpringProps } from "@react-spring/three"; import { ThreeElements } from "@react-three/fiber"; +import { Cloud, Clouds, Tube } from "@react-three/drei"; const GroupForTests = (props: ThreeElements["group"]) => // @ts-expect-error Property does not exist on type JSX.IntrinsicElements @@ -612,8 +613,9 @@ jest.mock("@react-three/drei", () => {
{name}
, Trail: ({ name }: { name: string }) =>
{name}
, - Tube: ({ name, children }: { name: string, children: ReactNode }) => -
{children}
, + Tube: (props: React.ComponentProps) => + // @ts-expect-error geometry props not assignable to div +
{props.children}
, Center: ({ children }: { children: ReactNode }) =>
{children}
, Text3D: ({ children }: { children: ReactNode }) => @@ -644,10 +646,12 @@ jest.mock("@react-three/drei", () => {
{children}
, Image: ({ name, url }: { name: string, url: string }) =>
{name} {url}
, - Clouds: ({ name }: { name: string }) => -
{name}
, - Cloud: ({ name }: { name: string }) => -
{name}
, + Clouds: (props: React.ComponentProps) => + // @ts-expect-error geometry props not assignable to div +
{props.children}
, + Cloud: (props: React.ComponentProps) => + // @ts-expect-error geometry props not assignable to div +
, OrthographicCamera: ({ name }: { name: string }) =>
{name}
, }; diff --git a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx index 4492f15ca..e963f4745 100644 --- a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx @@ -37,7 +37,7 @@ import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { WateringAnimations } from "../watering_animations"; jest.mock("../watering_animations", () => ({ - WateringAnimations: jest.fn(() =>
WateringAnimations
), + WateringAnimations: jest.fn(), })); describe("", () => { diff --git a/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx b/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx index a1504fc3c..b3c0735ae 100644 --- a/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx @@ -1,6 +1,8 @@ import React from "react"; import { render, renderHook } from "@testing-library/react"; -import { WaterStream, useWaterFlowTexture } from "../water_stream"; +import { + WaterStream, WaterStreamProps, useWaterFlowTexture, +} from "../water_stream"; let frameCallback: (state: unknown, delta: number) => void; jest.mock("@react-three/fiber", () => ({ @@ -10,11 +12,14 @@ jest.mock("@react-three/fiber", () => ({ })); describe("", () => { + const fakeProps = (): WaterStreamProps => ({ + name: "mock-water-stream", + args: [], + waterFlow: true, + }); + it("renders", () => { - const wrapper = render(); + const wrapper = render(); expect(wrapper.container).toContainHTML("mock-water-stream"); }); }); diff --git a/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx b/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx index 53fcb4c3c..03d70df4e 100644 --- a/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/water_tube_test.tsx @@ -1,22 +1,21 @@ import React from "react"; import { render } from "@testing-library/react"; -import { WaterTube } from "../water_tube"; +import { WaterTube, WaterTubeProps } from "../water_tube"; +import { easyCubicBezierCurve3 } from "../../../helpers"; describe("", () => { - it("renders", () => { - const wrapper = render(); - expect(wrapper.container).toContainHTML("mock-tube-tube"); - expect(wrapper.container).toContainHTML("mock-tube-water-stream"); + const fakeProps = (): WaterTubeProps => ({ + tubeName: "mock-tube", + tubePath: easyCubicBezierCurve3([0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]), + tubularSegments: 1, + radius: 1, + radialSegments: 1, + waterFlow: false, }); - it("handles undefined args", () => { - const wrapper = render(); + it("renders", () => { + const p = fakeProps(); + const wrapper = render(); expect(wrapper.container).toContainHTML("mock-tube-tube"); expect(wrapper.container).toContainHTML("mock-tube-water-stream"); }); diff --git a/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx b/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx index a69bd977b..ce607bcb8 100644 --- a/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/watering_animations_test.tsx @@ -1,66 +1,25 @@ import React from "react"; import { render } from "@testing-library/react"; -import { WateringAnimations } from "../watering_animations"; - -jest.mock("@react-three/drei", () => ({ - Cloud: ({ position }: { position: number[] }) => ( -
- ), - Clouds: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})); - -jest.mock("../water_stream", () => ({ - WaterStream: ({ name }: { name: string }) => ( -
- ), -})); - -jest.mock("../../components", () => ({ - Group: ({ - children, - visible, - name, - ...rest - }: { - children: React.ReactNode; - visible: boolean; - name: string; - }) => ( -
- {children} -
- ), -})); +import { + WateringAnimations, WateringAnimationsProps, +} from "../watering_animations"; describe("", () => { - const fakeProps = { + const fakeProps = (): WateringAnimationsProps => ({ waterFlow: true, botPositionZ: 100, soilHeight: 0, - }; - - it("calculates correct nozzle to soil distance", () => { - const props = { - ...fakeProps, - botPositionZ: 200, - soilHeight: 50, - }; - const { container } = render(); - const clouds = container.querySelectorAll('[data-testid="mock-cloud"]'); - - expect(clouds[0]).toHaveAttribute("data-test-position", "0,0,35"); - expect(clouds[1]).toHaveAttribute("data-test-position", "0,0,190"); }); - it("renders water streams with correct names", () => { - const { container } = render(); - const waterStreams = container.querySelectorAll( - '[data-testid="mock-water-stream"]', - ); + it("renders", () => { + const p = fakeProps(); + p.botPositionZ = 200; + p.soilHeight = 50; + const { container } = render(); + const streams = container.querySelectorAll("[name*='water-stream']"); + expect(streams.length).toEqual(16); - expect(waterStreams[0]).toHaveAttribute("data-name", "water-stream-0"); - expect(waterStreams[15]).toHaveAttribute("data-name", "water-stream-15"); + const clouds = container.querySelectorAll("[name*='waterfall-mist-cloud']"); + expect(clouds.length).toEqual(2); }); }); diff --git a/frontend/three_d_garden/bot/components/solenoid.tsx b/frontend/three_d_garden/bot/components/solenoid.tsx index ea8d99a64..695fab3b6 100644 --- a/frontend/three_d_garden/bot/components/solenoid.tsx +++ b/frontend/three_d_garden/bot/components/solenoid.tsx @@ -25,9 +25,9 @@ export const Solenoid = (props: SolenoidProps) => { } = config; const solenoid = useGLTF(ASSETS.models.solenoid, LIB_DIR) as SolenoidPart; return - { threeSpace(20, bedWidthOuter), columnLength - 217, ], - ), 40, 5, 8]} /> + )} + tubularSegments={40} + radius={5} + radialSegments={8} /> { scale={1000} geometry={solenoid.nodes[PartName.solenoid].geometry} material={solenoid.materials.PaletteMaterial001} /> - { threeSpace(35, bedWidthOuter) + bedYOffset, columnLength + 90, ], - ), 20, 5, 8]} /> - + { threeSpace(y - 10, bedWidthOuter) + bedYOffset, columnLength + 180, ], - ), 20, 5, 8]} /> - + { threeSpace(y + 15, bedWidthOuter) + bedYOffset, columnLength - z - zGantryOffset + 75, ], - ), 20, 5, 8]} /> + )} + tubularSegments={20} + radius={5} + radialSegments={8} /> ; }; diff --git a/frontend/three_d_garden/bot/components/tools.tsx b/frontend/three_d_garden/bot/components/tools.tsx index c1510444f..978b22c5a 100644 --- a/frontend/three_d_garden/bot/components/tools.tsx +++ b/frontend/three_d_garden/bot/components/tools.tsx @@ -109,8 +109,6 @@ export const Tools = (props: ToolsProps) => { : reduceToolName(props.mountedToolName); const zZero = zZeroFunc(props.config); const zDir = zDirFunc(props.config); - const waterFlow = props.config.waterFlow; - const soilHeight = props.config.soilHeight; const toolbay3 = useGLTF(ASSETS.models.toolbay3, LIB_DIR) as Toolbay3; const toolbay1 = useGLTF(ASSETS.models.toolbay1, LIB_DIR) as Toolbay1; @@ -182,8 +180,7 @@ export const Tools = (props: ToolsProps) => { } const Tool = (toolProps: ToolProps) => { - const { toolPulloutDirection } = toolProps; - const inToolbay = toolProps.inToolbay; + const { toolPulloutDirection, inToolbay } = toolProps; const mounted = inToolbay && toolProps.toolName == mountedToolName; const position = { x: threeSpace(toolProps.x, bedLengthOuter) + bedXOffset, @@ -215,11 +212,11 @@ export const Tools = (props: ToolsProps) => { scale={1000} geometry={wateringNozzle.nodes[PartName.wateringNozzle].geometry} material={wateringNozzle.materials.PaletteMaterial001} /> - {!inToolbay && waterFlow && + {!inToolbay && props.config.waterFlow && } + soilHeight={props.config.soilHeight} />} ; case ToolName.seedBin: return diff --git a/frontend/three_d_garden/bot/components/water_stream.tsx b/frontend/three_d_garden/bot/components/water_stream.tsx index 1c8b22c40..91a535d7f 100644 --- a/frontend/three_d_garden/bot/components/water_stream.tsx +++ b/frontend/three_d_garden/bot/components/water_stream.tsx @@ -8,9 +8,7 @@ import { ASSETS } from "../../constants"; const waterTexture = new TextureLoader().load(ASSETS.textures.water); waterTexture.wrapS = waterTexture.wrapT = RepeatWrapping; -export interface WaterStreamProps { - name: string; - args: Parameters[0]["args"]; +export interface WaterStreamProps extends React.ComponentProps { waterFlow: boolean; } @@ -27,13 +25,13 @@ export const useWaterFlowTexture = (waterFlow: boolean): Texture | undefined => }; export const WaterStream = (props: WaterStreamProps) => { - const { name, args, waterFlow } = props; + const { waterFlow } = props; const waterTexture = useWaterFlowTexture(waterFlow); - return ; diff --git a/frontend/three_d_garden/bot/components/water_tube.tsx b/frontend/three_d_garden/bot/components/water_tube.tsx index 52adeaaed..a8f33023a 100644 --- a/frontend/three_d_garden/bot/components/water_tube.tsx +++ b/frontend/three_d_garden/bot/components/water_tube.tsx @@ -2,27 +2,32 @@ import React from "react"; import { Tube } from "@react-three/drei"; import { MeshPhongMaterial, Group } from "../../components"; import { WaterStream } from "./water_stream"; +import { Curve, Vector3 } from "three"; export interface WaterTubeProps { - name: string; - args: Parameters[0]["args"]; + tubeName: string; + tubePath: Curve; + tubularSegments: number; + radius: number; + radialSegments: number; waterFlow: boolean; } export const WaterTube = (props: WaterTubeProps) => { - const { name, args, waterFlow } = props; - const [tubePath, tubularSegments, radius = 5, radialSegments] = args || []; + const { + tubeName, tubePath, tubularSegments, radius, radialSegments, waterFlow, + } = props; - return - + + args={[tubePath, tubularSegments, radius, radialSegments]}> - ; diff --git a/frontend/three_d_garden/bot/components/watering_animations.tsx b/frontend/three_d_garden/bot/components/watering_animations.tsx index 7fab6e572..765471f6d 100644 --- a/frontend/three_d_garden/bot/components/watering_animations.tsx +++ b/frontend/three_d_garden/bot/components/watering_animations.tsx @@ -2,11 +2,11 @@ import React from "react"; import { range } from "lodash"; import { Group } from "../../components"; import { ASSETS } from "../../constants"; -import { Cloud, Clouds as DreiClouds } from "@react-three/drei"; +import { Cloud, Clouds } from "@react-three/drei"; import { WaterStream } from "./water_stream"; import { easyCubicBezierCurve3 } from "../../helpers"; -interface WateringAnimationsProps { +export interface WateringAnimationsProps { waterFlow: boolean; botPositionZ: number; soilHeight: number; @@ -19,8 +19,8 @@ export const WateringAnimations = (props: WateringAnimationsProps) => { return {range(16).map(i => { const angle = (i * Math.PI * 2) / 16; - return { [25 * Math.sin(angle), 25 * Math.cos(angle), nozzleToSoil], ), 8, 1.5, 6]} />; })} - - - - + - - + ; }; diff --git a/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx index e7b9b28e8..af5266b57 100644 --- a/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx +++ b/frontend/three_d_garden/bot/components/x_axis_water_tube.tsx @@ -27,9 +27,12 @@ export const XAxisWaterTube = (props: XAxisWaterTubeProps) => { ); return - + Date: Fri, 28 Feb 2025 12:01:17 -0800 Subject: [PATCH 15/19] add nav on 3D plant/point/weed/tool click --- frontend/__test_support__/three_d_mocks.tsx | 7 +-- .../bot/components/__tests__/tools_test.tsx | 18 ++++++- .../three_d_garden/bot/components/tools.tsx | 20 ++++++-- .../garden/__tests__/plants_test.tsx | 14 +++++- .../garden/__tests__/point_test.tsx | 13 ++++- .../garden/__tests__/weed_test.tsx | 12 ++++- frontend/three_d_garden/garden/plants.tsx | 8 ++++ frontend/three_d_garden/garden/point.tsx | 48 +++++++++++-------- frontend/three_d_garden/garden/weed.tsx | 10 +++- 9 files changed, 117 insertions(+), 33 deletions(-) diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index 79386996b..664ce6878 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -10,7 +10,7 @@ import * as THREE from "three"; import React, { ReactNode } from "react"; import { TransitionFn, UseSpringProps } from "@react-spring/three"; import { ThreeElements } from "@react-three/fiber"; -import { Cloud, Clouds, Tube } from "@react-three/drei"; +import { Cloud, Clouds, Image, Tube } from "@react-three/drei"; const GroupForTests = (props: ThreeElements["group"]) => // @ts-expect-error Property does not exist on type JSX.IntrinsicElements @@ -644,8 +644,9 @@ jest.mock("@react-three/drei", () => {
{name}
, Billboard: ({ name, children }: { name: string, children: ReactNode }) =>
{children}
, - Image: ({ name, url }: { name: string, url: string }) => -
{name} {url}
, + Image: (props: React.ComponentProps) => + // @ts-expect-error geometry props not assignable to div +
{props.name} {props.url}
, Clouds: (props: React.ComponentProps) => // @ts-expect-error geometry props not assignable to div
{props.children}
, diff --git a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx index e963f4745..74a4e211a 100644 --- a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx @@ -26,7 +26,7 @@ jest.mock("react", () => { }); import React from "react"; -import { render } from "@testing-library/react"; +import { fireEvent, render } from "@testing-library/react"; import { INITIAL } from "../../../config"; import { clone } from "lodash"; import { Tools, ToolsProps } from "../tools"; @@ -35,6 +35,7 @@ import { } from "../../../../__test_support__/fake_state/resources"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { WateringAnimations } from "../watering_animations"; +import { Path } from "../../../../internal_urls"; jest.mock("../watering_animations", () => ({ WateringAnimations: jest.fn(), @@ -136,4 +137,19 @@ describe("", () => { render(); expect(WateringAnimations).not.toHaveBeenCalled(); }); + + it("navigates to tool info", () => { + const p = fakeProps(); + const tool = fakeTool(); + tool.body.name = "soil sensor"; + tool.body.id = 2; + const toolSlot = fakeToolSlot(); + toolSlot.body.id = 1; + toolSlot.body.tool_id = tool.body.id; + p.toolSlots = [{ toolSlot, tool }]; + const { container } = render(); + const slot = container.querySelector("[name='slot'"); + slot && fireEvent.click(slot); + expect(mockNavigate).toHaveBeenCalledWith(Path.toolSlots("1")); + }); }); diff --git a/frontend/three_d_garden/bot/components/tools.tsx b/frontend/three_d_garden/bot/components/tools.tsx index 978b22c5a..8bbb62a95 100644 --- a/frontend/three_d_garden/bot/components/tools.tsx +++ b/frontend/three_d_garden/bot/components/tools.tsx @@ -23,6 +23,8 @@ import { import { Xyz } from "farmbot"; import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { WateringAnimations } from "./watering_animations"; +import { useNavigate } from "react-router"; +import { Path } from "../../../internal_urls"; type Toolbay3 = GLTF & { nodes: { @@ -70,6 +72,7 @@ export interface ToolsProps { } interface ConvertedTools { + id?: number | undefined; x: number; y: number; z: number; @@ -85,6 +88,7 @@ export const convertSlotsWithTools = const toolName = reduceToolName(swt.tool?.body.name); if (toolName == ToolName.seedTrough) { troughIndex++; } return { + id: swt.toolSlot.body.id, x: swt.toolSlot.body.x, y: swt.toolSlot.body.y, z: swt.toolSlot.body.z, @@ -144,17 +148,23 @@ export const Tools = (props: ToolsProps) => { children?: React.ReactNode; toolPulloutDirection: ToolPulloutDirection; mounted: boolean; + id: number | undefined; + inToolbay: boolean; } const ToolbaySlot = (slotProps: ToolbaySlotProps) => { const { position, children, toolPulloutDirection, mounted } = slotProps; const rotationMultiplier = rotationFactor(toolPulloutDirection); - return + ]} + onClick={() => { + slotProps.id && navigate(Path.toolSlots(slotProps.id)); + }}> {rotationMultiplier && @@ -180,14 +190,16 @@ export const Tools = (props: ToolsProps) => { } const Tool = (toolProps: ToolProps) => { - const { toolPulloutDirection, inToolbay } = toolProps; + const { toolPulloutDirection, inToolbay, id } = toolProps; const mounted = inToolbay && toolProps.toolName == mountedToolName; const position = { x: threeSpace(toolProps.x, bedLengthOuter) + bedXOffset, y: threeSpace(toolProps.y, bedWidthOuter) + bedYOffset, z: zZero - zDir * toolProps.z + (inToolbay ? 0 : (utmHeight / 2 - 15)), }; - const common = { mounted, position, toolPulloutDirection }; + const common: ToolbaySlotProps = { + mounted, position, toolPulloutDirection, id, inToolbay, + }; switch (toolProps.toolName) { case ToolName.rotaryTool: return diff --git a/frontend/three_d_garden/garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx index 7aed3aadb..0e7b9d5d3 100644 --- a/frontend/three_d_garden/garden/__tests__/plants_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { clone } from "lodash"; import { fakePlant } from "../../../__test_support__/fake_state/resources"; import { INITIAL } from "../../config"; @@ -10,6 +10,7 @@ import { ThreeDPlantProps, } from "../plants"; import { CROPS } from "../../../crops/constants"; +import { Path } from "../../../internal_urls"; describe("calculatePlantPositions()", () => { it("calculates plant positions", () => { @@ -59,6 +60,7 @@ describe("convertPlants()", () => { expect(convertedPlants).toEqual([{ icon: CROPS.spinach.icon, + id: 1, label: "Spinach", size: 50, spread: 0, @@ -67,6 +69,7 @@ describe("convertPlants()", () => { }, { icon: CROPS["generic-plant"].icon, + id: 2, label: "Unknown", size: 50, spread: 0, @@ -118,4 +121,13 @@ describe("", () => { const { container } = render(); expect(container).toContainHTML("image"); }); + + it("navigates to plant info", () => { + const p = fakeProps(); + p.plant.id = 1; + const { container } = render(); + const plant = container.querySelector("[name='0'"); + plant && fireEvent.click(plant); + expect(mockNavigate).toHaveBeenCalledWith(Path.plants("1")); + }); }); diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx index 84dd84b87..9058d1652 100644 --- a/frontend/three_d_garden/garden/__tests__/point_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { render } from "@testing-library/react"; +import { fireEvent, 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"; +import { Path } from "../../../internal_urls"; describe("", () => { const fakeProps = (): PointProps => ({ @@ -15,4 +16,14 @@ describe("", () => { const { container } = render(); expect(container).toContainHTML("cylinder"); }); + + it("navigates to point info", () => { + const p = fakeProps(); + p.point.body.id = 1; + const { container } = render(); + const point = container.querySelector("[name='marker'"); + point && fireEvent.click(point); + expect(mockNavigate).toHaveBeenCalledWith(Path.points("1")); + + }); }); diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx index d202fd152..cd513e3d3 100644 --- a/frontend/three_d_garden/garden/__tests__/weed_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { render } from "@testing-library/react"; +import { fireEvent, 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"; +import { Path } from "../../../internal_urls"; describe("", () => { const fakeProps = (): WeedProps => ({ @@ -15,4 +16,13 @@ describe("", () => { const { container } = render(); expect(container).toContainHTML("weed"); }); + + it("navigates to weed info", () => { + const p = fakeProps(); + p.weed.body.id = 1; + const { container } = render(); + const weed = container.querySelector("[name='weed'"); + weed && fireEvent.click(weed); + expect(mockNavigate).toHaveBeenCalledWith(Path.weeds("1")); + }); }); diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx index adcb35667..4bec2fb2f 100644 --- a/frontend/three_d_garden/garden/plants.tsx +++ b/frontend/three_d_garden/garden/plants.tsx @@ -8,8 +8,11 @@ import { threeSpace, zZero as zZeroFunc } from "../helpers"; import { Text } from "../elements"; import { findIcon } from "../../crops/find"; import { kebabCase } from "lodash"; +import { Path } from "../../internal_urls"; +import { useNavigate } from "react-router"; interface Plant { + id?: number | undefined; label: string; icon: string; size: number; @@ -23,6 +26,7 @@ export interface ThreeDGardenPlant extends Plant { } export const convertPlants = (config: Config, plants: TaggedPlant[]): Plant[] => { return plants.map(plant => { return { + id: plant.body.id, label: plant.body.name, icon: findIcon(plant.body.openfarm_slug), size: plant.body.radius * 2, @@ -90,6 +94,7 @@ export interface ThreeDPlantProps { export const ThreeDPlant = (props: ThreeDPlantProps) => { const { i, plant, labelOnly, config, hoveredPlant } = props; const alwaysShowLabels = config.labels && !config.labelsOnHover; + const navigate = useNavigate(); return { {plant.label} : { + plant.id && navigate(Path.plants(plant.id)); + }} transparent={true} renderOrder={1} />} ; diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx index e22e5561f..f94a45757 100644 --- a/frontend/three_d_garden/garden/point.tsx +++ b/frontend/three_d_garden/garden/point.tsx @@ -5,6 +5,8 @@ import { Group, MeshPhongMaterial } from "../components"; import { Cylinder, Sphere } from "@react-three/drei"; import { DoubleSide } from "three"; import { zero as zeroFunc, threeSpace } from "../helpers"; +import { useNavigate } from "react-router"; +import { Path } from "../../internal_urls"; export interface PointProps { point: TaggedGenericPointer; @@ -15,32 +17,38 @@ export const Point = (props: PointProps) => { const { point, config } = props; const RADIUS = 25; const HEIGHT = 100; + const navigate = useNavigate(); return - - - - - - + { + point.body.id && navigate(Path.points(point.body.id)); + }}> + + + + + + + { const { weed, config } = props; + const navigate = useNavigate(); return { + weed.body.id && navigate(Path.weeds(weed.body.id)); + }} position={[ - threeSpace(weed.body.x, config.bedLengthOuter), - threeSpace(weed.body.y, config.bedWidthOuter), + threeSpace(weed.body.x, config.bedLengthOuter) + config.bedXOffset, + threeSpace(weed.body.y, config.bedWidthOuter) + config.bedYOffset, zeroFunc(config).z - config.soilHeight, ]}> Date: Fri, 28 Feb 2025 16:30:50 -0800 Subject: [PATCH 16/19] fix promo page nav load error --- frontend/__test_support__/additional_mocks.tsx | 1 + frontend/promo/promo.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/__test_support__/additional_mocks.tsx b/frontend/__test_support__/additional_mocks.tsx index 7ecb5b793..0b3a033b3 100644 --- a/frontend/__test_support__/additional_mocks.tsx +++ b/frontend/__test_support__/additional_mocks.tsx @@ -43,6 +43,7 @@ global.mockNavigate = jest.fn(() => jest.fn()); jest.mock("react-router", () => ({ BrowserRouter: jest.fn(({ children }) =>
{children}
), + MemoryRouter: jest.fn(({ children }) =>
{children}
), Route: jest.fn(({ children }) =>
{children}
), Routes: jest.fn(({ children }) =>
{children}
), useNavigate: () => mockNavigate, diff --git a/frontend/promo/promo.tsx b/frontend/promo/promo.tsx index b001f4747..8453fe0f0 100644 --- a/frontend/promo/promo.tsx +++ b/frontend/promo/promo.tsx @@ -9,6 +9,7 @@ import { } from "../three_d_garden/config_overlays"; import { ASSETS } from "../three_d_garden/constants"; import { getFocusFromUrlParams } from "../three_d_garden/zoom_beacons_constants"; +import { MemoryRouter } from "react-router"; export const Promo = () => { const [config, setConfig] = React.useState(INITIAL); @@ -28,9 +29,11 @@ export const Promo = () => { return
- - - + + + + + {!config.config && setConfig({ ...config, config: true })} />} From 036d41ea7ad8f03b6f78993410d727baf9aafbf2 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 28 Feb 2025 16:31:36 -0800 Subject: [PATCH 17/19] add panel open control to 3D env --- frontend/__tests__/hotkeys_test.tsx | 17 +++++----- frontend/hotkeys.tsx | 22 ++++++------- frontend/three_d_garden/bot/bot.tsx | 2 ++ .../bot/components/__tests__/tools_test.tsx | 31 ++++++++++++++++--- .../three_d_garden/bot/components/tools.tsx | 7 ++++- .../garden/__tests__/plants_test.tsx | 17 ++++++++++ .../garden/__tests__/point_test.tsx | 16 ++++++++++ .../garden/__tests__/weed_test.tsx | 17 ++++++++++ frontend/three_d_garden/garden/plants.tsx | 9 ++++-- frontend/three_d_garden/garden/point.tsx | 8 ++++- frontend/three_d_garden/garden/weed.tsx | 8 ++++- frontend/three_d_garden/garden_model.tsx | 15 +++++++-- 12 files changed, 134 insertions(+), 35 deletions(-) diff --git a/frontend/__tests__/hotkeys_test.tsx b/frontend/__tests__/hotkeys_test.tsx index 2d38712eb..18b81c9ab 100644 --- a/frontend/__tests__/hotkeys_test.tsx +++ b/frontend/__tests__/hotkeys_test.tsx @@ -1,6 +1,5 @@ const mockSyncThunk = jest.fn(); jest.mock("../devices/actions", () => ({ sync: () => mockSyncThunk })); -jest.mock("../farm_designer/map/actions", () => ({ unselectPlant: jest.fn() })); import { fakeState } from "../__test_support__/fake_state"; const mockState = fakeState(); @@ -16,10 +15,10 @@ import { HotKey, HotKeys, HotKeysProps, hotkeysWithActions, toggleHotkeyHelpOverlay, } from "../hotkeys"; import { sync } from "../devices/actions"; -import { unselectPlant } from "../farm_designer/map/actions"; import { save } from "../api/crud"; import { Actions } from "../constants"; import { Path } from "../internal_urls"; +import { mockDispatch } from "../__test_support__/fake_dispatch"; describe("hotkeysWithActions()", () => { beforeEach(() => { @@ -59,14 +58,12 @@ describe("hotkeysWithActions()", () => { hotkeys[HotKey.addEvent].onKeyDown?.(e); expect(navigate).toHaveBeenCalledWith(Path.farmEvents("add")); - hotkeysSettingsPath[HotKey.backToPlantOverview].onKeyDown?.(e); - expect(navigate).toHaveBeenCalledWith(Path.plants()); - expect(unselectPlant).toHaveBeenCalled(); - jest.clearAllMocks(); - const hotkeysPhotosPath = hotkeysWithActions(navigate, dispatch, "photos"); - hotkeysPhotosPath[HotKey.backToPlantOverview].onKeyDown?.(e); - expect(navigate).not.toHaveBeenCalled(); - expect(unselectPlant).not.toHaveBeenCalled(); + const hotkeysWithDispatch = + hotkeysWithActions(navigate, mockDispatch(dispatch), ""); + hotkeysWithDispatch[HotKey.closePanel].onKeyDown?.(e); + expect(dispatch).toHaveBeenCalledWith({ + type: Actions.SET_PANEL_OPEN, payload: false, + }); }); }); diff --git a/frontend/hotkeys.tsx b/frontend/hotkeys.tsx index 25b4abdff..55382f130 100644 --- a/frontend/hotkeys.tsx +++ b/frontend/hotkeys.tsx @@ -2,8 +2,9 @@ import React from "react"; import { getLinks } from "./nav/nav_links"; import { sync } from "./devices/actions"; import { HotkeyConfig, useHotkeys, HotkeysDialog2 } from "@blueprintjs/core"; -import { unselectPlant } from "./farm_designer/map/actions"; -import { getPanelPath, PANEL_BY_SLUG } from "./farm_designer/panel_header"; +import { + getPanelPath, PANEL_BY_SLUG, setPanelOpen, +} from "./farm_designer/panel_header"; import { t } from "./i18next_wrapper"; import { store } from "./redux/store"; import { save } from "./api/crud"; @@ -25,7 +26,7 @@ export enum HotKey { navigateLeft = "navigateLeft", addPlant = "addPlant", addEvent = "addEvent", - backToPlantOverview = "backToPlantOverview", + closePanel = "closePanel", openGuide = "openGuide", } @@ -54,9 +55,9 @@ const HOTKEY_BASE_MAP = (): HotkeyConfigs => ({ combo: "ctrl + shift + e", label: t("Add Event"), }, - [HotKey.backToPlantOverview]: { + [HotKey.closePanel]: { combo: "escape", - label: t("Back to plant overview"), + label: t("Close panel"), }, [HotKey.openGuide]: { combo: "shift + ?", @@ -106,14 +107,9 @@ export const hotkeysWithActions = ( ...hotkeysBase[HotKey.addEvent], onKeyDown: () => { navigate(Path.farmEvents("add")); }, }, - [HotKey.backToPlantOverview]: { - ...hotkeysBase[HotKey.backToPlantOverview], - onKeyDown: () => { - if (slug != "photos") { - navigate(Path.plants()); - dispatch(unselectPlant(dispatch)); - } - }, + [HotKey.closePanel]: { + ...hotkeysBase[HotKey.closePanel], + onKeyDown: () => { dispatch(setPanelOpen(false)); }, }, [HotKey.openGuide]: hotkeysBase[HotKey.openGuide], }; diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index 0f6df1a84..18145f10e 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -118,6 +118,7 @@ export interface FarmbotModelProps { activeFocus: string; toolSlots?: SlotWithTool[]; mountedToolName?: string | undefined; + dispatch?: Function; } export const Bot = (props: FarmbotModelProps) => { @@ -803,6 +804,7 @@ export const Bot = (props: FarmbotModelProps) => { diff --git a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx index 74a4e211a..448a64a2f 100644 --- a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx +++ b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx @@ -25,6 +25,10 @@ jest.mock("react", () => { }; }); +jest.mock("../watering_animations", () => ({ + WateringAnimations: jest.fn(), +})); + import React from "react"; import { fireEvent, render } from "@testing-library/react"; import { INITIAL } from "../../../config"; @@ -36,10 +40,8 @@ import { import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { WateringAnimations } from "../watering_animations"; import { Path } from "../../../../internal_urls"; - -jest.mock("../watering_animations", () => ({ - WateringAnimations: jest.fn(), -})); +import { Actions } from "../../../../constants"; +import { mockDispatch } from "../../../../__test_support__/fake_dispatch"; describe("", () => { const fakeProps = (): ToolsProps => ({ @@ -140,6 +142,8 @@ describe("", () => { it("navigates to tool info", () => { const p = fakeProps(); + const dispatch = jest.fn(); + p.dispatch = mockDispatch(dispatch); const tool = fakeTool(); tool.body.name = "soil sensor"; tool.body.id = 2; @@ -150,6 +154,25 @@ describe("", () => { const { container } = render(); const slot = container.querySelector("[name='slot'"); slot && fireEvent.click(slot); + expect(dispatch).toHaveBeenCalledWith({ + type: Actions.SET_PANEL_OPEN, payload: true, + }); expect(mockNavigate).toHaveBeenCalledWith(Path.toolSlots("1")); }); + + it("doesn't navigate to tool info", () => { + const p = fakeProps(); + p.dispatch = undefined; + const tool = fakeTool(); + tool.body.name = "soil sensor"; + tool.body.id = 2; + const toolSlot = fakeToolSlot(); + toolSlot.body.id = 1; + toolSlot.body.tool_id = tool.body.id; + p.toolSlots = [{ toolSlot, tool }]; + const { container } = render(); + const slot = container.querySelector("[name='slot'"); + slot && fireEvent.click(slot); + expect(mockNavigate).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/three_d_garden/bot/components/tools.tsx b/frontend/three_d_garden/bot/components/tools.tsx index 8bbb62a95..47002fff1 100644 --- a/frontend/three_d_garden/bot/components/tools.tsx +++ b/frontend/three_d_garden/bot/components/tools.tsx @@ -25,6 +25,7 @@ import { ToolPulloutDirection } from "farmbot/dist/resources/api_resources"; import { WateringAnimations } from "./watering_animations"; import { useNavigate } from "react-router"; import { Path } from "../../../internal_urls"; +import { setPanelOpen } from "../../../farm_designer/panel_header"; type Toolbay3 = GLTF & { nodes: { @@ -69,6 +70,7 @@ export interface ToolsProps { config: Config; toolSlots?: SlotWithTool[]; mountedToolName?: string | undefined; + dispatch?: Function; } interface ConvertedTools { @@ -163,7 +165,10 @@ export const Tools = (props: ToolsProps) => { position.z, ]} onClick={() => { - slotProps.id && navigate(Path.toolSlots(slotProps.id)); + if (slotProps.id && !isUndefined(props.dispatch)) { + props.dispatch(setPanelOpen(true)); + navigate(Path.toolSlots(slotProps.id)); + } }}> {rotationMultiplier && { it("calculates plant positions", () => { @@ -124,10 +126,25 @@ describe("", () => { it("navigates to plant info", () => { const p = fakeProps(); + const dispatch = jest.fn(); + p.dispatch = mockDispatch(dispatch); p.plant.id = 1; const { container } = render(); const plant = container.querySelector("[name='0'"); plant && fireEvent.click(plant); + expect(dispatch).toHaveBeenCalledWith({ + type: Actions.SET_PANEL_OPEN, payload: true, + }); expect(mockNavigate).toHaveBeenCalledWith(Path.plants("1")); }); + + it("doesn't navigate to plant info", () => { + const p = fakeProps(); + p.dispatch = undefined; + p.plant.id = 1; + const { container } = render(); + const plant = container.querySelector("[name='0'"); + plant && fireEvent.click(plant); + expect(mockNavigate).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx index 9058d1652..b14545394 100644 --- a/frontend/three_d_garden/garden/__tests__/point_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx @@ -5,6 +5,8 @@ import { INITIAL } from "../../config"; import { clone } from "lodash"; import { fakePoint } from "../../../__test_support__/fake_state/resources"; import { Path } from "../../../internal_urls"; +import { Actions } from "../../../constants"; +import { mockDispatch } from "../../../__test_support__/fake_dispatch"; describe("", () => { const fakeProps = (): PointProps => ({ @@ -19,11 +21,25 @@ describe("", () => { it("navigates to point info", () => { const p = fakeProps(); + const dispatch = jest.fn(); + p.dispatch = mockDispatch(dispatch); p.point.body.id = 1; const { container } = render(); const point = container.querySelector("[name='marker'"); point && fireEvent.click(point); + expect(dispatch).toHaveBeenCalledWith({ + type: Actions.SET_PANEL_OPEN, payload: true, + }); expect(mockNavigate).toHaveBeenCalledWith(Path.points("1")); + }); + it("doesn't navigate to point info", () => { + const p = fakeProps(); + p.dispatch = undefined; + p.point.body.id = 1; + const { container } = render(); + const point = container.querySelector("[name='marker'"); + point && fireEvent.click(point); + expect(mockNavigate).not.toHaveBeenCalled(); }); }); diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx index cd513e3d3..c2effdea4 100644 --- a/frontend/three_d_garden/garden/__tests__/weed_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx @@ -5,6 +5,8 @@ import { INITIAL } from "../../config"; import { clone } from "lodash"; import { fakeWeed } from "../../../__test_support__/fake_state/resources"; import { Path } from "../../../internal_urls"; +import { Actions } from "../../../constants"; +import { mockDispatch } from "../../../__test_support__/fake_dispatch"; describe("", () => { const fakeProps = (): WeedProps => ({ @@ -19,10 +21,25 @@ describe("", () => { it("navigates to weed info", () => { const p = fakeProps(); + const dispatch = jest.fn(); + p.dispatch = mockDispatch(dispatch); p.weed.body.id = 1; const { container } = render(); const weed = container.querySelector("[name='weed'"); weed && fireEvent.click(weed); + expect(dispatch).toHaveBeenCalledWith({ + type: Actions.SET_PANEL_OPEN, payload: true, + }); expect(mockNavigate).toHaveBeenCalledWith(Path.weeds("1")); }); + + it("doesn't navigate to weed info", () => { + const p = fakeProps(); + p.dispatch = undefined; + p.weed.body.id = 1; + const { container } = render(); + const weed = container.querySelector("[name='weed'"); + weed && fireEvent.click(weed); + expect(mockNavigate).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx index 4bec2fb2f..bb18384e9 100644 --- a/frontend/three_d_garden/garden/plants.tsx +++ b/frontend/three_d_garden/garden/plants.tsx @@ -7,9 +7,10 @@ import { Vector3 } from "three"; import { threeSpace, zZero as zZeroFunc } from "../helpers"; import { Text } from "../elements"; import { findIcon } from "../../crops/find"; -import { kebabCase } from "lodash"; +import { isUndefined, kebabCase } from "lodash"; import { Path } from "../../internal_urls"; import { useNavigate } from "react-router"; +import { setPanelOpen } from "../../farm_designer/panel_header"; interface Plant { id?: number | undefined; @@ -89,6 +90,7 @@ export interface ThreeDPlantProps { labelOnly?: boolean; config: Config; hoveredPlant: number | undefined; + dispatch?: Function; } export const ThreeDPlant = (props: ThreeDPlantProps) => { @@ -112,7 +114,10 @@ export const ThreeDPlant = (props: ThreeDPlantProps) => { : { - plant.id && navigate(Path.plants(plant.id)); + if (plant.id && !isUndefined(props.dispatch)) { + props.dispatch(setPanelOpen(true)); + navigate(Path.plants(plant.id)); + } }} transparent={true} renderOrder={1} />} diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx index f94a45757..792506ea9 100644 --- a/frontend/three_d_garden/garden/point.tsx +++ b/frontend/three_d_garden/garden/point.tsx @@ -7,10 +7,13 @@ import { DoubleSide } from "three"; import { zero as zeroFunc, threeSpace } from "../helpers"; import { useNavigate } from "react-router"; import { Path } from "../../internal_urls"; +import { isUndefined } from "lodash"; +import { setPanelOpen } from "../../farm_designer/panel_header"; export interface PointProps { point: TaggedGenericPointer; config: Config; + dispatch?: Function; } export const Point = (props: PointProps) => { @@ -28,7 +31,10 @@ export const Point = (props: PointProps) => { ]}> { - point.body.id && navigate(Path.points(point.body.id)); + if (point.body.id && !isUndefined(props.dispatch)) { + props.dispatch(setPanelOpen(true)); + navigate(Path.points(point.body.id)); + } }}> { @@ -19,7 +22,10 @@ export const Weed = (props: WeedProps) => { const navigate = useNavigate(); return { - weed.body.id && navigate(Path.weeds(weed.body.id)); + if (weed.body.id && !isUndefined(props.dispatch)) { + props.dispatch(setPanelOpen(true)); + navigate(Path.weeds(weed.body.id)); + } }} position={[ threeSpace(weed.body.x, config.bedLengthOuter) + config.bedXOffset, diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx index 7bdd7ec34..20d0d5307 100644 --- a/frontend/three_d_garden/garden_model.tsx +++ b/frontend/three_d_garden/garden_model.tsx @@ -43,6 +43,7 @@ export interface GardenModelProps { export const GardenModel = (props: GardenModelProps) => { const { config } = props; + const dispatch = props.addPlantProps?.dispatch; const Camera = config.perspective ? PerspectiveCamera : OrthographicCamera; const plants = isUndefined(props.addPlantProps) @@ -120,6 +121,7 @@ export const GardenModel = (props: GardenModelProps) => { {(!props.addPlantProps || !!props.addPlantProps.getConfigValue(BooleanSetting.show_farmbot)) && { )} + hoveredPlant={hoveredPlant} + dispatch={dispatch} />)} {props.mapPoints?.map(point => - )} + )} {props.weeds?.map(weed => - )} + )} From 5f660147166799318a1028e764ece6e58fb70d2e Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Mon, 3 Mar 2025 13:16:31 -0800 Subject: [PATCH 18/19] add missing point radius edit UI --- .../farm_designer/farm_designer_panels.scss | 10 -- .../__tests__/point_edit_actions_test.tsx | 22 +++-- frontend/points/point_edit_actions.tsx | 92 +++++++++++++------ frontend/points/point_info.tsx | 44 ++------- frontend/weeds/weeds_edit.tsx | 24 ++--- 5 files changed, 94 insertions(+), 98 deletions(-) diff --git a/frontend/css/farm_designer/farm_designer_panels.scss b/frontend/css/farm_designer/farm_designer_panels.scss index 05b07cb3d..ac2839a85 100644 --- a/frontend/css/farm_designer/farm_designer_panels.scss +++ b/frontend/css/farm_designer/farm_designer_panels.scss @@ -362,16 +362,6 @@ } } -.additional-weed-properties { - li { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - align-items: center; - margin-top: 1rem; - } -} - .panel-section { .delete { height: 2rem; diff --git a/frontend/points/__tests__/point_edit_actions_test.tsx b/frontend/points/__tests__/point_edit_actions_test.tsx index 43bf9b4ae..d62970966 100644 --- a/frontend/points/__tests__/point_edit_actions_test.tsx +++ b/frontend/points/__tests__/point_edit_actions_test.tsx @@ -15,10 +15,10 @@ import { EditPointRadius, EditPointRadiusProps, EditPointColor, EditPointColorProps, updatePoint, EditPointName, EditPointNameProps, - AdditionalWeedProperties, - AdditionalWeedPropertiesProps, EditPointSoilHeightTag, EditPointSoilHeightTagProps, + EditWeedProperties, + EditWeedPropertiesProps, } from "../point_edit_actions"; import { fakePoint, fakeWeed, @@ -140,26 +140,32 @@ describe("", () => { }); describe("", () => { - const fakeProps = (): AdditionalWeedPropertiesProps => ({ - point: fakeWeed(), + const fakeProps = (): EditWeedPropertiesProps => ({ + weed: fakeWeed(), updatePoint: jest.fn(), + botOnline: true, + dispatch: jest.fn(), + defaultAxes: "XY", + arduinoBusy: false, + currentBotLocation: { x: 10, y: 20, z: 30 }, + movementState: fakeMovementState(), }); it("renders unknown source", () => { const p = fakeProps(); - p.point.body.meta = { + p.weed.body.meta = { meta_key: "meta value", created_by: undefined, key: undefined, color: "red", type: "weed", }; - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.text()).toContain("unknown"); expect(wrapper.text()).toContain("meta value"); }); it("changes method", () => { const p = fakeProps(); - p.point.body.meta = { removal_method: "automatic" }; - const wrapper = shallow(); + p.weed.body.meta = { removal_method: "automatic" }; + const wrapper = shallow(); wrapper.find("input").last().simulate("change"); expect(p.updatePoint).toHaveBeenCalledWith({ meta: { removal_method: "manual" } diff --git a/frontend/points/point_edit_actions.tsx b/frontend/points/point_edit_actions.tsx index 87c8b9f3d..f247faf51 100644 --- a/frontend/points/point_edit_actions.tsx +++ b/frontend/points/point_edit_actions.tsx @@ -31,20 +31,12 @@ export const updatePoint = } }; -interface EditPointPropertiesProps { - point: TaggedGenericPointer | TaggedWeedPointer; - updatePoint(update: PointUpdate): void; - botOnline: boolean; - defaultAxes: string; - arduinoBusy: boolean; - dispatch: Function; - currentBotLocation: BotPosition; - movementState: MovementState; +interface EditPointPropertiesProps extends EditPointLocationBaseProps { + point: TaggedGenericPointer; } -export interface AdditionalWeedPropertiesProps { - point: TaggedWeedPointer; - updatePoint(update: PointUpdate): void; +export interface EditWeedPropertiesProps extends EditPointLocationBaseProps { + weed: TaggedWeedPointer; } export const EditPointProperties = (props: EditPointPropertiesProps) => @@ -64,35 +56,72 @@ export const EditPointProperties = (props: EditPointPropertiesProps) => defaultAxes={props.defaultAxes} updatePoint={props.updatePoint} /> - {props.point.body.pointer_type == "GenericPointer" && - - - } + + + + + + + {Object.entries(props.point.body.meta).map(([key, value]) => { + switch (key) { + case "color": + case "at_soil_level": + case "removal_method": + case "type": + case "gridId": + return
; + case "created_by": + return + {lookupPointSource(value)} + ; + default: + return + {value || ""} + ; + } + })} ; -export const AdditionalWeedProperties = (props: AdditionalWeedPropertiesProps) => -