Tumax-复合状态管理(显隐问题)

Tumax-复合状态管理(显隐问题)

故事点介绍

在 Tumax 项目中,作为编辑器存在非常多的“视图交互”,其中关于显隐就是一块状态复杂、由多重因素影响的交互重灾区。

显隐逻辑简单Case

技术难点

随着项目的不断迭代,业务需求的不断增多,隐藏的交互事件也逐渐增多(墙体隐藏,吊顶隐藏,地面模型隐藏,硬装下隐藏,选择房间后隐藏,挂墙、门窗跟随隐藏,不同路由显隐不同),这么多显隐逻辑混在一起,逐渐发现我们以前原有的显隐逻辑变得异常混乱。

举个简单的例子:

在户型导航的逻辑中,要求对选中的房间以外的所有模型数据都隐藏,当我们执行这样的操作后,表现正常

在墙体隐藏的逻辑中,要求遮挡观察实现的墙体全部隐藏,当不遮挡视线后再恢复显示

  1. 选择单房间,转动相机,被户型导航隐藏的墙体显示了出来。
  2. 选择单房间,转动相机,返回完整户型,被摄像机隐藏的墙体显示了出来。
  3. 选择单房间,上下移动相机,其他房间的地面模型被显示了出来。
  4. 选择单房间,切换到顶面路由,其他房间的顶面模型被显示了出来。
  5. 在 2D、3D 同时存在的页面下,转动相机,2D 下的模型显隐跟随 3D 摄像机转动变化。

解决思路

在上述例子中,可以感受到如果单凭数据模型中的一个visible状态,来处理模型的显隐逻辑是远远不够的。

那么究竟应该如何解决这么多交互混合应用之后的显隐逻辑呢?

吉德林法则:把难题清清楚楚地写出来,便已经解决了一半。只有先认清问题,才能很好地解决问题

首先,遇到问题需要搞清楚问题到底是什么?

上面列举的问题例子都是属于个别 Bug,如果针对于这些 Bug 做对应的处理修复,就会陷入无休止的 Bug 循环,改了户型导航影响墙体隐藏,改了墙体隐藏影响路由隐藏,真正变成“捉襟见肘”。

所以,为了解决根本问题,需要分析得出最根本的印象因素。通过验证每个 case,我列出以下几个会影响任意模型显隐的影响因素。也就是说,在项目中的任意一个物体的显隐,都是以下几个因素共同作用下的结果。


影响因素:

通用生效(C):

  1. 总开关
  2. 选中的户型导航

仅 3D 生效(S3):

  1. 3D 开关
  2. 摄像机

仅 2D 生效(S2)

  1. 2D 开关

组合 Case

下面会列举出项目中所有的显隐 Case,以下所说的所有家具,包含组合家具。

公式解释:

S2.1:boolean ==> 2D影响因素1(2D 开关),值可能为true or false

C1.1:false ==> 通用影响因素1(总开关),值为false,也就是无论如何都不显示

S2.1:? && C.2:? ==> 显隐受 2D开关(S2.1)及户型导航(C.2)共同作用,只有两个值都为true时才显示

2D 设计路由

  • 地面家具:S2.1:? && C.2:? ==> 在 2D 路由下,地面家具显隐需要地面家具开关户型导航都为 true
  • 吊顶家具:S2.1:? && C.2:?
  • 天花家具:S2.1:false

3D 设计路由

  • 地面家具:C.2:? && S3.2:?
  • 地面家具:C.2:? && S3.2:?
  • 墙体显隐:C.2:?
  • 墙体透明:S3.1:? && S3.2:? ==> 3D 墙体分为透明及隐藏,透明有 3D 开关及摄像机决定

硬装设计路由

硬装路由下,存在 2D 场景与 3D 场景同时存在的情况,且显隐规则不同。所以需要分别列出他们的显隐规则。

6e17e9e7-7dc9-3da7-f661-91cc6407d998.png

3D 场景通用规则

  • 地面家具:C.2:? && S3.1:? && S3.2:? ==> 3D 地面家具显示需要 家具开关、户型导航、摄像机 都为true
  • 吊顶家具:C.2:? && S3.1:? && S3.2:?
  • 墙体显隐:C.2:?
  • 墙体透明:S3.1:? && S3.2:?

2D 场景

顶面(硬装)路由:

  • 地面家具:S2.1:false ==> 顶面路由下,不显示 2D 家具
  • 吊顶家具:S2.1:? && C.2:?

地面(硬装)路由:

  • 地面家具:S2.1:? && C.2:?
  • 吊顶家具:S2.1:false ==> 地面路由下,不显示顶面路由

墙面(硬装)路由:

  • 地面家具:S2.1:fasle ==> 墙面路由下,不显示任何家具
  • 吊顶家具:S2.1:false

渲染路由

3D 场景

  • 地面家具:C.2:? && S3.1:? && S3.2:? ==> 3D 地面家具显示需要 家具开关、户型导航、摄像机 都为true
  • 吊顶家具:C.2:? && S3.1:? && S3.2:?
  • 墙体显隐:C.2:?
  • 墙体透明:S3.1:? && S3.2:?

2D 场景

地面模式:

  • 地面家具:C.2:?
  • 吊顶家具:S2.1:false

顶面模式:

  • 地面家具:S2.1:fasle
  • 吊顶家具:C.2:?

复合状态

理清楚了上述所有的显隐规则后,所要解决的就已经变得清晰明了 了。原来,场景中的每个物体的显隐visible都取决于这几种影响因素的共同作用。

那么就需要有一套显隐规则来作为visible的计算依赖,代码实现:

interface IOptions {
  camera?: boolean;
  navigation?: boolean;
  switch?: boolean;
  switch2D?: boolean;
  switch3D?: boolean;
}

const DEFAULT_OPTIONS: IOptions = {
  camera: true,
  navigation: true,
  switch: true,
  switch2D: true,
  switch3D: true
};

export default class VisibleRuler {
  public camera: boolean; // 相机位置
  public navigation: boolean; // 户型导航
  public switch: boolean; // 总开关控制

  public switch2D: boolean; // 2D开关
  public switch3D: boolean; // 3D开关

  constructor(options?: IOptions) {
    this.init(options);
  }

  public get visible2D(): boolean {
    return this._navigation && this._switch && this._switch2D;
  }

  public get visible3D(): boolean {
    return this._navigation && this._switch && this._camera && this._switch3D;
  }

  public init(options: IOptions = {}) {
    options = {
      ...options,
      ...DEFAULT_OPTIONS
    };
    Object.keys(options).forEach(key => {
      if (Object.hasOwnProperty.call(this, key)) {
        this[key] = options[key];
      }
    });
  }

  public copy(visibleRuler: VisibleRuler) {
    Object.keys(DEFAULT_OPTIONS).forEach(key => {
      if (Object.hasOwnProperty.call(this, key)) {
        this[key] = visibleRuler[key];
      }
    });
  }
}

如果原来在数据模型中的visible,如下:

class Model {
  position: Vector3;
  scale: Vector3;
  rotate: Euler;
  matrix: Matrix4;
  visible: boolean = true;
  ...
}

就需要变为:

class Model {
  position: Vector3;
  scale: Vector3;
  rotate: Euler;
  matrix: Matrix4;
  visibleRuler: VisibleRuler = new VisibleRuelr();

  get visible3D(): boolean {
    return this.visibleRuler.visible3D;
  }

  get visible2D(): boolean {
    return this.visibleRuler.visible2D;
  }
}

接着再加入Mobx,实现数据响应:

import { action, computed, observable } from "mobx";

export default class VisibleRuler {
  @observable
  public camera: boolean; // 相机位置
  @observable
  public navigation: boolean; // 户型导航
  @observable
  public switch: boolean; // 总开关控制

  @observable
  public switch2D: boolean; // 2D开关
  @observable
  public switch3D: boolean; // 3D开关

  constructor(options?: IOptions) {
    this.init(options);
  }

  @computed
  public get visible2D(): boolean {
    return this._navigation && this._switch && this._switch2D;
  }

  @computed
  public get visible3D(): boolean {
    return this._navigation && this._switch && this._camera && this._switch3D;
  }

  @action
  public init(options: IOptions = {}) {
    options = {
      ...options,
      ...DEFAULT_OPTIONS
    };
    Object.keys(options).forEach(key => {
      if (Object.hasOwnProperty.call(this, key)) {
        this[key] = options[key];
      }
    });
  }
}

在改造完成后,那么,如果现在需要改变一个模型的显隐需要如何去做呢?

如摄像机高度高于当前楼层高度,则修改所有吊顶类模型的visibleRuler.camera,紧接着 Mobx 中的computed将会被触发重新计算,如果计算结果与上次的显隐状态不同,Mobx 将会触发所有订阅的更新,用于同步到对应的 2D、3D 需要显示显隐状态。

import homeModel from "../store";

const toggleCeilingModel = (b: boolean) => {
  const ceilingModels: Model[] = homeModel.curLevel.getCeilingModels;
  ceilingModels.forEach(model => model.visibleRuler.setCamera(b));
};

结语

以上内容均为实际项目中所遇到的问题,解决思路过程及时间占比大概分为:

  • 梳理问题,帮助产品理清显隐需求点(40%)
  • 编写代码(10%)
  • 测试、修改 影响点(50%)

这个问题在项目中反复出现、反复修改,每次“捉襟见肘”的修改都可能会出现下一次的 Bug,甚至在项目代码中出现以下注释:

636454df-1b0a-ea0b-0bfd-aea5f5f74acf.png

从解决问题的时间占比可以看出,在解决一些问题的过程中,编写代码的部分反而是占比最少的。

大量的时间是花费在与产品、测试沟通,编写完成后对项目的影响点测试、修改上面。

有人会问,为什么不在一开始设计的时候就做好呢?这样后面不就没这个问题了?

我认为设计是 Line,而非 Point。设计是一个持续的过程,有新的需求过来,在当前的设计下无法满足需求,就需要思考进一步的设计,在不断的理解中寻求好的设计与解决方案。

只有这样作为工程师才有差异化的竞争力与价值点,只有这样的工程师才能拥有自己的技术人力模型。

对于项目中的技术债务、项目中的污点,想要称为一名高级/资深工程师应该具有拥抱变化、解决根本问题、思考更优设计的能力和动力。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×