界面基本完成,下一步就是 工具的综合了。 常用建模工具。

 
--- rollout
global gt_Scatter_tool
/*
 ----这里我判断不用了
global localHit=[0,0,0], localNormal=[0,0,0], worldHit=[0,0,0], worldNormal=[0,0,0] ----原来这里的东西早就不用了
global helperOBJ = undefined
*/
global ScatOBJ = undefined  ---着一点值得我学习。是什么呢, 全局还要给初始值。 在我之前开发的脚本系类中最后的bug 都是因为这了、
global timeS1=timestamp(),  timeS2=timestamp() --主板行走时间
global SCT_TargetOBJ =#() , TobjST

  
struct TargetOBJ  (obj, randomValue=1, rotateV=0, sizeV=100, rotateX=0, rotateY=0, rotateZ=0, MoveV=0, OriginalScale=[1,1,1])
TobjST = TargetOBJ() ---定义结构 这里是为了随时的存入值,可以随时的取出值。


fn OBJsizeFN  obj = (   --如果输入的物体没有被删就求出最大减最小/2 之间的距离。
 if (isdeleted obj) == true do return 0
 ma= obj.max
 mi= obj.min
 
 return ((distance ma mi)/2.) -----这个函数在下面的应用主要说明笔刷的大小。
)

fn helperFN =
(
-- map=mapScreenToCP mouse.pos (getViewSize())
-- point pos:map
                           ---因为这里调用对话况的控件 古全局定义这个数。
 if ScatOBJ == undefined or gt_Scatter_tool.start_btn.state == OFF  do return false ----但我第一次拾取物体时。ScatOBJ 就定义了。
  ---在这个函数中以上两个条件有一个对就返回。 这里开始进行肯定是返回的
 mouseray = mapScreenToWorldRay mouse.pos --这里应该是鼠标屏幕对应的射线
 sectray = intersectRAY ScatOBJ mouseray   -------------------------------------鼠标工具与拾取物体的焦点。
 
  ---求出一个焦点,是这样的。节点是shape 返回undefined 和曲面没有焦点也是没有的。
 if sectray == undefined do return false --没有焦点结束函数              -------------由于时间是不断检查的,所以一旦有就会往下进行。

-- TobjST.randomValue = random 1  SCT_TargetOBJ.count

 if TobjST.obj == undefined  or  (isdeleted TobjST.obj) == true  then ----------------我现在觉得应该会进入这个事件中去。 -----第一次肯定是要进去,因为我们没有指定过,
 --如果结构中的obj没有指定,或者是被删了进行下面的操作,并且是带回车的。
 (
  undo on (
                                          ---其实下面是定义了一个空组。就是开始什么都没有,慢慢的在+ 中加入。
   TobjST.randomValue = random 1  SCT_TargetOBJ.count --结构中的数量等于1 到到列队中的个数。
   
   ---关于random 的用法1 0 是有事时1 有时时零。在这个中因为都是。integer所以返回的必定是整数、
   
 
   TobjST.obj = instance SCT_TargetOBJ[TobjST.randomValue] --定义了obj为赋值关联类表中的某一个。
   
   ------这里定义了结构中的这个节点
   
   
   TobjST.sizeV = random gt_Scatter_tool.MinScale_spi.value  gt_Scatter_tool.MaxScale_spi.value
   --sizeV 默认是100 这里面又要从新定义。这里是缩放函数 在这里重新的读取一下界面中的数。

   TobjST.rotateX = random gt_Scatter_tool.MinRotateX_spi.value  gt_Scatter_tool.MaxRotateX_spi.value
   TobjST.rotateY = random gt_Scatter_tool.MinRotateY_spi.value  gt_Scatter_tool.MaxRotateY_spi.value
   TobjST.rotateZ = random gt_Scatter_tool.MinRotateZ_spi.value  gt_Scatter_tool.MaxRotateZ_spi.value
            ----上面重新定义了旋转的随机函数。   
   
   TobjST.MoveV = random gt_Scatter_tool.MinMove_spi.value  gt_Scatter_tool.MaxMove_spi.value
            ---定义了高低的随机函数
   
   ---这里面定义了画笔的大小,用的是函数是制定物体的半径
   thePainterInterface.maxSize= OBJsizeFN  TobjST.obj

  --- thePainterInterface 是个绘画函数
   
   TobjST.obj.dir = sectray.dir---下面的每一个几何体和鼠标 对应 的射线
   --说的是什么轴向向上。
   
   --物体的射线方向
   TobjST.obj.name = "scatter_Helper" --- 这里 是明明几何体的名字。
   ---应为随着时间的绘画这里是不断的关联复制。
   
   
   in coordsys local TobjST.obj.rotation = (EulerAngles  TobjST.rotateX  TobjST.rotateY (TobjST.rotateV + TobjST.rotateZ) )
   ---在自身的坐标轴下,结构中物体的旋转, 在z轴上加入了一个旋转v 这个很重要。
   ----上面是说明在进行自身的旋转, 在z轴上加入了一些变化 明白了。加入了rotateV也就是说。打上自动旋转后。同时还可以按下shift 进行旋转处理。
   
   
   TobjST.OriginalScale = TobjST.obj.scale --已经有了定义在定义变化。
   --重新定义结构体中的 OriginalScale 的值,
   
   
   TobjST.obj.scale = (TobjST.OriginalScale * (TobjST.sizeV/100.)) ---实现了随机的缩放。
    ---算法。上面的上面是吧obj的上次缩放给 Originalscale  相当于本身 在乘以一个随机的%比。

   TobjST.obj.pos = sectray.pos ---鼠标与几何体的表面
    ---这个是最重要的一部了,就是把物体画到表面的东西
    
   in coordsys local TobjST.obj.pos = [0,0, TobjST.MoveV] ---在这里明白操作 就是不明白解释。
    
   ----明白了终于明白了,这里面说的是,物体的当给了变化值,在自身的方向,z轴在移动一次。相当于先给 了点,在在点上是为原点在移动

  )
 )
 else (
  TobjST.obj.pos = sectray.pos
  in coordsys local TobjST.obj.pos = [0,0, TobjST.MoveV]

  TobjST.obj.dir = sectray.dir
  in coordsys local TobjST.obj.rotation = (EulerAngles  TobjST.rotateX  TobjST.rotateY (TobjST.rotateV + TobjST.rotateZ) )
  TobjST.obj.scale = (TobjST.OriginalScale * (TobjST.sizeV/100.))
 )
 
 setSysCur #uscale

)


fn OBJrotateFN = (
 TobjST.rotateV += gt_Scatter_tool.add_del_.value
)
--------------------------对于绑定的操作我想加入-10 --------------
fn OBJrotateFN__ = (
 TobjST.rotateV -= gt_Scatter_tool.add_del_.value --------在此验证了+ —— 是不加空格的
)

fn  MouseOverObject = ( ---------------------------------------这里应该是鼠标移动事件。
 ga =$geometry as array ---把所有的做pick

-- ga = for i in ga where i.name != "scatter_Helper"  collect i

 delga = finditem ga $scatter_Helper ---在几何体中查找这个物体,
 if  delga != 0 do  deleteitem ga delga  ---如果查找 到了 $scatter_Helper 这个节点  就从数组中删除它。
 
 mouseray = mapScreenToWorldRay mouse.pos ---又做一条射线。
 coOBJ = #();   coRay=#() ---两个备用数组。
 for i in ga do  (
  sectray = intersectRAY i mouseray  --鼠标射线和所有的物体间的交汇处 ------------因为这些函数在时间的节点下实时进行。
  if sectray != undefind  do ( --- 这里面的交汇只能是个几何体 没有交上, 是undefind 
   append  coOBJ   i ---物体加入到
   append  coRay  sectray ----把没有找到物体的射线加入到这个里面。
  )
 )
 if coRay.count == 0 do return false  ---如果鼠标射线和所有物体没有交点 那么这个数组就是0 返回  不在往下进行。鼠标射线不可能所有的
  --物体没有交点,所以用到这里一般有效 我们知道这里是按下ctrl 找被刷物体时的事件。

 vv= (inverse (viewport.getTM())).pos  ---求出相反的矩阵 的pos
 cTemp= distance  vv coRay[1].pos;   cTempI = 1
 
 for i in 1 to coRay.count do (
  if cTemp > (distance  vv coRay[i].pos) do (
   cTemp= (distance  vv coRay[i].pos)
   cTempI = i
  )
 )

 format "%   %\n" coOBJ[cTempI].name   cTemp ----- 这里对于对于距离输出,还是很不好明白。
 return coOBJ[cTempI]
)


fn newOBJcheckFN =(                             -----------------------这个函数在中标中进行对于人的操作速度来说几乎是事实进行的。
 local mState= mouse.buttonStates               ----鼠标按下的状态。
 local mouseState=mState as array --转换成数组。花括号 转小括号。


 if keyboard.shiftPressed == ON and  keyboard.controlPressed == OFF  and  gt_Scatter_tool.start_btn.state  == ON   do (
  TobjST.rotateV= OBJrotateFN() ---旋转递加
  ---这个功能我刚刚用到
 )
 
 if keyboard.shiftPressed == ON and  keyboard.controlPressed == on  and  gt_Scatter_tool.start_btn.state  == ON   do (
  TobjST.rotateV= OBJrotateFN__() ---旋转递加
  ---这个功能我刚刚用到
 )

 
 --- keyboard ctrl  滚瓢捞 喘府搁 促弗坷宏璃飘 急琶
 if keyboard.controlPressed == ON  and  keyboard.shiftPressed == OFF   and  gt_Scatter_tool.start_btn.state  == ON   do (
  MOO = MouseOverObject()
--  print MOO
  if MOO == false do return false  -----这个事件主要是说明给要刷的物体换东西。
  gt_Scatter_tool.obj_btn.picked  MOO
 )
)

fn startStroke = (
 print "start"
)

fn paintStroke = (

-- thePainterInterface.getHitPointData &localHit &localNormal &worldHit &worldNormal 0.5 0.5 0
-- format "%  %  %  %\n" localHit localNormal  worldHit  worldNormal

 if TobjST.obj == undefined do return false
 timeS2=timestamp() ---重新定义取新的时间。
 timeT =  timeS2 - timeS1  ----------------------找出时间差  如果大于100 才创建东西。小于则不见。
 if timeT > gt_Scatter_tool.bugu_bul.value do  (
--  box pos:worldHit dir:worldNormal   --- 其牢飘牢磐其捞胶甫 荤侩且版快俊绰 捞巴阑 荤侩... face 俊 畴富蔼阑 舅酒晨
--  helperOBJPaint =  TobjST.obj
--  helperOBJPaint.pos = worldHit
--  helperOBJPaint.dir = worldNormal
  TobjST.obj.name = uniqueName "scatter_Helper" ---唯一的命名 也是第一个命名 在删除时则总是删除这个。
  timeS1=timestamp()  
  TobjST.obj = undefined
 )

 
)


fn cancelStroke = print "Handle cancel stroke here"
fn endStroke = (
 thePainterInterface.updateMeshes on
-- print "Update"
)


fn initFN_2 = (
-- nodeList = ScatOBJ   --- 盔贰绰 捞巴!!
 nodeList = undefined
 thePainterInterface.initializeNodes 0 nodeList
)

fn initFN = (
 thePainterInterface.endPaintSession()
-- if Dig_tool.obj_btn.object == undefined do return false
--- if (Superclassof Dig_tool.obj_btn.object) != GeometryClass  do return false

---   其牢磐 牢磐其捞胶甫 荤侩窍登 坷宏璃飘甫 固沥狼窍骨肺辑 加档甫 俺急矫糯
-- nodeList = ScatOBJ   --- 盔贰绰 捞巴!!
 nodeList = undefined
 
 
 thePainterInterface.initializeNodes 0 nodeList
 try( thePainterInterface.maxSize= OBJsizeFN  TobjST.obj  )catch() --这里的笔刷算法相当于物体的半径。最大减最小在除2 。
 thePainterInterface.ScriptFunctions startStroke paintStroke endStroke cancelStroke systemEnd
 thePainterInterface.offMeshHitType = 2
 
    thePainterInterface.updateOnMouseUp =false
 thePainterInterface.drawTrace =On
 thePainterInterface.drawNormal = off
 thePainterInterface.drawRing = off
  --thePainterInterface.maxSize=2000
 thePainterInterface.treeDepth = 2
 thePainterInterface.pressureEnable = OFF
 thePainterInterface.pressureAffects = 1
 --thePainterInterface.dragStrLimitMin=100
 --thePainterInterface.dragStrLimitMax=100

 
 thePainterInterface.startPaintSession()----画笔开始
 setSysCur #uscale --鼠标状态
)

try(destroyDialog gt_Scatter_tool)catch()
rollout gt_Scatter_tool "(盖天)繁殖:绑定:.." 210 height:300
(      
  fn  load_icon  = (
  a=openbitmap  ((GetDir #ui)+ "\\Icons\\"  + "Classic_16i.bmp")-- 位图的打开
  return a
  )
     ----------我在这里还要加入一个对于数据的控制 -------------------------
  --学习理论基础把控件名称变量成一个组,把控件名称变量的默认值成一个组,这样就好了 利用 打开和关闭进行数据的存储,这个效果很是好。


 pickbutton obj_btn "拾取表面物体" pos:[44,8] 88 height:21

 button Tobj_pick "+" pos:[4,38] 20 height:50 toolTip:"add selection Scatter Objects"
 button Tobj_del "-" pos:[4,93] 20 height:15 toolTip:"Delete Scatter Object"
 listbox SCT_lit "" pos:[33,39] 160 height:5 items:SCT_TargetOBJ
 spinner MinScale_spi "" pos:[60,128] 55 height:16 range:[0,500,100]
 spinner MaxScale_spi "" pos:[130,128] 55 height:16 range:[0,500,100]

 spinner MinRotateX_spi "" pos:[60,158] 55 height:16 range:[-500,500,0]
 spinner MaxRotateX_spi "" pos:[130,158] 55 height:16 range:[-500,500,0]
 spinner MinRotateY_spi "" pos:[60,178] 55 height:16 range:[-500,500,0]
 spinner MaxRotateY_spi "" pos:[130,178] 55 height:16 range:[-500,500,0]
 spinner MinRotateZ_spi "" pos:[60,198] 55 height:16 range:[-500,500,0]
 spinner MaxRotateZ_spi "" pos:[130,198] 55 height:16 range:[-500,500,0]

 spinner MinMove_spi "" pos:[60,228] 55 height:16 range:[-10000,10000,0]
 spinner MaxMove_spi "" pos:[130,228] 55 height:16 range:[-10000,10000,0]
 
 button ScaleIcon_btn "" pos:[20,125] 20 height:20 images:#(load_icon() , undefined, 78, 25, 25, 1, 1)   
 --基本明白了 路径引用图片 通道图 有多少子图 可用时 做的输入 输出 不可用是桌的输入输出。
 button RotateIcon_btn "" pos:[20,175] 20 height:20 images:#(load_icon(), undefined, 78, 23, 23, 1, 1)   
 button MoveIcon_btn "" pos:[20,226] 20 height:20 images:#(load_icon(), undefined, 78, 21, 21, 1, 1)
 GroupBox ScaleGroup "" pos:[10,113] 185 height:38
 GroupBox RotateGroup "" pos:[10,144] 185 height:77
 GroupBox MoveGroup "" pos:[10,213] 185 height:40
   
 spinner add_del_ "+_-" pos:[20,260] 70 height:16 range:[-360,360,10] type:#integer
 spinner bugu_bul "paint_fu" pos:[40,280] 70 height:16 range:[0,1200,100] type:#integer

 checkbutton start_btn "开始 !!" pos:[120,261] 80 height:25

 timer clock "testClock" interval:50 --有钟表这个东西一切都来的好。

 -------------------繁重事件的处理-----------------------------------------
  global oop_sel_lingshi_edt_text =undefined
 
  on pick_obj_btn37  picked obj do

(
 if obj != undefined do
(
pick_obj_btn37.text = obj.name
)
)--end on

    on  copy_one_to_big_btn38 pressed do with undo on
 ( 
  if oop_sel_lingshi_edt_text!= undefined then
  (
  if oop_sel_lingshi_edt_text.count >0 then
  (
            if  pick_obj_btn37.text!=  "拾取繁重物体" then
   (    
   
   for i in 1 to oop_sel_lingshi_edt_text.count do
   (
    doit_prog.value = 100.*i/oop_sel_lingshi_edt_text.count
    ui_pick_obj =   execute( "$" + pick_obj_btn37.text)
    if ui_pick_obj ==undefined then
    (
     messageBox "节点丢失。"title:"盖天编写"
     return false
    )
    
    pp_cy = instance ui_pick_obj
    
    pp_cy.name = uniquename  ("instance_"+pick_obj_btn37.text)
    
    pp_cy.transform =oop_sel_lingshi_edt_text[i].transform
    
    
   )
      )
   
   
  )
  doit_prog.value = 0  
 )
 )
 
 
    local ctrlSp=#(MinScale_spi,MaxScale_spi,MinRotateX_spi,MaxRotateX_spi, MinRotateY_spi, MaxRotateY_spi,MinRotateZ_spi, MaxRotateZ_spi ,MinMove_spi,MaxMove_spi\
    ,add_del_,bugu_bul),
    spIni =#(100,100,0,0,0,0,0,0,0,0,10,100   )
  -----创建点到点的处理
  global oop_sel_lingshi_lenght=undefined

 on clock tick do ----开始时钟就走。
 (
  helperFN()
  newOBJcheckFN()

  if thePainterInterface.inPaintMode() == OFF do start_btn.checked=OFF
 )

 on MinMove_spi changed val do (
  try (delete $scatter_Helper )catch()  ----这里为什么盈动是要删除的,而缩放选择就是不用。
  TobjST.obj = undefined          -----我们一直是利用结构。我想结构其实就是一个全局变量,
 )
 on MaxMove_spi changed val do (
  try (delete $scatter_Helper )catch()
  TobjST.obj = undefined
 )

 on Tobj_del pressed do (
  try(
   delval = gt_Scatter_tool.SCT_lit.selection
   deleteitem  SCT_TargetOBJ  delval
  )catch()

  TobjName=for i in SCT_TargetOBJ collect i.name
  gt_Scatter_tool.SCT_lit.items = TobjName
 )

 on Tobj_pick  pressed do (
  objs= selection as array
  join  SCT_TargetOBJ  objs
  
  TobjName=for i in SCT_TargetOBJ collect i.name
  
  SCT_lit.items = TobjName ---这里是典型的加入节点,收集名字。
 )


 on obj_btn picked obj do (
  ScatOBJ = obj
  obj_btn.text = obj.name

  initFN_2()
 )


 on  start_btn  changed state do   (
  if gt_Scatter_tool.SCT_lit.items.count == 0 do return false ----列队数列0 什么不做
 --Box lengthsegs:1 widthsegs:1 heightsegs:1 length:43.4603 77.2628 height:25.9733 mapcoords:on
  if state == ON do initFN() ---这个是最开始事件,下来就是时间进行的一系列的变化。
  
  ---------------------------好像是错了在没有按下之前就已经是种再走了。
  
  if state == OFF do  (
   thePainterInterface.endPaintSession() ---关闭画笔 删除根部节点。
   try (delete $scatter_Helper )catch()
  )
 )
 
 on gt_Scatter_tool open  do for i=1 to ctrlSp.count do (v=getIniSetting "$plugcfg/gt_fan_zhi_bangding_.ini" "disturbSel" (ctrlSp[i] as string);ctrlSp[i].value=if v=="" then spIni[i] else v as float )
 on gt_Scatter_tool close do for i=1 to ctrlSp.count do  setINISetting   "$plugcfg/gt_fan_zhi_bangding_.ini" "disturbSel" (ctrlSp[i] as string) (ctrlSp[i].value as string)
 
 
)


------自动模型 
 function  gray_material_balk_wire =
   (
    oo_mat = selection as array
     for i in oo_mat do
     ( 
      --其实应该判断他有没有贴图 如果有就不好了 这里呢我就用名字代替下
      if   classof meditMaterials[1]== Standardmaterial and  meditMaterials[1].name == "Standardmaterial"then
      (
        i.material=meditMaterials[1]
       i.wirecolor = black
      )else
      (
        meditMaterials[1]= Standardmaterial()
       meditMaterials[1].name ="Standardmaterial"
        i.material=meditMaterials[1]
       i.wirecolor = black
      
      )
      --这个不好没戏合并时都是很费事 所哟我用材质球 进行 
     
      )
   )--end fn
   ----自动poly
    function four_face =
   (
  
   addModifier   $  (Turn_to_Poly())

   $.modifiers[1].limitPolySize=true
   $.modifiers[1].maxPolySize=4
 macros.run "Modifier Stack" "Convert_to_Poly"
   )
   ------box
    function gt_box_box =
  (
   oop= selection as array 
  
   for i in oop  do
   (
      i.boxmode =not  i.boxmode
     
   )  
  )
  ----xform
  fn gt_Xform_tt =
  (
   ---下面的过程需要算下。
   --1.先找到物体的transform
   --2.在把物体归零,加入修改器
   --3.在转动他的 Gizmo
   --4.把它进行塌陷
   
   --脚本中指处理了物体的旋转 还要处理物体的缩放。
   for i in selection do  
   (
    ---2011.4.18 我想加入对线 和体的 操作。对于别的不进行操作。
    if  (superClassOf i ==GeometryClass and  ClassOf i != Targetobject)  or   superClassOf i ==shape then
    (
     if  InstanceMgr.GetInstances i &instances != 1 then
     (
      if superClassOf i ==shape then macros.run "Modifier Stack" "Convert_to_Spline"
      
      if superClassOf i ==GeometryClass then convertTo i Editable_Poly
     )
    
    oo = [i.rotation.x_rotation ,i.rotation.y_rotation ,i.rotation.z_rotation ]
     --我在制作时改了思路 这下就好了
     pos_pos = i.pos

    i.rotation.x_rotation =0
    i.rotation.y_rotation=0
    i.rotation.z_rotation =0
    
    ---处理缩放
     secle_a = i.scale
               
    i.scale=[1,1,1]

    addModifier   i (XForm())

              
    i.modifiers[1].Gizmo[2].controller[1].value  =oo[1]
    i.modifiers[1].Gizmo[2].controller[2].value  =oo[2]
    i.modifiers[1].Gizmo[2].controller[3].value  =oo[3]
                
    i.modifiers[1].Gizmo[3].controller.value =secle_a
    
    

          convertTo i Editable_Poly
         )   
   )
   ---上面就是脚本解决的过程 这下我有知道Xform 深的理解
   
   
  )

 try(cui.UnRegisterDialogBar gt_mode_big)catch()-----这个是法则。
try (DestroyDialog gt_mode_big )catch()
rollout gt_mode_big "模型常用" 160 height:132
(
 button Project_But "工程目录" pos:[29,4] 92 height:18
 button an_mode_btn "自动模型" pos:[9,26] 70 height:18
 button global_but "全局冻结" pos:[82,26] 70 height:18
 button automation_btn "自动poly" pos:[9,48] 70 height:18
 button box_btn "box盒" pos:[82,48] 70 height:18
 button form_btn "归Xform" pos:[9,69] 70 height:18
 button scatt_btn "散布" pos:[82,69] 70 height:18
 
 on Project_But pressed do
 (
  SubPathNameList = #(#animations,#archives,#downloads,#export,#expression,#image,#import,#matlib,#maxstart,#Photometric,#preview,#proxies,#renderassets,#renderPresets,#renderoutput,#scene,#sound,#vpost)
for i in SubPathNameList  do
 SetDir i ( "."+"\\"+i as string)
pathConfig.doProjectSetupSteps()
  
  
 )---end on
 on an_mode_btn pressed do
 (
  gray_material_balk_wire()
 )---end on
 on global_but pressed do
 (
  try (
   function all_freeze_tan =
   (
    global all_obj_freeze


   oop  = selection as array
    if oop.count !=0 then
    (
    all_obj_freeze= oop
    )

      for i in all_obj_freeze do
   (
          i. isFrozen =not  i. isFrozen
      i.showFrozenInGray =false
    -- i.showFrozenInGray = not i.showFrozenInGray
   )  
   )
   all_freeze_tan()
          )catch( messagebox ("全局变量已换。"))
  
 )-----end on
 on automation_btn pressed do
 (
   try (four_face ()) catch ()
 )---end on
 on box_btn pressed do
 (
   try ( gt_box_box()) catch ()
 )---end on
 on form_btn pressed do
 (
   try ( gt_Xform_tt()) catch ()
 )---end on
 on scatt_btn pressed do
 (

createdialog gt_Scatter_tool style:#(#style_titlebar, #style_border, #style_sysmenu,#style_minimizebox )

 )---end on
 
 
)

createdialog gt_mode_big

cui.RegisterDialogBar gt_mode_big style:#(#cui_dock_left,#cui_dock_right, #cui_floatable)
cui.DockDialogBar gt_mode_big #cui_dock_right

原文地址:https://www.cnblogs.com/gaitian00/p/2025669.html