diff --git a/src/project.ts b/src/project.ts index bda1694..e04c682 100644 --- a/src/project.ts +++ b/src/project.ts @@ -1,7 +1,7 @@ import { LiveObject, Room } from "@liveblocks/client"; import { ElementId, FileFormat } from "./fileFormat"; import { Sketch } from "./elements/sketch"; -import { ArcolObjectStore, ObjectChange, ObjectListener, ObjectObserver, StoreName } from "./arcolObjectStore"; +import { ArcolObjectFields, ArcolObjectStore, ChangeOrigin, ObjectChange, ObjectListener, ObjectObserver, StoreName } from "./arcolObjectStore"; import { Extrusion } from "./elements/extrusion"; import { Group } from "./elements/group"; import { Level } from "./elements/level"; @@ -10,9 +10,67 @@ import { generateKeyBetween } from "fractional-indexing"; import { HierarchyObserver } from "./hierarchyMixin"; import { ChangeManager } from "./changeManager"; -export type ElementListener = ObjectListener; export type ElementObserver = ObjectObserver; +/** + * A more strongly typed version of {@link ObjectChange}. + * + * It uses mapped types and index access types to produce a union of all the possible changes for + * a given object type. + * + * e.g. + * TypedObjectChange<{ k1: V1, k2: V2, ... }> = + * | { type: "create" | "delete" } + * | { type: "update", property: "k1", oldValue: V1 } + * | { type: "update", property: "k2", oldValue: V2 } + * ... + */ +type TypedObjectChange = + | { type: "create" | "delete" } + | { [K in keyof T]: { type: "update", property: K, oldValue: T[K] } }[keyof T] + +/** + * A more strongly typed version of {@link ObjectListener} for `Element`. The argument to the + * callback is a single `params` object which repeats the `type` from `obj.type`. This allows + * narrowing both `obj` and `change` based on the `type` of the `Element`. + * + * In more concrete terms, it allows the following to give the desired types: + * ``` + * if (params.type === "someElementType") { + * if (params.change.type === "update") { + * // params.change.property is a union of the possible fields of `someElementType` + * if (params.change.property) { + * // params.change.value is the type of the field [params.change.property] in `someElementType` + * } + * } + * } + * ``` + */ +type ElementListenerParam = + | { type: "sketch", obj: Sketch, change: TypedObjectChange, origin: ChangeOrigin } + | { type: "extrusion", obj: Extrusion, change: TypedObjectChange, origin: ChangeOrigin } + | { type: "group", obj: Group, change: TypedObjectChange, origin: ChangeOrigin } + | { type: "level", obj: Sketch, change: TypedObjectChange, origin: ChangeOrigin } + +type ElementListener = (params: ElementListenerParam) => void + +// This is just here as a test that our TypeScript types are able to perform the desired narrowing. +function foo(l: ElementListener) {} +foo((params) => { + if (params.type === "sketch") { + const obj: Sketch = params.obj; + if (params.change.type === "update") { + const property: "id" | "type" | "parentId" | "parentIndex" | "translate" | "color" = params.change.property; + if (params.change.property === "translate") { + const old: FileFormat.Vec3 = params.change.oldValue + void old; + } + void property; + } + } +}) + + class DeleteEmptyExtrusionObserver implements ElementObserver { private elementsToCheck = new Set();