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": [