import { COMMAND_TYPE, EDITOR_EVENT, SCENE_NODE_TYPE, TRANSFORM_MODE } from './constants';
import SceneEventBus from './event-bus';
import History from './history';
import SceneEventBridge from './scene-event-bridge';
import {
  buildObjectSnapshot,
  buildCustomSaveTree,
  buildCustomTree,
  buildTreeSnapshot,
  clampInsertIndex,
  createDefaultElementName,
  createDefaultGroupName,
  disposeObjectResources,
  ensureCustomRoot,
  findSelectableSceneObject,
  getSceneNodeType,
  getObjectByUuid,
  getObjectIndex,
  insertObjectAt,
  isAncestor,
  isCloneableObject,
  isCustomElement,
  isCustomGroup,
  isCustomRoot,
  isDeletableObject,
  isEditableObject,
  isOriginalModelObject,
  isObjectVisible,
  isSelectableObject,
} from './scene-helpers';
import {
  createElementByType,
  normalizeGeometryParams,
  createGroupElement,
} from './element-factory';
import AddElementCommand from './commands/add-element-command';
import AddGroupCommand from './commands/add-group-command';
import RemoveElementCommand from './commands/remove-element-command';
import CloneElementCommand from './commands/clone-element-command';
import MoveElementCommand from './commands/move-element-command';
import SetPositionCommand from './commands/set-position-command';
import SetRotationCommand from './commands/set-rotation-command';
import SetScaleCommand from './commands/set-scale-command';
import SetValueCommand from './commands/set-value-command';
import SetGeometryParamsCommand from './commands/set-geometry-params-command';
import SetOriginalModelStyleCommand from './commands/set-original-model-style-command';
import ResetOriginalModelStyleCommand from './commands/reset-original-model-style-command';
import MultiCommand from './commands/multi-command';

export default class SceneCommandService {
  constructor(options = {}) {
    this.THREE = options.THREE;
    this.getScene = options.getScene;
    this.requestRender = options.requestRender || function () {};
    this.selectedUuid = '';
    this.selectedNodeType = SCENE_NODE_TYPE.UNKNOWN;
    this.transformMode = TRANSFORM_MODE.TRANSLATE;
    this.objectLoader = new this.THREE.ObjectLoader();
    this.eventBus = new SceneEventBus();
    this.history = new History(this, options.historyOptions);
    this.originalModelBaselines = new Map();
    this.originalModelStyleChanges = new Map();
    this.originalModelNodeNames = new Map();
    this.eventBridge = new SceneEventBridge({
      eventBus: this.eventBus,
      getTreeSnapshot: () => this.getCustomTree(),
      getOriginalModelStyleChanges: () => this.getOriginalModelStyleChanges(),
    });
    this.ensureReady();
  }

  ensureReady() {
    const root = ensureCustomRoot(this.getSceneInstance(), this.THREE);
    this.normalizeCustomObjectFlags(root);
    this.normalizeCustomElementGeometryParams(root);
    return root;
  }

  destroy() {
    this.history.clear();
    this.eventBridge.destroy();
    this.selectedUuid = '';
    this.selectedNodeType = SCENE_NODE_TYPE.UNKNOWN;
    this.originalModelBaselines.clear();
    this.originalModelStyleChanges.clear();
    this.originalModelNodeNames.clear();
  }

  getSceneInstance() {
    return typeof this.getScene === 'function' ? this.getScene() : null;
  }

  getCustomRoot() {
    const root = ensureCustomRoot(this.getSceneInstance(), this.THREE);
    this.normalizeCustomObjectFlags(root);
    this.normalizeCustomElementGeometryParams(root);
    return root;
  }

  normalizeCustomObjectFlags(root) {
    const targetRoot = root || ensureCustomRoot(this.getSceneInstance(), this.THREE);
    if (!targetRoot) return;
    targetRoot.traverse(object => {
      if (!object.userData) return;
      if (object.userData.nodeType === 'custom-root') {
        object.userData.rootType = 'custom-root';
        return;
      }
      if (
        (object.userData.nodeType === 'custom-element' ||
          object.userData.nodeType === 'custom-group') &&
        object.userData.rootType === 'custom-root'
      ) {
        delete object.userData.rootType;
      }
    });
  }

  normalizeCustomElementGeometryParams(root) {
    const targetRoot = root || ensureCustomRoot(this.getSceneInstance(), this.THREE);
    if (!targetRoot) return;
    targetRoot.traverse(object => {
      if (!isCustomElement(object)) return;
      const elementType = object.userData ? object.userData.elementType : '';
      if (!elementType) return;
      object.userData.geometryParams = normalizeGeometryParams(
        elementType,
        object.userData.geometryParams || {}
      );
    });
  }

  parseObjectJSON(json) {
    return this.objectLoader.parse(json);
  }

  subscribe(eventName, handler) {
    return this.eventBus.on(eventName, handler);
  }

  emitEvent(eventName, payload) {
    this.eventBridge.emit(eventName, payload);
  }

  getObjectByUuid(uuid) {
    return getObjectByUuid(this.getSceneInstance(), uuid);
  }

  getSceneObjectDepth(object) {
    let depth = 0;
    let current = object;
    while (current && current.parent) {
      depth += 1;
      current = current.parent;
    }
    return depth;
  }

  getOriginalModelResolvePriority(object) {
    if (!object || !isOriginalModelObject(object)) return -1;
    let priority = 0;
    if (object.isInstancedMesh === true) {
      priority += 8;
    }
    if (object.type && object.type !== 'Group') {
      priority += 4;
    }
    if (this.getObjectMaterials(object).length > 0) {
      priority += 2;
    }
    return priority;
  }

  compareOriginalModelResolveTargets(left, right) {
    const leftPriority = this.getOriginalModelResolvePriority(left);
    const rightPriority = this.getOriginalModelResolvePriority(right);
    if (leftPriority !== rightPriority) {
      return leftPriority - rightPriority;
    }
    return this.getSceneObjectDepth(left) - this.getSceneObjectDepth(right);
  }

  findOriginalModelObjectByName(name) {
    const scene = this.getSceneInstance();
    if (!scene || !name) return null;
    const candidates = [];
    scene.traverse(object => {
      if (!isOriginalModelObject(object)) return;
      if ((object.name || '') === name) {
        candidates.push(object);
      }
    });
    if (candidates.length === 0) {
      return null;
    }
    candidates.sort((left, right) => this.compareOriginalModelResolveTargets(right, left));
    return candidates[0] || null;
  }

  resolveOriginalModelObject(locator = {}) {
    if (!locator || typeof locator !== 'object') return null;
    const { uuid = '', name = '' } = locator;
    const objectByUuid = uuid ? this.getObjectByUuid(uuid) : null;
    const objectByName = name ? this.findOriginalModelObjectByName(name) : null;
    if (objectByUuid && isOriginalModelObject(objectByUuid)) {
      if (objectByName && this.compareOriginalModelResolveTargets(objectByName, objectByUuid) > 0) {
        return objectByName;
      }
      return objectByUuid;
    }
    return objectByName;
  }

  getOriginalModelChangeLocatorKey(locator = {}) {
    if (!locator || typeof locator !== 'object') return '';
    const name = typeof locator.name === 'string' ? locator.name : '';
    const uuid = typeof locator.uuid === 'string' ? locator.uuid : '';
    return name || uuid;
  }

  setOriginalModelNodeName(locator = {}) {
    if (!locator || typeof locator !== 'object') return '';
    const nodeName = locator.nodeName !== undefined && locator.nodeName !== null
      ? String(locator.nodeName)
      : '';
    const keys = [locator.uuid, locator.name]
      .filter(key => key !== undefined && key !== null && String(key));
    if (keys.length === 0) return '';
    keys.forEach(key => {
      if (nodeName) {
        this.originalModelNodeNames.set(String(key), nodeName);
      } else {
        this.originalModelNodeNames.delete(String(key));
      }
    });
    if (nodeName) {
      const record = this.findOriginalModelStoredRecord(this.originalModelStyleChanges, locator);
      if (record) {
        record.nodeName = nodeName;
        this.eventBridge.emitOriginalModelChangeListChanged();
      }
    }
    return nodeName;
  }

  getOriginalModelNodeName(locator = {}) {
    if (!locator || typeof locator !== 'object') return '';
    const keys = [locator.uuid, locator.name]
      .filter(key => key !== undefined && key !== null && String(key));
    for (let index = 0; index < keys.length; index += 1) {
      const nodeName = this.originalModelNodeNames.get(String(keys[index]));
      if (nodeName) return nodeName;
    }
    return locator.nodeName || '';
  }

  cloneOriginalModelStyle(style = {}) {
    return this.mergeOriginalModelStyle({}, style || {});
  }

  isSameOriginalModelStyle(left = {}, right = {}) {
    return ['color', 'opacity', 'visible'].every(key => left[key] === right[key]);
  }

  buildOriginalModelStyleRecord(record = {}, options = {}) {
    const styleSource = this.getOriginalModelStyleSource(record);
    if (!record || !styleSource) return null;
    const key = this.getOriginalModelChangeLocatorKey(record);
    if (!key) return null;
    const changedKeys =
      Array.isArray(record.changedKeys) && record.changedKeys.length > 0
        ? record.changedKeys.slice()
        : ['color', 'opacity', 'visible'].filter(styleKey =>
            Object.prototype.hasOwnProperty.call(styleSource, styleKey)
          );
    return {
      uuid: record.uuid || '',
      name: record.name || '',
      nodeName: this.getOriginalModelNodeName(record),
      nodeType: record.nodeType || SCENE_NODE_TYPE.ORIGINAL_MODEL,
      changedKeys,
      before: record.before ? this.cloneOriginalModelStyle(record.before) : null,
      after: this.cloneOriginalModelStyle(styleSource),
      updatedAt: Number.isFinite(options.updatedAt)
        ? Number(options.updatedAt)
        : Number.isFinite(record.updatedAt)
        ? Number(record.updatedAt)
        : Date.now(),
    };
  }

  getOriginalModelStyleSource(record = {}) {
    if (!record || typeof record !== 'object') return null;
    if (record.after && typeof record.after === 'object') {
      return record.after;
    }
    if (record.style && typeof record.style === 'object') {
      return record.style;
    }
    const flatStyle = {};
    ['color', 'opacity', 'visible'].forEach(key => {
      if (Object.prototype.hasOwnProperty.call(record, key)) {
        flatStyle[key] = record[key];
      }
    });
    return Object.keys(flatStyle).length > 0 ? flatStyle : null;
  }

  normalizeCustomSaveNode(node) {
    if (!node || typeof node !== 'object') return node;
    const nextNode = {
      ...node,
      children: Array.isArray(node.children)
        ? node.children.map(child => this.normalizeCustomSaveNode(child))
        : [],
    };
    if (
      !Object.prototype.hasOwnProperty.call(nextNode, 'material') &&
      (Object.prototype.hasOwnProperty.call(nextNode, 'color') ||
        Object.prototype.hasOwnProperty.call(nextNode, 'opacity'))
    ) {
      nextNode.material = {};
      if (Object.prototype.hasOwnProperty.call(nextNode, 'color')) {
        nextNode.material.color = nextNode.color;
      }
      if (Object.prototype.hasOwnProperty.call(nextNode, 'opacity')) {
        nextNode.material.opacity = nextNode.opacity;
      }
    }
    return nextNode;
  }

  normalizeOriginalModelSaveItem(item = {}) {
    if (!item || typeof item !== 'object') return item;
    const nextItem = { ...item };
    if (!nextItem.style || typeof nextItem.style !== 'object') {
      nextItem.style = {};
      ['color', 'opacity', 'visible'].forEach(key => {
        if (Object.prototype.hasOwnProperty.call(item, key)) {
          nextItem.style[key] = item[key];
        }
      });
    }
    return nextItem;
  }

  normalizeSaveSnapshot(snapshot = {}) {
    const payload = snapshot && typeof snapshot === 'object' ? snapshot : {};
    const nextSnapshot = { ...payload };
    if (Object.prototype.hasOwnProperty.call(payload, 'customModelTree')) {
      nextSnapshot.customModelTree = this.normalizeCustomSaveNode(payload.customModelTree);
    }
    if (Object.prototype.hasOwnProperty.call(payload, 'originalModelList')) {
      nextSnapshot.originalModelList = Array.isArray(payload.originalModelList)
        ? payload.originalModelList.map(item => this.normalizeOriginalModelSaveItem(item))
        : [];
    }
    return nextSnapshot;
  }

  findOriginalModelStoredRecord(map, locator = {}) {
    if (!(map instanceof Map)) return null;
    const key = this.getOriginalModelChangeLocatorKey(locator);
    if (key && map.has(key)) {
      return map.get(key);
    }
    const { uuid = '', name = '' } = locator || {};
    let matchedRecord = null;
    map.forEach((record, recordKey) => {
      if (matchedRecord) return;
      if (uuid && (recordKey === uuid || (record && record.uuid === uuid))) {
        matchedRecord = record;
        return;
      }
      if (name && record && record.name === name) {
        matchedRecord = record;
      }
    });
    return matchedRecord;
  }

  deleteOriginalModelStoredRecord(map, locator = {}, excludeKey = '') {
    if (!(map instanceof Map)) return;
    const key = this.getOriginalModelChangeLocatorKey(locator);
    const { uuid = '', name = '' } = locator || {};
    Array.from(map.entries()).forEach(([recordKey, record]) => {
      if (excludeKey && recordKey === excludeKey) return;
      const matchByKey = !!key && recordKey === key;
      const matchByUuid = !!uuid && record && record.uuid === uuid;
      const matchByName = !!name && record && record.name === name;
      if (matchByKey || matchByUuid || matchByName) {
        map.delete(recordKey);
      }
    });
  }

  getOriginalModelBaseline(locator = {}) {
    const baseline = this.findOriginalModelStoredRecord(this.originalModelBaselines, locator);
    return baseline ? JSON.parse(JSON.stringify(baseline)) : null;
  }

  setOriginalModelBaseline(locator = {}, baseline = {}) {
    const key = this.getOriginalModelChangeLocatorKey(locator);
    if (!key) return null;
    const nextBaseline = {
      color:
        Object.prototype.hasOwnProperty.call(baseline, 'color') && baseline.color !== undefined
          ? baseline.color
          : null,
      opacity:
        Object.prototype.hasOwnProperty.call(baseline, 'opacity') && baseline.opacity !== undefined
          ? Number(baseline.opacity)
          : 1,
      visible:
        Object.prototype.hasOwnProperty.call(baseline, 'visible') && baseline.visible !== undefined
          ? !!baseline.visible
          : true,
      name: locator.name || baseline.name || '',
      uuid: locator.uuid || baseline.uuid || '',
      nodeName: this.getOriginalModelNodeName(locator) || baseline.nodeName || '',
    };
    this.deleteOriginalModelStoredRecord(this.originalModelBaselines, locator, key);
    this.originalModelBaselines.set(key, nextBaseline);
    return JSON.parse(JSON.stringify(nextBaseline));
  }

  setOriginalModelStyleRecord(record = {}, options = {}) {
    const nextRecord = this.buildOriginalModelStyleRecord(record, options);
    if (!nextRecord) return null;
    const key = this.getOriginalModelChangeLocatorKey(nextRecord);
    if (nextRecord.nodeName) {
      this.setOriginalModelNodeName(nextRecord);
    }
    this.deleteOriginalModelStoredRecord(this.originalModelStyleChanges, nextRecord, key);
    this.originalModelStyleChanges.set(key, nextRecord);
    return JSON.parse(JSON.stringify(nextRecord));
  }

  getOriginalModelStyleRecords() {
    return Array.from(this.originalModelStyleChanges.values())
      .sort((left, right) => right.updatedAt - left.updatedAt)
      .map(item => JSON.parse(JSON.stringify(item)));
  }

  findSelectableObject(target) {
    return findSelectableSceneObject(target);
  }

  getNodeType(uuid) {
    return getSceneNodeType(this.getObjectByUuid(uuid));
  }

  getSelectedNodeType() {
    return this.selectedNodeType;
  }

  getObjectMaterials(object) {
    if (!object || !object.material) return [];
    return Array.isArray(object.material)
      ? object.material.filter(Boolean)
      : [object.material].filter(Boolean);
  }

  getOriginalModelStyleValue(uuid) {
    const object = this.getObjectByUuid(uuid);
    const materials = this.getObjectMaterials(object);
    const primaryMaterial = materials[0] || null;
    return {
      color:
        primaryMaterial && primaryMaterial.color && primaryMaterial.color.isColor
          ? `#${primaryMaterial.color.getHexString()}`
          : null,
      opacity:
        primaryMaterial && typeof primaryMaterial.opacity === 'number'
          ? primaryMaterial.opacity
          : 1,
      visible: object ? object.visible !== false : true,
    };
  }

  mergeOriginalModelStyle(baseStyle = {}, patchStyle = {}) {
    return {
      color:
        patchStyle.color !== undefined
          ? patchStyle.color
          : Object.prototype.hasOwnProperty.call(baseStyle, 'color')
          ? baseStyle.color
          : null,
      opacity:
        patchStyle.opacity !== undefined
          ? Number(patchStyle.opacity)
          : Object.prototype.hasOwnProperty.call(baseStyle, 'opacity')
          ? Number(baseStyle.opacity)
          : 1,
      visible:
        patchStyle.visible !== undefined
          ? !!patchStyle.visible
          : Object.prototype.hasOwnProperty.call(baseStyle, 'visible')
          ? !!baseStyle.visible
          : true,
    };
  }

  // buildOriginalModelPath(object) {
  //   const path = [];
  //   let current = object;
  //   while (current && current.parent) {
  //     path.unshift(current.name || current.type || current.uuid);
  //     current = current.parent;
  //   }
  //   return path;
  // }

  ensureOriginalModelBaseline(uuid) {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isOriginalModelObject(object)) {
      throw new Error('当前节点不是原始模型节点');
    }
    const locator = {
      uuid,
      name: object.name || object.type || '未命名节点',
    };
    const nodeName = this.getOriginalModelNodeName(locator);
    const baseline = this.getOriginalModelBaseline(locator);
    if (baseline) {
      if (!this.originalModelBaselines.has(this.getOriginalModelChangeLocatorKey(locator))) {
        this.setOriginalModelBaseline(locator, baseline);
      }
      return baseline;
    }
    return this.setOriginalModelBaseline(locator, {
      ...this.getOriginalModelStyleValue(uuid),
      name: locator.name,
      uuid,
      nodeName,
      // path: this.buildOriginalModelPath(object),
    });
  }

  applyOriginalModelStyle(uuid, style = {}) {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isOriginalModelObject(object)) {
      throw new Error('当前节点不是原始模型节点');
    }
    const nextStyle = this.mergeOriginalModelStyle(this.getOriginalModelStyleValue(uuid), style);
    object.visible = nextStyle.visible !== false;
    this.getObjectMaterials(object).forEach(material => {
      if (nextStyle.color && material.color && material.color.isColor) {
        material.color.set(nextStyle.color);
      }
      if (Number.isFinite(nextStyle.opacity)) {
        material.opacity = Math.min(Math.max(Number(nextStyle.opacity), 0), 1);
        material.transparent = material.opacity < 1;
      }
      material.needsUpdate = true;
    });
    object.updateMatrixWorld(true);
    return this.getOriginalModelStyleValue(uuid);
  }

  getOriginalModelChangedKeys(baselineStyle = {}, currentStyle = {}) {
    return ['color', 'opacity', 'visible'].filter(key => baselineStyle[key] !== currentStyle[key]);
  }

  // buildOriginalModelChangeSummary(name, changedKeys = []) {
  //   const labelMap = {
  //     color: '变色',
  //     opacity: '透明',
  //     visible: '隐藏',
  //   };
  //   return `${name || '未命名节点'}【${changedKeys.map(key => labelMap[key] || key).join('、')}】`;
  // }

  syncOriginalModelStyleRecord(uuid, options = {}) {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isOriginalModelObject(object)) {
      return null;
    }
    const baseline = this.ensureOriginalModelBaseline(uuid);
    const currentStyle = this.getOriginalModelStyleValue(uuid);
    const changedKeys = this.getOriginalModelChangedKeys(baseline, currentStyle);
    const locator = {
      uuid,
      name: object.name || object.type || '未命名节点',
    };
    const nodeName = this.getOriginalModelNodeName(locator);
    if (changedKeys.length === 0) {
      this.deleteOriginalModelStoredRecord(this.originalModelStyleChanges, locator);
      return null;
    }
    const updatedAt = Number.isFinite(options.updatedAt) ? Number(options.updatedAt) : Date.now();
    return this.setOriginalModelStyleRecord(
      {
        uuid,
        name: locator.name,
        nodeName,
        // path: baseline.path || this.buildOriginalModelPath(object),
        changedKeys,
        before: {
          color: baseline.color,
          opacity: baseline.opacity,
          visible: baseline.visible,
        },
        after: currentStyle,
        // summary: this.buildOriginalModelChangeSummary(object.name || object.type, changedKeys),
        nodeType: SCENE_NODE_TYPE.ORIGINAL_MODEL,
      },
      {
        updatedAt,
      }
    );
  }

  getOriginalModelStyle(locatorKey) {
    if (!locatorKey) return null;
    const object = this.getObjectByUuid(locatorKey);
    const locator =
      object && isOriginalModelObject(object)
        ? {
            uuid: locatorKey,
            name: object.name || object.type || '未命名节点',
          }
        : {
            uuid: locatorKey,
            name: locatorKey,
          };
    const record = this.findOriginalModelStoredRecord(this.originalModelStyleChanges, locator);
    return record ? JSON.parse(JSON.stringify(record)) : null;
  }

  getOriginalModelChangeRecord(locatorKey) {
    return this.getOriginalModelStyle(locatorKey);
  }

  buildOriginalModelSnapshot(object) {
    if (!object) return null;
    const snapshot = buildObjectSnapshot(object);
    if (!snapshot) return null;
    const nodeName = this.getOriginalModelNodeName({
      uuid: object.uuid,
      name: object.name || object.type || '',
    });
    return {
      ...snapshot,
      nodeName: nodeName || snapshot.nodeName || '',
      nodeNameReady: !!(nodeName || snapshot.nodeName),
      nodeType: SCENE_NODE_TYPE.ORIGINAL_MODEL,
      elementType: '',
      geometryParams: null,
      material: snapshot.material || {
        color: null,
        opacity: 1,
      },
    };
  }

  buildOriginalModelSelectionPayload(uuid) {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isOriginalModelObject(object)) {
      return {
        uuid: '',
        nodeType: SCENE_NODE_TYPE.UNKNOWN,
        data: null,
        changedKeys: [],
      };
    }
    const currentRecord = this.getOriginalModelChangeRecord(uuid);
    const nodeName = this.getOriginalModelNodeName({
      uuid,
      name: object.name || object.type || '',
    });
    return {
      uuid,
      nodeType: SCENE_NODE_TYPE.ORIGINAL_MODEL,
      data: this.buildOriginalModelSnapshot(object),
      nodeName,
      nodeNameReady: !!nodeName,
      changedKeys:
        currentRecord && Array.isArray(currentRecord.changedKeys)
          ? currentRecord.changedKeys.slice()
          : [],
    };
  }

  buildOriginalModelChangeListItem(record) {
    if (!record) return null;
    const recordKey =
      record.uuid || record.name || `original-model-change-${record.updatedAt || 0}`;
    return {
      key: recordKey,
      uuid: record.uuid || record.name || '',
      name: record.name || '',
      nodeName: record.nodeName || record.name || '',
      nodeNameReady: true,
      nodeType: SCENE_NODE_TYPE.ORIGINAL_MODEL,
      changedKeys: Array.isArray(record.changedKeys) ? record.changedKeys.slice() : [],
    };
  }

  getOriginalModelStyleChanges() {
    return this.getOriginalModelStyleRecords()
      .map(item => this.buildOriginalModelChangeListItem(item))
      .filter(Boolean);
  }

  buildSelectionPayload(object) {
    const nodeType = getSceneNodeType(object);
    if (nodeType === SCENE_NODE_TYPE.ORIGINAL_MODEL) {
      return this.buildOriginalModelSelectionPayload(object ? object.uuid : '');
    }
    return {
      uuid: object ? object.uuid : '',
      nodeType,
      data: object ? buildObjectSnapshot(object) : null,
    };
  }

  buildObjectChangedPayload(object, changedKeys = []) {
    if (!object) {
      return {
        uuid: '',
        nodeType: '',
        data: null,
        changedKeys: Array.isArray(changedKeys) ? changedKeys.slice() : [],
      };
    }
    const snapshot = buildObjectSnapshot(object);
    const nodeType =
      snapshot && Object.prototype.hasOwnProperty.call(snapshot, 'nodeType')
        ? snapshot.nodeType || ''
        : '';
    return {
      uuid: object.uuid,
      nodeType,
      data: snapshot,
      changedKeys: Array.isArray(changedKeys) ? changedKeys.slice() : [],
    };
  }

  selectElement(uuid, options = {}) {
    const object = this.getObjectByUuid(uuid);
    if (
      !object ||
      !(isSelectableObject(object) || isOriginalModelObject(object))
      // || !isObjectVisible(object)
    ) {
      this.clearSelection(options);
      return null;
    }
    return this.selectObject(uuid, options);
  }

  selectObject(uuid, options = {}) {
    const object = this.getObjectByUuid(uuid);
    const nodeType = getSceneNodeType(object);
    if (
      object &&
      // isObjectVisible(object) &&
      (isSelectableObject(object) || isOriginalModelObject(object))
    ) {
      this.selectedUuid = uuid;
      this.selectedNodeType = nodeType;
      const selectionPayload = this.buildSelectionPayload(object);
      if (options.emitEvent !== false) {
        this.eventBridge.emit(EDITOR_EVENT.OBJECT_SELECTED, selectionPayload);
        if (nodeType === SCENE_NODE_TYPE.ORIGINAL_MODEL) {
          this.eventBridge.emit(EDITOR_EVENT.ORIGINAL_MODEL_SELECTED, selectionPayload);
        }
      }
      return nodeType === SCENE_NODE_TYPE.ORIGINAL_MODEL ? selectionPayload : object;
    }
    this.clearSelection(options);
    return null;
  }

  clearSelection(options = {}) {
    const previousNodeType = this.selectedNodeType;
    this.selectedUuid = '';
    this.selectedNodeType = SCENE_NODE_TYPE.UNKNOWN;
    if (options.emitEvent !== false) {
      this.eventBridge.emit(EDITOR_EVENT.OBJECT_SELECTED, {
        uuid: '',
        nodeType: SCENE_NODE_TYPE.UNKNOWN,
        data: null,
      });
      if (previousNodeType === SCENE_NODE_TYPE.ORIGINAL_MODEL) {
        this.eventBridge.emit(EDITOR_EVENT.ORIGINAL_MODEL_SELECTED, {
          uuid: '',
          data: null,
          changedKeys: [],
        });
      }
    }
  }

  setTransformMode(mode, options = {}) {
    const nextMode = Object.values(TRANSFORM_MODE).indexOf(mode) !== -1 ? mode : this.transformMode;
    if (!nextMode) return this.transformMode;
    const changed = nextMode !== this.transformMode;
    this.transformMode = nextMode;
    if ((changed || options.forceEmit === true) && options.emitEvent !== false) {
      this.eventBridge.emit(EDITOR_EVENT.TRANSFORM_MODE_CHANGED, {
        mode: this.transformMode,
      });
    }
    return this.transformMode;
  }

  getTransformMode() {
    return this.transformMode;
  }

  getObjectSnapshot(uuid) {
    return buildObjectSnapshot(this.getObjectByUuid(uuid));
  }

  getSelectedObjectSnapshot() {
    if (!this.selectedUuid) return null;
    return this.getObjectSnapshot(this.selectedUuid);
  }

  getSelectedOriginalModelStyleSnapshot() {
    if (!this.selectedUuid || this.selectedNodeType !== SCENE_NODE_TYPE.ORIGINAL_MODEL) {
      return null;
    }
    return this.buildOriginalModelSelectionPayload(this.selectedUuid);
  }

  resolveInsertTarget(selectedUuid = this.selectedUuid) {
    const root = this.getCustomRoot();
    const selected = this.getObjectByUuid(selectedUuid);
    if (!selected || !isSelectableObject(selected)) {
      return {
        parentUuid: root.uuid,
        insertMode: 'append',
      };
    }

    if (isCustomRoot(selected)) {
      return {
        parentUuid: selected.uuid,
        insertMode: 'append',
      };
    }

    if (isCustomGroup(selected)) {
      return {
        parentUuid: selected.uuid,
        insertMode: 'append',
      };
    }

    if (isCustomElement(selected)) {
      return {
        parentUuid: selected.parent ? selected.parent.uuid : root.uuid,
        referenceUuid: selected.uuid,
        insertMode: 'after',
      };
    }

    return {
      parentUuid: root.uuid,
      insertMode: 'append',
    };
  }

  resolveTargetParent(parentUuid) {
    const root = this.getCustomRoot();
    const parent = this.getObjectByUuid(parentUuid);
    if (!parent) return root;
    if (isCustomRoot(parent) || isCustomGroup(parent)) {
      return parent;
    }
    return root;
  }

  resolveInsertIndex(parent, referenceUuid, insertMode = 'append') {
    if (!parent) return 0;
    if (!referenceUuid || insertMode === 'append') {
      return parent.children.length;
    }
    const referenceObject = this.getObjectByUuid(referenceUuid);
    if (!referenceObject || referenceObject.parent !== parent) {
      return parent.children.length;
    }
    const referenceIndex = parent.children.indexOf(referenceObject);
    if (referenceIndex === -1) {
      return parent.children.length;
    }
    if (insertMode === 'before') {
      return referenceIndex;
    }
    if (insertMode === 'after') {
      return referenceIndex + 1;
    }
    return parent.children.length;
  }

  executeCommand(command, options = {}) {
    const result = this.history.execute(command);
    this.requestRender();
    this.emitCommandEvents(result.command, options.action || 'execute', {
      merged: result.merged,
      transformMode: options.transformMode,
    });
    return result.command.getObject ? result.command.getObject() : null;
  }

  emitCommandEvents(command, action, options = {}) {
    if (!command) return;

    if (command.type === COMMAND_TYPE.MULTI) {
      command.commands.forEach(childCommand => {
        this.emitSingleCommandEvents(childCommand, action, {
          ...options,
          skipHistoryChanged: true,
        });
      });
      this.emitHistoryChanged(action, command, options.merged);
      return;
    }

    this.emitSingleCommandEvents(command, action, options);
  }

  emitSingleCommandEvents(command, action, options = {}) {
    const object = command.getObject ? command.getObject() : null;
    const parentUuid = object && object.parent ? object.parent.uuid : command.parentUuid || '';
    if (
      command.type === COMMAND_TYPE.ADD_ELEMENT ||
      command.type === COMMAND_TYPE.ADD_GROUP ||
      command.type === COMMAND_TYPE.CLONE_ELEMENT
    ) {
      if (action === 'execute' || action === 'redo') {
        this.selectedUuid = object ? object.uuid : '';
        this.eventBridge.emit(EDITOR_EVENT.OBJECT_ADDED, {
          uuid: object ? object.uuid : '',
          parentUuid,
          nodeType: object && object.userData ? object.userData.nodeType : '',
          elementType: object && object.userData ? object.userData.elementType : '',
        });
        this.eventBridge.emit(EDITOR_EVENT.OBJECT_SELECTED, {
          uuid: object ? object.uuid : '',
          nodeType: object ? getSceneNodeType(object) : SCENE_NODE_TYPE.UNKNOWN,
          data: object ? this.getObjectSnapshot(object.uuid) : null,
        });
        this.eventBridge.emitSceneGraphChanged();
        this.setTransformMode(options.transformMode || TRANSFORM_MODE.TRANSLATE, {
          forceEmit: true,
        });
      } else {
        if (this.selectedUuid === (object && object.uuid)) {
          this.clearSelection();
        }
        this.eventBridge.emit(EDITOR_EVENT.OBJECT_REMOVED, {
          uuid: object ? object.uuid : '',
          parentUuid: command.parentUuid || '',
          removedTree: buildTreeSnapshot(object),
        });
        this.eventBridge.emitSceneGraphChanged();
      }
      this.emitHistoryChanged(action, command, options.merged);
      return;
    }

    if (command.type === COMMAND_TYPE.REMOVE_ELEMENT) {
      if (action === 'execute' || action === 'redo') {
        if (
          object &&
          this.selectedUuid &&
          (this.selectedUuid === object.uuid ||
            object.getObjectByProperty('uuid', this.selectedUuid))
        ) {
          this.clearSelection();
        }
        this.eventBridge.emit(EDITOR_EVENT.OBJECT_REMOVED, {
          uuid: object ? object.uuid : '',
          parentUuid: command.parentUuid || '',
          removedTree: buildTreeSnapshot(object),
        });
      } else {
        this.eventBridge.emit(EDITOR_EVENT.OBJECT_ADDED, {
          uuid: object ? object.uuid : '',
          parentUuid,
          nodeType: object && object.userData ? object.userData.nodeType : '',
          elementType: object && object.userData ? object.userData.elementType : '',
        });
      }
      this.eventBridge.emitSceneGraphChanged();
      this.emitHistoryChanged(action, command, options.merged);
      return;
    }

    if (command.type === COMMAND_TYPE.MOVE_ELEMENT) {
      this.eventBridge.emit(
        EDITOR_EVENT.OBJECT_CHANGED,
        this.buildObjectChangedPayload(object, ['parentUuid'])
      );
      this.eventBridge.emitSceneGraphChanged();
      this.emitHistoryChanged(action, command, options.merged);
      return;
    }

    if (
      command.type === COMMAND_TYPE.SET_ORIGINAL_MODEL_STYLE ||
      command.type === COMMAND_TYPE.RESET_ORIGINAL_MODEL_STYLE
    ) {
      const currentObject = object || this.getObjectByUuid(command.uuid);
      const originalModelPayload =
        currentObject && currentObject.uuid
          ? this.buildOriginalModelSelectionPayload(currentObject.uuid)
          : {
              uuid: command.uuid || '',
              data: null,
              changedKeys: [],
            };
      const payload = {
        uuid: originalModelPayload.uuid,
        nodeType: SCENE_NODE_TYPE.ORIGINAL_MODEL,
        data: originalModelPayload.data,
        changedKeys: Array.isArray(originalModelPayload.changedKeys)
          ? originalModelPayload.changedKeys.slice()
          : [],
      };
      this.eventBridge.emit(EDITOR_EVENT.OBJECT_CHANGED, payload);
      if (command.type === COMMAND_TYPE.RESET_ORIGINAL_MODEL_STYLE) {
        this.eventBridge.emit(EDITOR_EVENT.ORIGINAL_MODEL_STYLE_RESET, payload);
        // name: currentObject ? currentObject.name || currentObject.type || '未命名节点' : '',
      } else {
        this.eventBridge.emit(EDITOR_EVENT.ORIGINAL_MODEL_STYLE_CHANGED, payload);
      }
      this.eventBridge.emitOriginalModelChangeListChanged();
      if (
        currentObject &&
        this.selectedUuid === currentObject.uuid &&
        this.selectedNodeType === SCENE_NODE_TYPE.ORIGINAL_MODEL
      ) {
        this.eventBridge.emit(
          EDITOR_EVENT.ORIGINAL_MODEL_SELECTED,
          this.buildOriginalModelSelectionPayload(currentObject.uuid)
        );
      }
      this.emitHistoryChanged(action, command, options.merged);
      return;
    }

    this.eventBridge.emit(
      EDITOR_EVENT.OBJECT_CHANGED,
      this.buildObjectChangedPayload(object, command.getChangedKeys())
    );
    if (command.getChangedKeys().indexOf('name') !== -1) {
      this.eventBridge.emitSceneGraphChanged();
    }
    this.emitHistoryChanged(action, command, options.merged);
  }

  emitHistoryChanged(action, command, merged) {
    this.eventBridge.emit(EDITOR_EVENT.HISTORY_CHANGED, {
      action,
      commandType: command.type,
      commandName: command.name,
      merged: !!merged,
      ...this.history.getState(),
    });
  }

  addElement(options = {}) {
    this.ensureReady();
    const target =
      options.parentUuid || options.referenceUuid
        ? {
            parentUuid:
              options.parentUuid ||
              (() => {
                const referenceObject = this.getObjectByUuid(options.referenceUuid);
                return referenceObject && referenceObject.parent ? referenceObject.parent.uuid : '';
              })(),
            referenceUuid: options.referenceUuid,
            insertMode: options.insertMode || 'append',
          }
        : this.resolveInsertTarget(options.selectedUuid);
    const parent = this.resolveTargetParent(target.parentUuid);
    const object = createElementByType(this.THREE, options.type, parent, {
      name: options.name || createDefaultElementName(parent, options.type),
      transform: options.transform,
      geometryParams: options.geometryParams,
    });
    const index = this.resolveInsertIndex(parent, target.referenceUuid, target.insertMode);
    const command = new AddElementCommand(this, object, {
      parentUuid: parent.uuid,
      index,
    });
    return this.executeCommand(command, {
      transformMode: TRANSFORM_MODE.TRANSLATE,
    });
  }

  addGroup(options = {}) {
    this.ensureReady();
    const target =
      options.parentUuid || options.referenceUuid
        ? {
            parentUuid:
              options.parentUuid ||
              (() => {
                const referenceObject = this.getObjectByUuid(options.referenceUuid);
                return referenceObject && referenceObject.parent ? referenceObject.parent.uuid : '';
              })(),
            referenceUuid: options.referenceUuid,
            insertMode: options.insertMode || 'append',
          }
        : this.resolveInsertTarget(options.selectedUuid);
    const parent = this.resolveTargetParent(target.parentUuid);
    const object = createGroupElement(this.THREE, parent, {
      name: options.name || createDefaultGroupName(parent),
      transform: options.transform,
    });
    const index = this.resolveInsertIndex(parent, target.referenceUuid, target.insertMode);
    const command = new AddGroupCommand(this, object, {
      parentUuid: parent.uuid,
      index,
    });
    return this.executeCommand(command, {
      transformMode: TRANSFORM_MODE.TRANSLATE,
    });
  }

  removeElement(uuid) {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isDeletableObject(object)) {
      throw new Error('当前节点不允许删除');
    }
    const command = new RemoveElementCommand(this, uuid);
    this.executeCommand(command);
  }

  cloneElement(uuid, options = {}) {
    const source = this.getObjectByUuid(uuid);
    if (!source || !isCloneableObject(source)) {
      throw new Error('当前节点不允许复制');
    }
    const referenceObject = options.referenceUuid
      ? this.getObjectByUuid(options.referenceUuid)
      : null;
    const targetParent = this.resolveTargetParent(
      options.parentUuid ||
        (referenceObject && referenceObject.parent ? referenceObject.parent.uuid : '') ||
        (source.parent ? source.parent.uuid : '')
    );
    const index =
      typeof options.referenceUuid !== 'undefined' || typeof options.insertMode !== 'undefined'
        ? this.resolveInsertIndex(
            targetParent,
            options.referenceUuid,
            options.insertMode || 'append'
          )
        : clampInsertIndex(targetParent, getObjectIndex(source) + 1);
    const command = new CloneElementCommand(this, uuid, {
      parentUuid: targetParent.uuid,
      index,
      positionOffset: options.positionOffset,
      disableAutoOffset: options.disableAutoOffset,
    });
    return this.executeCommand(command, {
      transformMode: TRANSFORM_MODE.TRANSLATE,
    });
  }

  moveElement(uuid, targetParentUuid, referenceUuid, insertMode = 'append') {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isEditableObject(object)) {
      throw new Error('当前节点不允许移动');
    }
    const targetParent = this.resolveTargetParent(targetParentUuid);
    if (isAncestor(object, targetParent)) {
      throw new Error('不允许把节点移动到自己的后代节点下');
    }
    const index = this.resolveInsertIndex(targetParent, referenceUuid, insertMode);
    const command = new MoveElementCommand(this, uuid, {
      targetParentUuid: targetParent.uuid,
      index,
    });
    return this.executeCommand(command);
  }

  updateElement(options = {}) {
    const object = this.getObjectByUuid(options.uuid);
    if (!object || !isEditableObject(object)) {
      throw new Error('当前节点不允许编辑');
    }
    const commands = [];
    if (options.name !== undefined) {
      commands.push(new SetValueCommand(this, options.uuid, 'name', options.name));
    }
    if (options.visible !== undefined) {
      commands.push(new SetValueCommand(this, options.uuid, 'visible', !!options.visible));
    }
    if (Array.isArray(options.position)) {
      commands.push(new SetPositionCommand(this, options.uuid, options.position));
    }
    if (Array.isArray(options.rotation)) {
      commands.push(new SetRotationCommand(this, options.uuid, options.rotation));
    }
    if (Array.isArray(options.scale)) {
      commands.push(new SetScaleCommand(this, options.uuid, options.scale));
    }
    if (options.material && options.material.color !== undefined) {
      commands.push(
        new SetValueCommand(this, options.uuid, 'material.color', options.material.color)
      );
    }
    if (options.material && options.material.opacity !== undefined) {
      commands.push(
        new SetValueCommand(
          this,
          options.uuid,
          'material.opacity',
          Number(options.material.opacity)
        )
      );
    }
    if (!object.isGroup && options.geometryParams) {
      commands.push(new SetGeometryParamsCommand(this, options.uuid, options.geometryParams));
    }
    if (commands.length === 0) {
      return object;
    }
    const command =
      commands.length === 1
        ? commands[0]
        : new MultiCommand(this, commands, { name: '批量更新元素' });
    return this.executeCommand(command);
  }

  updateOriginalModelStyle(options = {}) {
    const object = this.getObjectByUuid(options.uuid);
    if (!object || !isOriginalModelObject(object)) {
      throw new Error('当前节点不是原始模型节点');
    }
    const nextStyle = {};
    if (options.color !== undefined) {
      nextStyle.color = options.color;
    }
    if (options.opacity !== undefined) {
      nextStyle.opacity = Number(options.opacity);
    }
    if (options.visible !== undefined) {
      nextStyle.visible = !!options.visible;
    }
    if (Object.keys(nextStyle).length === 0) {
      return object;
    }
    const currentStyle = this.getOriginalModelStyleValue(options.uuid);
    const mergedStyle = this.mergeOriginalModelStyle(currentStyle, nextStyle);
    if (this.getOriginalModelChangedKeys(currentStyle, mergedStyle).length === 0) {
      return object;
    }
    return this.executeCommand(new SetOriginalModelStyleCommand(this, options.uuid, nextStyle));
  }

  resetOriginalModelStyle(uuid) {
    const object = this.getObjectByUuid(uuid);
    if (!object || !isOriginalModelObject(object)) {
      throw new Error('当前节点不是原始模型节点');
    }
    const baseline = this.ensureOriginalModelBaseline(uuid);
    const currentStyle = this.getOriginalModelStyleValue(uuid);
    if (this.getOriginalModelChangedKeys(baseline, currentStyle).length === 0) {
      return object;
    }
    return this.executeCommand(new ResetOriginalModelStyleCommand(this, uuid));
  }

  setElementPosition(uuid, position, oldPosition) {
    return this.executeCommand(new SetPositionCommand(this, uuid, position, oldPosition));
  }

  setElementRotation(uuid, rotation, oldRotation) {
    return this.executeCommand(new SetRotationCommand(this, uuid, rotation, oldRotation));
  }

  setElementScale(uuid, scale, oldScale) {
    return this.executeCommand(new SetScaleCommand(this, uuid, scale, oldScale));
  }

  renameElement(uuid, name) {
    return this.executeCommand(new SetValueCommand(this, uuid, 'name', name));
  }

  setElementVisible(uuid, visible) {
    return this.executeCommand(new SetValueCommand(this, uuid, 'visible', !!visible));
  }

  undo() {
    const command = this.history.undo();
    if (!command) return null;
    this.requestRender();
    this.emitCommandEvents(command, 'undo');
    return command.getObject ? command.getObject() : null;
  }

  redo() {
    const command = this.history.redo();
    if (!command) return null;
    this.requestRender();
    this.emitCommandEvents(command, 'redo');
    return command.getObject ? command.getObject() : null;
  }

  getCustomTree() {
    return buildCustomTree(this.getCustomRoot());
  }

  getCustomSaveTree() {
    return buildCustomSaveTree(this.getCustomRoot());
  }

  buildOriginalModelSaveItem(record) {
    const styleRecord = this.buildOriginalModelStyleRecord(record);
    if (!styleRecord) return null;
    return {
      name: styleRecord.name || '',
      nodeName: styleRecord.nodeName || styleRecord.name || '',
      nodeNameReady: true,
      nodeType: SCENE_NODE_TYPE.ORIGINAL_MODEL,
      changedKeys: styleRecord.changedKeys.slice(),
      color: styleRecord.after.color,
      opacity: styleRecord.after.opacity,
      visible: styleRecord.after.visible,
    };
  }

  getOriginalModelSaveChanges() {
    return this.getOriginalModelStyleRecords()
      .map(item => this.buildOriginalModelSaveItem(item))
      .filter(Boolean);
  }

  getSaveSnapshot() {
    return {
      customModelTree: this.getCustomSaveTree(),
      originalModelList: this.getOriginalModelSaveChanges(),
    };
  }

  createCustomObjectFromSaveNode(node, parent) {
    if (!node || !parent) return null;
    const normalizedNode = this.normalizeCustomSaveNode(node);
    const nodeType = normalizedNode.nodeType || '';
    if (nodeType === SCENE_NODE_TYPE.CUSTOM_GROUP) {
      const group = createGroupElement(this.THREE, parent, {
        name: normalizedNode.name || '',
        transform: {
          position: normalizedNode.position,
          rotation: normalizedNode.rotation,
          scale: normalizedNode.scale,
        },
        source: 'load',
      });
      group.visible = normalizedNode.visible !== false;
      return group;
    }
    if (nodeType === SCENE_NODE_TYPE.CUSTOM_ELEMENT) {
      const element = createElementByType(this.THREE, node.elementType, parent, {
        name: normalizedNode.name || '',
        transform: {
          position: normalizedNode.position,
          rotation: normalizedNode.rotation,
          scale: normalizedNode.scale,
        },
        geometryParams: normalizedNode.geometryParams || {},
        material: normalizedNode.material || {},
        source: 'load',
      });
      element.visible = normalizedNode.visible !== false;
      return element;
    }
    return null;
  }

  restoreCustomSaveTree(snapshot) {
    const root = this.getCustomRoot();
    while (root.children.length > 0) {
      const child = root.children[0];
      root.remove(child);
      disposeObjectResources(child);
    }
    const sourceChildren =
      snapshot && Array.isArray(snapshot.children)
        ? snapshot.children
        : Array.isArray(snapshot)
        ? snapshot
        : [];
    const appendChildren = (children, parent) => {
      children.forEach(childNode => {
        const object = this.createCustomObjectFromSaveNode(childNode, parent);
        if (!object) return;
        insertObjectAt(parent, object, parent.children.length);
        if (Array.isArray(childNode.children) && childNode.children.length > 0) {
          appendChildren(childNode.children, object);
        }
      });
    };
    appendChildren(sourceChildren, root);
    this.normalizeCustomObjectFlags(root);
    this.normalizeCustomElementGeometryParams(root);
    return root;
  }

  restoreOriginalModelSaveChanges(changes = []) {
    const nextChanges = Array.isArray(changes)
      ? changes.map(item => this.normalizeOriginalModelSaveItem(item))
      : [];
    this.getOriginalModelStyleRecords().forEach(record => {
      const object = this.resolveOriginalModelObject(record);
      const baseline = this.getOriginalModelBaseline(record);
      if (!baseline || !object || !isOriginalModelObject(object)) {
        return;
      }
      this.applyOriginalModelStyle(object.uuid, baseline);
    });
    this.originalModelStyleChanges.clear();
    nextChanges.forEach(item => {
      this.setOriginalModelStyleRecord(item);
    });
    this.retryPendingOriginalModelSaveChanges();
  }

  retryPendingOriginalModelSaveChanges() {
    const retryChanges = this.getOriginalModelStyleRecords();
    if (retryChanges.length === 0) {
      return [];
    }
    const unresolved = [];
    let shouldRender = false;
    retryChanges.forEach(item => {
      if (!item || !item.after) return;
      const object = this.resolveOriginalModelObject(item);
      if (!object || !isOriginalModelObject(object)) {
        unresolved.push(item);
        return;
      }
      const targetStyle = this.cloneOriginalModelStyle(item.after);
      const currentStyle = this.getOriginalModelStyleValue(object.uuid);
      this.ensureOriginalModelBaseline(object.uuid);
      if (!this.isSameOriginalModelStyle(currentStyle, targetStyle)) {
        this.applyOriginalModelStyle(object.uuid, targetStyle);
        shouldRender = true;
      }
      this.syncOriginalModelStyleRecord(object.uuid, {
        updatedAt: item.updatedAt,
      });
    });
    if (shouldRender) {
      this.requestRender();
    }
    return unresolved.slice();
  }

  applySaveSnapshot(snapshot = {}) {
    const payload = this.normalizeSaveSnapshot(snapshot || {});
    const hasCustomModelTree = Object.prototype.hasOwnProperty.call(payload, 'customModelTree');
    const hasOriginalModelChanges = Object.prototype.hasOwnProperty.call(
      payload,
      'originalModelList'
    );
    if (hasCustomModelTree) {
      this.restoreCustomSaveTree(payload.customModelTree);
    }
    if (hasOriginalModelChanges) {
      this.restoreOriginalModelSaveChanges(payload.originalModelList);
    }
    this.history.clear();
    this.clearSelection();
    this.eventBridge.emitSceneGraphChanged();
    this.eventBridge.emitOriginalModelChangeListChanged();
    this.emitHistoryChanged('reset', {
      type: 'ApplySaveSnapshot',
      name: '回放技改方案',
    });
    this.requestRender();
    return this.getSaveSnapshot();
  }

  serializeCustomElements() {
    return this.getCustomRoot().toJSON();
  }

  deserializeCustomElements(json) {
    if (!json) return null;
    const root = this.getCustomRoot();
    while (root.children.length > 0) {
      const child = root.children[0];
      root.remove(child);
      disposeObjectResources(child);
    }
    const parsedRoot = this.parseObjectJSON(json);
    while (parsedRoot.children.length > 0) {
      const child = parsedRoot.children.shift();
      insertObjectAt(root, child, root.children.length);
    }
    this.normalizeCustomObjectFlags(root);
    this.normalizeCustomElementGeometryParams(root);
    this.history.clear();
    this.clearSelection();
    this.eventBridge.emitSceneGraphChanged();
    this.emitHistoryChanged('reset', {
      type: 'DeserializeCustomElements',
      name: '反序列化自定义元素',
    });
    return root;
  }

  removeAllCustomElements() {
    const root = this.getCustomRoot();
    while (root.children.length > 0) {
      const child = root.children[0];
      root.remove(child);
      disposeObjectResources(child);
    }
    this.history.clear();
    this.clearSelection();
    this.eventBridge.emitSceneGraphChanged();
    this.emitHistoryChanged('reset', {
      type: 'ClearCustomElements',
      name: '清空自定义元素',
    });
  }
}
