2020年12月29日 星期二

製作數學公式圖片的工具網站

 製作數學公式圖片的工具網站

前言

  在寫一些程式文章時會需要表達數學公式,但數學公式並不能在 HTML 直接輸入,但我找到有個網站可以編輯數學公式並且將結果做成圖片下載,在此把學習的過程做個紀錄。

 

內容

  使用這個工具要到 [ latex.codecogs.com ] Online LaTeX Equation Editor 裡,使用的畫面如下

Online LaTeX Equation Editor 的使用畫面


在畫面"1"的地方輸入語法來產生等式,結果會顯示在畫面"2"的地方,最後透過畫面"3"的地方可以下載結果。畫面示範了如何使用根號,語法不用記,上方有工具列,透過滑鼠點選需要的語法,如下圖

透過工具列來家語法,以根號為例

圖中以根號為例會加"\sqrt{}",內容放哪要自己試一下,利用下方的顯示結果試一下即可。


參考資料

[ latex.codecogs.com ] Online LaTeX Equation Editor

2020年12月22日 星期二

向量正規化的注意事項

 向量正規化的注意事項

前言

  最近的專案遽然發生了除0中斷,結果追了一下發生的地方,結果在自己寫的向量函示庫裡的向量正規化這裡發生,這裡把學習的過程做個紀錄。


內容

  計算向量正規化時,會先計算該向量長度,長度公式如下

向量長度公式

計算完長度後再將每個數值除以長度就可以完成正規化,但如果這個長度很小或根本是"0"的話就會發生問題,解決的方法是計算完長度後,如果小於"FLT_EPSILON"就放棄計算(或直接在結果填上零向量),"FLT_EPSILON"是 C++ 的標準 Macro ,可以解釋成 float 資料型態的最小精度。


  向量函示庫自己寫就是會碰到這種麻煩,不過也可以在這樣的過程中學習,這次可真是上了一課。


2020年12月15日 星期二

用佚代的方式瀏覽陣列

 用佚代的方式瀏覽陣列

前言

  在較新的 ECMAScript 標準中支援了用佚代的方式來瀏覽陣列,但由於舊的 For 迴圈語法相當強大,所以一直覺得沒必要更換,但由於新標準推行已經很久了,現在看別人的程式碼會看到佚代,就算自己不用也要看得懂別人的程式碼,所以就來研究學習,在此做個紀錄。


內容

  目前常看到的有 forEach() 、 map() 與 filter() ,這次會示範使用的範例與用 for 迴圈地等效範例。


  首先看到 forEach() 的用法,範例如下

let ar = [ 1 , 2 , 3 ];
ar.forEach( function( ele ){
  console.log( ele );
} );
//
//1
//2
//3


for 迴圈的等效程式碼如下

let ar = [ 1 , 2 , 3 ];
for(let i = 0 ; i < ar.length ; i++)
  console.log( ar[i] );
//
//1
//2
//3


相比 for 迴圈的作法, forEach() 比較簡潔,但沒辦法改變瀏覽對象,一定是全部的對象。接著看到 map() ,範例如下

let ar = [ 1 , 2 , 3 ];

let res = ar.map( function(ele){
  return ele ;
} );
console.log( res );
//
//[1, 2, 3]


for 迴圈的等效程式碼如下

let ar = [ 1 , 2 , 3 ];

let res = [];
for( let i = 0 ; i < ar.length ; i++ )
  res.push( ar[i] );
//
console.log( res );
//
//[1, 2, 3]


map() 相比之下少了變數宣告的過程,直接取得回傳值就可以了,但瀏覽的對象一定是全部,而且也沒辦法不回傳要推入的對象,也就是說 map() 的會傳的陣列數量會和原來的陣列一樣。最後看到 filter() ,範例如下

let ar = [ 1 , 2 , 3 ];

let res = ar.filter( function(ele){
  return ele > 1;
} );
console.log( res );
//
//[2, 3]



for 迴圈的等效程式碼如下

let ar = [ 1 , 2 , 3 ];

let res = [];
for( let i = 0 ; i < ar.length ; i++){
  if( ar[i] > 1)
    res.push( ar[i] );
}
  
console.log( res );
//
//[2, 3]


filter() 可以對新產生的陣列做出篩選,但內容無法運算,而 map() 可以對要輸出的對象做運算,但無法篩選要輸出的對象。


參考資料

[ developer.mozilla.org ] forEach

[ developer.mozilla.org ] map

[ developer.mozilla.org ] filter

2020年12月8日 星期二

方便產生 HTML 表格( Table )的工具

 方便產生 HTML 表格( Table )的工具

前言

  在寫 Blog 時有時需要用到表格,但工具列裡沒有表格( Table )的工具可以用,但可以用 HTML 的方式來插入,但 HTML 的表格( Table )直接用非常不方便,所以這次找了可以產生 HTML 的表格( Table )的工具。


內容

  這次介紹的工具是線上工具,到 [ www.tablesgenerator.com ] Tables Generator 就可以使用,進入後可看到下圖

Table Generator 的使用畫面


畫面看起來很複雜,但使用起來並不難懂,在畫面"3"的地方可以預視表格結果,要新增或移除欄位可以在畫面"1"地方就可以操作,在畫面"3"點擊或圈選欄位可以透過畫面"2"的地方修改欄位的屬性如字型、顏色...等,當完成 Table 後,按下"Generator"按鈕,可以在畫面"5"的地方得到 HTML 結果,在畫面"4"的地方記得勾選不要產生 CSS ,不然會產生多餘的 CSS 程式碼。


參考資料

[ www.tablesgenerator.com ] Tables Generator



2020年12月1日 星期二

WebGL的繪製三次方赫密特曲線( Cubic hermite curve )的切線

 WebGL的繪製三次方赫密特曲線( Cubic hermite curve )的切線

前言

  在先前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線 裡繪製了貝茲曲線的切線,這次來繪製三次方赫密特曲線( Cubic hermite curve )的切線,在此把學習的過程做個紀錄。


內容

  在先前的 WebGL的繪製三次方赫密特曲線( Cubic hermite curve ) 裡已經有一個繪製曲線的範例,這次的範例會從該專案改出來。在之前不確定"微分"後就可以得到切線,這次算是得到一個驗證,所以如果將赫密特曲線拿去為分會得到以下

切線公式

接著將這個公式套用到先前的範例,程式碼如下

HTML 的部分

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCubicHermiteCurve"type="button" value="DrawCubicHermiteCurve"/>
<br>
<input id="btnDrawCubicBezierCurve"type="button" value="DrawCubicBezierCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
<br>
<input type="checkbox" id="isShowTangent" checked><label>Show tangent </label>
<input type="range" min="0" max="1" value="0.5" step="0.001" class="slider" id="sliderTangent">
<label id="labelTangentValue">0.5</label>
</body>
</html>



Javascript 的部分

let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
let curveType = 0;
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateLineListArrayData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return tagAr;
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT * invT;
    let part1Value = 3 * t * invT * invT;
    let part2Value = 3 * t * t * invT;
    let part3Value = t * t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function calCubicHermiteCurvePosition(p0,s0,p1,s1,t){
  let invT = 1.0-t;
  let part0Value = (1 + ( 2 * t ) ) * ( invT * invT );
  let part1Value = t * ( ( invT * invT ) );
  let part2Value = ( t * t ) * ( 3 - ( 2 * t) );
  let part3Value = ( t * t ) * ( t - 1.0 );
  let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
  let part1 = [part1Value*s0[0], part1Value*s0[1], part1Value*s0[2] ];
  let part2 = [part2Value*p1[0], part2Value*p1[1], part2Value*p1[2] ];
  let part3 = [part3Value*s1[0], part3Value*s1[1], part3Value*s1[2] ];
  let position = [
    part0[0] + part1[0] + part2[0] + part3[0],
    part0[1] + part1[1] + part2[1] + part3[1],
    part0[2] + part1[2] + part2[2] + part3[2]
  ];
  return position;
}
function generateCubicHermiteCurve(p0,s0,p1,s1,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let pos = calCubicHermiteCurvePosition(p0,s0,p1,s1,t);
    tagAr.push( pos[0] );
    tagAr.push( pos[1] );
    tagAr.push( pos[2] );
  }
  return tagAr;
}
function calCubicHermiteCurveTangent(p0,s0,p1,s1,t){
  let part0Value = ( 6 * t * t ) - ( 6 * t);
  let part1Value = ( 3 * t * t ) - ( 4 * t) + 1;
  let part2Value = ( 6 * t) - ( 6 * t * t );
  let part3Value = ( 3 * t * t ) - ( 2 * t );
  let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
  let part1 = [part1Value*s0[0], part1Value*s0[1], part1Value*s0[2] ];
  let part2 = [part2Value*p1[0], part2Value*p1[1], part2Value*p1[2] ];
  let part3 = [part3Value*s1[0], part3Value*s1[1], part3Value*s1[2] ];
  let tangent = [
    part0[0] + part1[0] + part2[0] + part3[0],
    part0[1] + part1[1] + part2[1] + part3[1],
    part0[2] + part1[2] + part2[2] + part3[2]
  ];
  return tangent;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
let tagTangentValue=document.getElementById("sliderTangent").value;
function UpdateCubicBezierCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
function UpdateCubicHermiteCurveData(){
  let data=generateCubicHermiteCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[3],
    ctrlPointList[2],
    tagLerpValue);
  //
  let dataArray=generateLineListArrayData(data);
  let isShowTangent = document.getElementById("isShowTangent").checked;
  if(isShowTangent){    
    let tagT = tagTangentValue;
    let pos = calCubicHermiteCurvePosition(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[3],
      ctrlPointList[2],
      tagT
    );
    let tangent = calCubicHermiteCurveTangent(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[3],
      ctrlPointList[2],
      tagT
    );
    dataArray.push(pos[0]);
    dataArray.push(pos[1]);
    dataArray.push(pos[2]);
    dataArray.push(pos[0]+tangent[0]);
    dataArray.push(pos[1]+tangent[1]);
    dataArray.push(pos[2]+tangent[2]);
  }

  
  let tagDataArray = new Float32Array( dataArray );
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,tagDataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCubicBezierCurve").onclick=function(evt){
  UpdateCubicBezierCurveData();
  curveType = 1;
}
document.getElementById("btnDrawCubicHermiteCurve").onclick=function(evt){
  UpdateCubicHermiteCurveData();
  curveType = 2;
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  //UpdateCurveData();
  switch( curveType ){
    case 1:
      UpdateCubicBezierCurveData();
      break;
    case 2:
      UpdateCubicHermiteCurveData();
      break;
      
  }
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
document.getElementById("isShowTangent").onclick=function(){
  UpdateCubicHermiteCurveData();
}
document.getElementById("sliderTangent").oninput=function(evt){
  tagTangentValue=this.value;
  UpdateCubicHermiteCurveData();
  document.getElementById("labelTangentValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}


執行結果如下

範例的執行結果


這次的改法跟 WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線 的改法是一樣的,所以做不多做說明。


  這次算是對微分後得到切線方程式做一個驗證,以後碰到的曲線方程式都可以利用微分得到切線,算是這次的收穫。


參考資料

[ wiki ] Cubic Hermite spline


相關文章與資料

WebGL的繪製三次方赫密特曲線( Cubic hermite curve )

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線


2020年11月24日 星期二

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線

 WebGL的繪製三次方貝茲曲線( Cubic bezier curve )的切線

前言

  在先前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 繪製了曲線,但最近需要得到 tangent (切線) ,所以就研究了一下如何計算切線,在此把學習的過程做個紀錄。


內容

  要計算切線的話我在 [ stackoverflow.com ] Find the tangent of a point on a cubic bezier curve 裡找到了公式如下


這個公式也可以在 [ en.wikipedia.org ] Bézier curve 裡面找到,限定在英文的 Wiki 才找的到請注意。


  接著把公式套到 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 的範例,程式碼如下

HTML 的部分

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCurve"type="button" value="DrawCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
<br>
<input type="checkbox" id="isShowTangent" checked><label>Show tangent </label>
<input type="range" min="0" max="1" value="0.5" step="0.001" class="slider" id="sliderTangent">
<label id="labelTangentValue">0.5</label>
</body>
</html>



Javascript 的部分

let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateLineListArrayData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return tagAr;
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function calCubicBezierCurvePosition(p0,p1,p2,p3,t){
  let invT = 1.0 - t;
  let part0Value = invT * invT * invT;
  let part1Value = 3 * t * invT * invT;
  let part2Value = 3 * t * t * invT;
  let part3Value = t * t * t;
  let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
  let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
  let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
  let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
  let position = [
    part0[0] + part1[0] + part2[0] + part3[0],
    part0[1] + part1[1] + part2[1] + part3[1],
    part0[2] + part1[2] + part2[2] + part3[2]
  ];
  return position;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let pos = calCubicBezierCurvePosition(p0,p1,p2,p3,t);
    tagAr.push( pos[0] );
    tagAr.push( pos[1] );
    tagAr.push( pos[2] );
  }
  return tagAr;
}
function calCubicBezierCurveTangent(p0,p1,p2,p3,t){
  let del10 = [ p1[0] - p0[0] , p1[1] - p0[1] , p1[2] - p0[2] ];
  let del21 = [ p2[0] - p1[0] , p2[1] - p1[1] , p2[2] - p1[2] ];
  let del32 = [ p3[0] - p2[0] , p3[1] - p2[1] , p3[2] - p2[2] ];
  let invT = 1.0-t;
  let part0Value = 3 * invT * invT ;
  let part1Value = 6 * t * invT;
  let part2Value = 3 * t * t;
  let tangent = [
    part0Value * del10[0] + part1Value * del21[0] + part2Value * del32[0],
    part0Value * del10[1] + part1Value * del21[1] + part2Value * del32[1],
    part0Value * del10[2] + part1Value * del21[2] + part2Value * del32[2]
  ];
  return tangent;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
let tagTangentValue=document.getElementById("sliderTangent").value;
function UpdateCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  //
  let dataArray=generateLineListArrayData(data);
  let isShowTangent = document.getElementById("isShowTangent").checked;
  if(isShowTangent){    
    let tagT = tagTangentValue;
    let pos = calCubicBezierCurvePosition(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[2],
      ctrlPointList[3],
      tagT
    );
    let tangent = calCubicBezierCurveTangent(
      ctrlPointList[0],
      ctrlPointList[1],
      ctrlPointList[2],
      ctrlPointList[3],
      tagT
    );
    dataArray.push(pos[0]);
    dataArray.push(pos[1]);
    dataArray.push(pos[2]);
    dataArray.push(pos[0]+tangent[0]);
    dataArray.push(pos[1]+tangent[1]);
    dataArray.push(pos[2]+tangent[2]);
  }

  
  let tagDataArray = new Float32Array( dataArray );
  //
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,tagDataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCurve").onclick=function(evt){
  UpdateCurveData();
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  UpdateCurveData();
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
document.getElementById("isShowTangent").onclick=function(){
  UpdateCurveData();
}
document.getElementById("sliderTangent").oninput=function(evt){
  tagTangentValue=this.value;
  UpdateCurveData();
  document.getElementById("labelTangentValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}


執行結果如下

範例的執行結果

這次的程式碼改得有點亂,跟之前的範例不一樣的部分會用紅字來表示,這次提供了 generateLineListArrayData() ,跟之前 generateLineListData() 的差異是回傳的資料型態不一樣,

generateLineListArrayData()  回傳的型態就是單純的 Array ,這是方便這次需要多畫切線要插入資料。 calCubicBezierCurvePosition() 是提供利用百分比來取得曲線的位置, generateCubicBezierCurve() 跟上次不太一樣,取得位置的部分會用 calCubicBezierCurvePosition() 來替代,計算的結果是一樣的,單純的程式碼優化。 calCubicBezierCurveTangent() 這次的主角,套用公式就可以取得該點的切線。接著看到 UpdateCurveData() ,這次由於需要多畫切線,所以用 generateLineListArrayData() 取得資料後再來插入切線資料,最後才轉成 Float32Array ,切線算出來的是變化量,所以要加上該點後才能形成切線的資料。


  切線的公式似乎是將原本的曲線公式作微分的動作,不過由於我數學不好所以不太確定,以後有機會再來研究看看。


參考資料

[ en.wikipedia.org ] Bézier curve

[ stackoverflow.com ] Find the tangent of a point on a cubic bezier curve


相關文章與資料

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

2020年11月17日 星期二

在特製 Widget 的使用右鍵選單

 在特製 Widget 的使用右鍵選單

前言

  在先前的 在 TreeView 使用右鍵選單 使用過右鍵選單,但 TreeView 是由外部控制,而特製 Widget 時是採用"繼承"的方法來控制,這次把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/CustomMenu,執行結果如下

範例的執行結果

在特製的 Widget 上按下右鍵會跳出右鍵選單。


  看到程式碼的部分,看到特製 Widget 的宣告,程式碼如下

#include <QWidget>

#include <QPainter>
#include <QMenu>
#include <QAction>
class MyWidget : public QWidget
{
  Q_OBJECT
public:
  explicit MyWidget(QWidget *parent = nullptr);

protected:
  void paintEvent(QPaintEvent *event) override;
  void onTreeViewCustomMenu(const QPoint &pos);
  void onTreeViewAction1Trigger();
  void onTreeViewAction2Trigger();
  void onTreeViewAction3Trigger();
signals:
  //
private:
  QMenu* m_pTreeViewCustomMenu;
};


右鍵選單在宣告的部分和之前的 TreeView 用馬差不多,只是這次是把事件直接宣告在 Widget ,而非 MainWindow 。接著看到實現的部分,程式碼如下

#include "mywidget.h"

MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
  this->setContextMenuPolicy(Qt::CustomContextMenu);
  connect( this , &QWidget::customContextMenuRequested , this , &MyWidget::onTreeViewCustomMenu );
  //Set up custom menu
  m_pTreeViewCustomMenu = new QMenu( this );
  QAction* pAct1 = new QAction( "Action 1" , this );
  connect( pAct1 , &QAction::triggered , this , &MyWidget::onTreeViewAction1Trigger );
  m_pTreeViewCustomMenu->addAction( pAct1 );
  QAction* pAct2 = new QAction( "Action 2" , this );
  connect( pAct2 , &QAction::triggered , this , &MyWidget::onTreeViewAction2Trigger );
  m_pTreeViewCustomMenu->addAction( pAct2 );
  QAction* pAct3 = new QAction( "Action 3" , this );
  connect( pAct3 , &QAction::triggered , this , &MyWidget::onTreeViewAction3Trigger );
  m_pTreeViewCustomMenu->addAction( pAct3 );
}

void MyWidget::paintEvent(QPaintEvent *event)
{
  QPainter painter( this );
  QPen pen( Qt::black );
  pen.setWidth( 4 );
  painter.setPen( pen );
  QRect rc( 4 , 4 , this->width() - 8, this->height() - 8 );
  painter.drawRect( rc );

}

void MyWidget::onTreeViewCustomMenu(const QPoint &pos)
{
  //
  m_pTreeViewCustomMenu->popup( this->mapToGlobal( pos ) );
}

void MyWidget::onTreeViewAction1Trigger()
{
  qDebug( "Action1 triggered" );
}
void MyWidget::onTreeViewAction2Trigger()
{
  qDebug( "Action2 triggered" );
}
void MyWidget::onTreeViewAction3Trigger()
{
  qDebug( "Action3 triggered" );
}


看到 MyWidget::MyWidget() ,開頭需要執行 setContextMenuPolicy() ,這很重要,接著綁定 customContextMenuRequested() 事件,最後是 Menu 的初始化的部分,這部分和之前的 TiewView 範例一樣。觸發事件的部分就和之前 TiewView 的範例一樣,就只是剪貼過來。 右鍵選單的用法不管是外部控制或繼承幾乎是走同一個流程,本來還擔心繼承的用法會有所不同,看來是我多慮了。


參考資料

[ doc.qt.io ] QWidget Class


相關文章與資料

[ GitLab ] HelloQt

在 TreeView 使用右鍵選單

2020年11月10日 星期二

WebGL的繪製三次方赫密特曲線( Cubic hermite curve )

 WebGL的繪製三次方赫密特曲線( Cubic hermite curve )

前言

  在之前的 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 繪製了貝茲曲線,這次來繪製赫密特曲線( Cubic hermite curve ) ,後者常常用在動畫的內插,所以有必要了解與實現,在此把學習的過程做個紀錄。


內容

  赫密特曲線( Cubic hermite curve ) 跟貝茲曲線(  Cubic bezier curve ) 有所不同,並不採用控制點,而採用 Slope ,從 2 個控制點換成 2 個 Slope ,由於我不太會形容差異,但我找到可以線上繪製的網站在 [ www.desmos.com ] 線上繪製赫密特曲線 ,先到該網站體驗一下差異再執行範例會比較好。

  接著就來看範例,如下
HTML 的部分

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<canvas id="myCanvas1" width=400 height=300></canvas>
<br>
<input id="btnDrawCtrlPoints"type="button" value="DrawCtrlPoints"/>
<br>
<input id="btnDrawCubicHermiteCurve"type="button" value="DrawCubicHermiteCurve"/>
<br>
<input id="btnDrawCubicBezierCurve"type="button" value="DrawCubicBezierCurve"/>
<input type="range" min="2" max="100" value="30" class="slider" id="sliderLerp">
<label id="labelLerpValue">30</label>
</body>
</html>


Javascript 的部分

let canvas1 = document.getElementById('myCanvas1');
let glCTX1 = canvas1.getContext('webgl');
let vboPrimitiveCon = 0;
let vbo=createDynamicBuffer(glCTX1);
let shaderProg = createShader(glCTX1);
let curveType = 0;
//
function createShader(glContext){
  let vertShader = glContext.createShader(glContext.VERTEX_SHADER);
  glContext.shaderSource(
    vertShader , 
    'attribute vec3 pos;void main(void){gl_Position=vec4(pos, 1.0);}'
  );
  glContext.compileShader(vertShader);
  let fragShader = glContext.createShader(glContext.FRAGMENT_SHADER);
  glContext.shaderSource(
    fragShader, 
    'void main(void){gl_FragColor=vec4(1,1,1,1);}'
  );
  glContext.compileShader(fragShader);
  let prog = glContext.createProgram();
  glContext.attachShader(prog, vertShader);
  glContext.attachShader(prog, fragShader);
  glContext.linkProgram(prog);
  glContext.useProgram(prog);  
  
  return prog;
}
function createDynamicBuffer(glContext){
  let vertexBuf = glContext.createBuffer();
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vertexBuf);
  let dataArray=new Float32Array([ 
       0.0, 0.5, 0.0,  
      -0.5,-0.5, 0.0,  
      -0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.5,-0.5, 0.0,
       0.0, 0.5, 0.0
    ]);
  glContext.bufferData(
    glContext.ARRAY_BUFFER, 
    3000, 
    glContext.DYNAMIC_DRAW
  );
  //write deafult data...
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
  return vertexBuf;
}
function simpleDraw(glContext){
  glContext.useProgram(shaderProg);
  //
  glContext.viewport(0,0,glContext.canvas.width,glContext.canvas.height);
  glContext.clearColor(0, 0, 1, 1);
  glContext.clear(glContext.COLOR_BUFFER_BIT);
  //
  glContext.bindBuffer(glContext.ARRAY_BUFFER, vbo);
  let posLoc = glContext.getAttribLocation(shaderProg, "pos");
  glContext.vertexAttribPointer(posLoc, 3, glContext.FLOAT, false, 0, 0);
  glContext.enableVertexAttribArray(posLoc);

  glContext.drawArrays(glContext.LINES, 0, vboPrimitiveCon);
}
function generateLineListData(ar){
  let tagAr=[];
  let mod=ar.length%3;
  let elementAmount=(ar.length-mod)/3;
  if(elementAmount>=2 && mod===0){
    tagAr.push(ar[0]);
    tagAr.push(ar[1]);
    tagAr.push(ar[2]);
    //
    for(let i=3;i<(ar.length-3);i+=3){
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
      //
      tagAr.push(ar[i]);
      tagAr.push(ar[i+1]);
      tagAr.push(ar[i+2]);
    }
    //
    tagAr.push(ar[ar.length-3]);
    tagAr.push(ar[ar.length-2]);
    tagAr.push(ar[ar.length-1]);
  }
  return new Float32Array(tagAr);
}
function generateBezierCurve(p0,p1,p2,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT;
    let part1Value = 2 * t * invT;
    let part2Value = t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0]);
    tagAr.push(part0[1] + part1[1] + part2[1]);
    tagAr.push(part0[2] + part1[2] + part2[2]);
  }
  return tagAr;
}
function generateCubicBezierCurve(p0,p1,p2,p3,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = invT * invT * invT;
    let part1Value = 3 * t * invT * invT;
    let part2Value = 3 * t * t * invT;
    let part3Value = t * t * t;
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*p1[0], part1Value*p1[1], part1Value*p1[2] ];
    let part2 = [part2Value*p2[0], part2Value*p2[1], part2Value*p2[2] ];
    let part3 = [part3Value*p3[0], part3Value*p3[1], part3Value*p3[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function generateCubicHermiteCurve(p0,s0,p1,s1,lerp){
  if(lerp < 2)
    return [];
  //
  let tagAr = [];
  for(let i=0;i < lerp;i++){
    let t = i/(lerp-1);
    let invT = 1.0-t;
    let part0Value = (1 + ( 2 * t ) ) * ( invT * invT );
    let part1Value = t * ( ( invT * invT ) );
    let part2Value = ( t * t ) * ( 3 - ( 2 * t) );
    let part3Value = ( t * t ) * ( t - 1.0 );
    let part0 = [part0Value*p0[0], part0Value*p0[1], part0Value*p0[2] ];
    let part1 = [part1Value*s0[0], part1Value*s0[1], part1Value*s0[2] ];
    let part2 = [part2Value*p1[0], part2Value*p1[1], part2Value*p1[2] ];
    let part3 = [part3Value*s1[0], part3Value*s1[1], part3Value*s1[2] ];
    tagAr.push(part0[0] + part1[0] + part2[0] + part3[0]);
    tagAr.push(part0[1] + part1[1] + part2[1] + part3[1]);
    tagAr.push(part0[2] + part1[2] + part2[2] + part3[2]);
  }
  return tagAr;
}
function myRender(){
  simpleDraw(glCTX1);
  //
  window.requestAnimationFrame(myRender);
}
//
let ctrlPointList=[
  [-0.9,0.0,0.0],
  [-0.45,0.9,0.0],
  [0.45,0.9,0.0],
  [0.9,0.0,0.0],
];
let tagLerpValue=document.getElementById("sliderLerp").value;
function UpdateCubicBezierCurveData(){
  let data=generateCubicBezierCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[2],
    ctrlPointList[3],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
function UpdateCubicHermiteCurveData(){
  let data=generateCubicHermiteCurve(
    ctrlPointList[0],
    ctrlPointList[1],
    ctrlPointList[3],
    ctrlPointList[2],
    tagLerpValue);
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCtrlPoints").onclick=function(evt){
  let data = [];
  for(let i=0;i<ctrlPointList.length;i++)
    data.push(...ctrlPointList[i]);
  //
  let dataArray=generateLineListData(data);
  glCTX1.bindBuffer(glCTX1.ARRAY_BUFFER, vbo);
  glCTX1.bufferSubData(glCTX1.ARRAY_BUFFER,0,dataArray);
  vboPrimitiveCon = dataArray.length / 3;
}
document.getElementById("btnDrawCubicBezierCurve").onclick=function(evt){
  UpdateCubicBezierCurveData();
  curveType = 1;
}
document.getElementById("btnDrawCubicHermiteCurve").onclick=function(evt){
  UpdateCubicHermiteCurveData();
  curveType = 2;
}
document.getElementById("sliderLerp").oninput=function(evt){
  tagLerpValue=this.value;
  //UpdateCurveData();
  switch( curveType ){
    case 1:
      UpdateCubicBezierCurveData();
      break;
    case 2:
      UpdateCubicHermiteCurveData();
      break;
      
  }
  document.getElementById("labelLerpValue").innerHTML = this.value;
}
window.onload = function(){
  window.requestAnimationFrame(myRender);
  
}


執行結果如下

範例的執行結果

這次的範例事由 WebGL的繪製三次方貝茲曲線( Cubic bezier curve ) 的範例更改而來,重複的部分就不解說了,主要要注意的地方在 updateCubicHermiteCurveData() ,注意控制點的輸入順序和 updateCubicBeizerCurveData() 的順序不一致,這是因為曲線的節點定義不同所造成的,並不是範例有錯。


  赫密特曲線的 Slope 控制起來不是很能預測它會如何彎曲,這會造成很難調成自己想要的曲線,這個部分可能還要再研究一下要如何找出可以預測怎麼預測彎曲的方法。

參考資料

[ wiki ] Cubic Hermite spline


相關文章與資料

WebGL的繪製三次方貝茲曲線( Cubic bezier curve )

[ www.desmos.com ] 線上繪製赫密特曲線

2020年11月3日 星期二

用 JavaScript 裡綁定類別

用 JavaScript 裡綁定類別

前言

  在之前的 用 JavaScript 控制 Widget  裡使用了 Qt 的 Widget 在 JavaScript 中,如果要在 JavaScript 裡使用自己設計的類別有可能嗎?答案是可以的,要如何做呢?在此把學習的過程作紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/WrapClass,執行結果如下

範例的執行結果



範例的上方會有預設的程式碼,按下右下的"Run"可以執行,執行預設的程式碼看列印的偵錯訊息。


  要在 JavaScript 綁定自己的類別,該類別必須繼承 QObject ,這很重要!利用 Qt Creator 的精靈產生類別的話,如下圖

使用精靈產生繼承 QObject 的類別

在精靈的"Class name"填上類別名稱,在"Base class"選擇 QObject 後即可,範例已產生"MyClass",所以看到"MyClass"的宣告,如下

#include <QObject>

class MyClass : public QObject
{
  Q_OBJECT
  Q_PROPERTY(int dataInt READ myInt WRITE setMyInt)
  Q_PROPERTY(double dataDouble READ myDouble WRITE setMyDouble)
  Q_PROPERTY(QString dataString READ myString WRITE setMyString)
public:
  explicit MyClass(QObject *parent = nullptr);
//
  void setMyInt( int value );
  int myInt() const;
  void setMyDouble( double value );
  double myDouble() const;
  void setMyString( const QString& value );
  QString myString() const;
  Q_INVOKABLE void callMethod();
signals:
private:
  int m_MyInt;
  double m_MyDouble;
  QString m_MyString;
};


MyClass 會綁定三個 Property ,"m_MyInt"、"m_MyDouble"與"m_MyString",並綁定一個 Function 名為"callMethod",綁定 Function 到 JavaScript 的部分比較簡單,只需再開頭加"Q_INVOKABLE"即可,但要注意輸入的引數與回傳值得資料型態必須是 JavaScript 可以接受的型態,至於那些型態可以接受以後來研究,這次都先使用基本的型態。在綁定 Property 的部份就比較麻煩一些,綁定些要用到"Q_PROPERTY"來綁定,後方的內容要照一定的格式寫,就拿 m_MyInt 來說,"int"是綁定到 JavaScript 的型態,這個型態最後會被自動轉成 JavaScript 的 "Number" ,"dataInt" 是 Property 的名稱,可以自己命名,不一定要和 C++ 這邊的變數同名,"READ myInt"指的是讀取時所喚起的 Function ,在這裡會喚起 myInt() ,接著是"WRITE setMyInt"指的是寫的時候所喚起的 Function ,這裡會喚起 setMyInt() 。在讀與寫的 Function 也要注意資料型態的問題,字串的部份要用 QString 來綁定而非 std::string, MyClass 的實作部分沒什麼好說明就直接略過,接著看到 MainWindow::MainWindow() ,如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->runButton , &QPushButton::clicked , this , &MainWindow::onRunButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  MyClass* pMyClass = new MyClass();
  m_cJSEngine.globalObject().setProperty( "myClass" ,m_cJSEngine.newQObject( pMyClass ) );
  //
  ui->textEdit->setText( tr(
    "console.log( myClass.dataInt );\n"\
    "console.log( myClass.dataDouble );\n"\
    "console.log( myClass.dataString );\n"\
    "myClass.dataInt = 5678;\n"\
    "myClass.dataDouble = 1.414;\n"\
    "myClass.dataString = 'My string';\n"\
    "console.log( myClass.dataInt );\n"\
    "console.log( myClass.dataDouble );\n"\
    "console.log( myClass.dataString );\n"\
    "myClass.callMethod();\n"\
  ) );
}


程式開頭依舊初始化與綁定事件,這次就不說明了,接著直接 New 一個 MyClass ,透過  QJSEngine::newQObject() 來新增變數,就像上次在 用 JavaScript 控制 Widget  裡做的一樣,就著就是寫下預設的 JavaScript 程式碼,單純列印變數與喚起 Method ,這就不說明了。


參考資料

[ doc.qt.io ] QJSEngine Class


相關文章與資料

[ GitLab ] HelloQt

用 JavaScript 控制 Widget

2020年10月27日 星期二

用 JavaScript 控制 Widget

 用 JavaScript 控制 Widget 

前言

  在之前的 在 Qt 環境使用 JavaScript 裡基本的使用 JavaScript ,這次要用 JavaScript 來控制 Widget 

,在此把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/UseWidget,執行結果如下

範例的執行結果

上方會有預設的程式碼,按下右下的"Run"可以執行,執行完後可以看到左下的 PushButton 會改變顯示的字,按下後會列印偵錯訊息。


  看到 MainWindow::MainWindow() ,程式碼如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->runButton , &QPushButton::clicked , this , &MainWindow::onRunButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  m_cJSEngine.globalObject().setProperty( "myButton" , m_cJSEngine.newQObject( ui->myButton ) );
  //Default code
  ui->textEdit->setText( tr(
    "myButton.text = 'Hello myButton.';\n"\
    "myButton.clicked.connect( function( clicked ){\n"\
    "  console.log('myButton clicked!');\n"\
    "});"
  ) );
}


程式開頭綁定"Run"的事件與初始化 QJSEngine ,接著就是在 JavaScript cript 產生對應的變數,變數名為"myButton",要產生這樣的變數要透過 QJSEngine::newQObject() 來產生,參數的型態是 QObject ,由於大部份的 Widget 都繼承 QObject ,所以直接輸入就可以,不需要轉型,變數會透過 QJSEngine::globalObject() 來取得 JavaScript 的 root object ,接著透過 QJSValue::setProperty() 來新增變數就完成新增 Widget 的變數。最後的部分是新增 JavaScript 的預設程式碼 ,程式碼直接透過"myButton"來控制,範例透過"text"來控制按鈕顯示的字串,接著是事件的綁定,跟 C++ 不太一樣,要透過 (事件名稱).connect() 來綁定事件,這裡單純就顯示偵錯訊息。


  這次的範例只是簡單的應用,重點放在如何產生變數讓 JavaScript 來控制,至於要如何控制的部分目前沒找到可以查找可以用的變數(像是"text"),似乎只能用猜的(跟在設計介面的參數同名),以後有機會再多多研究。


參考資料

[ doc.qt.io ] QJSEngine Class


相關文章與資料

[ GitLab ] HelloQt

在 Qt 環境使用 JavaScript

2020年10月20日 星期二

在 Python 中使用 Qt

在 Python 中使用 Qt

前言

  Qt 不只提供 C++ 使用,也提供 Python 來使用,不過使用需要一些前置作業,在此把學習的過程做個紀錄。


內容

  在 Python 裡使用 Qt 必須先安裝 Qt 相關套件,這些套件會安裝在 Python 的套件管理,透過以下命令來安裝

pip3 install PyQt5
pip3 install pyqt5-tools


pip3 是 Python 自帶的套件管理,在 Windows 時並不能直接執行,這個命令會在"(安裝目錄)/Scripts",安裝完後到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑

(HelloQt' directory)/PyQt/Basic ,這次的專案檔的附檔名為".pyproject",和 C++ 的".pro"不一樣。
如果要自己創建一個專案的話請依下圖操作
創建專案的操作


接著為專案命名,如下圖
專案的命名


這裡取好專案名後就下一步到下一步驟
設定預設專案的類別

這裡需要設定設定預設專案的名稱,這個名稱會關係到專案檔的名稱,這個預設的類別會用不到,所以不用在乎類別的名稱,要在乎只有專案名稱,接著到下一步驟
完成創建專案精靈

在完成專案後需要在創建一個名為"MainWindow"的類別,創建的步驟如下
創建類別精靈


依上圖操作後到下一步
設定類別名稱與相關設定

步驟2選擇"PyQt5",繼承類別選擇"QMainWindow",接著到下一步驟
完成創建類別精靈

在創建完類別後還要在創建一個".ui"的設計檔,有這個設計檔可以在 Qt Creator 裡直接編輯 UI,就像 C++ 使用 Qt 一樣,操作如下
創建設計檔的精靈

操作完到下一步
選擇設計檔的樣本

選擇"MainWindow"後就到下一步
設定設計檔的名稱

命好設計檔檔名後到下一步
完成新增設計檔精靈


完成後在專案的狀況如下
完成上述的新增後專案的狀況

 
雙擊畫面的"mainwindow.ui"可以直接到設計介面修改設計,範例的設計如下
範例的設計

範例只新增一個 PushButton ,接著看到"mainwindow.py",程式碼如下
# This Python file uses the following encoding: utf-8
from PyQt5 import QtCore , QtWidgets , uic
from PyQt5.QtCore import pyqtSlot , qDebug

class MainWindow( QtWidgets.QMainWindow ):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        super().__init__()
        uic.loadUi("mainwindow.ui", self)
        #BindEvent
        self.pushButton.clicked.connect( self.onPushButtonClicked )
    @pyqtSlot()
    def onPushButtonClicked(self):
        qDebug( 'Hello' )

在建構式裡會透過"uic.load()"來讀取設計檔,接著透過"connect()"來綁定事件,在要綁定的 Function 前要加"@pyqtSlot()",這個地方跟 C++ 的格式很像,事件的內容就單純列應偵錯訊息,接著看到"pyqt_basic.py",程式碼如下
# This Python file uses the following encoding: utf-8
import sys
from PyQt5 import QtWidgets
from MainWindow import MainWindow
#Follow code add by wizard,but it will not used
#class PyQt_Basic:
#    def __init__(self):
#        pass  # call __init__(self) of the custom base class here

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    #Follow code add by wizard,but it will not used
    # window = PyQt_Basic()
    ## window.show()
    #
    mainWin = MainWindow()
    mainWin.show()
    sys.exit(app.exec_())

開頭有個類別"PyQt_Basic",這是由精靈產生的類別,但由於用不到救註解掉。在主程式的開頭有精靈產生程式碼,由於用不到一樣註解掉,接著將"MainWindow"建出來,並喚起"show()"來顯示即可。

2020年10月13日 星期二

處理 TreeView 的 Drag 與 Drop

處理 TreeView 的 Drag 與 Drop

前言

  在先前的 TreeView 的基本應用 與 在 TreeView 使用右鍵選單 裡介紹了 TreeView 的常用功能,但少提到了一個常用的功能,就是 Drag 與 Drop ,節點的資料能夠直接在 Widget 上編輯是一個非常方便的功能,在此把學習的過程做個紀錄。


內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/TreeView/DragAndDrop,執行結果如下

範例的執行結果


範例會給予預設的節點,並可以在 Widget 上透過拖曳(Drag)來編輯節點。這次雖然只是處理 Drag 與 Drop 的事件,但不幸的是 Qt 並沒有提供透過 Connect 事件的方式來處理,這次使用的方式是透過"繼承"的方式來處理 Drag 與 Drop 的事件,這種透過繼承的方式可以參考 特製 Widget ,兩者的差別是該篇繼承的是 QWidget ,而本篇繼承的是 QTreeView,在新增 Class 的精靈介面可以參考下圖

新增繼承 QTreeView 的類別


精靈介面加完後的宣告程式碼並不完整,像是 include 的相關程式碼並不會自動被加入,可以比對範例的宣告來看差異是什麼,接著就直接看到宣告的部分,程式碼如下

#include <QTreeView>
#include <QVector>
#include <QDragEnterEvent>
#include <QDragMoveEvent>
#include <QDragLeaveEvent>
#include <QDropEvent>
#include <QStandardItemModel>
class MyTreeView : public QTreeView
{
  Q_OBJECT
public:
  explicit MyTreeView(QWidget *parent = nullptr);
  //
  void CustomSetModel(QStandardItemModel* pModel);
  void HideColumnByList();
protected:
  void dragEnterEvent(QDragEnterEvent *event) override;
  void dragMoveEvent(QDragMoveEvent *event) override;
  void dragLeaveEvent(QDragLeaveEvent *event) override;
  void dropEvent(QDropEvent *event) override;
//
private:
  QVector<int> m_cHideColumnList;
};

處理 Drag 與 Drop 的事件有 dragEngterEvent() 、 dragMoveEvent() 、 dragLeaveEvent() 與 dropEvent() , CustomSetModel() 與 HideColumnByList() 是自己定義的 Function ,待會兒在實作的部分會進一步說明它們的功能,接下來就看到實作的部分,先看到自訂的 Function ,如下

void MyTreeView::CustomSetModel(QStandardItemModel *pModel)
{
  this->setModel( pModel );
  HideColumnByList();
}

void MyTreeView::HideColumnByList()
{
  for( auto i = 0 ; i < m_cHideColumnList.size() ; i++ )
  {
    this->hideColumn( m_cHideColumnList[i] );
  }
}


HideColumnByList() 的部分會依據成員"m_cHideColumn"裡所存的要隱藏的 Column 來喚起 QTreeView::hideColumn() ,為什麼要提供這個 Function 呢?因為 QTreeView 在處理某些事件後會把隱藏的 Column 再次變成顯示 ,所以需要這個 Function 來恢復我們期望隱藏的 Column ,CustomSetModel() 會在喚起 QTreeView::setModel() 後喚起 HideColumnByList() 隱藏指定的 Column ,接著看到主程式的 MainWindow::MainWindow() ,如下

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  //
  QStandardItemModel* pModel = new QStandardItemModel( ui->treeView );
  pModel->setHorizontalHeaderLabels( QStringList()<<QStringLiteral( "Name" )<<QStringLiteral( "Comment" ) );
  QStandardItem* pItemRoot1 = new QStandardItem( QStringLiteral( "root1" ) );
  //first
  pItemRoot1->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
  pItemRoot1->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
  //second
  pItemRoot1->setChild( 1 , 0 , new QStandardItem( QStringLiteral( "child1" ) ) );
  //
  pModel->appendRow( pItemRoot1 );

  //
  QStandardItem* pItemRoot2 = new QStandardItem( QStringLiteral( "root2" ) );
  pItemRoot2->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
  pItemRoot2->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
  //
  pModel->appendRow( pItemRoot2 );

  //
  ui->treeView->CustomSetModel( pModel );
}


前面就單純的加入預設節點不再贅述,可以參考 TreeView 的基本應用 ,重點在最後一行使用的是 MyTreeView::CustomSetModel() 而非 QTreeView::setModel() ,這是因為這樣可以隱藏指定的 Column 。接著看到 MyTreeView::MyTreeView() ,如下

MyTreeView::MyTreeView(QWidget *parent):
  QTreeView(parent)
{
  this->setDragEnabled( true );
  this->setDragDropMode( QAbstractItemView::InternalMove );
  //
  this->setHeaderHidden( true );
  this->setSelectionMode( QAbstractItemView::ExtendedSelection );
  //
  m_cHideColumnList.push_back( 1 );
}


由於 QTreeView 預設是把 Drag 與 Drop 的功能關閉,所以透過 setDragEnabled() 與 setDragDropMode() 來開啟,接著透過 setHeaderHidden() 來關閉上方的欄位,然後透過 setSelectionMode() 來開啟可多選,最後在成員"m_cHideColumnList"加入要隱藏的 Column ,最後就直接看到 Drag 和 Drop 事件的實作,如下

void MyTreeView::dragEnterEvent(QDragEnterEvent *event)
{
  qDebug( "dragEnterEvent" );
  //
  QTreeView::dragEnterEvent( event );
}

void MyTreeView::dragMoveEvent(QDragMoveEvent *event)
{
  qDebug( "dragMoveEvent" );
  //
  QTreeView::dragMoveEvent( event );
}

void MyTreeView::dragLeaveEvent(QDragLeaveEvent *event)
{
  qDebug( "dragLeaveEvent" );
  //
  QTreeView::dragLeaveEvent( event );
}
void MyTreeView::dropEvent(QDropEvent *event)
{
  qDebug( "dropEvent" );
  QModelIndex index = indexAt(event->pos() );
  qDebug( "dropEvent node:%s" , index.data().toString().toLocal8Bit().data() );

  DropIndicatorPosition dropIndicator = dropIndicatorPosition();
  switch ( dropIndicator ) {
  case DropIndicatorPosition::OnItem:
    qDebug("onItem");
    break;
  case DropIndicatorPosition::AboveItem:
    qDebug("AboveItem");
    break;
  case DropIndicatorPosition::BelowItem:
    qDebug("BelowItem");
    break;
  defadefault:
    qDebug("Unknown DropIndicatorPosition!");
    break;
  }
  //
  for( auto i = 0 ; i < this->selectedIndexes().size() ; i++ )
  {
    if( this->selectedIndexes().at(i).column() != 0 )
      continue;
    //
    qDebug( "source node:%s" , this->selectedIndexes().at(i).data().toString().toLocal8Bit().data() );
  }
  //
  HideColumnByList();
  //
  QTreeView::dropEvent(event);
}


 dragEngterEvent() 、 dragMoveEvent() 與 dragLeaveEvent() 由於這次用不到,所以只列印偵錯訊息,這樣方便了解事件觸發的時機,所以範例重點會放在 dropEvent() ,這裡說明一下,範例希望得到的資訊是把那些節點丟到某個節點 的相關資訊,但不幸的是 QDropEvent 裡所提供的資訊並不完整,必須搭配 QTreeView 的相關 Funciton 才能達到目的,要得到被丟到的節點資訊 可以透過 QTreeEvent::Pos() 與 QTreeView::indexAt() 來的到資料,但丟到某個節點的想法有點瑕疵,如果丟到 root 那這個節點會是無效的嗎?答案是不會,因為 Qt 的這裡設計得不是很直覺, QTreeView 提供 DropIndicatorPoisition 來說明"丟到這個節點的裡面"、"丟到這個節點的前面"與"丟到這個節點的後面",來解說要丟到哪個節點的詳細狀況,接著來接解決如何取得要丟的節點,這個資訊會存在 QTreeView::selectedIndexs() ,其實就是最後選擇的節點,因為之前有開啟多選,所以這裡可能會有多個節點,最後透過 HideColumnByList() 來隱藏指定的 Column ,最後的 QTreeView::dropEvent() 是執行 DropEvent 的預設行為,但這個預設行為並不包含把節點的資料變成丟完後的資料,整個 dropEvent() 會發生在資料被修改完之前,所以要注意這個事件是發生在丟完資料之前,也就是 QTreeView 的樹節點資料都還沒改變時。


參考資料

[ doc.qt.io ] QTreeView Class


相關文章與資料

[ GitLab ] HelloQt

特製 Widget

TreeView 的基本應用

在 TreeView 使用右鍵選單

2020年10月6日 星期二

在 Qt 方便顯示偵錯訊息的作法

 在 Qt 方便顯示偵錯訊息的作法 

前言

  在 [ GitLab ] HelloQt 的範例裡常常會用 qDebug() 來顯示,用起來類似 printf() ,但由於 Qt 有一些類別是自己定義的,像是 QString ,如果使用 qDebug() 來顯示會讓程式看起來很長,這次找到有變短的方法,在此做個紀錄。


內容

  在 [ GitLab ] HelloQt 的範例要顯示 QString 的內容在偵錯訊息會使用如下

QString str( "I'm a string" );
qDebug( "%s" , str.toLocal8Bit().data() );


可以看到要顯示 QString 需要取得字串指標時需要打很長的字,是否有比較簡單的方法呢?答案是肯定的,可以用以下

#include <QDebug>
//
QString str( "I'm a string" );
qDebug() << str;


這個方法需要 include <QDebug> ,接著就可以像 std::cout 一樣來顯示訊息,用起來相當方便。

 

參考資料

[ doc.qt.io ] QDebug Class


相關文章與資料

[ GitLab ] HelloQt

2020年9月29日 星期二

特製 Widget 的滑鼠事件

特製 Widget 的滑鼠事件

前言

  在先前的 特製 Widget 裡特製了 Widget ,這次來學習如何在特製的 Widget 裡處理滑鼠事件,在此做個紀錄。

內容

    先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/MouseEvent,ouseEvent,執行結果如下
範例的執行結果

在 Widget 上做出點擊可以看到偵錯訊息。

  由於這次的範例是依據 特製 Widget 的範例改造而來,所以不會說明如何特製一個 Widget 。特製的 Widget 是如何處理滑鼠事件呢?透過 Qt 常見的綁定事件嗎?還記得特製 Widget 時事透過繼承 QWidget 這個類別嗎?這個類別提供滑鼠事件的 Virtual function 介面,只需要做覆寫(override)的動作就可以處理滑鼠的相關事件。接著看到宣告的部分,程式碼如下
#include <QWidget>

#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
class MyWidget : public QWidget
{
  Q_OBJECT
public:
  explicit MyWidget(QWidget *parent = nullptr);
protected:
  void paintEvent(QPaintEvent *event) override;
  void mouseDoubleClickEvent(QMouseEvent *event) override;
  void mouseMoveEvent(QMouseEvent *event) override;
  void mousePressEvent(QMouseEvent *event) override;
  void mouseReleaseEvent(QMouseEvent *event) override;
  void wheelEvent(QWheelEvent *event) override;
signals:

};

滑鼠的事件介面分別為 mouseDoubleClickEvent() 、 mouseMoveEvent() 、MousePressEvent() 、mouseReleaseEvent() 與 WheelEvent() ,  mouseDoubleClickEvent() 處理雙擊事件,但要注意的是發生雙擊之前必定會發生單擊事件, mouseMoveEvent() 處理的是壓住某個按鍵後拖動的事件,MousePressEvent() 處理單擊事件, mouseReleaseEvent() 處理按鍵放開, WheelEvent() 處理耍的滾輪,由於事件處理需要用到 QMouseEvent 與 QWheelEvent ,所以記得要 include 相關檔案。看到實作的程式碼如下
void MyWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
  qDebug( "mouseDoubleClickEvent" );
}

void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
  qDebug( "mouseMoveEvent" );
}

void MyWidget::mousePressEvent(QMouseEvent *event)
{
  qDebug( "mousePressEvent" );
  //
  switch( event->button() )
  {
  case Qt::LeftButton:
    qDebug( "Left button clicked" );
    break;
  case Qt::MidButton:
    qDebug( "Middle button clicked" );
    break;
  case Qt::RightButton:
    qDebug( "Right button clicked" );
    break;
  default:
    qDebug( "Unknown button clicked" );
    break;
  }
  //
  qDebug( "PoxX:%ld PosY:%ld" ,event->x() , event->y() );
}

void MyWidget::mouseReleaseEvent(QMouseEvent *event)
{
  qDebug( "mouseReleaseEvent" );
}

void MyWidget::wheelEvent(QWheelEvent *event)
{
  qDebug( "wheelEvent" );
  qDebug( "wheel:%ld" , event->delta() );

}

滑鼠的事件都會把詳細資訊記錄在 QMouseEvent 裡,如是滑鼠左鍵或滑鼠右鍵來觸發該事件,看到 MousePressEvent() ,可以透過 QMouseEvent::button() 來取得是哪個按鍵來處理,並可透過 QMouseEvent::x() 與 QMouseEvent::y() 來取得觸發事件的位置, mouseDoubleClickEvent() 、 mouseMoveEvent()  與 mouseReleaseEvent() 處理方法類似所以就只單純顯示偵錯訊息來知道觸發事件的時機。滑鼠滾輪的事件會把詳細資訊記錄在 QWheelEvent 裡,可以透過 QWheelEvent::delta() 來取得上滾或下滾的數值,這個數值基本上都是以 120 為單位,上跟下滾是用數值的正負來表達。

  最後來說一下滾輪事件的奇怪現象,在範例看到的數值不是 120 就是 -120 ,但我在另一個比較複雜的專案使用同樣的方法來取得滾輪數值時可以看到 240 或 360 之類的數值,也許你會很直覺的認定這是滾輪速度的差別,但經我實驗發現同樣的滾動在本範例會送多次的 wheelEvent()
,數值都是 120 ,但在複雜的範例卻發動少次 wheelEvent() ,但有些數值是 240 或 360  ,所以就這實驗來看,其實看到大於 120 數值所代表的是 Qt 的事件觸發時機比較慢所造成滾輪數值會比較大。

2020年9月22日 星期二

關於 Blender 的 IK 動作不如預期的問題

 關於 Blender 的 IK 動作不如預期的問題

前言

  最近在用 Blender 的時候發現 IK (Inverse Kinematics) 的動作不如預期,但用同樣的調整方式在 Blender 的人骨架卻可以正常動作,所以我做了個實驗,在此把實驗的過程做個紀錄。


內容

  為了實驗,我拉了個能重現問題的骨架,骨架如下圖

有問題的骨架


動起來會如下圖

IK (Inverse Kinematics) 動作不正常


圖中不論如何調整 Pole Angle 都無法正常動作,經過一段實驗發現原來骨架一定要是"彎"

的才能讓 IK (Inverse Kinematics)  正常動作,如下圖

調整後的骨架


動作結果就正常,如下圖

動作正常的骨架


  Pole Target 在 Rig 時難以避免使用,由於我的 Rig 是看入門教學學來的,入門教學通常不會教骨架不能是直的,不然動作會不正常,這次的經驗學到了實務經驗的重要。


2020年9月15日 星期二

在 Qt 環境使用 JavaScript

在 Qt 環境使用 JavaScript

前言

  Qt 提供使用 JavaScript 的環境,但需要經過一些設定才能使用,為了怕日後忘記怎麼設定,在此作個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/JSEngine/Basic,執行結果如下
範例的執行結果

範例的輸入對話框可以輸入 JavaScript 的程式碼,按下右下的"Run"按鈕執行程式。在開始看程式碼之前要先看到專案檔,在開頭可以看到以下
QT       += core gui qml

紅字的部分是要自己加的,這就是使用 JavaScript 的前置作業。

  看到程式碼宣告的部分,如下
#include <QMainWindow>
//
#include <QJSEngine>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
  Q_OBJECT

public:
  MainWindow(QWidget *parent = nullptr);
  ~MainWindow();

protected slots:
  void onPushButtonClicked(bool clicked);
private:
  Ui::MainWindow *ui;
  //
  QJSEngine m_cJSEngine;
};

Qt 要使用 JavaScript 是透過 QJSEngine 這個類別來處理,也就是成員"m_cJSEngine",在使用這個類別需要先 include "QJSEngine"。範例會需要使用到  TextEdit ,如果不熟悉如何使用可以參考 TextEdit 的基本應用。接著就看到實作的部分,看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
  ui->setupUi(this);
  //
  connect( ui->pushButton , &QPushButton::clicked , this , &MainWindow::onPushButtonClicked );
  //
  m_cJSEngine.installExtensions( QJSEngine::ConsoleExtension );
  //
  ui->textEdit->setText( tr("console.log('Hello world')") );
}

開頭先綁定"Run"按鈕的事件,接著會喚起 QJSEngine::installExtensions() ,這行可以讓 QJSEngine 支援 console 相關的 Funciton ,最後是設定 TextEdit 給予預設的程式碼,接著來看按鈕事件的程式碼,如下
void MainWindow::onPushButtonClicked(bool clicked)
{
  QJSValue result = m_cJSEngine.evaluate( ui->textEdit->toPlainText() );
  if( result.isError() )
  {
    //
    QJSValue errorStr = result.property( tr("stack") );
    //Filter format string "stack:@:"
    QString errorLine = errorStr.toString().mid(8);
    //
    qDebug( "Error at line(%s) %s" , errorLine.toLocal8Bit().data() ,result.toString().toLocal8Bit().data() );
  }

}

開頭會透過 QJSEngine::evaluate() 來執行結果,引數是目前 TextEdit 的內容,這就幾乎等同在 JavaScript 裡喚起 eval() ,看到回傳的"result",類別為 QJSValue ,其實就 JavaScript 的變數,範例透過喚起 QJSValue::isError() 來確定是否發生 Exception ,接著取出"stack"這個 Property 
,取出來的變數類別依舊是 QJSValue ,這個變數會用來取得當 Exception 發生的行數,但他有一點不完美,它的內容是一個格式的文字,所以透過 mid() 來過濾掉開頭的格式文字,最後過濾完的行樹就會存在"errorLine",最後顯示偵錯訊息的部分,行數是"errorLine",而錯誤的訊息就直接存在"result",所以透過 QJSValue::toString() 就可以把錯誤訊息拿出來。

參考資料


相關文章與資料

2020年9月8日 星期二

在 Qt 使用 Direct3D10 來繪圖

在 Qt 使用 Direct3D10 來繪圖

前言

  在之前的 特製 Widget 裡有提到如何特製一個 Widget ,利用 Qt 提供的繪圖來繪製,如果要用 Direct3D 來繪圖可以嗎?答案是肯定的,在 [ github.com ] QtDirect3D 裡有提供完整的範例,但由於該專案的使用習慣跟我的習慣不太一樣,所以我將參照了該專案的範例做了一個範例,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/Widget/CustomDirect3D10Widegt,執行結果如下
範例的執行結果

畫面上的藍色區塊是用 Direct3D10 來繪製的一個特製的 Widget ,如果不熟悉如何特製 Widget 可以參考 特製 Widget 。

  為何使用 Direct3D10 ?這個理由只是因為 Direct3D10 的初始化流程較為簡單,可以用比較少的程式碼看到結果,方便於理解 Qt 是如何跟 Direct3D 合併使用。

  接著來看到特製的 Widget 的部分,宣告的程式碼如下
#include <QWidget>
#include <QTimer>
#include <QEvent>
//
#include <exception>
//
#include <d3d10.h>
#include <DirectXMath.h>
#include <D3Dcompiler.h>

class Direct3D10Widget : public QWidget
{
  Q_OBJECT
public:
  explicit Direct3D10Widget(QWidget *parent = nullptr);
  ~Direct3D10Widget();
  void release();
private:
  bool event(QEvent * event) override;
  void showEvent(QShowEvent * event) override;
  QPaintEngine *paintEngine() const override;
  void paintEvent(QPaintEvent * event) override;
  void resizeEvent(QResizeEvent * event) override;
signals:
  void widgetResized();
  void rendered();
  void d3dReady();
private Q_SLOTS:
  void onFrame();
  void onReset();
private:
  bool m_bIsTimerStart;
  QTimer m_cFrameTimer;
  //
  ID3D10Device* m_pDevice;
  IDXGISwapChain* m_pSwapChain;
  ID3D10RenderTargetView* m_pRTView;

};

比上次的 特製 Widget 還要複雜不少,這次需要一個  Timer 來做為畫面的刷新基礎,這部分不會太難理解,就是每隔一段時間觸發事件來繪圖,也就是"m_cFrameTimer", Direct3D 的初始化整個程式只需做一次,會在某個事件裡做檢查,所以需要"m_bIsTimerStart",剩下的三個成員就是 Direct3D 的相關成員。

  實作的部分先看到 Direct3DWidget::Direct3DWidget() ,程式碼如下
Direct3D10Widget::Direct3D10Widget(QWidget *parent) :
  QWidget(parent),
  m_bIsTimerStart(false),
  m_pDevice(nullptr),
  m_pSwapChain(nullptr),
  m_pRTView(nullptr)
{
  QPalette pal = palette();
  pal.setColor( QPalette::Background, Qt::black );
  setAutoFillBackground( true );
  setPalette( pal );
  //
  setFocusPolicy( Qt::StrongFocus );
  setAttribute( Qt::WA_NativeWindow );
  //
  setAttribute( Qt::WA_PaintOnScreen );
  setAttribute( Qt::WA_NoSystemBackground );
}

這裡設定 Widget 的預設的顏色與必要的屬性,接著看到 Direct3DWidget::showEvent(),程式碼如下
void Direct3D10Widget::showEvent(QShowEvent *event)
{
  if(!m_bIsTimerStart)
  {
    //
    DXGI_SWAP_CHAIN_DESC sd = {};
    sd.BufferCount = 2;
    sd.BufferDesc.Width = width();
    sd.BufferDesc.Height = height();
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = reinterpret_cast<HWND>(winId());
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;
    sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;

    UINT iCreateFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    iCreateFlags |= D3D10_CREATE_DEVICE_DEBUG;
#endif
    auto hr = D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_HARDWARE,
                                         NULL, iCreateFlags,
                                         D3D10_SDK_VERSION, &sd,
                                         &m_pSwapChain, &m_pDevice);
    if (FAILED(hr))
    {
      throw std::runtime_error("D3D10CreateDeviceAndSwapChain() has error");
    }
    //
    onReset();
    emit d3dReady();
    //
    connect(&m_cFrameTimer, &QTimer::timeout, this, &Direct3D10Widget::onFrame);
    m_cFrameTimer.start(16);
    m_bIsTimerStart=true;
  }
  //
  QWidget::showEvent(event);
}

這個事件會在 Qt 需要更新畫面時啟動, Diretc3D 的初始化就放在這裡,但由於這個事件不只觸發一次,所以需要"m_bIsTimerStart"來輔助, Direct3D 初始化完後會喚起 onRest() ,這個 Function 的功用待會再說明,接著觸發 d3dReady() ,這是給使用 Widget 時綁定的事件,接著就綁定 Timer 的事件,並透過 start() 來啟動。看到 Direct3DWidget::resizeEvent() ,程式碼如下
void Direct3D10Widget::resizeEvent(QResizeEvent *event)
{
  if( m_bIsTimerStart )
  {
    onReset();
    emit widgetResized();
  }
  //
  QWidget::resizeEvent( event );
}

這個事件會發生在 Widget 的大小改變時,一樣會喚起 onResst() ,並觸發 widgetResized()。接著看到 Direct3DWidget::onFrame() ,這個 Function 會在 Timer 觸發,程式碼如下
void Direct3D10Widget::onFrame()
{
  //
  const float clearColor[ 4 ] = { 0.0f , 0.0f , 1.0f , 1.0f  };
  m_pDevice->OMSetRenderTargets( 1, &m_pRTView, nullptr );
  m_pDevice->ClearRenderTargetView( m_pRTView , clearColor );
  //
  emit rendered();
  //
  if ( FAILED ( m_pSwapChain->Present( 1, 0 ) ) )
  {
    onReset();
  }
}

這裡就是 Direct3D 常見的把畫面清除成指定顏色的程式碼,並且觸發 rendered() ,當 Present() 失敗時喚起 onReset()。最後看到之前一直沒解釋的 Direct3DWidget::onReset() ,程式碼如下
void Direct3D10Widget::onReset()
{
  ID3D10Texture2D* pBackBuffer;
  if( m_pRTView != nullptr )
  {
    m_pRTView->Release();
    m_pRTView = nullptr;
  }

  auto hr = m_pSwapChain->ResizeBuffers( 0, width() , height() , DXGI_FORMAT_UNKNOWN , 0 );
  if (FAILED( hr ) )
  {
    throw std::runtime_error("ResizeBuffers() has error");
  }
  hr = m_pSwapChain->GetBuffer( 0, IID_PPV_ARGS( &pBackBuffer ) );
  if (FAILED( hr ) )
  {
    throw std::runtime_error("GetBuffer() has error");
  }
  hr = m_pDevice->CreateRenderTargetView( pBackBuffer , nullptr , &m_pRTView );
  if (FAILED( hr ) )
  {
    throw std::runtime_error("CreateRenderTargetView() has error");
  }
  //
  if( pBackBuffer != nullptr )
  {
    pBackBuffer->Release();
    pBackBuffer = nullptr;
  }
  //
  qDebug("wiget resize");
}

這個部分是重新製作一塊跟目前 Widget 一樣大小的繪圖緩衝區,這個部分在一般的 Direct3D 範例是不會這樣用,因為範例幾乎都是固定大小的視窗,所以這邊的處理我是第一次看到,因為做成 Widget ,難免需要改變大小,所以這裡我個人認為是最重要的部分。

  最後一個小提醒,由於 Direct3D 只能在 Windows 平台工作,所以 Qt 專案的編譯器請選擇 MSVC ,由於選擇 MSVC ,所以可以用特別的方式鏈結(Link),如下
#pragma comment(lib, "d3d10.lib")

這個就是鏈結(Link)"d3d10.lib",但這個用法只能用在 MSVC ,正統的作法應該是在 Qt 的專案檔搭配平台的參數來鏈結(Link),如不清楚請參考 Qt 的專案檔 。

參考資料


相關文章與資料

2020年9月1日 星期二

在 TreeView 使用右鍵選單

在 TreeView 使用右鍵選單

前言

  在之前的 TreeView 的基本應用 裡簡單的操作 TreeView ,但 TreeView 很容易需要可以對選取的節點做操作,這個時候就需要使用到右鍵選單,在此把學習的過程做個紀錄。

內容

  先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/TreeView/CustomMenu,執行結果如下
範例的執行結果

範例會產生基本的樹節點,在選取節點並按下右鍵可以跳出選單。

  接著來看看程式的操作,先看到 MainWindow::MainWindow() ,程式碼如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_pTreeViewCustomMenu( NULL )
{
  ui->setupUi(this);
  //
  QStandardItemModel* pModel = new QStandardItemModel(ui->treeView);
  pModel->setHorizontalHeaderLabels( QStringList()<<QStringLiteral("Name")<<QStringLiteral("Comment") );
  QStandardItem* pItemRoot1 = new QStandardItem(QStringLiteral("root1") );
  //first
  pItemRoot1->setChild( 0 ,0 ,new QStandardItem(QStringLiteral("child") ) );
  pItemRoot1->setChild( 0 ,1 ,new QStandardItem(QStringLiteral("I'm child") ) );
  //
  pModel->appendRow( pItemRoot1 );
  //
  ui->treeView->setModel( pModel );

  //
  ui->treeView->setContextMenuPolicy( Qt::CustomContextMenu );

  //
  connect( ui->treeView , &QTreeView::customContextMenuRequested , this , &MainWindow::onTreeViewCustomMenu );

  //Set up custom menu
  m_pTreeViewCustomMenu = new QMenu( this );
  QAction* pAct1 = new QAction( "Action 1" , this );
  connect( pAct1 , &QAction::triggered , this , &MainWindow::onTreeViewAction1Trigger );
  m_pTreeViewCustomMenu->addAction( pAct1 );
  QAction* pAct2 = new QAction( "Action 2" , this );
  connect( pAct2 , &QAction::triggered , this , &MainWindow::onTreeViewAction2Trigger );
  m_pTreeViewCustomMenu->addAction( pAct2 );
  QAction* pAct3 = new QAction( "Action 3" , this );
  connect( pAct3 , &QAction::triggered , this , &MainWindow::onTreeViewAction3Trigger );
  m_pTreeViewCustomMenu->addAction( pAct3 );
}

先看到初始化的部分,裡面有個成員"m_pTreeViewCustomMenu",這是一個型別為"QMenu"的指標,這個類別其實就是 Qt 用來處理右鍵選單的類別,接著會程式開始製造樹的節點並綁定到 TreeView ,然後這點很重要, TreeView 能否對右鍵有反應是需要經過設定,預設值沒辦法,所以透過"setContextMenuPolicy()"來設定,設為後還要綁定事件,讓它可以在右鍵點選後觸發事件,最後的部分是對"m_pTreeViewCustomMenu"做初始化的動作,用法類似樹的結構,每個節點都是"QAction",範例連續產生 3 個"QAction",並個別對其綁定事件來觸發行為。

  接著來看事件的實作,程式碼如下
void MainWindow::onTreeViewCustomMenu( const QPoint& point )
{
  //
  m_pTreeViewCustomMenu->popup( ui->treeView->viewport()->mapToGlobal( point ) );
}

void MainWindow::onTreeViewAction1Trigger()
{
  qDebug( "Action1 triggered" );
}

void MainWindow::onTreeViewAction2Trigger()
{
  qDebug( "Action2 triggered" );
}
void MainWindow::onTreeViewAction3Trigger()
{
  qDebug( "Action3 triggered" );
}

在"onTreeViewCustomMenu()"裡直接透過"popup()"來顯示選單,事件本身雖然會傳送一個位址過來,但該位址並不能直接用,要經過"mapToGlobal()"轉換才能使用,"QAction"的事件的部分就單純地為個別顯示偵錯訊息,如果需要知道目前選取的節點可以參考 TreeView 的基本應用 裡的按鈕事件。

參考資料

[ doc.qt.io ] QTreeView Class
[ doc.qt.io ] QMenu Class

相關文章與資料

[ GitLab ] HelloQt
TreeView 的基本應用