From d11c79355e41fde7c1be422b61d4895e55221e1d Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 19 Mar 2025 16:59:51 -0700 Subject: [PATCH 01/10] upgrade deps --- Gemfile.lock | 15 ++++++++------- package.json | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a9bd18e87..5574e4904 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,7 +68,7 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) amq-protocol (2.3.3) - ast (2.4.2) + ast (2.4.3) base64 (0.2.0) bcrypt (3.1.20) bigdecimal (3.1.9) @@ -144,7 +144,8 @@ GEM google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.2.1) + google-cloud-env (2.2.2) + base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.5.0) google-cloud-storage (1.55.0) @@ -157,7 +158,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.1.0) - googleauth (1.13.1) + googleauth (1.14.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -171,7 +172,7 @@ GEM mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.10.1) + json (2.10.2) jsonapi-renderer (0.2.2) jwt (2.10.1) base64 @@ -204,7 +205,7 @@ GEM marcel (1.0.4) method_source (1.1.0) mini_mime (1.1.5) - minitest (5.25.4) + minitest (5.25.5) multi_json (1.15.0) mutations (0.9.1) activesupport @@ -221,9 +222,9 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.3-aarch64-linux-gnu) + nokogiri (1.18.5-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.3-x86_64-linux-gnu) + nokogiri (1.18.5-x86_64-linux-gnu) racc (~> 1.4) orm_adapter (0.5.0) os (1.1.4) diff --git a/package.json b/package.json index bf201559a..9ce6ba870 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,11 @@ "@parcel/watcher": "2.1.0" }, "dependencies": { - "@blueprintjs/core": "5.17.5", - "@blueprintjs/select": "5.3.17", + "@blueprintjs/core": "5.17.6", + "@blueprintjs/select": "5.3.18", "@monaco-editor/react": "4.7.0", - "@parcel/transformer-sass": "2.13.3", - "@parcel/transformer-typescript-tsc": "2.13.3", + "@parcel/transformer-sass": "2.14.1", + "@parcel/transformer-typescript-tsc": "2.14.1", "@react-spring/three": "9.7.5", "@react-three/drei": "9.122.0", "@react-three/fiber": "8.18.0", @@ -50,18 +50,18 @@ "@types/markdown-it": "14.1.2", "@types/node": "22.13.10", "@types/promise-timeout": "1.3.3", - "@types/react": "19.0.10", + "@types/react": "19.0.12", "@types/react-color": "3.0.13", "@types/react-dom": "19.0.4", "@types/three": "0.174.0", "@types/ws": "8.18.0", "@xterm/xterm": "5.5.0", - "axios": "1.8.2", + "axios": "1.8.4", "bowser": "2.11.0", "browser-speech": "1.1.1", "events": "3.3.0", "farmbot": "15.8.8", - "i18next": "24.2.2", + "i18next": "24.2.3", "lodash": "4.17.21", "markdown-it": "14.1.0", "markdown-it-emoji": "3.0.0", @@ -69,7 +69,7 @@ "monaco-editor": "0.52.2", "mqtt": "5.10.4", "npm": "11.2.0", - "parcel": "2.13.3", + "parcel": "2.14.1", "process": "0.11.10", "promise-timeout": "1.3.0", "punycode": "1.4.1", @@ -78,7 +78,7 @@ "react-color": "2.19.3", "react-dom": "18.3.1", "react-redux": "9.2.0", - "react-router": "7.3.0", + "react-router": "7.4.0", "redux": "5.0.1", "redux-immutable-state-invariant": "2.1.0", "redux-thunk": "3.1.0", @@ -121,7 +121,7 @@ "raf": "3.4.1", "react-addons-test-utils": "15.6.2", "react-test-renderer": "18.3.1", - "sass": "1.85.1", + "sass": "1.86.0", "sass-lint": "1.13.1", "ts-jest": "29.2.6", "tslint": "5.20.1" From 228026967f73fcdc0c2ba48cdbeede3149ddfbcc Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Mon, 24 Mar 2025 16:08:47 -0700 Subject: [PATCH 02/10] adjust 3D point and weed creation interactions --- .../grid/__tests__/generate_grid_test.ts | 2 +- frontend/plants/grid/generate_grid.ts | 2 +- .../three_d_garden/bed/__tests__/bed_test.tsx | 133 ++++++++++++++---- frontend/three_d_garden/bed/bed.tsx | 59 ++++---- .../garden/__tests__/point_test.tsx | 19 +++ frontend/three_d_garden/garden/point.tsx | 38 +++-- frontend/three_d_garden/garden/weed.tsx | 19 ++- frontend/three_d_garden/garden_model.tsx | 9 +- 8 files changed, 196 insertions(+), 85 deletions(-) diff --git a/frontend/plants/grid/__tests__/generate_grid_test.ts b/frontend/plants/grid/__tests__/generate_grid_test.ts index 15c3dc6c2..b2bba1b7a 100644 --- a/frontend/plants/grid/__tests__/generate_grid_test.ts +++ b/frontend/plants/grid/__tests__/generate_grid_test.ts @@ -86,7 +86,7 @@ describe("initPlantGrid", () => { }); expect(result.length).toEqual(expectedGrid.length); expect(result[0].pointer_type).toEqual("GenericPointer"); - expect(result[0].radius).toEqual(25); + expect(result[0].radius).toEqual(0); }); }); diff --git a/frontend/plants/grid/generate_grid.ts b/frontend/plants/grid/generate_grid.ts index 111c1a067..3c046e4b4 100644 --- a/frontend/plants/grid/generate_grid.ts +++ b/frontend/plants/grid/generate_grid.ts @@ -70,7 +70,7 @@ const createPointGridMapper = ( const [x, y] = vec; return { name: pointName, - radius: radius || 25, + radius: radius || 0, z: z || 0, x, y, diff --git a/frontend/three_d_garden/bed/__tests__/bed_test.tsx b/frontend/three_d_garden/bed/__tests__/bed_test.tsx index d5ee38703..54cdeb2ee 100644 --- a/frontend/three_d_garden/bed/__tests__/bed_test.tsx +++ b/frontend/three_d_garden/bed/__tests__/bed_test.tsx @@ -7,22 +7,38 @@ jest.mock("../../../screen_size", () => ({ isMobile: () => mockIsMobile, })); -const mockSetPosition = jest.fn(); -const mockSetScale = jest.fn(); +const mockSetPlantPosition = jest.fn(); +const mockSetRadiusScale = jest.fn(); +const mockSetBillboardPosition = jest.fn(); +const mockSetImageScale = jest.fn(); interface MockPlantRefCurrent { position: { set: Function; }; } interface MockRadiusRefCurrent { scale: { set: Function; }; } +interface MockBillboardRefCurrent { + position: { set: Function; }; +} +interface MockImageRefCurrent { + scale: { set: Function; }; +} interface MockPlantRef { current: MockPlantRefCurrent | undefined; } interface MockRadiusRef { current: MockRadiusRefCurrent | undefined; } +interface MockBillboardRef { + current: MockBillboardRefCurrent | undefined; +} +interface MockImageRef { + current: MockImageRefCurrent | undefined; +} const mockPlantRef: MockPlantRef = { current: undefined }; const mockRadiusRef: MockRadiusRef = { current: undefined }; +const mockBillboardRef: MockBillboardRef = { current: undefined }; +const mockImageRef: MockImageRef = { current: undefined }; jest.mock("react", () => ({ ...jest.requireActual("react"), useRef: jest.fn(), @@ -39,17 +55,22 @@ import { fakeAddPlantProps } from "../../../__test_support__/fake_props"; import { Actions } from "../../../constants"; import { fakeDrawnPoint } from "../../../__test_support__/fake_designer_state"; import { mockDispatch } from "../../../__test_support__/fake_dispatch"; +import { fakePoint } from "../../../__test_support__/fake_state/resources"; +import { SpecialStatus } from "farmbot"; describe("", () => { beforeEach(() => { React.useRef = jest.fn() .mockImplementationOnce(() => mockPlantRef) - .mockImplementationOnce(() => mockRadiusRef); + .mockImplementationOnce(() => mockRadiusRef) + .mockImplementationOnce(() => mockBillboardRef) + .mockImplementationOnce(() => mockImageRef); }); const fakeProps = (): BedProps => ({ config: clone(INITIAL), activeFocus: "", + mapPoints: [], }); it("renders bed", () => { @@ -68,6 +89,31 @@ describe("", () => { expect(container).toContainHTML("bed-group"); }); + it.each<[string, SpecialStatus]>([ + ["doesn't render", SpecialStatus.DIRTY], + ["renders", SpecialStatus.SAVED], + ])("%s pointer point", (title, gridPointSpecialStatus) => { + location.pathname = Path.mock(Path.points("add")); + mockIsMobile = false; + const p = fakeProps(); + p.addPlantProps = fakeAddPlantProps([]); + const point0 = fakePoint(); + point0.specialStatus = gridPointSpecialStatus; + point0.body.meta = { gridId: "123" }; + p.mapPoints = [point0]; + const point = fakeDrawnPoint(); + point.cx = undefined; + point.cy = undefined; + point.r = 0; + p.addPlantProps.designer.drawnPoint = point; + const { container } = render(); + if (title == "renders") { + expect(container).toContainHTML("drawn-point"); + } else { + expect(container).not.toContainHTML("drawn-point"); + } + }); + it("adds a plant", () => { location.pathname = Path.mock(Path.cropSearch("mint")); const p = fakeProps(); @@ -94,6 +140,7 @@ describe("", () => { it("adds a drawn point: xy", () => { location.pathname = Path.mock(Path.points("add")); + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; const p = fakeProps(); const addPlantProps = fakeAddPlantProps([]); const point = fakeDrawnPoint(); @@ -105,6 +152,7 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.click(soil); + expect(mockSetPlantPosition).toHaveBeenCalledWith(0, 0, 0); expect(p.addPlantProps.dispatch).toHaveBeenCalledWith({ type: Actions.SET_DRAWN_POINT_DATA, payload: { ...point, cx: 1360, cy: 660, z: -500 }, @@ -114,6 +162,7 @@ describe("", () => { it("adds a drawn point: radius", () => { location.pathname = Path.mock(Path.points("add")); + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; const p = fakeProps(); const addPlantProps = fakeAddPlantProps([]); const dispatch = jest.fn(); @@ -126,6 +175,7 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.click(soil); + expect(mockSetPlantPosition).toHaveBeenCalledWith(0, 0, 0); expect(p.addPlantProps.dispatch).toHaveBeenCalledWith({ type: Actions.SET_DRAWN_POINT_DATA, payload: { ...point, r: 1490 }, @@ -144,13 +194,13 @@ describe("", () => { it("updates pointer plant position", () => { location.pathname = Path.mock(Path.cropSearch("mint")); mockIsMobile = false; - mockPlantRef.current = { position: { set: mockSetPosition } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).toHaveBeenCalledWith(0, 0, 0); + expect(mockSetPlantPosition).toHaveBeenCalledWith(0, 0, 0); }); it("handles missing ref", () => { @@ -162,38 +212,38 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).not.toHaveBeenCalled(); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); }); it("doesn't update pointer plant position: mobile", () => { location.pathname = Path.mock(Path.cropSearch("mint")); mockIsMobile = true; - mockPlantRef.current = { position: { set: mockSetPosition } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).not.toHaveBeenCalled(); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); }); it("doesn't update pointer point position", () => { location.pathname = Path.mock(Path.points("add")); mockIsMobile = false; - mockPlantRef.current = { position: { set: mockSetPosition } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); p.addPlantProps.designer.drawnPoint = undefined; render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).not.toHaveBeenCalled(); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); }); it("updates pointer point position", () => { location.pathname = Path.mock(Path.points("add")); mockIsMobile = false; - mockPlantRef.current = { position: { set: mockSetPosition } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); const point = fakeDrawnPoint(); @@ -204,14 +254,16 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).toHaveBeenCalledWith(0, 0, 0); + expect(mockSetPlantPosition).toHaveBeenCalledWith(0, 0, 0); }); it("updates pointer point radius", () => { location.pathname = Path.mock(Path.points("add")); mockIsMobile = false; - mockPlantRef.current = { position: { set: mockSetPosition } }; - mockRadiusRef.current = { scale: { set: mockSetScale } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; + mockRadiusRef.current = { scale: { set: mockSetRadiusScale } }; + mockBillboardRef.current = { position: { set: mockSetBillboardPosition } }; + mockImageRef.current = { scale: { set: mockSetImageScale } }; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); const point = fakeDrawnPoint(); @@ -222,15 +274,19 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).not.toHaveBeenCalled(); - expect(mockSetScale).toHaveBeenCalledWith(1510, 1, 1510); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); + expect(mockSetRadiusScale).toHaveBeenCalledWith(1510, 1, 1510); + expect(mockSetBillboardPosition).toHaveBeenCalledWith(0, 0, 755); + expect(mockSetImageScale).toHaveBeenCalledWith(1510, 1510, 1510); }); it("updates pointer weed radius", () => { location.pathname = Path.mock(Path.weeds("add")); mockIsMobile = false; - mockPlantRef.current = { position: { set: mockSetPosition } }; - mockRadiusRef.current = { scale: { set: mockSetScale } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; + mockRadiusRef.current = { scale: { set: mockSetRadiusScale } }; + mockBillboardRef.current = { position: { set: mockSetBillboardPosition } }; + mockImageRef.current = { scale: { set: mockSetImageScale } }; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); const point = fakeDrawnPoint(); @@ -241,15 +297,19 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).not.toHaveBeenCalled(); - expect(mockSetScale).toHaveBeenCalledWith(1510, 1510, 1510); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); + expect(mockSetRadiusScale).toHaveBeenCalledWith(1510, 1510, 1510); + expect(mockSetBillboardPosition).toHaveBeenCalledWith(0, 0, 755); + expect(mockSetImageScale).toHaveBeenCalledWith(1510, 1510, 1510); }); - it("doesn't update pointer point radius", () => { + it("doesn't update pointer point radius: no ref", () => { location.pathname = Path.mock(Path.points("add")); mockIsMobile = false; - mockPlantRef.current = { position: { set: mockSetPosition } }; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; mockRadiusRef.current = undefined; + mockBillboardRef.current = undefined; + mockImageRef.current = undefined; const p = fakeProps(); p.addPlantProps = fakeAddPlantProps([]); const point = fakeDrawnPoint(); @@ -260,7 +320,32 @@ describe("", () => { render(); const soil = screen.getAllByText("soil")[0]; fireEvent.pointerMove(soil); - expect(mockSetPosition).not.toHaveBeenCalled(); - expect(mockSetScale).not.toHaveBeenCalled(); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); + expect(mockSetRadiusScale).not.toHaveBeenCalled(); + expect(mockSetBillboardPosition).not.toHaveBeenCalled(); + expect(mockSetImageScale).not.toHaveBeenCalled(); + }); + + it("doesn't update pointer point radius: already set", () => { + location.pathname = Path.mock(Path.points("add")); + mockIsMobile = false; + mockPlantRef.current = { position: { set: mockSetPlantPosition } }; + mockRadiusRef.current = { scale: { set: mockSetRadiusScale } }; + mockBillboardRef.current = { position: { set: mockSetBillboardPosition } }; + mockImageRef.current = { scale: { set: mockSetImageScale } }; + const p = fakeProps(); + p.addPlantProps = fakeAddPlantProps([]); + const point = fakeDrawnPoint(); + point.cx = 1; + point.cy = 1; + point.r = 100; + p.addPlantProps.designer.drawnPoint = point; + render(); + const soil = screen.getAllByText("soil")[0]; + fireEvent.pointerMove(soil); + expect(mockSetPlantPosition).not.toHaveBeenCalled(); + expect(mockSetRadiusScale).not.toHaveBeenCalled(); + expect(mockSetBillboardPosition).not.toHaveBeenCalled(); + expect(mockSetImageScale).not.toHaveBeenCalled(); }); }); diff --git a/frontend/three_d_garden/bed/bed.tsx b/frontend/three_d_garden/bed/bed.tsx index d571259ba..376ab655e 100644 --- a/frontend/three_d_garden/bed/bed.tsx +++ b/frontend/three_d_garden/bed/bed.tsx @@ -1,8 +1,6 @@ import React from "react"; import { Billboard, Box, Detailed, Extrude, useTexture, Image, - Cylinder, - Sphere, } from "@react-three/drei"; import { DoubleSide, Path as LinePath, Shape, RepeatWrapping, Group as GroupType, @@ -20,7 +18,7 @@ import { AxisNumberProperty, Mode, TaggedPlant, } from "../../farm_designer/map/interfaces"; import { dropPlant } from "../../farm_designer/map/layers/plants/plant_actions"; -import { TaggedCurve } from "farmbot"; +import { SpecialStatus, TaggedCurve, TaggedGenericPointer } from "farmbot"; import { GetWebAppConfigValue } from "../../config_storage/actions"; import { DesignerState, DrawnPointPayl } from "../../farm_designer/interfaces"; import { isMobile } from "../../screen_size"; @@ -79,6 +77,7 @@ export interface AddPlantProps { export interface BedProps { config: Config; activeFocus: string; + mapPoints: TaggedGenericPointer[]; addPlantProps?: AddPlantProps; } @@ -147,6 +146,12 @@ export const Bed = (props: BedProps) => { // eslint-disable-next-line no-null/no-null const radiusRef = React.useRef(null); + // eslint-disable-next-line no-null/no-null + const billboardRef = React.useRef(null); + + // eslint-disable-next-line no-null/no-null + const imageRef = React.useRef(null); + type XY = AxisNumberProperty; const getGardenPosition = (threeDPosition: XY): XY => ({ @@ -187,6 +192,7 @@ export const Bed = (props: BedProps) => { }); } if (DRAW_POINT_MODES.includes(getMode())) { + pointerPlantRef.current?.position.set(0, 0, 0); const cursor = getGardenPosition(e.point); const { drawnPoint } = addPlantProps.designer; if (isUndefined(drawnPoint)) { return; } @@ -235,6 +241,7 @@ export const Bed = (props: BedProps) => { if (isUndefined(drawnPoint.cx) || isUndefined(drawnPoint.cy)) { pointerPlantRef.current.position.set(position.x, position.y, 0); } else { + if (drawnPoint.r > 0) { return; } const radius = round(xyDistance( { x: drawnPoint.cx, y: drawnPoint.cy }, getGardenPosition(e.point))); @@ -242,6 +249,8 @@ export const Bed = (props: BedProps) => { radius, getMode() == Mode.createPoint ? 1 : radius, radius); + billboardRef.current?.position.set(0, 0, radius / 2); + imageRef.current?.scale.set(radius, radius, radius); } } } @@ -262,7 +271,11 @@ export const Bed = (props: BedProps) => { }; const drawnPoint = props.addPlantProps && props.addPlantProps.designer.drawnPoint; + const settingRadius = !(isUndefined(drawnPoint?.cx) || isUndefined(drawnPoint.cy)); const soilZ = zZero(props.config) - props.config.soilHeight; + const gridPreview = props.mapPoints + .filter(p => p.specialStatus == SpecialStatus.DIRTY && p.body.meta.gridId) + .length > 0; return @@ -340,39 +353,17 @@ export const Bed = (props: BedProps) => { {HOVER_OBJECT_MODES.includes(getMode()) && !isMobile() && - + {DRAW_POINT_MODES.includes(getMode()) && props.addPlantProps && + !gridPreview && drawnPoint && - - {(isUndefined(drawnPoint.cx) || isUndefined(drawnPoint.cy)) - ? - : - {getMode() == Mode.createPoint - ? - - - : - - } - } - } + } {getMode() == Mode.clickToAdd && ", () => { const fakeProps = (): PointProps => ({ @@ -20,6 +21,15 @@ describe("", () => { it("renders", () => { const { container } = render(); expect(container).toContainHTML("cylinder"); + expect(container).toContainHTML("opacity=\"1\""); + }); + + it("renders: unsaved", () => { + const p = fakeProps(); + p.point.specialStatus = SpecialStatus.DIRTY; + const { container } = render(); + expect(container).toContainHTML("cylinder"); + expect(container).not.toContainHTML("opacity=\"1\""); }); it("navigates to point info", () => { @@ -67,6 +77,15 @@ describe("", () => { expect(container).toContainHTML("position=\"0,0,0\""); }); + it("doesn't draw point", () => { + location.pathname = Path.mock(Path.points("add")); + const p = fakeProps(); + p.usePosition = true; + p.designer.drawnPoint = undefined; + const { container } = render(); + expect(container).not.toContainHTML("position=\"0,0,0\""); + }); + it("draws weed", () => { location.pathname = Path.mock(Path.weeds("add")); const p = fakeProps(); diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx index 7ec220d95..8a4daa095 100644 --- a/frontend/three_d_garden/garden/point.tsx +++ b/frontend/three_d_garden/garden/point.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { TaggedGenericPointer, Xyz } from "farmbot"; +import { SpecialStatus, TaggedGenericPointer, Xyz } from "farmbot"; import { Config } from "../config"; import { Group, MeshPhongMaterial } from "../components"; import { Cylinder, Sphere } from "@react-three/drei"; -import { DoubleSide } from "three"; +import { DoubleSide, Mesh, Group as GroupType } from "three"; import { zero as zeroFunc, threeSpace } from "../helpers"; import { useNavigate } from "react-router"; import { Path } from "../../internal_urls"; @@ -15,6 +15,10 @@ import { Mode } from "../../farm_designer/map/interfaces"; import { WeedBase } from "."; import { HOVER_OBJECT_MODES } from "../constants"; +const POINT_CYLINDER_HEIGHT = 50; +const POINT_PIN_RADIUS = 25; +const POINT_PIN_HEIGHT = 100; + export interface PointProps { point: TaggedGenericPointer; config: Config; @@ -24,9 +28,10 @@ export interface PointProps { export const Point = (props: PointProps) => { const { point, config } = props; const navigate = useNavigate(); + const unsaved = point.specialStatus !== SpecialStatus.SAVED; return ; + billboardRef?: React.RefObject; + imageRef?: React.RefObject; } export const DrawnPoint = (props: DrawnPointProps) => { @@ -66,7 +74,10 @@ export const DrawnPoint = (props: DrawnPointProps) => { position={props.usePosition ? drawnPointPosition : undefined} color={drawnPoint?.color} config={config} - radius={drawnPoint?.r || 0} />; + radius={drawnPoint?.r || 0} + radiusRef={props.radiusRef} + billboardRef={props.billboardRef} + imageRef={props.imageRef} />; }; interface PointBaseProps { @@ -77,12 +88,13 @@ interface PointBaseProps { radius: number; alpha: number; config: Config; + radiusRef?: React.RefObject; + billboardRef?: React.RefObject; + imageRef?: React.RefObject; } const PointBase = (props: PointBaseProps) => { - const RADIUS = 25; - const HEIGHT = 100; - const { config } = props; + const { config, radius } = props; return { + args={[POINT_PIN_RADIUS, 0, POINT_PIN_HEIGHT, 32, 32, true]} + position={[0, POINT_PIN_HEIGHT / 2, 0]}> { opacity={1 * props.alpha} /> + args={[POINT_PIN_RADIUS, 32, 32]} + position={[0, POINT_PIN_HEIGHT, 0]}> { + ref={props.radiusRef} + scale={[radius, 1, radius]} + args={[1, 1, POINT_CYLINDER_HEIGHT, 32, 32, true]}> ; + billboardRef?: React.RefObject; + imageRef?: React.RefObject; } export const WeedBase = (props: WeedBaseProps) => { const { config } = props; - const iconSize = props.radius ? props.radius : 50; + const iconSize = props.radius == 0 ? 50 : props.radius; return { ] : [0, 0, 0]} onClick={props.onClick}> - - { {showFarmbot && { config={config} dispatch={dispatch} />)} - {addPlantProps && DRAW_POINT_MODES.includes(getMode()) && - } {props.weeds?.map(weed => From 95482c42dcb6dfc37e4ee149efe3018d9bf41f6e Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 26 Mar 2025 15:04:03 -0700 Subject: [PATCH 03/10] add 3D map beta toggle --- .../__test_support__/fake_designer_state.ts | 1 + frontend/constants.ts | 14 +- frontend/css/farm_designer/farm_designer.scss | 38 +++ .../farm_designer/__tests__/reducer_test.ts | 9 + frontend/farm_designer/index.tsx | 18 +- frontend/farm_designer/interfaces.ts | 1 + .../__tests__/garden_map_legend_test.tsx | 25 +- .../map/legend/garden_map_legend.tsx | 45 ++-- .../farm_designer/map/legend/layer_toggle.tsx | 2 + frontend/farm_designer/move_to.tsx | 2 +- frontend/farm_designer/reducer.ts | 5 + frontend/farm_designer/three_d_garden_map.tsx | 2 + frontend/plants/select_plants.tsx | 2 +- frontend/settings/__tests__/index_test.tsx | 8 + frontend/settings/dev/dev_settings.tsx | 7 +- frontend/settings/index.tsx | 7 +- .../three_d_garden/__tests__/camera_test.ts | 15 +- .../__tests__/garden_model_test.tsx | 17 +- .../three_d_garden/__tests__/index_test.tsx | 80 +++++- .../three_d_garden/bed/__tests__/bed_test.tsx | 48 +++- frontend/three_d_garden/bed/bed.tsx | 199 ++++----------- .../__tests__/pointer_objects_test.tsx | 97 +++++++ .../bed/objects/pointer_objects.tsx | 240 ++++++++++++++++++ frontend/three_d_garden/camera.ts | 14 +- frontend/three_d_garden/components.tsx | 4 + frontend/three_d_garden/config.ts | 9 +- frontend/three_d_garden/garden/point.tsx | 67 ++++- frontend/three_d_garden/garden/weed.tsx | 30 ++- frontend/three_d_garden/garden_model.tsx | 23 +- frontend/three_d_garden/index.tsx | 68 +++++ 30 files changed, 857 insertions(+), 240 deletions(-) create mode 100644 frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx create mode 100644 frontend/three_d_garden/bed/objects/pointer_objects.tsx diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index 1ffae18e7..56a89232f 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -51,6 +51,7 @@ export const fakeDesignerState = (): DesignerState => ({ cropRadius: undefined, distanceIndicator: "", panelOpen: true, + threeDTopDownView: false, }); export const fakeHelpState = (): HelpState => ({ diff --git a/frontend/constants.ts b/frontend/constants.ts index 10a7738df..8550508d8 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -1064,6 +1064,17 @@ export namespace Content { trim(`Are you sure you want to delete all logs? A page refresh will be required.`); + export const SHOW_3D_VIEW_DESCRIPTION_DESKTOP = + (`**3D Controls** + - Scroll to zoom + - Click and drag to rotate + - Right-click and drag to pan`); + + export const SHOW_3D_VIEW_DESCRIPTION_MOBILE = + (`**3D Controls** + - Pinch to zoom and pan + - Touch and drag to rotate`); + // Front Page export const TOS_UPDATE = trim(`The terms of service have recently changed. You must accept the @@ -2215,7 +2226,7 @@ export enum DeviceSetting { showReadingsMapLayer = `Show Readings Map Layer`, showMoisture = `Moisture`, showMoistureInterpolationMapLayer = `Show Moisture Interpolation Map Layer`, - show3DMap = `3D Map`, + show3DMap = `3D Map beta`, // Controls invertJogButtonXAxis = `X Axis`, @@ -2475,6 +2486,7 @@ export enum Actions { // 3D SET_DISTANCE_INDICATOR = "SET_DISTANCE_INDICATOR", + TOGGLE_3D_TOP_DOWN_VIEW = "TOGGLE_3D_TOP_DOWN_VIEW", // Regimens PUSH_WEEK = "PUSH_WEEK", diff --git a/frontend/css/farm_designer/farm_designer.scss b/frontend/css/farm_designer/farm_designer.scss index a9f5649fa..3e09d83d8 100644 --- a/frontend/css/farm_designer/farm_designer.scss +++ b/frontend/css/farm_designer/farm_designer.scss @@ -805,3 +805,41 @@ transform: translateX(-22.5rem); } } + +.three-d-map-toggle-menu { + position: fixed; + bottom: 0; + right: 0; + padding: 1rem; + grid-template-columns: 1fr 1fr 1fr auto; + button { + height: 3rem; + width: 3rem; + i { + font-size: 1.5rem; + } + &.active { + background-color: $blue !important; + } + } + .three-d-map-toggle { + margin: auto; + padding: 0.5rem; + grid-template-columns: auto auto auto; + background-color: var(--main-bg); + border-radius: 5px; + height: 3rem; + .bp5-popover-wrapper, + fieldset, + label { + font-weight: bold; + margin: auto; + } + fieldset { + button { + width: 3.5rem; + margin: auto; + } + } + } +} diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index cdbf437c4..950985f05 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -126,6 +126,15 @@ describe("designer reducer", () => { expect(newState.distanceIndicator).toEqual("setting"); }); + it("sets top down view", () => { + const action: ReduxAction = { + type: Actions.TOGGLE_3D_TOP_DOWN_VIEW, + payload: true, + }; + const newState = designer(oldState(), action); + expect(newState.threeDTopDownView).toEqual(true); + }); + it("sets panel open state", () => { const action: ReduxAction = { type: Actions.SET_PANEL_OPEN, diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index 5e2d3862e..74042a276 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -28,6 +28,8 @@ import { Outlet } from "react-router"; import { ErrorBoundary } from "../error_boundary"; import { get3DConfigValueFunction } from "../settings/three_d_settings"; import { isDesktop, isMobile } from "../screen_size"; +import { NavigationContext } from "../routes_helpers"; +import { ThreeDGardenToggle } from "../three_d_garden"; export const getDefaultAxisLength = (getConfigValue: GetWebAppConfigValue): Record => { @@ -139,6 +141,10 @@ export class RawFarmDesigner get mapPanelClassName() { return mapPanelClassName(this.props.designer); } + static contextType = NavigationContext; + context!: React.ContextType; + navigate = this.context; + render() { const { legend_menu_open, @@ -163,6 +169,8 @@ export class RawFarmDesigner const mapPadding = getMapPadding(getPanelStatus(this.props.designer)); const padHeightOffset = mapPadding.top - mapPadding.top / zoom_level; + const threeDGarden = !!this.props.getConfigValue(BooleanSetting.three_d_garden); + return
- {this.props.getConfigValue(BooleanSetting.three_d_garden) + {threeDGarden ? } - {!this.props.getConfigValue(BooleanSetting.three_d_garden) && + {!threeDGarden && } + + ; } } diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index dac57756b..5982c63e1 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -177,6 +177,7 @@ export interface DesignerState { cropRadius: number | undefined; distanceIndicator: string; panelOpen: boolean; + threeDTopDownView: boolean; } export type TaggedExecutable = TaggedSequence | TaggedRegimen; diff --git a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx index 11cd0e8d7..b0a439ed8 100644 --- a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx +++ b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx @@ -10,16 +10,12 @@ jest.mock("../../../../config_storage/actions", () => ({ setWebAppConfigValue: jest.fn(), })); -let mockDev = false; -jest.mock("../../../../settings/dev/dev_support", () => ({ - DevSettings: { futureFeaturesEnabled: () => mockDev } -})); - import React from "react"; import { shallow, mount } from "enzyme"; import { GardenMapLegend, ZoomControls, PointsSubMenu, FarmbotSubMenu, PlantsSubMenu, MapSettingsContent, SettingsSubMenuProps, + ZoomControlsProps, } from "../garden_map_legend"; import { GardenMapLegendProps } from "../../interfaces"; import { BooleanSetting } from "../../../../session_keys"; @@ -83,23 +79,16 @@ describe("", () => { wrapper.find(".fb-toggle-button").last().simulate("click"); expect(wrapper.html()).toContain("-100"); }); - - it("renders 3D map toggle", () => { - mockDev = true; - const p = fakeProps(); - const wrapper = mount(); - expect(wrapper.text().toLowerCase()).toContain("3d map"); - wrapper.find(".fb-layer-toggle").last().simulate("click"); - expect(setWebAppConfigValue).toHaveBeenCalledWith( - BooleanSetting.three_d_garden, true); - }); }); describe("", () => { + const fakeProps = (): ZoomControlsProps => ({ + zoom: jest.fn(), + getConfigValue: jest.fn(), + }); + const expectDisabledBtnCountToEqual = (expected: number) => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(".disabled").length).toEqual(expected); }; diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx index 2122e3b94..c37cd2148 100644 --- a/frontend/farm_designer/map/legend/garden_map_legend.tsx +++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx @@ -23,23 +23,29 @@ import { getModifiedClassName } from "../../../settings/default_values"; import { Position } from "@blueprintjs/core"; import { MapSizeInputs } from "../../map_size_setting"; import { OriginSelector } from "../../../settings/farm_designer_settings"; -import { DevSettings } from "../../../settings/dev/dev_support"; -export const ZoomControls = ({ zoom, getConfigValue }: { - zoom: (value: number) => () => void, - getConfigValue: GetWebAppConfigValue -}) => { +export interface ZoomControlsProps { + zoom(value: number): () => void; + getConfigValue: GetWebAppConfigValue; +} + +export const ZoomControls = (props: ZoomControlsProps) => { + const { zoom, getConfigValue } = props; const plusBtnClass = atMaxZoom(getConfigValue) ? "disabled" : ""; const minusBtnClass = atMinZoom(getConfigValue) ? "disabled" : ""; return
; }; diff --git a/frontend/farm_designer/map/legend/layer_toggle.tsx b/frontend/farm_designer/map/legend/layer_toggle.tsx index caac1d6b7..5fa31cae0 100644 --- a/frontend/farm_designer/map/legend/layer_toggle.tsx +++ b/frontend/farm_designer/map/legend/layer_toggle.tsx @@ -13,6 +13,7 @@ export interface LayerToggleProps { onClick(): void; popover?: React.ReactElement; submenuTitle?: string; + className?: string; } /** A flipper type switch for showing/hiding the layers of the garden map. */ @@ -25,6 +26,7 @@ export function LayerToggle(props: LayerToggleProps) { "fb-layer-toggle", value ? "green" : "red", getModifiedClassName(props.settingName), + props.className, ].join(" "); return