diff --git a/client/src/components/generic/Property.tsx b/client/src/components/generic/Property.tsx
index f75a3d91..b8bf3b25 100644
--- a/client/src/components/generic/Property.tsx
+++ b/client/src/components/generic/Property.tsx
@@ -6,7 +6,6 @@ import { truncate } from "lodash";
import { ReactNode } from "react";
import { Flex } from "./Flex";
import { Space } from "./Space";
-import YAML from "js-yaml";
type Props = {
label?: ReactNode;
@@ -24,7 +23,7 @@ function stringify(obj: any) {
default:
return (
- {truncate(YAML.dump(obj).replace("\n", ", "), {
+ {truncate(JSON.stringify(obj).replace("\n", ", "), {
length: 30,
})}
@@ -34,7 +33,7 @@ function stringify(obj: any) {
export function Property({ label, value, type }: Props) {
return (
-
+
{
+ const ctx: Context = { a: 1 };
const result = parse<{
rect: { x: number; y: string; z: number; w: number };
}>([{ $: "rect2", y: 4 }], {
@@ -10,27 +11,23 @@ describe("parse", () => {
{ $: "rect", x: 2, y: "{{ctx.y + 1}}" },
{ $: "rect", y: "{{1 + 2}} a", z: "{{1 + 2}}", w: "{{ctx.a}}" },
],
- });
- const ctx: Context = { a: 1 };
+ })(ctx);
it("flattens correctly", () => {
expect(result).toMatchObject([{ $: "rect" }, { $: "rect" }]);
});
it("creates a deferred property", () => {
- expect(result[0].x(ctx)).toEqual(2);
+ expect(result[0].x).toEqual(2);
});
it("evaluates a computed property", () => {
- expect(result[1].z(ctx)).toEqual(3);
+ expect(result[1].z).toEqual(3);
});
it("evaluates a template string computed property", () => {
- expect(result[1].y(ctx)).toEqual("3 a");
+ expect(result[1].y).toEqual("3 a");
});
it("reads from context", () => {
- expect(result[1].w(ctx)).toEqual(1);
+ expect(result[1].w).toEqual(1);
});
it("propagates context correctly", () => {
- expect(result[0].y(ctx)).toEqual(5);
- });
- it("propagates context correctly, despite an override in context", () => {
- expect(result[0].y({ ...ctx, y: 12 })).toEqual(5);
+ expect(result[0].y).toEqual(5);
});
-});
\ No newline at end of file
+});
diff --git a/client/src/components/renderer/parser/applyScope.ts b/client/src/components/renderer/parser/applyScope.ts
index e2e94583..0f549a50 100644
--- a/client/src/components/renderer/parser/applyScope.ts
+++ b/client/src/components/renderer/parser/applyScope.ts
@@ -1,18 +1,13 @@
import { Properties as Props } from "protocol";
import { Context, PropMap } from "./Context";
import { mapProperties } from "./mapProperties";
-import { normalizeConstant } from "./normalize";
export function applyScope(
scope: PropMap,
props: PropMap
): PropMap {
return Object.setPrototypeOf(
- mapProperties(
- props,
- (prop) => (provided: Context) =>
- prop(applyScope(normalizeConstant(provided), scope))
- ),
+ mapProperties(props, (prop) => (provided: Context) => prop(scope)),
scope
);
}
diff --git a/client/src/components/renderer/parser/parse.ts b/client/src/components/renderer/parser/parse.ts
index a5637a5c..8376a6f3 100644
--- a/client/src/components/renderer/parser/parse.ts
+++ b/client/src/components/renderer/parser/parse.ts
@@ -1,28 +1,42 @@
-import { Dictionary as Dict, range } from "lodash";
+import { Dictionary as Dict, flatMap, map, mapValues, range } from "lodash";
import {
+ CompiledComponent,
ComponentDefinition,
ComponentDefinitionMap,
IntrinsicComponentMap,
- ParsedComponent,
- ParsedComponentDefinition,
TraceComponent,
} from "protocol";
-import { Context, Key } from "./Context";
+import { Context } from "./Context";
import { applyScope } from "./applyScope";
-import { normalize, normalizeConstant } from "./normalize";
+import { mapProperties } from "./mapProperties";
+import { normalize } from "./normalize";
+import { parseProperty } from "./parseProperty";
-function parseFor(component: TraceComponent>) {
+function transformOne(component: TraceComponent>) {
const { $for, ...rest } = component;
if ($for) {
const { $let = "i", $from = 0, $to = 1, $step = 1 } = $for;
- return range($from, $to, $step).map((i) =>
- applyScope(normalizeConstant({ [$let]: i }), normalize(rest as any))
- );
+ const from = parseProperty($from);
+ const to = parseProperty($to);
+ const step = parseProperty($step);
+ return (context: Context = {}) => {
+ return range(from(context), to(context), step(context)).map((i) => {
+ const scoped = applyScope(context, normalize({ [$let]: i }));
+ return applyScope(scoped, normalize(rest as any));
+ });
+ };
} else {
- return [component];
+ return (context: Context = {}) => {
+ const scoped = applyScope(context, normalize(rest as any));
+ return [scoped];
+ };
}
}
+type Compiled = (
+ context: Context
+) => Compiled[] | CompiledComponent>[];
+
/**
* A parser for a list of Components
* @param definition a list of Components
@@ -31,20 +45,28 @@ function parseFor(component: TraceComponent>) {
*/
export function parse(
definition: ComponentDefinition>,
- components: ComponentDefinitionMap,
- context: Context]> = {}
-): ParsedComponentDefinition {
- return definition.flatMap((c) => {
- const { $ } = c;
- const c2 = parseFor(c);
- return c2.flatMap((component) => {
- const scoped = applyScope(
- normalizeConstant(context),
- normalize(component)
- );
- return $ in components
- ? parse(components[$], components, scoped)
- : [scoped as ParsedComponent, T[Key]>];
- });
- });
+ components: ComponentDefinitionMap
+): (
+ context: Context
+) => CompiledComponent, Record>[] {
+ const parseOne = (element: TraceComponent>) => {
+ const { $ } = element;
+ const scoped = transformOne(element);
+ return $ in components
+ ? (context: Context) =>
+ flatMap(scoped(context), (elem) => flatMap(store[$], (f) => f(elem)))
+ : (context: Context) =>
+ map(scoped(context), (elem) =>
+ Object.setPrototypeOf(
+ mapProperties(elem, (prop) => prop(elem)),
+ null
+ )
+ );
+ };
+ const store: Dict>[]> = mapValues(
+ components,
+ (elements) => map(elements, parseOne)
+ );
+ const entry = flatMap(definition, parseOne);
+ return (context) => flatMap(entry, (e) => e(context));
}
diff --git a/client/src/components/renderer/parser/parseToken.ts b/client/src/components/renderer/parser/parseToken.ts
index c183f062..ea12176b 100644
--- a/client/src/components/renderer/parser/parseToken.ts
+++ b/client/src/components/renderer/parser/parseToken.ts
@@ -1,18 +1,16 @@
-import memo from "memoizee";
import { Prop } from "./Context";
-import { normalizeConstant } from "./normalize";
+import { normalize } from "./normalize";
-export const parseToken = memo(
- (token: string): Prop => {
- const f = Function("$", `return ${token};`);
- return (ctx) =>
- f(
- new Proxy(normalizeConstant(ctx), {
- get(target, prop: string) {
- return target[prop]?.({});
- },
- })
- );
- },
- { primitive: true }
-);
+export const parseToken = (token: string): Prop => {
+ const f = Function("$", `return ${token};`);
+ return (ctx) =>
+ f(
+ new Proxy(normalize(ctx), {
+ get(target, prop: string) {
+ return typeof target?.[prop] === "function"
+ ? target[prop]({})
+ : target?.[prop];
+ },
+ })
+ );
+};
diff --git a/client/src/components/renderer/parser/parseTrace.worker.ts b/client/src/components/renderer/parser/parseTrace.worker.ts
index dfb763b1..9d475ce4 100644
--- a/client/src/components/renderer/parser/parseTrace.worker.ts
+++ b/client/src/components/renderer/parser/parseTrace.worker.ts
@@ -1,11 +1,16 @@
-import { chunk, flatMap, map } from "lodash";
-import { usingWorkerTask } from "workers/usingWorker";
+import { chunk, flatMap, map, range } from "lodash";
+import { usingWorkerTask } from "../../../workers/usingWorker";
import {
ParseTraceWorkerParameters,
ParseTraceWorkerReturnType,
} from "./parseTraceSlave.worker";
import parseTraceWorkerUrl from "./parseTraceSlave.worker.ts?worker&url";
+const { min } = Math;
+
+const SLAVE_COUNT = navigator.hardwareConcurrency ?? 8;
+const CHUNK_SIZE = 16;
+
export class ParseTraceWorker extends Worker {
constructor() {
super(parseTraceWorkerUrl, { type: "module" });
@@ -22,17 +27,25 @@ async function parse({
context,
view = "main",
}: ParseTraceWorkerParameters): Promise {
- const chunks = chunk(trace?.events, (trace?.events?.length ?? 0) / 8);
-
- const outs = await Promise.all(
- map(chunks, (chunk1) =>
- parseTraceWorker({
- trace: { ...trace, events: chunk1 },
- context,
- view,
- })
- )
- );
+ const chunks = range(0, trace?.events?.length, CHUNK_SIZE);
+ const tasks = chunk(chunks, SLAVE_COUNT);
+ const outs = [];
+
+ for (const task of tasks) {
+ outs.push(
+ ...(await Promise.all(
+ map(task, (i) =>
+ parseTraceWorker({
+ trace,
+ context,
+ view,
+ from: i,
+ to: min(i + CHUNK_SIZE, trace?.events?.length ?? 0),
+ })
+ )
+ ))
+ );
+ }
return {
stepsPersistent: flatMap(outs, "stepsPersistent"),
diff --git a/client/src/components/renderer/parser/parseTraceSlave.worker.ts b/client/src/components/renderer/parser/parseTraceSlave.worker.ts
index e5c28e83..f3ab5522 100644
--- a/client/src/components/renderer/parser/parseTraceSlave.worker.ts
+++ b/client/src/components/renderer/parser/parseTraceSlave.worker.ts
@@ -1,14 +1,7 @@
-import { chain, findLast, map, mapValues, negate } from "lodash";
-import {
- CompiledComponent,
- EventContext,
- ParsedComponent,
- Trace,
- TraceEvent,
-} from "protocol";
+import { chain, findLast, map, mapValues, negate, range } from "lodash";
+import { CompiledComponent, EventContext, Trace } from "protocol";
import { ComponentEntry } from "renderer";
-import { mapProperties } from "./mapProperties";
-import { normalizeConstant } from "./normalize";
+import { normalize, normalizeConstant } from "./normalize";
import { parse as parseComponents } from "./parse";
const isNullish = (x: KeyRef): x is Exclude =>
@@ -25,56 +18,47 @@ function parse({
trace,
context,
view = "main",
+ from = 0,
+ to = trace?.events?.length ?? 0,
}: ParseTraceWorkerParameters): ParseTraceWorkerReturnType {
const parsed = parseComponents(
trace?.render?.views?.[view]?.components ?? [],
trace?.render?.components ?? {}
);
- const apply = (
- event: TraceEvent,
- ctx?: EventContext
- ): CompiledComponent>[] =>
- parsed.map((p) =>
- mapProperties<
- ParsedComponent,
- CompiledComponent>
- >(p, (c) =>
- c(
- normalizeConstant({
- alpha: 1,
- ...context,
- ...ctx,
- event,
- events: trace?.events,
- })
- )
- )
- );
-
const isVisible = (c: CompiledComponent) =>
c && Object.hasOwn(c, "alpha") ? c!.alpha! > 0 : true;
const makeEntryIteratee =
(step: number) =>
- (component: CompiledComponent>) => ({
- component,
- meta: { source: "trace", step: step },
- });
+ (component: CompiledComponent>) => {
+ return {
+ component,
+ meta: { source: "trace", step, info: component.$info },
+ };
+ };
const r = chain(trace?.events)
.map((c, i) => ({ step: i, id: c.id, data: c, pId: c.pId }))
.groupBy("id")
.value();
- const steps = chain(trace?.events)
- .map((e, i, esx) => {
- const component = apply(e, {
- step: i,
- parent: !isNullish(e.pId)
- ? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0]
- : undefined,
- });
+ const steps = chain(range(from, to))
+ .map((i) => {
+ const e = trace!.events![i]!;
+ const esx = trace!.events!;
+ const component = parsed(
+ normalizeConstant({
+ alpha: 1,
+ ...context,
+ step: i,
+ parent: !isNullish(e.pId)
+ ? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0]
+ : undefined,
+ event: e,
+ events: esx,
+ })
+ );
const persistent = component.filter(isPersistent);
const transient = component.filter(negate(isPersistent));
return { persistent, transient };
@@ -92,6 +76,8 @@ export type ParseTraceWorkerParameters = {
trace?: Trace;
context: EventContext;
view?: string;
+ from?: number;
+ to?: number;
};
export type ParseTraceWorkerReturnType = {
diff --git a/client/src/layers/query/index.tsx b/client/src/layers/query/index.tsx
index 70460bf6..9149735d 100644
--- a/client/src/layers/query/index.tsx
+++ b/client/src/layers/query/index.tsx
@@ -7,7 +7,13 @@ import {
import { Box, Typography as Type } from "@mui/material";
import { FeaturePicker } from "components/app-bar/FeaturePicker";
import { useSnackbar } from "components/generic/Snackbar";
+import { Heading, Option } from "components/layer-editor/Option";
+import { TracePreview } from "components/layer-editor/TracePreview";
import { getParser } from "components/renderer";
+import { useEffectWhenAsync } from "hooks/useEffectWhen";
+import { LayerController, inferLayerName } from "layers";
+import { MapLayer, MapLayerData } from "layers/map";
+import { TraceLayerData, controller as traceController } from "layers/trace";
import { filter, find, map, merge, reduce, set } from "lodash";
import { nanoid as id } from "nanoid";
import { produce, withProduce } from "produce";
@@ -15,14 +21,6 @@ import { useMemo } from "react";
import { Connection, useConnections } from "slices/connections";
import { useFeatures } from "slices/features";
import { Layer, useLayer, useLayers } from "slices/layers";
-import { useEffectWhenAsync } from "hooks/useEffectWhen";
-import { LayerController, inferLayerName } from "layers";
-import { Heading, Option } from "components/layer-editor/Option";
-import { TracePreview } from "components/layer-editor/TracePreview";
-import { MapLayer, MapLayerData } from "layers/map";
-import { TraceLayerData, controller as traceController } from "layers/trace";
-
-const TraceLayerSelectionInfoProvider = traceController.getSelectionInfo;
async function findConnection(
connections: Connection[],
@@ -184,6 +182,7 @@ export const controller = {
}),
inferName: (l) => l.source?.trace?.name ?? "Untitled Query",
getSelectionInfo: ({ children, event, layer: key }) => {
+ const TraceLayerSelectionInfoProvider = traceController.getSelectionInfo;
const { layer, setLayer, layers } = useLayer(key);
const mapLayerData = useMemo(() => {
const filteredLayers = filter(layers, {
diff --git a/client/src/layers/trace/index.tsx b/client/src/layers/trace/index.tsx
index 19154837..6829e123 100644
--- a/client/src/layers/trace/index.tsx
+++ b/client/src/layers/trace/index.tsx
@@ -1,5 +1,5 @@
import { ArrowOutwardRounded } from "@mui/icons-material";
-import { Box, useTheme } from "@mui/material";
+import { Box, Typography, useTheme } from "@mui/material";
import { FeaturePicker } from "components/app-bar/FeaturePicker";
import { TracePicker } from "components/app-bar/Input";
import { PropertyList } from "components/inspector/PropertyList";
@@ -7,7 +7,6 @@ import { LazyNodeList, NodeList } from "components/renderer/NodeList";
import { colorsHex, getColorHex } from "components/renderer/colors";
import { parseString } from "components/renderer/parser/parseString";
import { useTraceParser } from "components/renderer/parser/parseTrace";
-import { ParseTraceWorkerReturnType } from "components/renderer/parser/parseTrace.worker";
import { useEffectWhen } from "hooks/useEffectWhen";
import {
chain,
@@ -16,6 +15,7 @@ import {
forEach,
head,
isUndefined,
+ keyBy,
last,
map,
merge,
@@ -37,6 +37,7 @@ import {
PlaybackLayerData,
PlaybackService,
} from "components/app-bar/Playback";
+import { ParseTraceWorkerReturnType } from "components/renderer/parser/parseTraceSlave.worker";
const isNullish = (x: KeyRef): x is Exclude =>
x === undefined || x === null;
@@ -217,18 +218,35 @@ export const controller = {
.filter(negate(isUndefined))
.sort((a, b) => a - b)
.value() as number[];
+ const info = chain(event?.info?.components)
+ .filter((c) => c.meta?.sourceLayer === layer?.key)
+ .map((c) => c.meta)
+ .value() as any[];
if (steps.length && layer) {
const step = last(steps)!;
const event = events[step];
if (event) {
return {
+ ...keyBy(
+ map(info, (x) => ({
+ primary: `Selection in ${inferLayerName(layer)}`,
+ items: {
+ info: {
+ index: -1,
+ primary: ,
+ },
+ },
+ })),
+ "primary"
+ ),
[layer.key]: {
primary: inferLayerName(layer),
items: {
properties: {
- index: -1,
+ index: -2,
primary: ,
},
+
[`${event}`]: {
primary: `Go to Step ${step}`,
secondary: `${startCase(event.type)}`,
diff --git a/client/src/public/manifest.json b/client/src/public/manifest.json
index 3c262b81..728c06b0 100644
--- a/client/src/public/manifest.json
+++ b/client/src/public/manifest.json
@@ -1,9 +1,9 @@
{
"short_name": "Visualiser",
"name": "Visualiser",
- "version": "1.0.5",
+ "version": "1.1.0",
"description": "Visualise pathfinding search and more",
- "version_name": "1.0.5; mid October 2023",
+ "version_name": "1.1.0; mid November 2023",
"repository": "https://github.com/path-visualiser/app",
"docs": "https://github.com/path-visualiser/app/blob/master/docs",
"icons": [