diff --git a/src/elements/extrusion.ts b/src/elements/extrusion.ts index cef4531..f99ab44 100644 --- a/src/elements/extrusion.ts +++ b/src/elements/extrusion.ts @@ -23,6 +23,10 @@ export class Extrusion extends ArcolObject { return "extrusion" as const; } + public getFields(): FileFormat.Sketch { + return this.fields as FileFormat.Sketch; + } + get height(): FileFormat.Extrusion["height"] { return this.fields.height; } diff --git a/src/elements/group.ts b/src/elements/group.ts index e687923..227a8f3 100644 --- a/src/elements/group.ts +++ b/src/elements/group.ts @@ -22,6 +22,10 @@ export class Group extends ArcolObject { get type() { return "group" as const; } + + public getFields(): FileFormat.Sketch { + return this.fields as FileFormat.Sketch; + } } applyArcolObjectMixins(Group, [HierarchyMixin, HideableMixin]); diff --git a/src/elements/level.ts b/src/elements/level.ts index 05a2995..ac5eaf2 100644 --- a/src/elements/level.ts +++ b/src/elements/level.ts @@ -22,6 +22,10 @@ export class Level extends ArcolObject { get type() { return "level" as const; } + + public getFields(): FileFormat.Sketch { + return this.fields as FileFormat.Sketch; + } } applyArcolObjectMixins(Level, [HierarchyMixin, HideableMixin]); diff --git a/src/elements/sketch.ts b/src/elements/sketch.ts index 86fd2d0..3e93d29 100644 --- a/src/elements/sketch.ts +++ b/src/elements/sketch.ts @@ -23,6 +23,10 @@ export class Sketch extends ArcolObject { return "sketch" as const; } + public getFields(): FileFormat.Sketch { + return this.fields as FileFormat.Sketch; + } + get translate(): FileFormat.Sketch["translate"] { return this.fields.translate; } diff --git a/src/project.ts b/src/project.ts index bda1694..d49ea6d 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,69 @@ 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` + * } + * } + * } + * ``` + * + * The weird `T extends any` syntax is called "Distributive Conditional Types" and allows writing + * generic that maps a union type to a different union of types. + * https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types + */ +type ElementListener = (params: T extends any ? { + type: T["type"], + obj: T, + change: TypedObjectChange>, + origin: ChangeOrigin, +} : never) => 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();