Skip to content

Commit

Permalink
refactor(ecs-browser): create useComponentValueStream hook and use it…
Browse files Browse the repository at this point in the history
… in ecs-browser
  • Loading branch information
Kooshaba committed Jul 1, 2022
1 parent 92828c5 commit 8b78ab9
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 43 deletions.
3 changes: 3 additions & 0 deletions packages/ecs-browser/src/Browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ export const Browser = ({
layers,
setContractComponentValue,
devHighlightComponent,
hoverHighlightComponent,
world,
}: {
entities: EntityID[];
layers: Layers;
setContractComponentValue: SetContractComponentFunction<Schema>;
devHighlightComponent: Component<{ value: Type.OptionalNumber }>;
hoverHighlightComponent: Component<{ x: Type.OptionalNumber; y: Type.OptionalNumber; }>;
world: World;
}) => {
const [filteredEntities, setFilteredEntities] = useState<EntityID[]>([]);
Expand All @@ -27,6 +29,7 @@ export const Browser = ({
<BrowserContainer>
<QueryBuilder
devHighlightComponent={devHighlightComponent}
hoverHighlightComponent={hoverHighlightComponent}
allEntities={entities}
setFilteredEntities={setFilteredEntities}
layers={layers}
Expand Down
9 changes: 3 additions & 6 deletions packages/ecs-browser/src/ComponentEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react";
import { Layers, removeComponent, toUpdate } from "@latticexyz/recs";
import { Layers, removeComponent } from "@latticexyz/recs";
import { AnyComponent, EntityIndex, Schema } from "@latticexyz/recs/src/types";
import { ComponentBrowserButton, ComponentEditorContainer, ComponentTitle } from "./StyledComponents";
import { ComponentValueEditor } from "./ComponentValueEditor";
import { SetContractComponentFunction } from "./types";
import { useStream } from "@latticexyz/std-client";
import { useComponentValueStream } from "@latticexyz/std-client";

export const ComponentEditor = ({
entity,
Expand All @@ -17,10 +17,7 @@ export const ComponentEditor = ({
layers: Layers;
setContractComponentValue: SetContractComponentFunction<Schema>;
}) => {
const componentUpdate = useStream(component.update$, toUpdate(entity, component));
if (!componentUpdate) return null;

const value = componentUpdate.value[0];
const value = useComponentValueStream(component, entity);
if (!value) return null;

return (
Expand Down
17 changes: 13 additions & 4 deletions packages/ecs-browser/src/EntityEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import {
Layers,
removeComponent,
Expand All @@ -10,13 +10,16 @@ import {
World,
EntityID,
getEntityComponents,
Has,
defineQuery,
} from "@latticexyz/recs";
import { Collapse } from "react-collapse";
import { ComponentBrowserButton, EntityEditorContainer } from "./StyledComponents";
import { SetContractComponentFunction } from "./types";
import { ComponentEditor } from "./ComponentEditor";
import { observer } from "mobx-react-lite";

export const EntityEditor = ({
export const EntityEditor = observer(({
entityId,
layers,
setContractComponentValue,
Expand All @@ -30,9 +33,15 @@ export const EntityEditor = ({
world: World;
}) => {
const [opened, setOpened] = useState(false);
const devHighlightedEntities = defineQuery([Has(devHighlightComponent)], { runOnInit: true }).matching;
const entity = world.entityToIndex.get(entityId);
if (!entity) return null;

const clearPreviousDevHighlights = useCallback(() => {
if(!devHighlightedEntities) return;
[...devHighlightedEntities].forEach((e) => removeComponent(devHighlightComponent, e));
}, [devHighlightedEntities]);

const [entityComponents, setEntityComponents] = useState<AnyComponent[]>([]);
useEffect(() => {
if (opened) {
Expand All @@ -44,7 +53,7 @@ export const EntityEditor = ({
return (
<EntityEditorContainer
onMouseEnter={() => {
[...layers.network.world.entityToIndex.values()].forEach((e) => removeComponent(devHighlightComponent, e));
clearPreviousDevHighlights();
setComponent(devHighlightComponent, entity, {
value: null,
});
Expand Down Expand Up @@ -74,4 +83,4 @@ export const EntityEditor = ({
</Collapse>
</EntityEditorContainer>
);
};
});
44 changes: 44 additions & 0 deletions packages/ecs-browser/src/QueryBuilder/PositionFilterButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, Type } from "@latticexyz/recs";
import { useComponentValueStream } from "@latticexyz/std-client";
import React, { useEffect, useState } from "react";
import { ComponentBrowserButton } from "../StyledComponents";

export const PositionFilterButton: React.FC<{
editQuery: (queryText: string) => void;
queryInputRef: React.RefObject<HTMLInputElement>;
hoverHighlightComponent: Component<{ x: Type.OptionalNumber; y: Type.OptionalNumber }>;
}> = ({ hoverHighlightComponent, editQuery, queryInputRef }) => {
const [selectingPosition, setSelectingPosition] = useState(false);
const hoverPosition = useComponentValueStream(hoverHighlightComponent);

useEffect(() => {
if (!selectingPosition) return;

function onMouseDown() {
if (!hoverPosition) return;

setSelectingPosition(false);
editQuery(`[q.HasValue(c.LocalPosition, { x: ${hoverPosition.x}, y: ${hoverPosition.y} })]`);
queryInputRef.current?.focus();
}

document.addEventListener("mousedown", onMouseDown);
return () => {
document.removeEventListener("mousedown", onMouseDown);
};
}, [selectingPosition, hoverPosition]);

return (
<ComponentBrowserButton
style={{ margin: "8px auto" }}
active={selectingPosition}
onClick={() => {
setSelectingPosition(true);
}}
>
{selectingPosition
? `[q.HasValue(c.LocalPosition, { x: ${hoverPosition?.x}, y: ${hoverPosition?.y} })]`
: "Select Entities at a position"}
</ComponentBrowserButton>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,40 @@ import {
Type,
AnyComponent,
Component,
QueryFragments,
runQuery,
EntityID,
World,
defineQuery,
EntityQueryFragment,
} from "@latticexyz/recs";
import {
ComponentBrowserButton,
ComponentBrowserInput,
QueryBuilderForm,
QueryShortcutContainer,
} from "./StyledComponents";
import { ComponentBrowserButton, ComponentBrowserInput } from "../StyledComponents";
import { QueryBuilderForm, QueryShortcutContainer } from "./StyledComponents";
import * as recs from "@latticexyz/recs";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { dracula } from "react-syntax-highlighter/dist/esm/styles/prism";
import { flatten, orderBy } from "lodash";
import { PositionFilterButton } from "./PositionFilterButton";

export const QueryBuilder = function ({
allEntities,
setFilteredEntities,
layers,
world,
devHighlightComponent,
hoverHighlightComponent,
}: {
world: World;
layers: Layers;
allEntities: EntityID[];
setFilteredEntities: (es: EntityID[]) => void;
devHighlightComponent: Component<{ value: Type.OptionalNumber }>;
hoverHighlightComponent: Component<{ x: Type.OptionalNumber; y: Type.OptionalNumber }>;
}) {
const queryInputRef = useRef<HTMLInputElement>(null);
const [componentFilters, setComponentFilters] = useState<AnyComponent[]>([]);
const [isManuallyEditing, setIsManuallyEditing] = useState(true);
const [entityQueryText, setEntityQueryText] = useState("");
const [errorMessage, setErrorMessage] = useState("");

const allComponents = flatten(Object.values(layers).map((layer) => Object.values(layer.components)));

const resetFilteredEntities = useCallback(() => {
Expand All @@ -55,6 +55,8 @@ export const QueryBuilder = function ({
}
}, [setFilteredEntities, resetFilteredEntities, allEntities, entityQueryText]);

// If the user is not manually typing a query, build a query
// based on the selected Component filters
useEffect(() => {
if (isManuallyEditing) return;

Expand All @@ -63,6 +65,8 @@ export const QueryBuilder = function ({
setEntityQueryText(query);
}, [componentFilters, isManuallyEditing]);

// When the user edits a query manually,
// clear the selectable filters
const editQuery = useCallback((text: string) => {
setIsManuallyEditing(true);
setEntityQueryText(text);
Expand Down Expand Up @@ -93,13 +97,13 @@ export const QueryBuilder = function ({
}, {});

try {
const queryArray = eval(entityQueryText) as QueryFragments;
const queryArray = eval(entityQueryText) as EntityQueryFragment[];
if (!queryArray || queryArray.length === 0 || !Array.isArray(queryArray)) {
resetFilteredEntities();
throw new Error("Invalid query");
}

const selectedEntities = runQuery(queryArray);
const selectedEntities = defineQuery(queryArray, { runOnInit: true }).matching;
setFilteredEntities([...selectedEntities].map((idx) => world.entities[idx]));

selectedEntities.forEach((idx) => removeComponent(devHighlightComponent, idx));
Expand Down Expand Up @@ -154,15 +158,22 @@ export const QueryBuilder = function ({
borderBottom: "2px grey solid",
}}
>
<h3>Query Shortcuts</h3>
<QueryShortcutContainer>
<h2>Query Shortcuts</h2>
<PositionFilterButton
editQuery={editQuery}
hoverHighlightComponent={hoverHighlightComponent}
queryInputRef={queryInputRef}
/>
<h3>Filter by Component</h3>
<QueryShortcutContainer style={{ margin: "8px auto" }}>
{orderBy(allComponents, (c) => c.id)
.filter((c) => !c.id.includes("-"))
.map((component) => {
const filterActive = componentFilters.includes(component);

return (
<ComponentBrowserButton
key={`filter-toggle-${component.id}`}
active={filterActive}
onClick={() => {
setIsManuallyEditing(false);
Expand Down
15 changes: 15 additions & 0 deletions packages/ecs-browser/src/QueryBuilder/StyledComponents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import styled from "styled-components";

export const QueryBuilderForm = styled.form`
padding: 8px;
border-bottom: 2px grey solid;
margin-bottom: 8px;
width: 100%;
`;

export const QueryShortcutContainer = styled.div`
flex: "row wrap";
margin-top: "8px";
height: 200px;
overflow: auto;
`;
1 change: 1 addition & 0 deletions packages/ecs-browser/src/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./QueryBuilder";
14 changes: 0 additions & 14 deletions packages/ecs-browser/src/StyledComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,6 @@ export const BrowserContainer = styled.div`
pointer-events: all;
`;

export const QueryBuilderForm = styled.form`
padding: 8px;
border-bottom: 2px grey solid;
margin-bottom: 8px;
width: 100%;
`;

export const QueryShortcutContainer = styled.div`
flex: "row wrap";
margin-top: "8px";
height: 200px;
overflow: auto;
`;

export const DraggableNumberLabelContainer = styled.label`
cursor: ew-resize;
user-select: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,23 @@ export function registerComponentBrowser() {
rowEnd: 13,
},
(layers) => {
return from([{
layers,
devHighlightComponent: layers.phaser.components.DevHighlight,
world: layers.network.world,
}]);
return from([
{
layers,
devHighlightComponent: layers.phaser.components.DevHighlight,
hoverHighlightComponent: layers.phaser.components.HoverHighlight,
world: layers.network.world,
},
]);
},
({ layers, world, devHighlightComponent }) => {
({ layers, world, devHighlightComponent, hoverHighlightComponent }) => {
return (
<Browser
world={world}
entities={world.entities}
layers={layers}
devHighlightComponent={devHighlightComponent}
hoverHighlightComponent={hoverHighlightComponent}
setContractComponentValue={layers.network.api.setContractComponentValue}
/>
);
Expand Down
14 changes: 13 additions & 1 deletion packages/std-client/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, EntityIndex, Schema, toUpdate } from "@latticexyz/recs";
import { useEffect, useState } from "react";
import { Observable } from "rxjs";
import { filter, Observable } from "rxjs";

export function useStream<T>(stream: Observable<T>, defaultValue?: T) {
const [state, setState] = useState<T | undefined>(defaultValue);
Expand All @@ -11,3 +12,14 @@ export function useStream<T>(stream: Observable<T>, defaultValue?: T) {

return state;
}

export function useComponentValueStream<T extends Schema>(component: Component<T>, entity?: EntityIndex) {
let stream = component.update$.asObservable();
if (entity) {
stream = stream.pipe(filter((c) => c.entity === entity));
}

const update = useStream(stream, entity && toUpdate(entity, component));
if (!update) return null;
return update.value[0];
}

0 comments on commit 8b78ab9

Please sign in to comment.