Crocsx
Crocsx

Reputation: 7640

refactoring the same function with different items

I have the following

  public static carthographicLerp(v1: Cartographic, v2: Cartographic, t: number): Cartographic {
    Math.min(Math.max(t, 0), 1);
    const result = new Cesium.Cartographic();
    result.longitude = v1.longitude + (v2.longitude - v1.longitude) * t;
    result.latitude = v1.latitude + (v2.latitude - v1.latitude) * t;
    result.height = v1.height + (v2.height - v1.height) * t;
    return result;
  }

  public static cartesianLerp(v1: Cartesian3, v2: Cartesian3, t: number): Cartesian3 {
    Math.min(Math.max(t, 0), 1);
    const result = new Cesium.Cartesian3();
    result.x = v1.x + (v2.x - v1.x) * t;
    result.y = v1.y + (v2.y - v1.y) * t;
    result.z = v1.z + (v2.z - v1.z) * t;
    return result;
  }

  public static headingPitchRollLerp(v1: HeadingPitchRoll, v2: HeadingPitchRoll, t: number): HeadingPitchRoll {
    Math.min(Math.max(t, 0), 1);
    const result = new Cesium.HeadingPitchRoll();
    result.heading = v1.heading + (v2.heading - v1.heading) * t;
    result.pitch = v1.pitch + (v2.pitch - v1.pitch) * t;
    result.roll = v1.roll + (v2.roll - v1.roll) * t;
    return result;
  }

the code is exactly the same, but the object are differents.

Is it possible to make a single function from this, that is not to ugly and to call it simply lerp?

Upvotes: 0

Views: 38

Answers (1)

jcalz
jcalz

Reputation: 330346

I don't have your Cesium library anywhere, so I'm going to assume we have this minimum set of definitions:

interface Cartographic {
  longitude: number;
  latitude: number;
  height: number;
}
interface Cartesian3 {
  x: number;
  y: number;
  z: number;
}
interface HeadingPitchRoll {
  heading: number;
  pitch: number;
  roll: number;
}
const Cesium = {
  Cartographic: ((() => ({})) as any) as new () => Cartographic,
  Cartesian3: ((() => ({})) as any) as new () => Cartesian3,
  HeadingPitchRoll: ((() => ({})) as any) as new () => HeadingPitchRoll
};

Here's one way to generalize your *Lerp() functions into a single curried function that takes a constructor and set of keys and returns a function specialized for those types:

type CesiumTypes = Cartographic | Cartesian3 | HeadingPitchRoll;

const genericLerp = <K extends keyof T, T extends Record<K, number>>(
  ctor: new () => T,
  keys: K[]
) => (v1: T, v2: T, t: number): T => {
  Math.min(Math.max(t, 0), 1);
  const result = new ctor();
  for (let k of keys) {
    (result[k] as number) = v1[k] + (v2[k] - v1[k]) * t; // assertion here
  }
  return result;
};

Note that I had to use a type assertion in result[t] as number = .... That's because the assignment is technically not safe. If the type T has any numeric properties at keys K that are narrower than number, like {foo: -1 | 0 | 1}, then it would be a mistake to do result.foo = v1.foo + (v2.foo - v1.foo) * t, since there'd be no guarantee that such a calculation would result in the -1 | 0 | 1 type. I'm going to just assume you're not going to use types like that when you call genericLerp(). If one wanted they could harden genericLerp() to prevent being called with such pathological types, but it's probably not worth it.

Anyway, now you can write your original three functions by calling genericLerp():

const carthographicLerp = genericLerp(Cesium.Cartographic, [
  "latitude",
  "longitude",
  "height"
]);

const cartesianLerp = genericLerp(Cesium.Cartesian3, ["x", "y", "z"]);

const headingPitchRollLerp = genericLerp(Cesium.HeadingPitchRoll, [
  "heading",
  "pitch",
  "roll"
]);

That's probably as far as I'd want to go here. It's harder to make a single function which acts like all of those. At compile time you can make a single overloaded function that matches all the signatures of your three separate functions, but you'd still need to inspect the values at runtime to determine which of those three functions to use (that is, which constructor and which set of keys). There's no getting around implementing tests, which is why I think this is probably not worth it. For completeness, here's how one might do it.

First let's create some type guard functions to show the compiler that we intend to take a value and check which concrete type it belongs to. I'll do it just by checking for the presence of certain keys:

// Type guards to figure out what the value is
function isCartographic(x: CesiumTypes): x is Cartographic {
  return "longitude" in x;
}
function isCartesian3(x: CesiumTypes): x is Cartesian3 {
  return "x" in x;
}
function isHeadingPitchRoll(x: CesiumTypes): x is HeadingPitchRoll {
  return "heading" in x;
}

Now we can make a single overloaded function, which inspects values and dispatches to the other implementations:

const lerp = ((v1: CesiumTypes, v2: CesiumTypes, t: number): CesiumTypes => {
  if (isCartographic(v1) && isCartographic(v2)) {
    return carthographicLerp(v1, v2, t);
  }
  if (isCartesian3(v1) && isCartesian3(v2)) {
    return cartesianLerp(v1, v2, t);
  }
  if (isHeadingPitchRoll(v1) && isHeadingPitchRoll(v2)) {
    return headingPitchRollLerp(v1, v2, t);
  }
  throw new Error("arguments don't match");
}) as typeof carthographicLerp &
  typeof cartesianLerp &
  typeof headingPitchRollLerp;

Okay, hope that helps. Good luck!

Link to code

Upvotes: 1

Related Questions