diff --git a/.circleci/config.yml b/.circleci/config.yml index 5eb39b83c1..ee5d6e0e59 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,10 +88,16 @@ commands: - run: name: Report coverage to Coveralls command: | - if [ "$COVERAGE_AVAILABLE" ] + if [ "$CIRCLE_BRANCH" == "staging" ]; then set +eo pipefail; fi + if [ "$COVERAGE_AVAILABLE" ] && [ "$COVERALLS_REPO_TOKEN" ] then - sudo docker compose run -e COVERALLS_REPO_TOKEN="$COVERALLS_REPO_TOKEN" web npm run coverage || [ $CIRCLE_BRANCH == "staging" ] + curl -sLO https://github.com/coverallsapp/coverage-reporter/releases/latest/download/coveralls-linux.tar.gz + curl -sLO https://github.com/coverallsapp/coverage-reporter/releases/latest/download/coveralls-checksums.txt + cat coveralls-checksums.txt | grep coveralls-linux.tar.gz | sha256sum --check + tar -xzf coveralls-linux.tar.gz + ./coveralls report coverage_fe/lcov.info fi + if [ "$CIRCLE_BRANCH" == "staging" ]; then echo; fi when: always # change to `on_success` for a stricter comparison diff --git a/Gemfile.lock b/Gemfile.lock index 88e57cf222..4e0d566ee4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,7 +114,7 @@ GEM docile (1.4.0) e2mmap (0.1.0) erubi (1.12.0) - factory_bot (6.4.2) + factory_bot (6.4.4) activesupport (>= 5.0.0) factory_bot_rails (6.4.2) factory_bot (~> 6.4) @@ -222,7 +222,7 @@ GEM multipart-post (2.3.0) mutations (0.9.1) activesupport - net-imap (0.4.8) + net-imap (0.4.9) date net-protocol net-pop (0.1.2) @@ -232,7 +232,7 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.7.0) - nokogiri (1.15.5-x86_64-linux) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) os (1.1.4) @@ -336,7 +336,7 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) secure_headers (6.5.0) - set (1.0.4) + set (1.1.0) signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -433,4 +433,4 @@ RUBY VERSION ruby 3.1.4p223 BUNDLED WITH - 2.5.0 + 2.5.3 diff --git a/config/application.rb b/config/application.rb index 0ed0d00838..4d7dfbd143 100755 --- a/config/application.rb +++ b/config/application.rb @@ -126,6 +126,7 @@ class Application < Rails::Application "'unsafe-inline'", "'unsafe-eval'", "'self'", + "blob:", # 3D ], style_src: %w( maxcdn.bootstrapcdn.com diff --git a/db/migrate/20240118204046_add_enable3d_electronics_box_top_to_web_app_config.rb b/db/migrate/20240118204046_add_enable3d_electronics_box_top_to_web_app_config.rb new file mode 100644 index 0000000000..7e8c041cd2 --- /dev/null +++ b/db/migrate/20240118204046_add_enable3d_electronics_box_top_to_web_app_config.rb @@ -0,0 +1,9 @@ +class AddEnable3dElectronicsBoxTopToWebAppConfig < ActiveRecord::Migration[6.1] + def up + add_column :web_app_configs, :enable_3d_electronics_box_top, :boolean, default: true + end + + def down + remove_column :web_app_configs, :enable_3d_electronics_box_top + end +end diff --git a/db/structure.sql b/db/structure.sql index e3b9308799..71deecdafd 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2032,7 +2032,8 @@ CREATE TABLE public.web_app_configs ( go_button_axes character varying(3) DEFAULT 'XY'::character varying NOT NULL, show_uncropped_camera_view_area boolean DEFAULT false, default_plant_depth integer DEFAULT 5, - show_missed_step_plot boolean DEFAULT false + show_missed_step_plot boolean DEFAULT false, + enable_3d_electronics_box_top boolean DEFAULT true ); @@ -3979,6 +3980,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230712201622'), ('20230714010144'), ('20230714173031'), -('20230808192946'); +('20230808192946'), +('20240118204046'); diff --git a/frontend/__test_support__/additional_mocks.tsx b/frontend/__test_support__/additional_mocks.tsx index e3b6c6b62f..4afb22db17 100644 --- a/frontend/__test_support__/additional_mocks.tsx +++ b/frontend/__test_support__/additional_mocks.tsx @@ -35,3 +35,10 @@ jest.mock("../history", () => ({ push: jest.fn(), getPathArray: () => [], })); + +window.ResizeObserver = (() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any +})) as any; diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index b05c304aec..957f5b70b2 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -40,6 +40,7 @@ export const fakeDesignerState = (): DesignerState => ({ hoveredMapImage: undefined, cameraViewGridId: undefined, gridIds: [], + gridStart: { x: 100, y: 100 }, soilHeightLabels: false, profileOpen: false, profileAxis: "x", diff --git a/frontend/__test_support__/fake_state/resources.ts b/frontend/__test_support__/fake_state/resources.ts index 59e1c65ebc..6781372928 100644 --- a/frontend/__test_support__/fake_state/resources.ts +++ b/frontend/__test_support__/fake_state/resources.ts @@ -36,6 +36,9 @@ import { } from "farmbot/dist/resources/api_resources"; import { MessageType } from "../../sequences/interfaces"; import { TaggedPointGroup } from "../../resources/interfaces"; +import { + BooleanConfigKey as BooleanWebAppConfigKey, +} from "farmbot/dist/resources/configs/web_app"; export const resources: Everything["resources"] = buildResourceIndex(); let idCounter = 1; @@ -333,6 +336,7 @@ export function fakeWebAppConfig(): TaggedWebAppConfig { display_map_missed_steps: false, display_trail: false, dynamic_map: false, + ["enable_3d_electronics_box_top" as BooleanWebAppConfigKey]: true, encoder_figure: false, go_button_axes: "XY", hide_webcam_widget: false, diff --git a/frontend/connectivity/batch_queue.ts b/frontend/connectivity/batch_queue.ts index 07a045da5f..b1fbb727b2 100644 --- a/frontend/connectivity/batch_queue.ts +++ b/frontend/connectivity/batch_queue.ts @@ -3,6 +3,7 @@ import { store } from "../redux/store"; import { batchInitResources, bothUp } from "./connect_device"; import { maybeGetDevice } from "../resources/selectors"; import { deviceIsThrottled } from "./device_is_throttled"; +import { UnknownAction } from "redux"; /** Performs resource initialization (Eg: a storm of incoming logs) in batches * at a regular interval. We only need one work queue for the whole app, @@ -25,7 +26,7 @@ export class BatchQueue { work = () => { const dev = maybeGetDevice(store.getState().resources.index); if (!deviceIsThrottled(dev ? dev.body : undefined)) { - store.dispatch(batchInitResources(this.queue)); + store.dispatch(batchInitResources(this.queue) as unknown as UnknownAction); } this.clear(); bothUp(); diff --git a/frontend/connectivity/index.ts b/frontend/connectivity/index.ts index 138366c50e..09b50abe8e 100644 --- a/frontend/connectivity/index.ts +++ b/frontend/connectivity/index.ts @@ -2,6 +2,7 @@ import { store } from "../redux/store"; import { networkUp, networkDown } from "./actions"; import { Edge } from "./interfaces"; import { Actions } from "../constants"; +import { UnknownAction } from "redux"; /* ABOUT THIS FILE: These functions allow us to mark the network as up or down from anywhere within the app (even outside of React-Redux). I usually avoid @@ -39,13 +40,13 @@ export const dispatchQosStart = (id: string) => { export const dispatchNetworkUp = (edge: Edge, at: number) => { if (shouldThrottle(edge, at)) { return; } - store.dispatch(networkUp(edge, at)); + store.dispatch(networkUp(edge, at) as unknown as UnknownAction); bumpThrottle(edge, at); }; export const dispatchNetworkDown = (edge: Edge, at: number) => { if (shouldThrottle(edge, at)) { return; } - store.dispatch(networkDown(edge, at)); + store.dispatch(networkDown(edge, at) as unknown as UnknownAction); bumpThrottle(edge, at); }; diff --git a/frontend/constants.ts b/frontend/constants.ts index f6572b1ee0..d2d3a4c00a 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -782,6 +782,10 @@ export namespace Content { trim(`If not using sensors, use this setting to remove the panel from the Farm Designer.`); + export const ENABLE_3D_ELECTRONICS_BOX_TOP = + trim(`Show a 3D model of FarmBot's electronics box instead of a 2D view + in the Peripherals tab of the Controls pop-up.`); + export const BROWSER_SPEAK_LOGS = trim(`Have the browser also read aloud log messages on the "Speak" channel that are spoken by FarmBot.`); @@ -1868,7 +1872,7 @@ export namespace SetupWizardContent { trim(`Customize which Action or Sequence you want FarmBot to execute when you press Button 3, 4, or 5 on the electronics box. To start, we recommend setting Button 5 to the 'Find Home' sequence. You can change - this later from the controls panel.`); + this later from the controls pop-up.`); export const PROBLEM_GETTING_IMAGE = trim(`There is a 'camera not detected' or 'problem getting image' error @@ -2197,6 +2201,7 @@ export enum DeviceSetting { showSecondsInTime = `Show seconds in time`, hideWebcamWidget = `Hide Webcam widget`, hideSensorsPanel = `Hide Sensors panel`, + enable3dElectronicsBox = `Enable 3D electronics box`, readSpeakLogsInBrowser = `Read speak logs in browser`, landingPage = `Landing page`, browserFarmbotActivityBeep = `Browser FarmBot activity beep`, @@ -2391,6 +2396,7 @@ export enum Actions { HIGHLIGHT_MAP_IMAGE = "HIGHLIGHT_MAP_IMAGE", SHOW_CAMERA_VIEW_POINTS = "SHOW_CAMERA_VIEW_POINTS", TOGGLE_GRID_ID = "TOGGLE_GRID_ID", + SET_GRID_START = "SET_GRID_START", TOGGLE_SOIL_HEIGHT_LABELS = "TOGGLE_SOIL_HEIGHT_LABELS", SET_PROFILE_OPEN = "SET_PROFILE_OPEN", SET_PROFILE_AXIS = "SET_PROFILE_AXIS", diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index 3a2e1b5585..f1d8fe1b92 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -83,6 +83,7 @@ export class ControlsPanel extends React.Component { Peripherals = () => { return
", () => { dispatch: jest.fn(), firmwareHardware: undefined, resources: buildResourceIndex([]).index, + getConfigValue: () => false, }); it("renders", () => { diff --git a/frontend/controls/peripherals/index.tsx b/frontend/controls/peripherals/index.tsx index ab76f0412f..b9dfcb906f 100644 --- a/frontend/controls/peripherals/index.tsx +++ b/frontend/controls/peripherals/index.tsx @@ -10,8 +10,10 @@ import { Content } from "../../constants"; import { uniq, isNumber } from "lodash"; import { t } from "../../i18next_wrapper"; import { DIGITAL } from "farmbot"; -import { isBotOnlineFromState } from "../../devices/must_be_online"; -import { BoxTopButtons } from "../../settings/pin_bindings/box_top_gpio_diagram"; +import { isBotOnline } from "../../devices/must_be_online"; +import { getStatus } from "../../connectivity/reducer_support"; +import { BoxTop } from "../../settings/pin_bindings/box_top"; +import { BooleanSetting } from "../../session_keys"; export class Peripherals extends React.Component { @@ -20,9 +22,16 @@ export class Peripherals this.state = { isEditing: false }; } + get botOnline() { + const { hardware, connectivity } = this.props.bot; + const { sync_status } = hardware.informational_settings; + const botToMqttStatus = getStatus(connectivity.uptime["bot.mqtt"]); + return isBotOnline(sync_status, botToMqttStatus); + } + get disabled() { return !!this.props.bot.hardware.informational_settings.busy - || !isBotOnlineFromState(this.props.bot); + || !this.botOnline; } toggle = () => this.setState({ isEditing: !this.state.isEditing }); @@ -114,15 +123,15 @@ export class Peripherals : t("Edit"); return
{!this.props.hidePinBindings && - } -
+ firmwareHardware={this.props.firmwareHardware} + bot={this.props.bot} + botOnline={this.botOnline} />} 0 || isEditing} graphic={EmptyStateGraphic.regimens} diff --git a/frontend/controls/peripherals/interfaces.ts b/frontend/controls/peripherals/interfaces.ts index af7097aa10..47b96ac51b 100644 --- a/frontend/controls/peripherals/interfaces.ts +++ b/frontend/controls/peripherals/interfaces.ts @@ -1,6 +1,7 @@ import { Pins, TaggedPeripheral, FirmwareHardware } from "farmbot"; import { BotState } from "../../devices/interfaces"; import { ResourceIndex } from "../../resources/interfaces"; +import { GetWebAppConfigValue } from "../../config_storage/actions"; export interface PeripheralState { isEditing: boolean; @@ -26,4 +27,5 @@ export interface PeripheralsProps { firmwareHardware: FirmwareHardware | undefined; resources: ResourceIndex; hidePinBindings?: boolean; + getConfigValue: GetWebAppConfigValue; } diff --git a/frontend/controls/pin_form_fields.tsx b/frontend/controls/pin_form_fields.tsx index 9337fccb5c..af298dcfdd 100644 --- a/frontend/controls/pin_form_fields.tsx +++ b/frontend/controls/pin_form_fields.tsx @@ -19,7 +19,7 @@ interface NameInputBoxProps { export const NameInputBox = (props: NameInputBoxProps) => props.dispatch(edit(props.resource, { diff --git a/frontend/css/global.scss b/frontend/css/global.scss index b3c9e5ddaa..29b9c61808 100644 --- a/frontend/css/global.scss +++ b/frontend/css/global.scss @@ -858,11 +858,6 @@ fieldset { margin-top: 2rem; } -.peripherals-tab { - overflow-y: scroll; - overflow-x: hidden; -} - .webcam-stream-unavailable { position: relative; width: 100%; @@ -2011,6 +2006,69 @@ ul { } } +.electronics-box-3d-model { + margin-bottom: 1rem; + .led-label, + .btn-label { + display: block; + margin: auto; + width: max-content; + color: $off_white; + font-size: 1rem; + background: $dark_gray; + padding: 0.25rem 0.5rem; + border-radius: 0.5rem; + white-space: normal; + max-height: 3.4rem; + overflow-y: hidden; + font-weight: 700; + line-height: 1; + text-align: center; + &.hovered { + background: $black; + } + } + .led-label { + max-width: 7rem; + } + .btn-label { + max-width: 5.8rem; + } + .filter-search { + max-width: 5.5rem; + .fa-caret-down { + bottom: -0.5rem; + right: 0.25rem; + color: $off_white; + } + button { + padding: 0.25rem; + background: $dark_gray !important; + border-radius: 0.5rem; + height: max-content !important; + min-height: 0; + &:hover { + background: $dark_gray !important; + } + } + span { + color: $off_white; + white-space: normal !important; + text-overflow: revert !important; + font-weight: 700; + font-size: 1rem; + text-align: center; + margin-right: 0.5rem; + } + } + div { + z-index: 0 !important; + } + canvas { + height: 23rem; + } +} + .webcam-widget { .no-flipper-image-container { background: none !important; @@ -2030,7 +2088,6 @@ ul { } .peripheral-list { - margin-top: 1rem; label { margin-top: 0 !important; } @@ -2043,7 +2100,12 @@ ul { } } -.box-top-wrapper { +.peripheral-form, +.peripheral-list { + padding-top: 1rem; +} + +.box-top-2d-wrapper { margin-top: 2rem; .box-top-leds, .box-top-buttons { diff --git a/frontend/devices/connectivity/generate_data.ts b/frontend/devices/connectivity/generate_data.ts index 0459983cb1..fdbabb95c9 100644 --- a/frontend/devices/connectivity/generate_data.ts +++ b/frontend/devices/connectivity/generate_data.ts @@ -26,7 +26,7 @@ export const connectivityData = (props: ConnectivityDataProps) => { userMQTT: browserToMQTT({ state: "up", at: moment().valueOf() }), userAPI: browserToAPI({ state: "up", at: moment().valueOf() }), botMQTT: botToMQTT({ state: "up", at: moment().valueOf() }), - botAPI: botToAPI(moment().toISOString()), + botAPI: botToAPI("" + moment().toISOString()), botFirmware: botToFirmware("0.0.0.E", "express_k10"), } : { diff --git a/frontend/extras/__tests__/fallback_widget_test.tsx b/frontend/extras/__tests__/fallback_widget_test.tsx index fb992ad150..c8bfc0a462 100644 --- a/frontend/extras/__tests__/fallback_widget_test.tsx +++ b/frontend/extras/__tests__/fallback_widget_test.tsx @@ -21,6 +21,6 @@ describe("", () => { p.helpText = "This is a fake widget."; const wrapper = shallow(); expect(wrapper.html()) - .toContain(""); + .toContain(" { expect(newState.gridIds).toEqual([]); }); + it("sets grid start", () => { + const state = oldState(); + state.gridStart = { x: 100, y: 100 }; + const action: ReduxAction> = { + type: Actions.SET_GRID_START, payload: { x: 200, y: 300 } + }; + const newState = designer(state, action); + expect(newState.gridStart).toEqual({ x: 200, y: 300 }); + }); + it("toggle soil height labels", () => { const state = oldState(); state.soilHeightLabels = false; diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index bbf966016a..4078f9f8bc 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -181,6 +181,7 @@ export interface DesignerState { hoveredMapImage: number | undefined; cameraViewGridId: string | undefined; gridIds: string[]; + gridStart: Record<"x" | "y", number>; soilHeightLabels: boolean; profileOpen: boolean; profileAxis: "x" | "y"; diff --git a/frontend/farm_designer/map/__tests__/actions_test.ts b/frontend/farm_designer/map/__tests__/actions_test.ts index 88e2d16294..d67f3064e3 100644 --- a/frontend/farm_designer/map/__tests__/actions_test.ts +++ b/frontend/farm_designer/map/__tests__/actions_test.ts @@ -194,7 +194,7 @@ describe("clickMapPlant", () => { mockPath = Path.mock(Path.plants("select")); const state = fakeState(); const plant = fakePlant(); - plant.uuid = "fakePlantUuid"; + plant.uuid = "Point.fakePlantUuid"; state.resources = buildResourceIndex([plant]); const dispatch = jest.fn(); const getState: GetState = jest.fn(() => state); @@ -209,7 +209,7 @@ describe("clickMapPlant", () => { mockPath = Path.mock(Path.plants("select")); const state = fakeState(); const plant = fakePlant(); - plant.uuid = "fakePlantUuid"; + plant.uuid = "Point.fakePlantUuid"; state.resources = buildResourceIndex([plant]); state.resources.consumers.farm_designer.selectedPoints = [plant.uuid]; const dispatch = jest.fn(); diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index a71d8648d8..02e1a2338f 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -54,6 +54,7 @@ export const initialState: DesignerState = { hoveredMapImage: undefined, cameraViewGridId: undefined, gridIds: [], + gridStart: { x: 100, y: 100 }, soilHeightLabels: false, profileOpen: false, profileAxis: "x", @@ -262,6 +263,10 @@ export const designer = generateReducer(initialState) : s.gridIds.concat(payload); return s; }) + .add>(Actions.SET_GRID_START, (s, { payload }) => { + s.gridStart = payload; + return s; + }) .add(Actions.TOGGLE_SOIL_HEIGHT_LABELS, (s) => { s.soilHeightLabels = !s.soilHeightLabels; return s; diff --git a/frontend/farm_events/add_farm_event.tsx b/frontend/farm_events/add_farm_event.tsx index 01c2a34421..faf103284f 100644 --- a/frontend/farm_events/add_farm_event.tsx +++ b/frontend/farm_events/add_farm_event.tsx @@ -57,8 +57,8 @@ export class RawAddFarmEvent const { uuid } = this.props.findExecutable(executable_type, executable_id); const varData = this.props.resources.sequenceMetas[uuid]; const action = init("FarmEvent", { - end_time: moment().add(63, "minutes").toISOString(), - start_time: moment().add(3, "minutes").toISOString(), + end_time: "" + moment().add(63, "minutes").toISOString(), + start_time: "" + moment().add(3, "minutes").toISOString(), time_unit: "never", executable_id, executable_type, diff --git a/frontend/farm_events/map_state_to_props_add_edit.ts b/frontend/farm_events/map_state_to_props_add_edit.ts index 5f827a6bf8..70c5a86324 100644 --- a/frontend/farm_events/map_state_to_props_add_edit.ts +++ b/frontend/farm_events/map_state_to_props_add_edit.ts @@ -60,7 +60,7 @@ const handleTime = ( .toISOString(); // Set the time of the already existing iso string - const newStartISO = moment(currentStartISO) + const newStartISO = "" + moment(currentStartISO) .set("hours", hours) .set("minutes", minutes) .toISOString(); @@ -71,7 +71,7 @@ const handleTime = ( const currentEndISO = new Date((currentISO || "").toString()) .toISOString(); - const newEndISO = moment(currentEndISO) + const newEndISO = "" + moment(currentEndISO) .set("hours", hours) .set("minutes", minutes) .toISOString(); diff --git a/frontend/folders/actions.ts b/frontend/folders/actions.ts index 6e6911c4d3..2487330c0b 100644 --- a/frontend/folders/actions.ts +++ b/frontend/folders/actions.ts @@ -13,6 +13,7 @@ import { stepGet, STEP_DATATRANSFER_IDENTIFER } from "../draggable/actions"; import { joinKindAndId } from "../resources/reducer_support"; import { maybeGetSequence } from "../resources/selectors"; import { Path } from "../internal_urls"; +import { UnknownAction } from "redux"; export const setFolderColor = (id: number, color: Color) => { const d = store.dispatch as Function; @@ -51,7 +52,7 @@ export const addNewSequenceToFolder = (config: DeepPartial = {}) => { kind: "sequence", body: [], }; - store.dispatch(init("Sequence", newSequence)); + store.dispatch(init("Sequence", newSequence) as unknown as UnknownAction); push(Path.sequences(urlFriendly(newSequence.name))); setActiveSequenceByName(); }; diff --git a/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx b/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx index 2623e9cebb..739a9b070a 100644 --- a/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx +++ b/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx @@ -113,8 +113,7 @@ describe("", () => { currentTarget: { value: "" } }); expect(wrapper.instance().state[filter]).toEqual(undefined); - // eslint-disable-next-line no-null/no-null - expect(edit).toHaveBeenCalledWith(config, { [key]: null }); + expect(edit).toHaveBeenCalledWith(config, { [key]: undefined }); expect(save).toHaveBeenCalledWith(config.uuid); }); diff --git a/frontend/photos/photo_filter_settings/image_filter_menu.tsx b/frontend/photos/photo_filter_settings/image_filter_menu.tsx index 8216eade77..ad8ca5324b 100644 --- a/frontend/photos/photo_filter_settings/image_filter_menu.tsx +++ b/frontend/photos/photo_filter_settings/image_filter_menu.tsx @@ -84,7 +84,9 @@ export class ImageFilterMenu let value = undefined; switch (datetime) { case "beginDate": - value = offsetTime(input, beginTime || "00:00", timeSettings); + if (input) { + value = offsetTime(input, beginTime || "00:00", timeSettings); + } this.setValues({ photo_filter_begin: value }); break; case "beginTime": @@ -94,7 +96,9 @@ export class ImageFilterMenu } break; case "endDate": - value = offsetTime(input, endTime || "00:00", timeSettings); + if (input) { + value = offsetTime(input, endTime || "00:00", timeSettings); + } this.setValues({ photo_filter_end: value }); break; case "endTime": diff --git a/frontend/photos/photo_filter_settings/util.ts b/frontend/photos/photo_filter_settings/util.ts index 25368f2cb1..3086c6c2e6 100644 --- a/frontend/photos/photo_filter_settings/util.ts +++ b/frontend/photos/photo_filter_settings/util.ts @@ -32,7 +32,7 @@ export const parseFilterSetting = (getConfigValue: GetWebAppConfigValue) => }; export const filterTime = (direction: "before" | "after", seconds = 1) => - (image: TaggedImage) => + (image: TaggedImage): string => moment(image.body.created_at) .add(direction == "before" ? -seconds : seconds, "second") .toISOString(); diff --git a/frontend/plants/__tests__/plant_inventory_item_test.tsx b/frontend/plants/__tests__/plant_inventory_item_test.tsx index 33b811209d..3761195347 100644 --- a/frontend/plants/__tests__/plant_inventory_item_test.tsx +++ b/frontend/plants/__tests__/plant_inventory_item_test.tsx @@ -48,7 +48,7 @@ describe("", () => { const p = fakeProps(); const plant = fakePlant(); plant.body.name = ""; - plant.body.planted_at = moment().toISOString(); + plant.body.planted_at = "" + moment().toISOString(); p.plant = plant; const wrapper = shallow(); expect(wrapper.text()).toEqual("Unknown plant1 day old"); diff --git a/frontend/plants/edit_plant_status.tsx b/frontend/plants/edit_plant_status.tsx index 3b78211072..e4d6f13fa1 100644 --- a/frontend/plants/edit_plant_status.tsx +++ b/frontend/plants/edit_plant_status.tsx @@ -84,7 +84,7 @@ const getUpdateByPlantStage = (plant_stage: PlantStage): PlantOptions => { update.planted_at = undefined; break; case "planted": - update.planted_at = moment().toISOString(); + update.planted_at = "" + moment().toISOString(); } return update; }; @@ -164,7 +164,7 @@ export const PlantDateBulkUpdate = (props: PlantDateBulkUpdateProps) => { })) && plants.map(plant => { props.dispatch(edit(plant, { - planted_at: moment(e.currentTarget.value) + planted_at: "" + moment(e.currentTarget.value) .utcOffset(props.timeSettings.utcOffset).toISOString() })); props.dispatch(save(plant.uuid)); diff --git a/frontend/plants/grid/__tests__/plant_grid_test.tsx b/frontend/plants/grid/__tests__/plant_grid_test.tsx index d09bb81c74..329563ede5 100644 --- a/frontend/plants/grid/__tests__/plant_grid_test.tsx +++ b/frontend/plants/grid/__tests__/plant_grid_test.tsx @@ -176,6 +176,16 @@ describe("", () => { expect(wrapper.state().grid.numPlantsH).toEqual(6); }); + it("handles data changes: starting coordinates", () => { + const props = fakeProps(); + const wrapper = mount(); + wrapper.instance().onChange("startX", 600); + expect(wrapper.state().grid.startX).toEqual(600); + expect(props.dispatch).toHaveBeenCalledWith({ + type: Actions.SET_GRID_START, payload: { x: 600, y: 100 }, + }); + }); + it("uses current position", () => { const props = fakeProps(); const wrapper = mount(); diff --git a/frontend/plants/grid/plant_grid.tsx b/frontend/plants/grid/plant_grid.tsx index 0eb7691656..6a1449a54c 100644 --- a/frontend/plants/grid/plant_grid.tsx +++ b/frontend/plants/grid/plant_grid.tsx @@ -29,9 +29,10 @@ export class PlantGrid extends React.Component { get initGridState() { const spread = (this.props.spread || DEFAULT_PLANT_RADIUS) * 10; + const gridStart = this.props.designer?.gridStart || { x: 100, y: 100 }; return { - startX: 100, - startY: 100, + startX: gridStart.x, + startY: gridStart.y, spacingH: spread, spacingV: spread, numPlantsH: 2, @@ -46,6 +47,11 @@ export class PlantGrid extends React.Component { onChange = (key: PlantGridKey, val: number) => { const grid = { ...this.state.grid, [key]: val }; + ["startX", "startY"].includes(key) && + this.props.dispatch({ + type: Actions.SET_GRID_START, + payload: { x: grid.startX, y: grid.startY }, + }); this.setState({ grid }, this.performPreview()); }; diff --git a/frontend/plants/plant_panel.tsx b/frontend/plants/plant_panel.tsx index d4432a583d..a2b510602e 100644 --- a/frontend/plants/plant_panel.tsx +++ b/frontend/plants/plant_panel.tsx @@ -66,7 +66,7 @@ export const EditDatePlanted = (props: EditDatePlantedProps) => { value={datePlanted?.utcOffset(timeSettings.utcOffset) .format("YYYY-MM-DD") || ""} onCommit={e => updatePlant(uuid, { - planted_at: moment(e.currentTarget.value) + planted_at: "" + moment(e.currentTarget.value) .utcOffset(timeSettings.utcOffset).toISOString() })} />; }; diff --git a/frontend/points/create_points.tsx b/frontend/points/create_points.tsx index 86bcad2f86..3d6c4375b9 100644 --- a/frontend/points/create_points.tsx +++ b/frontend/points/create_points.tsx @@ -220,7 +220,7 @@ export class RawCreatePoints diff --git a/frontend/points/point_edit_actions.tsx b/frontend/points/point_edit_actions.tsx index a6ca2a2cbc..b9f8b00b0f 100644 --- a/frontend/points/point_edit_actions.tsx +++ b/frontend/points/point_edit_actions.tsx @@ -140,7 +140,7 @@ export const EditPointName = (props: EditPointNameProps) => props.updatePoint({ name: e.currentTarget.value })} /> diff --git a/frontend/redux/store.ts b/frontend/redux/store.ts index 9bb2562714..1e32560d70 100644 --- a/frontend/redux/store.ts +++ b/frontend/redux/store.ts @@ -1,14 +1,13 @@ -import { createStore, PreloadedState } from "redux"; +import { createStore } from "redux"; import { EnvName, Store } from "./interfaces"; import { rootReducer } from "./root_reducer"; import { registerSubscribers } from "./subscribers"; import { getMiddleware } from "./middlewares"; import { set } from "lodash"; -import { Everything } from "../interfaces"; function getStore(envName: EnvName): Store { return createStore(rootReducer, - {} as PreloadedState, + {}, getMiddleware(envName)); } diff --git a/frontend/redux/version_tracker_middleware.ts b/frontend/redux/version_tracker_middleware.ts index 47cbcef17e..d7485dc13a 100644 --- a/frontend/redux/version_tracker_middleware.ts +++ b/frontend/redux/version_tracker_middleware.ts @@ -18,7 +18,7 @@ function getVersionFromState(state: Everything) { const fn: MW = (store: Store) => - (dispatch: Dispatch>) => + (dispatch: Dispatch>) => // eslint-disable-next-line @typescript-eslint/no-explicit-any (action: any) => { const fbos = getVersionFromState(store.getState()); diff --git a/frontend/regimens/set_active_regimen_by_name.ts b/frontend/regimens/set_active_regimen_by_name.ts index 946e18d28c..a781294a44 100644 --- a/frontend/regimens/set_active_regimen_by_name.ts +++ b/frontend/regimens/set_active_regimen_by_name.ts @@ -3,8 +3,10 @@ import { store } from "../redux/store"; import { urlFriendly } from "../util"; import { selectRegimen } from "./actions"; import { Path } from "../internal_urls"; +import { UnknownAction } from "redux"; -const setRegimen = (uuid: string) => store.dispatch(selectRegimen(uuid)); +const setRegimen = (uuid: string) => + store.dispatch(selectRegimen(uuid) as unknown as UnknownAction); export function setActiveRegimenByName() { const chunk = Path.getLastChunk(); diff --git a/frontend/resources/reducer_support.ts b/frontend/resources/reducer_support.ts index 3113a2f7e7..4ba797fd19 100644 --- a/frontend/resources/reducer_support.ts +++ b/frontend/resources/reducer_support.ts @@ -1,7 +1,7 @@ import { ResourceName, SpecialStatus, TaggedResource, TaggedSequence, } from "farmbot"; -import { combineReducers, ReducersMapObject } from "redux"; +import { combineReducers, ReducersMapObject, UnknownAction } from "redux"; import { helpReducer as help } from "../help/reducer"; import { designer as farm_designer } from "../farm_designer/reducer"; import { photosReducer as photos } from "../photos/reducer"; @@ -40,6 +40,7 @@ import { getFbosConfig } from "./getters"; import { ingest, PARENTLESS as NO_PARENT } from "../folders/data_transfer"; import { FolderNode, FolderMeta } from "../folders/interfaces"; import { pointsSelectedByGroup } from "../point_groups/criteria/apply"; +import { Everything } from "../interfaces"; export function findByUuid(index: ResourceIndex, uuid: string): TaggedResource { const x = index.references[uuid]; @@ -275,8 +276,8 @@ const consumerReducer = combineReducers({ farmware, help, alerts - // eslint-disable-next-line @typescript-eslint/no-explicit-any -} as ReducersMapObject); +} as ReducersMapObject, +) as Function; /** The resource reducer must have the first say when a resource-related action * fires off. Afterwards, sub-reducers are allowed to make sense of data diff --git a/frontend/saved_gardens/garden_snapshot.tsx b/frontend/saved_gardens/garden_snapshot.tsx index 7b350891f2..0992a424d1 100644 --- a/frontend/saved_gardens/garden_snapshot.tsx +++ b/frontend/saved_gardens/garden_snapshot.tsx @@ -42,7 +42,7 @@ export class GardenSnapshot render() { return
- this.setState({ gardenName: e.currentTarget.value })} value={this.state.gardenName} /> diff --git a/frontend/sequences/set_active_sequence_by_name.ts b/frontend/sequences/set_active_sequence_by_name.ts index f87be74f31..8dd030029e 100644 --- a/frontend/sequences/set_active_sequence_by_name.ts +++ b/frontend/sequences/set_active_sequence_by_name.ts @@ -4,8 +4,10 @@ import { urlFriendly } from "../util"; import { selectSequence } from "./actions"; import { setMenuOpen } from "./test_button"; import { Path } from "../internal_urls"; +import { UnknownAction } from "redux"; -const setSequence = (uuid: string) => store.dispatch(selectSequence(uuid)); +const setSequence = (uuid: string) => + store.dispatch(selectSequence(uuid) as unknown as UnknownAction); export function setActiveSequenceByName() { const chunk = Path.getLastChunk(); diff --git a/frontend/session_keys.ts b/frontend/session_keys.ts index 3c9033dfb2..73e225a2ce 100644 --- a/frontend/session_keys.ts +++ b/frontend/session_keys.ts @@ -4,7 +4,8 @@ import { StringConfigKey as WebAppStringConfigKey, } from "farmbot/dist/resources/configs/web_app"; -type WebAppBooleanConfigKeyAll = WebAppBooleanConfigKey; +type WebAppBooleanConfigKeyAll = WebAppBooleanConfigKey + | "enable_3d_electronics_box_top"; type WebAppNumberConfigKeyAll = WebAppNumberConfigKey; type WebAppStringConfigKeyAll = WebAppStringConfigKey; @@ -62,6 +63,8 @@ export const BooleanSetting: BooleanSettings = { disable_i18n: "disable_i18n", hide_webcam_widget: "hide_webcam_widget", hide_sensors: "hide_sensors", + enable_3d_electronics_box_top: + "enable_3d_electronics_box_top" as WebAppBooleanConfigKey, enable_browser_speak: "enable_browser_speak", discard_unsaved: "discard_unsaved", time_format_24_hour: "time_format_24_hour", diff --git a/frontend/settings/account/account_settings.tsx b/frontend/settings/account/account_settings.tsx index 41e0e47df3..c29c3db47a 100644 --- a/frontend/settings/account/account_settings.tsx +++ b/frontend/settings/account/account_settings.tsx @@ -40,7 +40,7 @@ export const AccountSettings = (props: AccountSettingsProps) => { props.dispatch(edit( @@ -155,6 +155,11 @@ const APP_SETTINGS = (): SettingDescriptionProps[] => ([ description: Content.HIDE_SENSORS_WIDGET, setting: BooleanSetting.hide_sensors, }, + { + title: DeviceSetting.enable3dElectronicsBox, + description: Content.ENABLE_3D_ELECTRONICS_BOX_TOP, + setting: BooleanSetting.enable_3d_electronics_box_top, + }, { title: DeviceSetting.readSpeakLogsInBrowser, description: Content.BROWSER_SPEAK_LOGS, diff --git a/frontend/settings/default_values.ts b/frontend/settings/default_values.ts index d5fe4e281d..6ba086369f 100644 --- a/frontend/settings/default_values.ts +++ b/frontend/settings/default_values.ts @@ -19,6 +19,7 @@ const DEFAULT_WEB_APP_CONFIG_VALUES: Record = { disable_i18n: false, display_trail: true, dynamic_map: false, + ["enable_3d_electronics_box_top" as Key]: true, encoder_figure: false, go_button_axes: "XY", hide_webcam_widget: false, diff --git a/frontend/settings/fbos_settings/name_row.tsx b/frontend/settings/fbos_settings/name_row.tsx index 2b0f87fd61..dd3fe20133 100644 --- a/frontend/settings/fbos_settings/name_row.tsx +++ b/frontend/settings/fbos_settings/name_row.tsx @@ -9,7 +9,7 @@ import { getModifiedClassNameSpecifyDefault } from "../default_values"; export class NameRow extends React.Component { NameInput = () => - ({ + overwrite: jest.fn(), + save: jest.fn(), + destroy: jest.fn(), + initSave: jest.fn(), +})); + +import { + buildResourceIndex, +} from "../../../__test_support__/resource_index_builder"; +import { + fakePinBinding, fakeSequence, +} from "../../../__test_support__/fake_state/resources"; +import { + PinBindingType, StandardPinBinding, +} from "farmbot/dist/resources/api_resources"; +import { destroy, overwrite, initSave, save } from "../../../api/crud"; +import { SetPinBindingProps, setPinBinding } from "../actions"; +import { PinBindingListItems } from "../interfaces"; +import { error } from "../../../toast/toast"; + +describe("setPinBinding()", () => { + const fakeProps = (): SetPinBindingProps => { + const pinBinding = fakePinBinding(); + pinBinding.body.pin_num = 20; + pinBinding.body.binding_type = PinBindingType.standard; + (pinBinding.body as StandardPinBinding).sequence_id = 1; + const sequence = fakeSequence(); + sequence.body.id = 1; + sequence.body.name = "my sequence"; + const resources = buildResourceIndex([sequence, pinBinding]).index; + const binding: PinBindingListItems = { + pin_number: pinBinding.body.pin_num, + sequence_id: sequence.body.id, + uuid: pinBinding.uuid, + binding_type: PinBindingType.standard, + special_action: undefined, + }; + return { + binding, + dispatch: jest.fn(), + resources, + pinNumber: 20, + }; + }; + + it("un-binds pin", () => { + const p = fakeProps(); + setPinBinding(p)({ + isNull: true, label: "", value: "", + }); + expect(error).not.toHaveBeenCalled(); + expect(initSave).not.toHaveBeenCalled(); + expect(overwrite).not.toHaveBeenCalled(); + expect(destroy).toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); + }); + + it("re-binds pin: standard", () => { + const p = fakeProps(); + setPinBinding(p)({ + headingId: PinBindingType.standard, label: "", value: 1, + }); + expect(error).not.toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalled(); + expect(initSave).not.toHaveBeenCalled(); + expect(overwrite).toHaveBeenCalledWith(expect.any(Object), + expect.objectContaining({ + pin_num: 20, sequence_id: 1, binding_type: PinBindingType.standard, + special_action: undefined, + })); + expect(save).toHaveBeenCalled(); + }); + + it("re-binds pin: special", () => { + const p = fakeProps(); + setPinBinding(p)({ + headingId: PinBindingType.special, label: "", value: "sync", + }); + expect(error).not.toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalled(); + expect(initSave).not.toHaveBeenCalled(); + expect(overwrite).toHaveBeenCalledWith(expect.any(Object), + expect.objectContaining({ + pin_num: 20, special_action: "sync", binding_type: PinBindingType.special, + sequence_id: undefined, + })); + expect(save).toHaveBeenCalled(); + }); + + it("binds new pin", () => { + const p = fakeProps(); + p.pinNumber = 5; + p.binding = undefined; + setPinBinding(p)({ + headingId: PinBindingType.special, label: "", value: "sync", + }); + expect(error).not.toHaveBeenCalled(); + expect(destroy).not.toHaveBeenCalled(); + expect(overwrite).not.toHaveBeenCalled(); + expect(initSave).toHaveBeenCalledWith("PinBinding", { + pin_num: 5, special_action: "sync", binding_type: PinBindingType.special, + }); + }); +}); diff --git a/frontend/settings/pin_bindings/__tests__/box_top_gpio_diagram_test.tsx b/frontend/settings/pin_bindings/__tests__/box_top_gpio_diagram_test.tsx index c074a26957..b4e04d5cf1 100644 --- a/frontend/settings/pin_bindings/__tests__/box_top_gpio_diagram_test.tsx +++ b/frontend/settings/pin_bindings/__tests__/box_top_gpio_diagram_test.tsx @@ -3,18 +3,10 @@ jest.mock("../../../devices/actions", () => ({ sendRPC: jest.fn(), })); -jest.mock("../../../api/crud", () => ({ - overwrite: jest.fn(), - save: jest.fn(), - destroy: jest.fn(), - initSave: jest.fn(), -})); - import React from "react"; import { mount } from "enzyme"; import { BoxTopButtons, - BoxTopButtonsProps, BoxTopGpioDiagram, BoxTopGpioDiagramProps, } from "../box_top_gpio_diagram"; @@ -29,7 +21,8 @@ import { PinBindingSpecialAction, PinBindingType, SpecialPinBinding, StandardPinBinding, } from "farmbot/dist/resources/api_resources"; -import { destroy, overwrite, initSave, save } from "../../../api/crud"; +import { BoxTopBaseProps } from "../interfaces"; +import { bot } from "../../../__test_support__/fake_state/bot"; describe("", () => { const fakeProps = (): BoxTopGpioDiagramProps => ({ @@ -76,7 +69,7 @@ describe("", () => { }); describe("", () => { - const fakeProps = (): BoxTopButtonsProps => { + const fakeProps = (): BoxTopBaseProps => { const pinBinding = fakePinBinding(); pinBinding.body.pin_num = 20; pinBinding.body.binding_type = PinBindingType.standard; @@ -85,17 +78,32 @@ describe("", () => { sequence.body.id = 1; sequence.body.name = "my sequence"; const resources = buildResourceIndex([sequence, pinBinding]).index; + bot.hardware.informational_settings.sync_status = "synced"; + bot.hardware.informational_settings.locked = false; return { firmwareHardware: "arduino", isEditing: true, dispatch: jest.fn(), resources, botOnline: true, - syncStatus: "synced", - locked: false, + bot, }; }; + it("renders: genesis", () => { + const p = fakeProps(); + p.firmwareHardware = "arduino"; + const wrapper = mount(); + expect(wrapper.find("#button").length).toEqual(9); + }); + + it("renders: express", () => { + const p = fakeProps(); + p.firmwareHardware = "express_k10"; + const wrapper = mount(); + expect(wrapper.find("#button").length).toEqual(1); + }); + it("renders: not editing", () => { const p = fakeProps(); p.isEditing = false; @@ -107,58 +115,13 @@ describe("", () => { it("renders: blinking", () => { const p = fakeProps(); - p.syncStatus = "syncing"; - p.locked = true; + p.bot.hardware.informational_settings.sync_status = "syncing"; + p.bot.hardware.informational_settings.locked = true; const wrapper = mount(); expect(wrapper.find(".fast-blink").length).toEqual(1); expect(wrapper.find(".slow-blink").length).toEqual(1); }); - it("un-binds pin", () => { - const wrapper = mount(); - wrapper.instance().bind(20)({ - isNull: true, label: "", value: "", - }); - expect(destroy).toHaveBeenCalled(); - expect(save).not.toHaveBeenCalled(); - }); - - it("re-binds pin: standard", () => { - const wrapper = mount(); - wrapper.instance().bind(20)({ - headingId: PinBindingType.standard, label: "", value: 1, - }); - expect(overwrite).toHaveBeenCalledWith(expect.any(Object), - expect.objectContaining({ - pin_num: 20, sequence_id: 1, binding_type: PinBindingType.standard, - special_action: undefined, - })); - expect(save).toHaveBeenCalled(); - }); - - it("re-binds pin: special", () => { - const wrapper = mount(); - wrapper.instance().bind(20)({ - headingId: PinBindingType.special, label: "", value: "sync", - }); - expect(overwrite).toHaveBeenCalledWith(expect.any(Object), - expect.objectContaining({ - pin_num: 20, special_action: "sync", binding_type: PinBindingType.special, - sequence_id: undefined, - })); - expect(save).toHaveBeenCalled(); - }); - - it("binds new pin", () => { - const wrapper = mount(); - wrapper.instance().bind(5)({ - headingId: PinBindingType.special, label: "", value: "sync", - }); - expect(initSave).toHaveBeenCalledWith("PinBinding", { - pin_num: 5, special_action: "sync", binding_type: PinBindingType.special, - }); - }); - it("executes sequence", () => { const wrapper = mount(); wrapper.find("#button").first().simulate("click"); diff --git a/frontend/settings/pin_bindings/__tests__/box_top_test.tsx b/frontend/settings/pin_bindings/__tests__/box_top_test.tsx new file mode 100644 index 0000000000..fd6c905b67 --- /dev/null +++ b/frontend/settings/pin_bindings/__tests__/box_top_test.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { mount } from "enzyme"; +import { BoxTop } from "../box_top"; +import { BoxTopProps } from "../interfaces"; +import { + buildResourceIndex, +} from "../../../__test_support__/resource_index_builder"; +import { bot } from "../../../__test_support__/fake_state/bot"; + +describe("", () => { + const fakeProps = (): BoxTopProps => ({ + threeDimensions: false, + firmwareHardware: "arduino", + isEditing: true, + dispatch: jest.fn(), + resources: buildResourceIndex().index, + botOnline: true, + bot, + }); + + it("renders 2D box", () => { + const p = fakeProps(); + p.threeDimensions = false; + const wrapper = mount(); + expect(wrapper.find(".box-top-2d-wrapper").length).toEqual(1); + expect(wrapper.find(".electronics-box-3d-model").length).toEqual(0); + }); + + it("renders 3D box", () => { + const p = fakeProps(); + p.threeDimensions = true; + const wrapper = mount(); + expect(wrapper.find(".box-top-2d-wrapper").length).toEqual(0); + expect(wrapper.find(".electronics-box-3d-model").length).toEqual(1); + }); +}); diff --git a/frontend/settings/pin_bindings/__tests__/model_test.tsx b/frontend/settings/pin_bindings/__tests__/model_test.tsx new file mode 100644 index 0000000000..a0fb7657ef --- /dev/null +++ b/frontend/settings/pin_bindings/__tests__/model_test.tsx @@ -0,0 +1,231 @@ +jest.mock("@react-three/drei", () => { + const useGLTF = jest.fn(() => ({ + nodes: { + Electronics_Box: { + geometry: jest.fn(), + material: { color: { set: jest.fn() } }, + }, + Electronics_Box_Gasket: { + geometry: jest.fn(), + material: { color: { set: jest.fn() } }, + }, + Electronics_Box_Lid: { + geometry: jest.fn(), + material: { color: { set: jest.fn() } }, + }, + ["Push_Button_-_Red"]: { + geometry: jest.fn(), + material: { color: { set: jest.fn() } }, + }, + LED: { + geometry: jest.fn(), + material: { color: { set: jest.fn() } }, + }, + }, + materials: { + [Material.box]: { + color: { set: jest.fn() }, + transparent: false, + }, + [Material.gasket]: { + color: { set: jest.fn() }, + transparent: false, + }, + [Material.lid]: { + color: { set: jest.fn() }, + transparent: false, + }, + [Material.button]: { + color: { set: jest.fn() }, + transparent: false, + }, + [Material.led]: { + color: { set: jest.fn() }, + transparent: false, + }, + }, + })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (useGLTF as any).preload = jest.fn(); + return { + useGLTF, + Cylinder: () =>
, + Html: ({ children }: { children: ReactNode }) =>
{children}
, + PerspectiveCamera: () =>
, + useCursor: jest.fn(), + }; +}); + +let mockElapsedTime = 0; +jest.mock("@react-three/fiber", () => ({ + Canvas: () =>
, + ThreeEvent: jest.fn(), + useFrame: jest.fn(x => x({ + clock: { getElapsedTime: jest.fn(() => mockElapsedTime) } + })), +})); + +const mockSetColor = jest.fn(); +jest.mock("react", () => { + const originReact = jest.requireActual("react"); + const mockRef = jest.fn(() => ({ + current: { + material: { color: { set: mockSetColor } }, + setOptions: jest.fn(), + } + })); + return { + ...originReact, + useRef: mockRef, + }; +}); + +const lodash = require("lodash"); +lodash.debounce = jest.fn(x => x); + +jest.mock("../../../devices/actions", () => ({ + execSequence: jest.fn(), +})); + +import React, { ReactNode } from "react"; +import { mount } from "enzyme"; +import { ThreeEvent } from "@react-three/fiber"; +import { IColor, Material, Model, setZForAllInGroup } from "../model"; +import { + buildResourceIndex, +} from "../../../__test_support__/resource_index_builder"; +import { + StandardPinBinding, +} from "farmbot/dist/resources/api_resources"; +import { + fakePinBinding, fakeSequence, +} from "../../../__test_support__/fake_state/resources"; +import { bot } from "../../../__test_support__/fake_state/bot"; +import { execSequence } from "../../../devices/actions"; +import { ButtonPin } from "../list_and_label_support"; +import { BoxTopBaseProps } from "../interfaces"; + +describe("setZForAllInGroup()", () => { + it("sets z", () => { + const e = { + object: { + parent: { + children: [ + { name: "button-center", position: { z: 0 }, children: [] }, + ] + } + } + } as unknown as ThreeEvent; + setZForAllInGroup(e, 100); + expect(e.object.parent?.children[0].position.z).toEqual(100); + }); +}); + +describe("", () => { + const fakeProps = (): BoxTopBaseProps => { + const binding = fakePinBinding(); + binding.body.pin_num = ButtonPin.estop; + (binding.body as StandardPinBinding).sequence_id = 1; + const sequence = fakeSequence(); + sequence.body.id = 1; + sequence.body.name = "e-stop"; + return { + isEditing: false, + dispatch: jest.fn(), + resources: buildResourceIndex([binding, sequence]).index, + botOnline: true, + bot, + firmwareHardware: "arduino", + }; + }; + + const e = { + object: { + parent: { + children: [ + { name: "button-center", position: { z: 0 }, children: [] }, + ] + } + } + }; + + it("triggers binding", () => { + const p = fakeProps(); + p.isEditing = false; + p.botOnline = true; + const wrapper = mount(); + wrapper.find({ name: "action-group" }).first().simulate("pointerdown", e); + expect(execSequence).toHaveBeenCalledWith(1); + }); + + it("hovers button", () => { + const wrapper = mount(); + const btnBefore = wrapper.find({ name: "button-center" }).first(); + expect(btnBefore.props()["material-color"]).toEqual(13421772); + wrapper.find({ name: "action-group" }).first().simulate("pointerover", e); + const btnAfter = wrapper.find({ name: "button-center" }).first(); + expect(btnAfter.props()["material-color"]).toEqual(14540253); + expect(e.object.parent?.children[0].position.z).toEqual(128); + }); + + it("un-hovers button", () => { + const wrapper = mount(); + wrapper.find({ name: "action-group" }).first().simulate("pointerout", e); + expect(e.object.parent?.children[0].position.z).toEqual(131); + }); + + it("resets z", () => { + const wrapper = mount(); + wrapper.find({ name: "button-group" }).first().simulate("pointerup", e); + expect(e.object.parent?.children[0].position.z).toEqual(131); + }); + + it("renders: off", () => { + const p = fakeProps(); + p.isEditing = true; + p.botOnline = false; + p.bot.hardware.informational_settings.locked = false; + p.bot.hardware.informational_settings.sync_status = "booting"; + const sequence = fakeSequence(); + p.resources = buildResourceIndex([sequence]).index; + mount(); + expect(mockSetColor).toHaveBeenCalledWith(IColor.estop.off); + }); + + it("renders: on", () => { + const p = fakeProps(); + p.botOnline = true; + p.bot.hardware.informational_settings.locked = false; + p.bot.hardware.informational_settings.busy = false; + mount(); + expect(mockSetColor).toHaveBeenCalledWith(IColor.estop.on); + }); + + it("renders: blinking on", () => { + const p = fakeProps(); + p.isEditing = true; + p.bot.hardware.informational_settings.locked = true; + p.bot.hardware.informational_settings.sync_status = "syncing"; + mount(); + expect(mockSetColor).toHaveBeenCalledWith(IColor.unlock.on); + }); + + it("renders: blinking off", () => { + mockElapsedTime = 1; + const p = fakeProps(); + p.bot.hardware.informational_settings.locked = true; + p.bot.hardware.informational_settings.sync_status = "syncing"; + mount(); + expect(mockSetColor).toHaveBeenCalledWith(IColor.unlock.off); + }); + + it("renders: express", () => { + mockElapsedTime = 1; + const p = fakeProps(); + p.bot.hardware.informational_settings.locked = true; + p.bot.hardware.informational_settings.sync_status = "syncing"; + p.firmwareHardware = "express_k10"; + mount(); + expect(mockSetColor).not.toHaveBeenCalledWith(IColor.unlock.on); + }); +}); diff --git a/frontend/settings/pin_bindings/actions.ts b/frontend/settings/pin_bindings/actions.ts new file mode 100644 index 0000000000..a177c936c3 --- /dev/null +++ b/frontend/settings/pin_bindings/actions.ts @@ -0,0 +1,101 @@ +import { DropDownItem } from "../../ui"; +import { + PinBinding, + PinBindingSpecialAction, PinBindingType, SpecialPinBinding, StandardPinBinding, +} from "farmbot/dist/resources/api_resources"; +import { initSave, overwrite, save, destroy } from "../../api/crud"; +import { pinBindingBody } from "./tagged_pin_binding_init"; +import { ResourceIndex } from "../../resources/interfaces"; +import { findByUuid } from "../../resources/reducer_support"; +import { cloneDeep, isUndefined } from "lodash"; +import { error } from "../../toast/toast"; +import { t } from "../../i18next_wrapper"; +import { PinBindingListItems } from "./interfaces"; +import { apiPinBindings } from "./pin_bindings_content"; +import { execSequence, sendRPC } from "../../devices/actions"; +import { RpcRequestBodyItem } from "farmbot"; + +export interface SetPinBindingProps { + dispatch: Function; + resources: ResourceIndex; + binding: PinBindingListItems | undefined; + pinNumber: number | undefined; +} + +export const setPinBinding = (props: SetPinBindingProps) => + // eslint-disable-next-line complexity + (ddi: DropDownItem): boolean => { + const { binding, dispatch, resources, pinNumber } = props; + const bindingUuid = binding?.uuid; + if (isUndefined(pinNumber)) { + error(t("Pin number cannot be blank.")); + return false; + } + if (bindingUuid && ddi.isNull) { + dispatch(destroy(bindingUuid)); + return false; + } + const bindingType = ddi.headingId as PinBindingType | undefined; + const sequenceIdInput = ddi.headingId == PinBindingType.standard + ? parseInt("" + ddi.value) + : undefined; + const specialActionInput = ddi.headingId == PinBindingType.special + ? ddi.value as PinBindingSpecialAction + : undefined; + const noSequenceId = isUndefined(sequenceIdInput) || isNaN(sequenceIdInput); + if (isUndefined(bindingType) + || (noSequenceId && isUndefined(specialActionInput))) { + error(t("Please select a sequence or action.")); + return false; + } + const body = pinBindingBody(bindingType == PinBindingType.special + ? { + pin_num: pinNumber, + special_action: specialActionInput, + binding_type: bindingType + } + : { + pin_num: pinNumber, + sequence_id: sequenceIdInput, + binding_type: bindingType + }); + if (bindingUuid) { + const binding = findByUuid(resources, bindingUuid); + const newBody = cloneDeep(binding.body as PinBinding); + newBody.binding_type = bindingType; + if (bindingType == PinBindingType.standard && sequenceIdInput) { + (newBody as StandardPinBinding).sequence_id = sequenceIdInput; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (newBody as any).special_action = undefined; + } + if (bindingType == PinBindingType.special && specialActionInput) { + (newBody as SpecialPinBinding).special_action = specialActionInput; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (newBody as any).sequence_id = undefined; + } + dispatch(overwrite(binding, newBody)); + dispatch(save(binding.uuid)); + } else { + dispatch(initSave("PinBinding", body)); + } + return true; + }; + +export const findBinding = (resources: ResourceIndex) => (pin: number) => + apiPinBindings(resources).filter(b => b.pin_number == pin)[0]; + +export const triggerBinding = + (resources: ResourceIndex, botOnline: boolean) => + (pin: number) => + () => { + const binding = findBinding(resources)(pin); + if (!botOnline || !binding) { return; } + if (binding.sequence_id) { + execSequence(binding.sequence_id); + } + if (binding.special_action) { + sendRPC({ + kind: binding.special_action, args: {}, + } as RpcRequestBodyItem); + } + }; diff --git a/frontend/settings/pin_bindings/box_top.tsx b/frontend/settings/pin_bindings/box_top.tsx new file mode 100644 index 0000000000..a8bcc97602 --- /dev/null +++ b/frontend/settings/pin_bindings/box_top.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { ElectronicsBoxModel } from "./model"; +import { BoxTopButtons } from "./box_top_gpio_diagram"; +import { BoxTopProps } from "./interfaces"; + +export const BoxTop = (props: BoxTopProps) => +
+ {props.threeDimensions + ? + : } +
; diff --git a/frontend/settings/pin_bindings/box_top_gpio_diagram.tsx b/frontend/settings/pin_bindings/box_top_gpio_diagram.tsx index 54ad97febd..e2781da239 100644 --- a/frontend/settings/pin_bindings/box_top_gpio_diagram.tsx +++ b/frontend/settings/pin_bindings/box_top_gpio_diagram.tsx @@ -1,21 +1,12 @@ import React from "react"; import { t } from "../../i18next_wrapper"; import { Color } from "../../ui/colors"; -import { FirmwareHardware, RpcRequestBodyItem, SyncStatus } from "farmbot"; +import { FirmwareHardware, SyncStatus } from "farmbot"; import { hasExtraButtons } from "../firmware/firmware_hardware_support"; -import { DropDownItem } from "../../ui"; import { BindingTargetDropdown, pinBindingLabel } from "./pin_binding_input_group"; -import { - PinBinding, - PinBindingSpecialAction, PinBindingType, SpecialPinBinding, StandardPinBinding, -} from "farmbot/dist/resources/api_resources"; -import { initSave, overwrite, save, destroy } from "../../api/crud"; -import { pinBindingBody } from "./tagged_pin_binding_init"; -import { ResourceIndex } from "../../resources/interfaces"; -import { findByUuid } from "../../resources/reducer_support"; -import { apiPinBindings } from "./pin_bindings_content"; -import { execSequence, sendRPC } from "../../devices/actions"; -import { cloneDeep, isUndefined } from "lodash"; +import { isUndefined } from "lodash"; +import { findBinding, setPinBinding, triggerBinding } from "./actions"; +import { BoxTopBaseProps } from "./interfaces"; export interface BoxTopGpioDiagramProps { boundPins: number[] | undefined; @@ -126,22 +117,12 @@ export class BoxTopGpioDiagram } } -export interface BoxTopButtonsProps { - firmwareHardware: FirmwareHardware | undefined; - isEditing: boolean; - dispatch: Function; - resources: ResourceIndex; - botOnline: boolean; - syncStatus: SyncStatus | undefined; - locked: boolean; -} - interface BoxTopButtonsState { hoveredPin: number | undefined; } export class BoxTopButtons - extends React.Component { + extends React.Component { state: BoxTopGpioDiagramState = { hoveredPin: undefined }; hover = (hovered: number | undefined) => @@ -154,73 +135,34 @@ export class BoxTopButtons this.setState({ hoveredPin: hovered }); }; - findBinding = (pin: number) => apiPinBindings(this.props.resources) - .filter(b => b.pin_number == pin)[0]; - - bind = (pin: number) => (ddi: DropDownItem) => { - const bindingUuid = this.findBinding(pin)?.uuid; - if (bindingUuid && ddi.isNull) { - this.props.dispatch(destroy(bindingUuid)); - return; - } - const bindingType = ddi.headingId as PinBindingType; - const sequenceIdInput = ddi.headingId == PinBindingType.standard - ? parseInt("" + ddi.value) - : undefined; - const specialActionInput = ddi.headingId == PinBindingType.special - ? ddi.value as PinBindingSpecialAction - : undefined; - const body = pinBindingBody(bindingType == PinBindingType.special - ? { - pin_num: pin, - special_action: specialActionInput, - binding_type: bindingType - } - : { - pin_num: pin, - sequence_id: sequenceIdInput, - binding_type: bindingType - }); - if (bindingUuid) { - const binding = findByUuid(this.props.resources, bindingUuid); - const newBody = cloneDeep(binding.body as PinBinding); - newBody.binding_type = bindingType; - if (bindingType == PinBindingType.standard && sequenceIdInput) { - (newBody as StandardPinBinding).sequence_id = sequenceIdInput; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (newBody as any).special_action = undefined; - } - if (bindingType == PinBindingType.special && specialActionInput) { - (newBody as SpecialPinBinding).special_action = specialActionInput; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (newBody as any).sequence_id = undefined; - } - this.props.dispatch(overwrite(binding, newBody)); - this.props.dispatch(save(binding.uuid)); - } else { - this.props.dispatch(initSave("PinBinding", body)); - } - }; + findBinding = findBinding(this.props.resources); render() { - const { firmwareHardware, botOnline, locked, syncStatus } = this.props; + const { + firmwareHardware, botOnline, resources, dispatch, bot, + } = this.props; + const { locked, sync_status } = bot.hardware.informational_settings; + const syncStatus = sync_status; const circlesProps = { firmwareHardware, clean: true }; const buttons = CIRCLES(circlesProps).filter(circle => circle.r > 4); const leds = CIRCLES(circlesProps).filter(circle => circle.r < 5); - return
+ return
{buttons.map(circle => { const binding = this.findBinding(circle.pin); return this.props.isEditing && [5, 20, 26].includes(circle.pin) ? :

{(pinBindingLabel({ - resources: this.props.resources, + resources: resources, sequenceIdInput: binding?.sequence_id, specialActionInput: binding?.special_action, }) || circle).label} @@ -236,18 +178,7 @@ export class BoxTopButtons on={ledOn(statusProps)} blinking={ledBlinking(statusProps)} hasBinding={!!binding} - press={() => { - const binding = this.findBinding(circle.pin); - if (!botOnline || !binding) { return; } - if (binding.sequence_id) { - execSequence(binding.sequence_id); - } - if (binding.special_action) { - sendRPC({ - kind: binding.special_action, args: {}, - } as RpcRequestBodyItem); - } - }} />; + press={triggerBinding(resources, botOnline)(circle.pin)} />; })}

diff --git a/frontend/settings/pin_bindings/interfaces.ts b/frontend/settings/pin_bindings/interfaces.ts index a37d49ef0d..d7c9744380 100644 --- a/frontend/settings/pin_bindings/interfaces.ts +++ b/frontend/settings/pin_bindings/interfaces.ts @@ -5,6 +5,7 @@ import { } from "farmbot/dist/resources/api_resources"; import { FirmwareHardware } from "farmbot"; import { SettingsPanelState } from "../../interfaces"; +import { BotState } from "../../devices/interfaces"; export interface PinBindingsProps { dispatch: Function; @@ -47,3 +48,16 @@ export interface PinBindingInputGroupState { specialActionInput: PinBindingSpecialAction | undefined; bindingType: PinBindingType; } + +export interface BoxTopBaseProps { + isEditing: boolean; + dispatch: Function; + resources: ResourceIndex; + botOnline: boolean; + bot: BotState; + firmwareHardware: FirmwareHardware | undefined; +} + +export interface BoxTopProps extends BoxTopBaseProps { + threeDimensions: boolean; +} diff --git a/frontend/settings/pin_bindings/model.tsx b/frontend/settings/pin_bindings/model.tsx new file mode 100644 index 0000000000..f8b4724c56 --- /dev/null +++ b/frontend/settings/pin_bindings/model.tsx @@ -0,0 +1,381 @@ +/* eslint-disable react/no-unknown-property */ +/* eslint-disable no-null/no-null */ +import React, { useRef } from "react"; +import { + Cylinder, Html, PerspectiveCamera, useCursor, useGLTF, +} from "@react-three/drei"; +import { Canvas, ThreeEvent, useFrame } from "@react-three/fiber"; +import { GLTF } from "three-stdlib"; +import { BindingTargetDropdown, pinBindingLabel } from "./pin_binding_input_group"; +import { BoxTopBaseProps, PinBindingListItems } from "./interfaces"; +import { setPinBinding, findBinding, triggerBinding } from "./actions"; +import { BufferGeometry } from "three"; +import { debounce, isUndefined, some } from "lodash"; +import { t } from "../../i18next_wrapper"; +import { isExpress } from "../../settings/firmware/firmware_hardware_support"; +import { ButtonPin } from "./list_and_label_support"; + +const ASSETS = "/3D/"; +const LIB_DIR = `${ASSETS}lib/`; + +const MODELS = { + box: `${ASSETS}models/box.glb`, + btn: `${ASSETS}models/push_button.glb`, + led: `${ASSETS}models/led_indicator.glb`, +}; + +type Box = GLTF & { + nodes: { + Electronics_Box: THREE.Mesh; + Electronics_Box_Gasket: THREE.Mesh; + Electronics_Box_Lid: THREE.Mesh; + }; + materials: { + [Material.box]: THREE.MeshStandardMaterial; + [Material.gasket]: THREE.MeshStandardMaterial; + [Material.lid]: THREE.MeshStandardMaterial; + }; +} + +export enum Material { + box = "0.901961_0.901961_0.901961_0.000000_0.000000", + gasket = "0.301961_0.301961_0.301961_0.000000_0.000000", + lid = "0.564706_0.811765_0.945098_0.000000_0.623529", + button = "0.701961_0.701961_0.701961_0.000000_0.000000", + led = "0.600000_0.600000_0.600000_0.000000_0.000000", +} + +type Btn = GLTF & { + nodes: { + ["Push_Button_-_Red"]: THREE.Mesh; + }; + materials: { + [Material.button]: THREE.MeshStandardMaterial; + }; +} + +type Led = GLTF & { + nodes: { + LED: THREE.Mesh; + }; + materials: { + [Material.led]: THREE.MeshStandardMaterial; + }; +} + +Object.values(MODELS).map(model => useGLTF.preload(model, LIB_DIR)); + +type MeshObject = THREE.Mesh; + +const Z = 131; +export namespace IColor { + export enum estop { + on = 0xef4037, + off = 0xd89a97, + } + export enum unlock { + on = 0xf5e909, + off = 0xe1de94, + } + export enum connect { + on = 0x1073e0, + off = 0x88a4c3, + } + export enum sync { + on = 0x62c020, + off = 0x94b87b, + } + export enum blank { + on = 0xffffff, + off = 0xf4f4f4, + } +} + +const changeItemsInGroup = ( + meshObject: MeshObject, + cb: (x: MeshObject) => void, + items = ["button-center", "button-color"], +) => { + meshObject.children.map(child => { + const object = child as MeshObject; + if (some(items.map(item => child.name.includes(item)))) { + cb(object); + } + changeItemsInGroup(object, cb, items); + }); +}; + +export const setZForAllInGroup = (e: ThreeEvent, z: number) => { + changeItemsInGroup( + e.object.parent as MeshObject, + x => x.position.z = z); +}; + +interface ButtonOrLedItem { + label: string; + pinNumber: number; + blink?: boolean; + on?: boolean; + position: number; + color: { on: number, off: number }; + ref?: React.MutableRefObject; +} + +export const Model = (props: BoxTopBaseProps) => { + const box = useGLTF(MODELS.box, LIB_DIR) as Box; + const btn = useGLTF(MODELS.btn, LIB_DIR) as Btn; + const led = useGLTF(MODELS.led, LIB_DIR) as Led; + const SCALE = 1000; + + const syncLed = useRef(null); + const connLed = useRef(null); + const unlock = useRef(null); + const estop = useRef(null); + + const { + locked, sync_status, + } = props.bot.hardware.informational_settings; + const findPinBinding = findBinding(props.resources); + const clickBinding = triggerBinding(props.resources, props.botOnline); + const express = isExpress(props.firmwareHardware); + + const BUTTONS: ButtonOrLedItem[] = [ + { + label: t("E-Stop"), + pinNumber: ButtonPin.estop, + on: props.botOnline && !locked, + position: -60, + color: { + on: IColor.estop.on, + off: IColor.estop.off, + }, + ref: estop, + }, + { + label: t("Unlock"), + pinNumber: ButtonPin.unlock, + blink: props.botOnline && locked, + position: -30, + color: { + on: IColor.unlock.on, + off: IColor.unlock.off, + }, + ref: unlock, + }, + { + label: t("Button 3"), + pinNumber: ButtonPin.btn3, + position: 0, + color: { + on: IColor.blank.on, + off: IColor.blank.off, + }, + }, + { + label: t("Button 4"), + pinNumber: ButtonPin.btn4, + position: 30, + color: { + on: IColor.blank.on, + off: IColor.blank.off, + }, + }, + { + label: t("Button 5"), + pinNumber: ButtonPin.btn5, + position: 60, + color: { + on: IColor.blank.on, + off: IColor.blank.off, + }, + }, + ]; + + const LEDS: ButtonOrLedItem[] = [ + { + label: t("Sync"), + pinNumber: -1, + on: sync_status == "synced", + blink: sync_status == "syncing", + position: -45, + color: { + on: IColor.sync.on, + off: IColor.sync.off, + }, + ref: syncLed, + }, + { + label: t("Connectivity"), + pinNumber: -1, + on: props.botOnline, + position: -15, + color: { + on: IColor.connect.on, + off: IColor.connect.off, + }, + ref: connLed, + }, + { + label: t("LED 3"), + pinNumber: -1, + position: 15, + color: { + on: IColor.blank.on, + off: IColor.blank.off, + }, + }, + { + label: t("LED 4"), + pinNumber: -1, + position: 45, + color: { + on: IColor.blank.on, + off: IColor.blank.off, + }, + }, + ]; + + useFrame((state) => { + const t = state.clock.getElapsedTime(); + BUTTONS.concat(LEDS).map(item => { + const current = item.ref?.current; + const { on, off } = item.color; + if (current) { + if (item.blink) { + current.material.color.set(t % 2 < 1 ? on : off); + } else { + current.material.color.set(item.on ? on : off); + } + } + }); + }); + + const getLabel = (binding: PinBindingListItems | undefined) => { + return pinBindingLabel({ + resources: props.resources, + sequenceIdInput: binding?.sequence_id, + specialActionInput: binding?.special_action, + })?.label; + }; + + const [hovered, setHovered] = React.useState(); + useCursor(!isUndefined(hovered)); + const leave = (e: ThreeEvent) => { + setHovered(undefined); + setZForAllInGroup(e, Z); + }; + return + + + + + + + + {BUTTONS + .filter((_, index) => express ? index == 0 : true) + .map(button => { + const { position, color, ref, label, pinNumber } = button; + const btnPosition = express ? 0 : position; + const binding = findPinBinding(pinNumber); + const isHovered = hovered == pinNumber; + const click = debounce(clickBinding(pinNumber)); + const enter = () => !props.isEditing && setHovered(pinNumber); + return + + { + if (!props.isEditing) { + setZForAllInGroup(e, Z - 3); + click(); + } + }}> + + + + {props.isEditing + ? + :

+ {getLabel(binding) || label} +

} + +
+
; + })} + {LEDS + .filter(() => !express) + .map(ledIndicator => { + const { position, color, ref } = ledIndicator; + return + + + +

{ledIndicator.label}

+ +
; + })} +
; +}; + +export const ElectronicsBoxModel = (props: BoxTopBaseProps) => { + return
+ + + +
; +}; diff --git a/frontend/settings/pin_bindings/pin_binding_input_group.tsx b/frontend/settings/pin_bindings/pin_binding_input_group.tsx index a2498797a2..e49df2f33a 100644 --- a/frontend/settings/pin_bindings/pin_binding_input_group.tsx +++ b/frontend/settings/pin_bindings/pin_binding_input_group.tsx @@ -7,8 +7,6 @@ import { PinBindingInputGroupState, } from "./interfaces"; import { isNumber, includes } from "lodash"; -import { initSave } from "../../api/crud"; -import { pinBindingBody } from "./tagged_pin_binding_init"; import { error, warning } from "../../toast/toast"; import { validGpioPins, sysBindings, generatePinLabel, RpiPinList, @@ -25,6 +23,7 @@ import { BoxTopGpioDiagram } from "./box_top_gpio_diagram"; import { findSequenceById, selectAllSequences } from "../../resources/selectors"; import { ResourceIndex } from "../../resources/interfaces"; import { FirmwareHardware } from "farmbot"; +import { setPinBinding } from "./actions"; export class PinBindingInputGroup extends React.Component { @@ -60,34 +59,26 @@ export class PinBindingInputGroup /** Validate and save a pin binding. */ bindPin = () => { - const { dispatch } = this.props; + const { dispatch, resources } = this.props; const { pinNumberInput, sequenceIdInput, bindingType, specialActionInput } = this.state; - if (isNumber(pinNumberInput)) { - if (bindingType && (sequenceIdInput || specialActionInput)) { - bindingType == PinBindingType.special - ? dispatch(initSave("PinBinding", pinBindingBody({ - pin_num: pinNumberInput, - special_action: specialActionInput, - binding_type: bindingType - }))) - : dispatch(initSave("PinBinding", pinBindingBody({ - pin_num: pinNumberInput, - sequence_id: sequenceIdInput, - binding_type: bindingType - }))); - this.setState({ - pinNumberInput: undefined, - sequenceIdInput: undefined, - specialActionInput: undefined, - bindingType: PinBindingType.standard, - }); - } else { - error(t("Please select a sequence or action.")); - } - } else { - error(t("Pin number cannot be blank.")); + const success = setPinBinding({ + binding: undefined, + dispatch, + resources, + pinNumber: pinNumberInput, + })({ + headingId: bindingType, label: "", + value: "" + (sequenceIdInput || specialActionInput), + }); + if (success) { + this.setState({ + pinNumberInput: undefined, + sequenceIdInput: undefined, + specialActionInput: undefined, + bindingType: PinBindingType.standard, + }); } }; @@ -219,12 +210,13 @@ export const BindingTargetDropdown = (props: BindingTargetDropdownProps) => { }); return dropDownList; }; - + const selectedItem = pinBindingLabel(props); return ; + customNullLabel={t("Select")} />; }; interface PinBindingLabelProps { diff --git a/frontend/tools/add_tool.tsx b/frontend/tools/add_tool.tsx index eea285f162..1b71814add 100644 --- a/frontend/tools/add_tool.tsx +++ b/frontend/tools/add_tool.tsx @@ -188,7 +188,7 @@ export class RawAddTool extends React.Component { env={this.props.env} /> this.setState({ toolName: e.currentTarget.value })} /> {reduceToolName(toolName) == ToolName.wateringNozzle && diff --git a/frontend/tools/edit_tool.tsx b/frontend/tools/edit_tool.tsx index 1d062ed9e7..b56c7a1c1f 100644 --- a/frontend/tools/edit_tool.tsx +++ b/frontend/tools/edit_tool.tsx @@ -134,7 +134,7 @@ export class RawEditTool extends React.Component { saveFarmwareEnv={this.props.saveFarmwareEnv} env={this.props.env} /> - this.setState({ toolName: e.currentTarget.value })} /> {reduceToolName(toolName) == ToolName.wateringNozzle && diff --git a/frontend/wizard/__tests__/checks_test.tsx b/frontend/wizard/__tests__/checks_test.tsx index 2b9d16f0bd..569020913e 100644 --- a/frontend/wizard/__tests__/checks_test.tsx +++ b/frontend/wizard/__tests__/checks_test.tsx @@ -601,6 +601,7 @@ describe("", () => { const p = fakeProps(); const pinBinding = fakePinBinding(); p.resources = buildResourceIndex([pinBinding]).index; + p.getConfigValue = () => false; const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("button 5"); diff --git a/frontend/wizard/__tests__/step_test.tsx b/frontend/wizard/__tests__/step_test.tsx index 6a125bc304..95fb1ae801 100644 --- a/frontend/wizard/__tests__/step_test.tsx +++ b/frontend/wizard/__tests__/step_test.tsx @@ -197,7 +197,7 @@ describe("", () => { const p = fakeProps(); p.step.pinBindingOptions = { editing: false }; const wrapper = mount(); - expect(wrapper.find(".box-top-buttons").length).toEqual(1); + expect(wrapper.find(".electronics-box-top").length).toEqual(1); }); }); diff --git a/frontend/wizard/actions.ts b/frontend/wizard/actions.ts index 6e0a62d562..7496d0c15a 100644 --- a/frontend/wizard/actions.ts +++ b/frontend/wizard/actions.ts @@ -31,7 +31,7 @@ export const destroyAllWizardStepResults = export const completeSetup = (device: TaggedDevice | undefined) => device && setDeviceProperty(device, { - setup_completed_at: moment().toISOString() + setup_completed_at: "" + moment().toISOString() }); export const resetSetup = (device: TaggedDevice | undefined) => diff --git a/frontend/wizard/checks.tsx b/frontend/wizard/checks.tsx index 3511f83ba8..d033f582d8 100644 --- a/frontend/wizard/checks.tsx +++ b/frontend/wizard/checks.tsx @@ -63,7 +63,7 @@ import { BooleanSetting } from "../session_keys"; import { BooleanConfigKey as BooleanWebAppConfigKey, } from "farmbot/dist/resources/configs/web_app"; -import { toggleWebAppBool } from "../config_storage/actions"; +import { GetWebAppConfigValue, toggleWebAppBool } from "../config_storage/actions"; import { PLACEHOLDER_FARMBOT } from "../photos/images/image_flipper"; import { OriginSelector } from "../settings/farm_designer_settings"; import { Sensors } from "../sensors"; @@ -90,7 +90,6 @@ import { BotPositionRows } from "../controls/move/bot_position_rows"; import { BootSequenceSelector, } from "../settings/fbos_settings/boot_sequence_selector"; -import { BoxTopButtons } from "../settings/pin_bindings/box_top_gpio_diagram"; import { getImageJobs } from "../photos/state_to_props"; import { ResourceIndex } from "../resources/interfaces"; import { BotState } from "../devices/interfaces"; @@ -99,6 +98,7 @@ import { } from "../farm_designer/map/tool_graphics/all_tools"; import { WaterFlowRateInput } from "../tools/edit_tool"; import { RPI_OPTIONS } from "../settings/fbos_settings/rpi_model"; +import { BoxTop } from "../settings/pin_bindings/box_top"; export const Language = (props: WizardStepComponentProps) => { const user = getUserAccountSettings(props.resources); @@ -575,6 +575,7 @@ export const PeripheralsCheck = (props: WizardStepComponentProps) => { const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources)); return
{ @@ -605,13 +607,14 @@ export const PinBinding = (props: PinBindingProps) => { onClick={() => emergencyUnlock()}> {t("UNLOCK")} - : ; }; diff --git a/frontend/wizard/step.tsx b/frontend/wizard/step.tsx index addf23d018..fd967ab87b 100644 --- a/frontend/wizard/step.tsx +++ b/frontend/wizard/step.tsx @@ -95,6 +95,7 @@ export const WizardStepContainer = (props: WizardStepContainerProps) => { controlsCheckOptions={step.controlsCheckOptions} />} {step.pinBindingOptions && { {zone ?
- { this.props.dispatch(edit(zone, { name: e.currentTarget.value })); diff --git a/jest.config.js b/jest.config.js index f9f891eac9..fdabad36f0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,7 +18,8 @@ module.exports = { "./frontend/__test_support__/localstorage.js", "./frontend/__test_support__/mock_fbtoaster.ts", "./frontend/__test_support__/mock_i18next.ts", - "./frontend/__test_support__/additional_mocks.tsx" + "./frontend/__test_support__/additional_mocks.tsx", + "jest-canvas-mock", ], transform: { ".(ts|tsx)": "ts-jest" diff --git a/package.json b/package.json index 43972ffa2e..a338b7cc5a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "url": "https://github.com/farmbot/farmbot-web-app" }, "scripts": { - "coverage": "cat **/*lcov.info | ./node_modules/coveralls/bin/coveralls.js", "test-very-slow": "node --expose-gc ./node_modules/.bin/jest -i --colors --coverage", "test-slow": "./node_modules/.bin/jest -w 6 --colors", "test": "./node_modules/.bin/jest -w 5 --no-coverage", @@ -31,33 +30,36 @@ "author": "farmbot.io", "license": "MIT", "dependencies": { - "@blueprintjs/core": "5.7.2", - "@blueprintjs/select": "5.0.20", + "@blueprintjs/core": "5.8.2", + "@blueprintjs/select": "5.0.23", "@monaco-editor/react": "4.6.0", - "@parcel/transformer-sass": "2.10.3", - "@parcel/transformer-typescript-tsc": "2.10.3", + "@parcel/transformer-sass": "2.11.0", + "@parcel/transformer-typescript-tsc": "2.11.0", + "@react-three/drei": "9.96.0", + "@react-three/fiber": "8.15.14", "@types/lodash": "4.14.202", "@types/markdown-it": "13.0.7", - "@types/node": "20.10.4", + "@types/node": "20.11.5", "@types/promise-timeout": "1.3.3", - "@types/react": "18.2.45", - "@types/react-color": "3.0.10", - "@types/react-dom": "18.2.17", + "@types/react": "18.2.48", + "@types/react-color": "3.0.11", + "@types/react-dom": "18.2.18", + "@types/three": "0.160.0", "@types/ws": "8.5.10", - "axios": "1.6.2", + "axios": "1.6.5", "bowser": "2.11.0", "browser-speech": "1.1.1", "events": "3.3.0", "farmbot": "15.8.5", - "i18next": "23.7.11", + "i18next": "23.7.16", "lodash": "4.17.21", "markdown-it": "14.0.0", "markdown-it-emoji": "3.0.0", - "moment": "2.29.4", + "moment": "2.30.1", "monaco-editor": "0.45.0", "mqtt": "5.1.4", - "npm": "10.2.5", - "parcel": "2.10.3", + "npm": "10.3.0", + "parcel": "2.11.0", "process": "0.11.10", "promise-timeout": "1.3.0", "punycode": "1.4.1", @@ -65,11 +67,12 @@ "react": "18.2.0", "react-color": "2.19.3", "react-dom": "18.2.0", - "react-redux": "8.1.3", - "redux": "4.2.1", + "react-redux": "9.1.0", + "redux": "5.0.1", "redux-immutable-state-invariant": "2.1.0", "redux-thunk": "3.1.0", "takeme": "0.12.0", + "three": "0.160.0", "typescript": "5.3.3", "url": "0.11.3", "xterm": "5.3.0" @@ -78,20 +81,20 @@ "@types/enzyme": "3.10.12", "@types/jest": "29.5.11", "@types/readable-stream": "4.0.10", - "@typescript-eslint/eslint-plugin": "6.14.0", - "@typescript-eslint/parser": "6.14.0", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", - "coveralls": "3.1.1", "enzyme": "3.11.0", - "eslint": "8.55.0", + "eslint": "8.56.0", "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "2.29.1", - "eslint-plugin-jest": "27.6.0", + "eslint-plugin-jest": "27.6.3", "eslint-plugin-no-null": "1.0.2", "eslint-plugin-promise": "6.1.1", "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "jest": "29.7.0", + "jest-canvas-mock": "2.5.2", "jest-cli": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-junit": "16.0.0", @@ -101,7 +104,7 @@ "raf": "3.4.1", "react-addons-test-utils": "15.6.2", "react-test-renderer": "18.2.0", - "sass": "1.69.5", + "sass": "1.70.0", "sass-lint": "1.13.1", "ts-jest": "29.1.1", "tslint": "6.1.3" diff --git a/public/3D/lib/draco_decoder.wasm b/public/3D/lib/draco_decoder.wasm new file mode 100644 index 0000000000..469904ebcb Binary files /dev/null and b/public/3D/lib/draco_decoder.wasm differ diff --git a/public/3D/lib/draco_wasm_wrapper.js b/public/3D/lib/draco_wasm_wrapper.js new file mode 100644 index 0000000000..43f1556838 --- /dev/null +++ b/public/3D/lib/draco_wasm_wrapper.js @@ -0,0 +1,116 @@ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE? +$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))}; +$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)}; +var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q=y}},"es6","es3"); +$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(kp;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3"); +$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5"); +$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5"); +var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e); +e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)}); +if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+ +12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+ +16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0=d?b++:2047>=d?b+=2:55296<=d&&57343>= +d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"=== +typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]= +this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr= +Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3=e[1]?!0:0!=e[0]||10< +e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e, +b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1>>=0;if(2147483648=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)} +function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")|| +P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0= +function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0= +a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0= +a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0= +function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0= +a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0= +function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1= +function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb= +a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0= +a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb= +a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null, +arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0= +a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0= +a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0= +a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib= +a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0= +function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1= +a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1= +function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0= +a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0= +function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0= +a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0= +function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2= +a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null, +arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0= +function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2= +a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)}, +oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3= +function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc= +a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3= +a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3= +a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3= +a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1= +a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM= +function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM= +a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION= +a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD= +a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD= +a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc= +a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32= +a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32= +function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT= +a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER= +function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)}; +a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0=r.size?(0>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;gb.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule); diff --git a/public/3D/models/box.glb b/public/3D/models/box.glb new file mode 100644 index 0000000000..dbdd356530 Binary files /dev/null and b/public/3D/models/box.glb differ diff --git a/public/3D/models/led_indicator.glb b/public/3D/models/led_indicator.glb new file mode 100644 index 0000000000..797ae12367 Binary files /dev/null and b/public/3D/models/led_indicator.glb differ diff --git a/public/3D/models/push_button.glb b/public/3D/models/push_button.glb new file mode 100644 index 0000000000..fab66989e5 Binary files /dev/null and b/public/3D/models/push_button.glb differ diff --git a/run_all_ci_tasks.sh b/run_all_ci_tasks.sh index 14bac878d8..f1b169f61b 100755 --- a/run_all_ci_tasks.sh +++ b/run_all_ci_tasks.sh @@ -9,7 +9,4 @@ P2=$! sudo docker compose run web npm run test-slow & P3=$! -sudo docker compose run web npm run coverage & -P4=$! - -wait $P1 $P2 $P3 $P4 +wait $P1 $P2 $P3 diff --git a/spec/controllers/api/ai/ai_controller_spec.rb b/spec/controllers/api/ai/ai_controller_spec.rb index 43f98cc342..d6e83779c3 100644 --- a/spec/controllers/api/ai/ai_controller_spec.rb +++ b/spec/controllers/api/ai/ai_controller_spec.rb @@ -119,11 +119,17 @@ def chunk(content, done=nil) expect(response.status).to eq(200) expect(response.body).to eq("title") - (0..20).map do |_| + statuses = [] + bodies = [] + + (0..30).map do |_| post :create, body: payload.to_json + statuses.push(response.status) + bodies.push(response.body) end - expect(response.status).to eq(403) - expect(response.body).to eq({error: "Too many requests. Try again later."}.to_json) + if statuses.last() != 403 then puts statuses.join(" ") end + expect(statuses).to include(403) + expect(bodies).to include({error: "Too many requests. Try again later."}.to_json) end end diff --git a/tsconfig.json b/tsconfig.json index 2c63ffef76..963525a772 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "noUnusedParameters": true, "pretty": true, "removeComments": true, + "skipLibCheck": true, "sourceMap": true, "strictNullChecks": true, "target": "es5",