@things-factory/figure-ui 10.1.270 → 10.1.272
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/graphql/index.ts +2 -0
- package/client/modeller/canvas.ts +4 -2
- package/client/modeller/clips.ts +74 -4
- package/client/modeller/data-interface.ts +14 -0
- package/client/modeller/editor-styles.ts +11 -0
- package/client/modeller/editor.ts +39 -11
- package/client/modeller/fasten-form.ts +73 -3
- package/client/modeller/fasten-geometry.ts +11 -1
- package/client/modeller/figure-cavity-drawing.ts +1 -3
- package/client/modeller/figure-clips.ts +101 -8
- package/client/modeller/figure-fasten-dialog.ts +41 -30
- package/client/modeller/figure-fasten-drawing.ts +5 -4
- package/client/modeller/figure-first-save.ts +4 -3
- package/client/modeller/figure-links.ts +85 -28
- package/client/modeller/figure-preview.ts +50 -4
- package/client/modeller/figure-size-editor.ts +7 -2
- package/client/modeller/first-save.ts +8 -0
- package/client/modeller/focus-on-render.ts +4 -0
- package/client/modeller/palette.ts +27 -0
- package/client/modeller/pivot.ts +76 -0
- package/client/modeller/product-session.ts +5 -1
- package/client/modeller/size-response.ts +14 -0
- package/client/modeller/stage-model.ts +50 -6
- package/client/types.ts +2 -0
- package/dist-client/graphql/index.js +2 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +1 -0
- package/dist-client/modeller/canvas.js +3 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +23 -6
- package/dist-client/modeller/clips.js +69 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +5 -0
- package/dist-client/modeller/data-interface.js +15 -0
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +11 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +6 -0
- package/dist-client/modeller/editor.js +35 -11
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +8 -0
- package/dist-client/modeller/fasten-form.js +68 -5
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +6 -0
- package/dist-client/modeller/fasten-geometry.js +10 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.js +1 -3
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +104 -8
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +39 -24
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +5 -4
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.js +4 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -0
- package/dist-client/modeller/figure-links.js +89 -35
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +48 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.js +7 -3
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +5 -0
- package/dist-client/modeller/first-save.js +7 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/focus-on-render.d.ts +2 -0
- package/dist-client/modeller/focus-on-render.js +4 -0
- package/dist-client/modeller/focus-on-render.js.map +1 -0
- package/dist-client/modeller/links.d.ts +1 -1
- package/dist-client/modeller/palette.d.ts +14 -0
- package/dist-client/modeller/palette.js +23 -0
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/pivot.d.ts +12 -0
- package/dist-client/modeller/pivot.js +59 -0
- package/dist-client/modeller/pivot.js.map +1 -0
- package/dist-client/modeller/product-session.js +6 -2
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/size-response.d.ts +6 -0
- package/dist-client/modeller/size-response.js +14 -0
- package/dist-client/modeller/size-response.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +17 -3
- package/dist-client/modeller/stage-model.js +44 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +2 -0
- package/dist-client/types.js.map +1 -1
- package/package.json +5 -5
- package/test/browser/editing.check.mjs +6 -5
- package/test/browser/section.check.mjs +21 -0
- package/test/clips.test.ts +49 -4
- package/test/data-motion.test.ts +8 -0
- package/test/fasten-form.test.ts +57 -0
- package/test/fasten-geometry.test.ts +25 -1
- package/test/first-part-on-plane.test.ts +52 -0
- package/test/first-save.test.ts +11 -1
- package/test/pivot.test.ts +57 -0
- package/test/save-landed.test.ts +20 -1
- package/test/size-response.test.ts +10 -2
- package/test/stage-model.test.ts +23 -1
- package/translations/en.json +9 -0
- package/translations/es.json +9 -0
- package/translations/ja.json +9 -0
- package/translations/ko.json +9 -0
- package/translations/ms.json +9 -0
- package/translations/zh.json +9 -0
- package/tutorial/humanoid/README.md +22 -0
- package/tutorial/humanoid/chapters.mjs +247 -0
- package/tutorial/humanoid/encode.mjs +26 -0
- package/tutorial/humanoid/lib.mjs +197 -0
- package/tutorial/humanoid/record.mjs +65 -0
package/translations/es.json
CHANGED
|
@@ -484,6 +484,8 @@
|
|
|
484
484
|
"figure.text.pick-a-shape-above-to-start": "elija una forma arriba para añadir la primera pieza.",
|
|
485
485
|
"figure.text.placement-explained": "define el plano de anclaje en la escena 3D (suelo: AGV/transportador, techo: OHT/polipasto, centro: sensor/dron).",
|
|
486
486
|
"figure.text.pose-as-default-explained": "Escribe la postura que ha definido como valores predeterminados de los parámetros, para que la figura quede así cuando nadie le dé un valor.",
|
|
487
|
+
"figure.text.control-played": "En reproducción",
|
|
488
|
+
"figure.text.control-played-explained": "El clip {clip} está moviendo este control. Detén el clip para darle un valor.",
|
|
487
489
|
"figure.text.clear-tried-explained": "borra los valores de control probados aquí. la pose por defecto guardada no cambia y los clips vuelven a moverla.",
|
|
488
490
|
"figure.text.preview-blocked-by-errors": "no se puede mostrar la vista previa por errores de formato. consulte el motivo en la pestaña de madurez.",
|
|
489
491
|
"figure.text.put-on-floor": "Mueve la pieza una vez para que su base quede en el suelo (Y=0). No se mantiene ahí cuando cambia el tamaño; para ello, use “Seguir el tamaño, unida al suelo”.",
|
|
@@ -715,6 +717,10 @@
|
|
|
715
717
|
"figure.label.motion-own-control": "su propio control",
|
|
716
718
|
"figure.label.motion-multiplier": "multiplicador",
|
|
717
719
|
"figure.label.motion-offset": "desplazamiento",
|
|
720
|
+
"figure.label.pivot-at": "girar en",
|
|
721
|
+
"figure.label.pivot-top": "extremo superior",
|
|
722
|
+
"figure.label.pivot-middle": "centro",
|
|
723
|
+
"figure.label.pivot-bottom": "extremo inferior",
|
|
718
724
|
"figure.label.motion-through": "el eje pasa por",
|
|
719
725
|
"figure.label.motion-follows": "se mueve con {joint} × {multiplier} + {offset}",
|
|
720
726
|
"figure.label.face-opening-left": "abertura izquierda",
|
|
@@ -1020,6 +1026,7 @@
|
|
|
1020
1026
|
"figure.text.slot-id-taken": "{slot} ya es el nombre de un lugar en esta figura.",
|
|
1021
1027
|
"figure.label.placement": "sujeción",
|
|
1022
1028
|
"figure.label.motion-groups": "grupos de movimiento",
|
|
1029
|
+
"figure.label.motion-group-named": "movimiento",
|
|
1023
1030
|
"figure.label.motion-group": "grupo de movimiento",
|
|
1024
1031
|
"figure.label.no-motion-group": "sin grupo",
|
|
1025
1032
|
"figure.label.motion-group-name": "nombre",
|
|
@@ -1281,6 +1288,7 @@
|
|
|
1281
1288
|
"figure.text.conflict-other-changed": "También difieren ajustes fuera de las partes (nombre, entradas, capacidades...).",
|
|
1282
1289
|
"figure.text.conflict-latest-unread": "No se pudo leer la última versión, así que no se comparó. No se puede sobrescribir.",
|
|
1283
1290
|
"figure.button.save-as-copy": "guardar como copia",
|
|
1291
|
+
"figure.button.save-as-new-type": "guardar como un nuevo tipo de figura",
|
|
1284
1292
|
"figure.button.open-latest": "abrir la última versión en una pestaña nueva",
|
|
1285
1293
|
"figure.button.overwrite-latest": "sobrescribir con mis ediciones",
|
|
1286
1294
|
"figure.text.overwrite-latest-warning": "Lo que cambió en la última versión se reemplaza por tus ediciones y no se conserva. ¿Sobrescribir?",
|
|
@@ -1321,6 +1329,7 @@
|
|
|
1321
1329
|
"figure.button.shift-track": "desplazar",
|
|
1322
1330
|
"figure.button.copy-track-apply": "copiar",
|
|
1323
1331
|
"figure.text.copy-clip-needs-group": "Una copia mueve los mismos controles, así que solo se copia un clip que está en un grupo de movimiento.",
|
|
1332
|
+
"figure.text.copy-clip-makes-group": "Copia todas las pistas en un clip con un nombre nuevo. La copia mueve los mismos controles, así que ambos van a un grupo de movimiento nuevo con el nombre de este clip (se reproduce uno a la vez).",
|
|
1324
1333
|
"figure.label.primitive-pipe": "tubo",
|
|
1325
1334
|
"figure.label.primitive-duct": "conducto",
|
|
1326
1335
|
"figure.label.primitive-tapered-box": "caja ahusada",
|
package/translations/ja.json
CHANGED
|
@@ -484,6 +484,8 @@
|
|
|
484
484
|
"figure.text.pick-a-shape-above-to-start": "pick a shape above to add the first part.",
|
|
485
485
|
"figure.text.placement-explained": "3D シーンでの基準面を決めます (床: AGV/コンベヤ、天井: OHT/ホイスト、中心: センサー/ドローン)。",
|
|
486
486
|
"figure.text.pose-as-default-explained": "今の姿勢をパラメータの既定値として書き込みます。値が与えられないとき、この姿勢で立ちます。",
|
|
487
|
+
"figure.text.control-played": "再生中",
|
|
488
|
+
"figure.text.control-played-explained": "{clip} クリップがこの操作値を動かしています。クリップを止めると値を与えられます。",
|
|
487
489
|
"figure.text.clear-tried-explained": "プレビューで変えた操作値を消します。保存された既定の姿勢は変わらず、クリップが再び動かします。",
|
|
488
490
|
"figure.text.preview-blocked-by-errors": "形式エラーのためプレビューを表示できません。成熟度タブで理由を確認してください。",
|
|
489
491
|
"figure.text.put-on-floor": "今の位置だけ底面を床(Y=0)に合わせます。サイズが変わると追従しません。付けたままにするには「床に付けてサイズに追従」を使ってください。",
|
|
@@ -715,6 +717,10 @@
|
|
|
715
717
|
"figure.label.motion-own-control": "自分の操作値",
|
|
716
718
|
"figure.label.motion-multiplier": "倍率",
|
|
717
719
|
"figure.label.motion-offset": "オフセット",
|
|
720
|
+
"figure.label.pivot-at": "回転の中心",
|
|
721
|
+
"figure.label.pivot-top": "上端",
|
|
722
|
+
"figure.label.pivot-middle": "中央",
|
|
723
|
+
"figure.label.pivot-bottom": "下端",
|
|
718
724
|
"figure.label.motion-through": "軸が通る点",
|
|
719
725
|
"figure.label.motion-follows": "{joint} に連動 × {multiplier} + {offset}",
|
|
720
726
|
"figure.label.face-opening-left": "開口部 左",
|
|
@@ -1020,6 +1026,7 @@
|
|
|
1020
1026
|
"figure.text.slot-id-taken": "{slot} はこの図形ですでに使われている場所の名前です。",
|
|
1021
1027
|
"figure.label.placement": "取り付け",
|
|
1022
1028
|
"figure.label.motion-groups": "モーショングループ",
|
|
1029
|
+
"figure.label.motion-group-named": "動作",
|
|
1023
1030
|
"figure.label.motion-group": "モーショングループ",
|
|
1024
1031
|
"figure.label.no-motion-group": "グループなし",
|
|
1025
1032
|
"figure.label.motion-group-name": "名前",
|
|
@@ -1281,6 +1288,7 @@
|
|
|
1281
1288
|
"figure.text.conflict-other-changed": "部品以外の設定(名前・入力・能力など)も異なります。",
|
|
1282
1289
|
"figure.text.conflict-latest-unread": "最新版を読み込めなかったため比較できませんでした。上書きはできません。",
|
|
1283
1290
|
"figure.button.save-as-copy": "コピーとして保存",
|
|
1291
|
+
"figure.button.save-as-new-type": "新しいフィギュアタイプとして保存",
|
|
1284
1292
|
"figure.button.open-latest": "最新版を新しいタブで開く",
|
|
1285
1293
|
"figure.button.overwrite-latest": "自分の編集で上書き",
|
|
1286
1294
|
"figure.text.overwrite-latest-warning": "最新版で変更された内容は自分の編集に置き換えられ、残りません。上書きしますか?",
|
|
@@ -1321,6 +1329,7 @@
|
|
|
1321
1329
|
"figure.button.shift-track": "ずらす",
|
|
1322
1330
|
"figure.button.copy-track-apply": "複製",
|
|
1323
1331
|
"figure.text.copy-clip-needs-group": "複製は同じ操作値を動かすため、動作グループに入れたクリップだけ複製できます。",
|
|
1332
|
+
"figure.text.copy-clip-makes-group": "すべてのトラックを新しい名前のクリップへ複製します。複製は同じ操作値を動かすため、このクリップの名前で動作グループを作り、両方を入れます(同時に再生されるのは一つ)。",
|
|
1324
1333
|
"figure.label.primitive-pipe": "パイプ",
|
|
1325
1334
|
"figure.label.primitive-duct": "ダクト",
|
|
1326
1335
|
"figure.label.primitive-tapered-box": "先細りの箱",
|
package/translations/ko.json
CHANGED
|
@@ -484,6 +484,8 @@
|
|
|
484
484
|
"figure.text.pick-a-shape-above-to-start": "위에서 하나를 골라 첫 부품을 놓으세요.",
|
|
485
485
|
"figure.text.placement-explained": "3D 씬에서 객체의 기준면을 정합니다 (바닥: AGV/컨베이어, 천장: OHT/호이스트, 중심: 센서/드론).",
|
|
486
486
|
"figure.text.pose-as-default-explained": "현재 자세를 파라미터의 기본값으로 저장합니다. 값이 지정되지 않으면 피규어에 이 자세가 적용됩니다.",
|
|
487
|
+
"figure.text.control-played": "재생 중",
|
|
488
|
+
"figure.text.control-played-explained": "{clip} 클립이 이 조작값을 움직이고 있습니다. 클립을 멈추면 값을 줄 수 있습니다.",
|
|
487
489
|
"figure.text.clear-tried-explained": "미리 보기에서 바꾼 조작값을 지웁니다. 저장된 기본 자세는 바뀌지 않고, 클립이 다시 움직입니다.",
|
|
488
490
|
"figure.text.preview-blocked-by-errors": "형식 오류로 미리보기를 표시할 수 없습니다. 검사 탭에서 사유를 확인하세요.",
|
|
489
491
|
"figure.text.put-on-floor": "지금 위치만 바닥(Y=0)에 맞춥니다. 크기가 바뀌면 따라가지 않습니다. 계속 붙어 있게 하려면 ‘바닥에 붙여 크기 따라가기’를 쓰세요.",
|
|
@@ -715,6 +717,10 @@
|
|
|
715
717
|
"figure.label.motion-own-control": "제 조작값으로",
|
|
716
718
|
"figure.label.motion-multiplier": "배율",
|
|
717
719
|
"figure.label.motion-offset": "더하기",
|
|
720
|
+
"figure.label.pivot-at": "회전 중심",
|
|
721
|
+
"figure.label.pivot-top": "위 끝",
|
|
722
|
+
"figure.label.pivot-middle": "가운데",
|
|
723
|
+
"figure.label.pivot-bottom": "아래 끝",
|
|
718
724
|
"figure.label.motion-through": "축이 지나는 점",
|
|
719
725
|
"figure.label.motion-follows": "따라 움직임: {joint} × {multiplier} + {offset}",
|
|
720
726
|
"figure.label.face-opening-left": "개구부 왼쪽",
|
|
@@ -1020,6 +1026,7 @@
|
|
|
1020
1026
|
"figure.text.slot-id-taken": "이 피규어에서 이미 쓰는 자리 이름입니다: {slot}.",
|
|
1021
1027
|
"figure.label.placement": "붙이기",
|
|
1022
1028
|
"figure.label.motion-groups": "동작 그룹",
|
|
1029
|
+
"figure.label.motion-group-named": "동작",
|
|
1023
1030
|
"figure.label.motion-group": "동작 그룹",
|
|
1024
1031
|
"figure.label.no-motion-group": "그룹 없음",
|
|
1025
1032
|
"figure.label.motion-group-name": "이름",
|
|
@@ -1281,6 +1288,7 @@
|
|
|
1281
1288
|
"figure.text.conflict-other-changed": "부품 밖의 설정(이름·입력·능력 등)도 다릅니다.",
|
|
1282
1289
|
"figure.text.conflict-latest-unread": "최신본을 읽지 못해 비교하지 못했습니다. 덮어쓰기는 할 수 없습니다.",
|
|
1283
1290
|
"figure.button.save-as-copy": "사본으로 저장",
|
|
1291
|
+
"figure.button.save-as-new-type": "새 피규어 타입으로 저장",
|
|
1284
1292
|
"figure.button.open-latest": "최신본을 새 탭에서 열기",
|
|
1285
1293
|
"figure.button.overwrite-latest": "내 편집으로 덮어쓰기",
|
|
1286
1294
|
"figure.text.overwrite-latest-warning": "최신본에서 바뀐 내용은 내 편집으로 바뀌고 남지 않습니다. 덮어쓸까요?",
|
|
@@ -1321,6 +1329,7 @@
|
|
|
1321
1329
|
"figure.button.shift-track": "시간 밀기",
|
|
1322
1330
|
"figure.button.copy-track-apply": "복사",
|
|
1323
1331
|
"figure.text.copy-clip-needs-group": "복사본은 같은 조작값을 움직이므로 동작 묶음에 넣은 클립만 복사할 수 있습니다.",
|
|
1332
|
+
"figure.text.copy-clip-makes-group": "모든 트랙을 새 이름의 클립으로 복사합니다. 복사본은 같은 조작값을 움직이므로, 이 클립의 이름으로 동작 묶음을 만들어 둘을 함께 넣습니다(한 번에 하나만 재생).",
|
|
1324
1333
|
"figure.label.primitive-pipe": "관",
|
|
1325
1334
|
"figure.label.primitive-duct": "덕트",
|
|
1326
1335
|
"figure.label.primitive-tapered-box": "가늘어지는 상자",
|
package/translations/ms.json
CHANGED
|
@@ -484,6 +484,8 @@
|
|
|
484
484
|
"figure.text.pick-a-shape-above-to-start": "pick a shape above to add the first part.",
|
|
485
485
|
"figure.text.placement-explained": "menetapkan satah rujukan dalam adegan 3D (lantai: AGV/penghantar, siling: OHT/hois, pusat: sensor/dron).",
|
|
486
486
|
"figure.text.pose-as-default-explained": "Menulis postur semasa sebagai nilai lalai parameter, supaya figura berdiri begini apabila tiada nilai diberi.",
|
|
487
|
+
"figure.text.control-played": "Dimainkan",
|
|
488
|
+
"figure.text.control-played-explained": "Klip {clip} sedang menggerakkan kawalan ini. Hentikan klip untuk memberinya nilai.",
|
|
487
489
|
"figure.text.clear-tried-explained": "mengosongkan nilai kawalan yang dicuba di sini. pose lalai yang disimpan tidak berubah, dan klip menggerakkannya semula.",
|
|
488
490
|
"figure.text.preview-blocked-by-errors": "pratonton tidak dapat dipaparkan kerana ralat format. lihat tab kematangan untuk sebabnya.",
|
|
489
491
|
"figure.text.put-on-floor": "Mengalihkan bahagian sekali supaya bahagian bawahnya di lantai (Y=0). Ia tidak kekal di situ apabila saiz berubah; untuk itu, gunakan “Ikut saiz, dilekatkan pada lantai”.",
|
|
@@ -715,6 +717,10 @@
|
|
|
715
717
|
"figure.label.motion-own-control": "kawalan sendiri",
|
|
716
718
|
"figure.label.motion-multiplier": "pendarab",
|
|
717
719
|
"figure.label.motion-offset": "ofset",
|
|
720
|
+
"figure.label.pivot-at": "pusing pada",
|
|
721
|
+
"figure.label.pivot-top": "hujung atas",
|
|
722
|
+
"figure.label.pivot-middle": "tengah",
|
|
723
|
+
"figure.label.pivot-bottom": "hujung bawah",
|
|
718
724
|
"figure.label.motion-through": "paksi melalui",
|
|
719
725
|
"figure.label.motion-follows": "bergerak bersama {joint} × {multiplier} + {offset}",
|
|
720
726
|
"figure.label.face-opening-left": "bukaan kiri",
|
|
@@ -1020,6 +1026,7 @@
|
|
|
1020
1026
|
"figure.text.slot-id-taken": "{slot} sudah menjadi nama tempat dalam figura ini.",
|
|
1021
1027
|
"figure.label.placement": "lekatan",
|
|
1022
1028
|
"figure.label.motion-groups": "kumpulan gerakan",
|
|
1029
|
+
"figure.label.motion-group-named": "gerakan",
|
|
1023
1030
|
"figure.label.motion-group": "kumpulan gerakan",
|
|
1024
1031
|
"figure.label.no-motion-group": "tiada kumpulan",
|
|
1025
1032
|
"figure.label.motion-group-name": "nama",
|
|
@@ -1281,6 +1288,7 @@
|
|
|
1281
1288
|
"figure.text.conflict-other-changed": "Tetapan di luar bahagian (nama, input, keupayaan...) juga berbeza.",
|
|
1282
1289
|
"figure.text.conflict-latest-unread": "Versi terkini tidak dapat dibaca, jadi tidak dibandingkan. Ia tidak boleh ditulis ganti.",
|
|
1283
1290
|
"figure.button.save-as-copy": "simpan sebagai salinan",
|
|
1291
|
+
"figure.button.save-as-new-type": "simpan sebagai jenis figure baharu",
|
|
1284
1292
|
"figure.button.open-latest": "buka versi terkini dalam tab baharu",
|
|
1285
1293
|
"figure.button.overwrite-latest": "tulis ganti dengan suntingan saya",
|
|
1286
1294
|
"figure.text.overwrite-latest-warning": "Perubahan dalam versi terkini akan diganti oleh suntingan anda dan tidak disimpan. Tulis ganti?",
|
|
@@ -1321,6 +1329,7 @@
|
|
|
1321
1329
|
"figure.button.shift-track": "anjak",
|
|
1322
1330
|
"figure.button.copy-track-apply": "salin",
|
|
1323
1331
|
"figure.text.copy-clip-needs-group": "Salinan menggerakkan kawalan yang sama, jadi hanya klip dalam kumpulan gerakan boleh disalin.",
|
|
1332
|
+
"figure.text.copy-clip-makes-group": "Menyalin semua trek ke klip bernama baharu. Salinan menggerakkan kawalan yang sama, jadi kedua-duanya dimasukkan ke kumpulan gerakan baharu bernama klip ini (satu dimainkan pada satu masa).",
|
|
1324
1333
|
"figure.label.primitive-pipe": "paip",
|
|
1325
1334
|
"figure.label.primitive-duct": "salur",
|
|
1326
1335
|
"figure.label.primitive-tapered-box": "kotak tirus",
|
package/translations/zh.json
CHANGED
|
@@ -484,6 +484,8 @@
|
|
|
484
484
|
"figure.text.pick-a-shape-above-to-start": "pick a shape above to add the first part.",
|
|
485
485
|
"figure.text.placement-explained": "设置 3D 场景中的基准面 (地面: AGV/输送机,天花板: OHT/吊车,中心: 传感器/无人机)。",
|
|
486
486
|
"figure.text.pose-as-default-explained": "把当前姿态写为参数默认值。没有给值时,图形以此姿态站立。",
|
|
487
|
+
"figure.text.control-played": "播放中",
|
|
488
|
+
"figure.text.control-played-explained": "{clip} 片段正在驱动此控制值。停止片段后即可赋值。",
|
|
487
489
|
"figure.text.clear-tried-explained": "清除在预览中修改的控制值。已保存的默认姿态不变,剪辑会再次驱动它。",
|
|
488
490
|
"figure.text.preview-blocked-by-errors": "由于格式错误无法显示预览。请在成熟度标签页查看原因。",
|
|
489
491
|
"figure.text.put-on-floor": "只把当前位置的底面对齐到地面(Y=0)。尺寸变化时不会跟随。若要保持附着,请使用“附着地面并跟随尺寸”。",
|
|
@@ -715,6 +717,10 @@
|
|
|
715
717
|
"figure.label.motion-own-control": "自身控制值",
|
|
716
718
|
"figure.label.motion-multiplier": "倍率",
|
|
717
719
|
"figure.label.motion-offset": "偏移",
|
|
720
|
+
"figure.label.pivot-at": "转动中心",
|
|
721
|
+
"figure.label.pivot-top": "上端",
|
|
722
|
+
"figure.label.pivot-middle": "中间",
|
|
723
|
+
"figure.label.pivot-bottom": "下端",
|
|
718
724
|
"figure.label.motion-through": "轴经过的点",
|
|
719
725
|
"figure.label.motion-follows": "跟随 {joint} × {multiplier} + {offset}",
|
|
720
726
|
"figure.label.face-opening-left": "开口 左侧",
|
|
@@ -1020,6 +1026,7 @@
|
|
|
1020
1026
|
"figure.text.slot-id-taken": "{slot} 已是此图形中某个位置的名称。",
|
|
1021
1027
|
"figure.label.placement": "附着",
|
|
1022
1028
|
"figure.label.motion-groups": "动作组",
|
|
1029
|
+
"figure.label.motion-group-named": "动作",
|
|
1023
1030
|
"figure.label.motion-group": "动作组",
|
|
1024
1031
|
"figure.label.no-motion-group": "不分组",
|
|
1025
1032
|
"figure.label.motion-group-name": "名称",
|
|
@@ -1281,6 +1288,7 @@
|
|
|
1281
1288
|
"figure.text.conflict-other-changed": "部件以外的设置(名称、输入、能力等)也不同。",
|
|
1282
1289
|
"figure.text.conflict-latest-unread": "无法读取最新版本,因此未能比较。无法覆盖。",
|
|
1283
1290
|
"figure.button.save-as-copy": "另存为副本",
|
|
1291
|
+
"figure.button.save-as-new-type": "另存为新的 figure 类型",
|
|
1284
1292
|
"figure.button.open-latest": "在新标签页中打开最新版本",
|
|
1285
1293
|
"figure.button.overwrite-latest": "用我的编辑覆盖",
|
|
1286
1294
|
"figure.text.overwrite-latest-warning": "最新版本中的更改将被你的编辑替换且不会保留。要覆盖吗?",
|
|
@@ -1321,6 +1329,7 @@
|
|
|
1321
1329
|
"figure.button.shift-track": "平移",
|
|
1322
1330
|
"figure.button.copy-track-apply": "复制",
|
|
1323
1331
|
"figure.text.copy-clip-needs-group": "副本驱动相同的控制值,因此只能复制已放入动作组的片段。",
|
|
1332
|
+
"figure.text.copy-clip-makes-group": "将所有轨道复制为新名称的片段。副本驱动相同的控制值,因此以此片段的名称创建动作组并放入两者(同一时间只播放一个)。",
|
|
1324
1333
|
"figure.label.primitive-pipe": "管",
|
|
1325
1334
|
"figure.label.primitive-duct": "风管",
|
|
1326
1335
|
"figure.label.primitive-tapered-box": "渐变箱",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Humanoid walk/run tutorial — recorder
|
|
2
|
+
|
|
3
|
+
The lesson itself lives in operato-figure: `helps/figure/tutorial-humanoid(.ko).html`, with its videos in
|
|
4
|
+
`helps/figure/media/humanoid/`. This folder re-records those videos whenever the modeller's screens change.
|
|
5
|
+
|
|
6
|
+
- `chapters.mjs` — the eight chapters, one per section of the lesson, in the order a person works. Values here and in
|
|
7
|
+
the lesson's tables must stay the same.
|
|
8
|
+
- `lib.mjs` — the "hands": finds a button or field by the words on the screen, moves a drawn pointer there and clicks
|
|
9
|
+
or types with the real mouse and keyboard. Nothing reaches into the editor's state.
|
|
10
|
+
- `record.mjs` — opens the modeller and plays the chapters, one WebM per chapter plus its last screen as PNG.
|
|
11
|
+
- `encode.mjs` — WebM → MP4 (H.264) and PNG → poster JPG for the lesson.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. a modeller page that opens an empty figure in Korean (the recordings show the Korean interface)
|
|
15
|
+
# 2. a dry run without video: every chapter must pass
|
|
16
|
+
node tutorial/humanoid/record.mjs --url http://localhost:5179/?fresh --out ./recordings --no-video
|
|
17
|
+
# 3. record at lesson pace, then encode into the lesson's media folder
|
|
18
|
+
node tutorial/humanoid/record.mjs --url http://localhost:5179/?fresh --out ./recordings --pace 1.2
|
|
19
|
+
node tutorial/humanoid/encode.mjs ./recordings ../../../operato-application/packages/operato-figure/helps/figure/media/humanoid
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A chapter that fails leaves `<id>-failed.png` showing where the hands stopped. ffmpeg must be on the PATH.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 휴머노이드 걷기·뛰기 튜토리얼의 장(章)들 -- 교재(operato-figure helps의 tutorial-humanoid)의 절과 하나씩 맞물린다.
|
|
3
|
+
* 한 장은 사람이 하는 그대로의 순서다: 부품을 고르고, 이름과 크기를 넣고, 붙이고, 관절을 넣고, 자세를 기록한다.
|
|
4
|
+
* 장은 앞 장이 끝난 모습에서 시작하므로 처음부터 차례로 실행한다.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Where a part's fastening dialog is: the attach link of a part not yet fastened, or the change link of one that is. */
|
|
8
|
+
const openAttach = h => h.click('「다른 부품에 붙이기」 or 「바꾸기」', t => t.byWords(['button', 'a'], '다른 부품에 붙이기')[0] || t.byWords(['button', 'a'], '바꾸기')[0])
|
|
9
|
+
const inDialog = (h, words) => h.click(`dialog 「${words}」`, (t, w) => t.byWords(['button', 'span', 'label', 'div', 'text'], w).filter(e => t.inDialog(e) && t.words(e) === w).pop(), words)
|
|
10
|
+
const apply = h => h.click('the dialog\'s 「붙이기」/「적용」', t => t.query('md-filled-button').filter(b => ['붙이기', '적용'].includes(t.words(b))).pop())
|
|
11
|
+
|
|
12
|
+
async function addPart(h, shape, name, size) {
|
|
13
|
+
await h.buttonTitled(shape)
|
|
14
|
+
await h.tab('속성')
|
|
15
|
+
await h.field('이름', name)
|
|
16
|
+
await h.tab('크기')
|
|
17
|
+
if (shape === '구') {
|
|
18
|
+
await h.field('X·Z · 지름', size[0])
|
|
19
|
+
await h.field('Y · 지름', size[1])
|
|
20
|
+
} else {
|
|
21
|
+
await h.field('X', size[0])
|
|
22
|
+
await h.field('Y', size[1])
|
|
23
|
+
await h.field('Z', size[2])
|
|
24
|
+
}
|
|
25
|
+
await h.tab('속성')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function attach(h, target, face) {
|
|
29
|
+
await openAttach(h)
|
|
30
|
+
await inDialog(h, target)
|
|
31
|
+
if (face) await inDialog(h, face)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function joint(h, name, min, max, pivot) {
|
|
35
|
+
await h.tab('움직임')
|
|
36
|
+
await h.field('조작 이름', name)
|
|
37
|
+
await h.typeInto('range min', t => t.fieldAfter('범위 (deg)', 0) || t.fieldAfter('범위 (도)', 0), min)
|
|
38
|
+
await h.typeInto('range max', t => t.fieldAfter('범위 (deg)', 1) || t.fieldAfter('범위 (도)', 1), max)
|
|
39
|
+
/* 회전 중심을 이름으로: 위 끝 · 가운데 · 아래 끝 -- 숫자는 버튼이 채운다. */
|
|
40
|
+
if (pivot) await h.click(`pivot ${pivot}`, (t, p) => t.query(`md-filter-chip[data-value="${p}"]`)[0], pivot)
|
|
41
|
+
await h.button('움직임 추가')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* A pose, on the pose bar right under the controls: the clip chosen there, its moment typed, the eight controls typed,
|
|
46
|
+
* then 「현재 자세 기록」 -- no scrolling down to the clip.
|
|
47
|
+
*/
|
|
48
|
+
async function pose(h, clip, seconds, values) {
|
|
49
|
+
const bar = 'div[pose-bar]'
|
|
50
|
+
const chosen = await h.page.evaluate((b, c) => {
|
|
51
|
+
const t = window.__tut
|
|
52
|
+
const choice = t.all().find(e => e.matches && e.matches(`${b} span[clip-choices] button`) && t.words(e) === c)
|
|
53
|
+
return !choice || choice.getAttribute('aria-pressed') === 'true'
|
|
54
|
+
}, bar, clip)
|
|
55
|
+
if (!chosen) await h.click(`pose bar ${clip}`, (t, b, c) => t.all().find(e => e.matches && e.matches(`${b} span[clip-choices] button`) && t.words(e) === c && t.visible(e)), bar, clip)
|
|
56
|
+
await h.typeInto(`${clip} time`, (t, b) => t.bring(`${b} input[type=number]`), seconds, bar)
|
|
57
|
+
for (const [control, value] of Object.entries(values))
|
|
58
|
+
await h.typeInto(`control ${control}`, (t, c) => t.bring(`input[type=number][aria-label="${c}"]`), value, control)
|
|
59
|
+
await h.click(`${clip} 「현재 자세 기록」`, (t, b) => t.bring(`${b} button[remember-pose]`), bar)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const chapters = [
|
|
63
|
+
{
|
|
64
|
+
id: '01-pelvis',
|
|
65
|
+
title: '1. 시작 — 골반',
|
|
66
|
+
note: '바닥에 서는 피규어를 만들고, 몸의 중심인 골반을 880 mm 띄웁니다',
|
|
67
|
+
async run(h) {
|
|
68
|
+
await h.say('어디에 두는 설비인지 고릅니다', '사람은 바닥에 섭니다')
|
|
69
|
+
await h.click('start 바닥', t => t.byWords(['button'], '바닥')[0])
|
|
70
|
+
await h.say('첫 부품으로 상자를 놓습니다')
|
|
71
|
+
await h.click('start box', t => t.query('button[start-shape="box"]')[0])
|
|
72
|
+
await h.say('이름을 pelvis로 바꿉니다', '이름은 나중에 관절·데이터 연결에 그대로 쓰입니다')
|
|
73
|
+
await h.field('이름', 'pelvis')
|
|
74
|
+
await h.say('크기 탭에서 300 × 180 × 200 mm', 'Y가 위쪽입니다')
|
|
75
|
+
await h.tab('크기')
|
|
76
|
+
await h.field('X', 300)
|
|
77
|
+
await h.field('Y', 180)
|
|
78
|
+
await h.field('Z', 200)
|
|
79
|
+
await h.tab('속성')
|
|
80
|
+
await h.say('「바꾸기」 → 세부 → 간격 880 mm', '다리가 들어갈 자리만큼 바닥에서 띄웁니다')
|
|
81
|
+
await h.button('바꾸기')
|
|
82
|
+
await h.click('세부', t => t.byWords(['summary'], '세부')[0] || t.byWords(['summary'], '세부 · ').pop())
|
|
83
|
+
await h.typeInDialog('gap', t => t.query('input[gap]')[0], 880)
|
|
84
|
+
await apply(h)
|
|
85
|
+
await h.say('')
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: '02-body',
|
|
90
|
+
title: '2. 몸통과 머리',
|
|
91
|
+
note: '「다른 부품에 붙이기」로 부품을 쌓습니다',
|
|
92
|
+
async run(h) {
|
|
93
|
+
await h.say('상자를 하나 더 놓고 torso라고 이름 붙입니다', '340 × 500 × 200 mm')
|
|
94
|
+
await addPart(h, '상자', 'torso', [340, 500, 200])
|
|
95
|
+
await h.say('pelvis에 붙입니다', '비어 있는 윗면 가운데가 기본입니다')
|
|
96
|
+
await attach(h, 'pelvis')
|
|
97
|
+
await apply(h)
|
|
98
|
+
await h.say('구를 놓고 head — 지름 200 × 240', '구는 가로(X·Z) 지름과 세로(Y) 지름 두 가지로 정합니다')
|
|
99
|
+
await addPart(h, '구', 'head', [200, 240])
|
|
100
|
+
await attach(h, 'torso')
|
|
101
|
+
await apply(h)
|
|
102
|
+
await h.say('')
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: '03-left-leg',
|
|
107
|
+
title: '3. 왼쪽 다리와 관절',
|
|
108
|
+
note: '허벅지 → 정강이 → 발, 고관절과 무릎',
|
|
109
|
+
async run(h) {
|
|
110
|
+
await h.say('l-thigh: 130 × 420 × 140', '왼쪽(−X) 부품은 l- 로 시작합니다 — 대칭 복사가 r- 로 바꿔 줍니다')
|
|
111
|
+
await addPart(h, '상자', 'l-thigh', [130, 420, 140])
|
|
112
|
+
await h.say('pelvis에 붙이면 비어 있는 아랫면이 기본입니다')
|
|
113
|
+
await attach(h, 'pelvis')
|
|
114
|
+
await h.say('세부 → 좌우 위치 「왼쪽 끝 정렬」', '골반의 왼쪽 가장자리에 맞춥니다')
|
|
115
|
+
await h.click('세부', t => t.byWords(['summary'], '세부')[0] || t.query('details[fasten-detail] summary')[0])
|
|
116
|
+
await h.click('왼쪽 끝 정렬', t => t.byWords(['button'], '왼쪽 끝 정렬')[0])
|
|
117
|
+
await apply(h)
|
|
118
|
+
await h.say('움직임 탭: 회전 · 축 X, 이름 l-hip, −60 ~ 60°', '다른 부품에 붙은 부품은 「회전」이 기본입니다')
|
|
119
|
+
await joint(h, 'l-hip', -60, 60)
|
|
120
|
+
await h.say('l-shin: 110 × 420 × 120 → l-thigh 아랫면')
|
|
121
|
+
await addPart(h, '상자', 'l-shin', [110, 420, 120])
|
|
122
|
+
await attach(h, 'l-thigh')
|
|
123
|
+
await apply(h)
|
|
124
|
+
await h.say('무릎 l-knee: 0 ~ 130°', '+ 가 뒤로 굽는 방향입니다 (반투명 미리보기로 확인)')
|
|
125
|
+
await joint(h, 'l-knee', 0, 130)
|
|
126
|
+
await h.say('l-foot: 110 × 70 × 240 → l-shin 아랫면')
|
|
127
|
+
await addPart(h, '상자', 'l-foot', [110, 70, 240])
|
|
128
|
+
await attach(h, 'l-shin')
|
|
129
|
+
await apply(h)
|
|
130
|
+
await h.say('')
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: '04-left-arm',
|
|
135
|
+
title: '4. 왼팔과 관절',
|
|
136
|
+
note: '옆면에 붙이고 위 끝에 맞추기, 회전 중심 옮기기',
|
|
137
|
+
async run(h) {
|
|
138
|
+
await h.say('l-upperarm: 90 × 300 × 90')
|
|
139
|
+
await addPart(h, '상자', 'l-upperarm', [90, 300, 90])
|
|
140
|
+
await h.say('torso에 붙이면 비어 있는 「왼쪽 면」이 기본입니다')
|
|
141
|
+
await attach(h, 'torso')
|
|
142
|
+
await h.say('세부 → 위아래 위치 「위 끝 정렬」', '어깨 높이에 맞춥니다')
|
|
143
|
+
await h.click('세부', t => t.byWords(['summary'], '세부')[0] || t.query('details[fasten-detail] summary')[0])
|
|
144
|
+
await h.click('위 끝 정렬', t => t.byWords(['button'], '위 끝 정렬')[0])
|
|
145
|
+
await apply(h)
|
|
146
|
+
await h.say('어깨 l-shoulder: −60 ~ 60°, 회전 중심 「위 끝」', '위 끝 정렬로 붙였으니 이미 위 끝이 골라져 있습니다 — 숫자는 버튼이 채웁니다')
|
|
147
|
+
await joint(h, 'l-shoulder', -60, 60, 'top')
|
|
148
|
+
await h.say('l-forearm: 80 × 280 × 80 → l-upperarm', '세로로 긴 팔에 붙이면 아랫면이 기본입니다')
|
|
149
|
+
await addPart(h, '상자', 'l-forearm', [80, 280, 80])
|
|
150
|
+
await attach(h, 'l-upperarm')
|
|
151
|
+
await apply(h)
|
|
152
|
+
await h.say('팔꿈치 l-elbow: −130 ~ 0°', '− 가 앞으로 굽는 방향입니다')
|
|
153
|
+
await joint(h, 'l-elbow', -130, 0)
|
|
154
|
+
await h.say('')
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: '05-mirror',
|
|
159
|
+
title: '5. 대칭 복사로 오른쪽',
|
|
160
|
+
note: '아래 붙은 부품과 관절까지 한 번에',
|
|
161
|
+
async run(h) {
|
|
162
|
+
for (const part of ['l-thigh', 'l-upperarm']) {
|
|
163
|
+
await h.say(`${part} 선택 → 속성 → 더 보기 → 「X 반대편에」`, '이름 앞머리 l- → r- 가 자동으로 채워집니다')
|
|
164
|
+
await h.click(`list ${part}`, (t, p) => t.all().filter(e => t.visible(e) && e.getBoundingClientRect().left < 220 && [...e.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('').trim() === p)[0], part)
|
|
165
|
+
await h.tab('속성')
|
|
166
|
+
await h.click('더 보기', t => t.byWords(['summary', 'button', 'span'], '더 보기')[0])
|
|
167
|
+
await h.click('X 반대편에', t => t.byWords(['button'], 'X 반대편에')[0])
|
|
168
|
+
await h.say('「움직임 방향 유지」를 켜면 좌우 관절의 + 방향이 같아집니다', '걷기 자세를 좌우 같은 숫자 규칙으로 넣을 수 있습니다')
|
|
169
|
+
await h.click('움직임 방향 유지', t => t.byWords(['label'], '움직임 방향 유지')[0])
|
|
170
|
+
await h.click('X 반대편에 (confirm)', t => t.byWords(['button'], 'X 반대편에').pop())
|
|
171
|
+
}
|
|
172
|
+
await h.page.keyboard.press('Escape')
|
|
173
|
+
await h.say('정면 뷰로 확인합니다')
|
|
174
|
+
await h.click('Front view', t => t.all().find(e => e.getAttribute && e.getAttribute('title') === 'Front' && t.visible(e)))
|
|
175
|
+
await h.wait(1500)
|
|
176
|
+
await h.click('Perspective view', t => t.all().find(e => e.getAttribute && e.getAttribute('title') === 'Perspective' && t.visible(e)))
|
|
177
|
+
await h.say('')
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: '06-walk',
|
|
182
|
+
title: '6. 걷기 — 자세 4개 기록',
|
|
183
|
+
note: '시간을 옮기고, 조작값을 넣고, 「현재 자세 기록」',
|
|
184
|
+
async run(h) {
|
|
185
|
+
await h.page.keyboard.press('Escape')
|
|
186
|
+
await h.tab('움직임')
|
|
187
|
+
await h.say('클립 이름 walk, 길이 1.1초 → 「재생 추가」', '첫 트랙 하나만 만들면 나머지는 자세 기록이 만들어 줍니다')
|
|
188
|
+
await h.field('clip 이름', 'walk')
|
|
189
|
+
await h.field('길이 (초)', 1.1)
|
|
190
|
+
await h.button('재생 추가')
|
|
191
|
+
const walk = [
|
|
192
|
+
[0, { 'l-hip': -25, 'r-hip': 15, 'l-knee': 5, 'r-knee': 20, 'l-shoulder': 20, 'r-shoulder': -20, 'l-elbow': -15, 'r-elbow': -30 }, '0초 — 왼발 내딛기'],
|
|
193
|
+
[0.275, { 'l-hip': -5, 'r-hip': -10, 'l-knee': 10, 'r-knee': 55, 'l-shoulder': 0, 'r-shoulder': 0, 'l-elbow': -20, 'r-elbow': -20 }, '0.275초 — 오른발 지나가기'],
|
|
194
|
+
[0.55, { 'l-hip': 15, 'r-hip': -25, 'l-knee': 20, 'r-knee': 5, 'l-shoulder': -20, 'r-shoulder': 20, 'l-elbow': -30, 'r-elbow': -15 }, '0.55초 — 오른발 내딛기'],
|
|
195
|
+
[0.825, { 'l-hip': -10, 'r-hip': -5, 'l-knee': 55, 'r-knee': 10, 'l-shoulder': 0, 'r-shoulder': 0, 'l-elbow': -20, 'r-elbow': -20 }, '0.825초 — 왼발 지나가기']
|
|
196
|
+
]
|
|
197
|
+
for (const [at, values, words] of walk) {
|
|
198
|
+
await h.say(words, '고관절 − 는 앞으로, 무릎 + 는 뒤로 굽힘. 1.1초(끝)는 0초와 같게 자동으로 맞춰집니다')
|
|
199
|
+
await pose(h, 'walk', at, values)
|
|
200
|
+
}
|
|
201
|
+
await h.say('')
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: '07-run',
|
|
206
|
+
title: '7. 뛰기 — 클립 복사 후 자세 바꾸기',
|
|
207
|
+
note: '복사하면 걷기·뛰기가 한 동작 그룹이 됩니다',
|
|
208
|
+
async run(h) {
|
|
209
|
+
await h.say('walk의 「클립 복사」', '복사본과 함께 동작 그룹이 생깁니다 — 한 번에 하나만 재생')
|
|
210
|
+
await h.click('copy walk', t => { const top = t.clipTop('walk'); return t.query('button[copy-clip]').find(b => b.getBoundingClientRect().top > top) })
|
|
211
|
+
await h.say('이름 run, 길이 0.7초', '키 시각은 길이에 맞춰 함께 줄어듭니다')
|
|
212
|
+
await h.typeInto('walk-copy name', t => { t.clipTop('walk-copy'); return t.all().find(e => e.tagName === 'INPUT' && e.value === 'walk-copy') }, 'run')
|
|
213
|
+
await h.typeInto('run length', t => { const top = t.clipTop('run'); return t.fieldAfter('길이 (초)', 0, { after: top - 5 }) }, 0.7)
|
|
214
|
+
const run = [
|
|
215
|
+
[0, { 'l-hip': -40, 'r-hip': 25, 'l-knee': 20, 'r-knee': 75, 'l-shoulder': 40, 'r-shoulder': -45, 'l-elbow': -90, 'r-elbow': -95 }, '0초 — 왼발 착지, 팔은 90° 굽혀 크게'],
|
|
216
|
+
[0.175, { 'l-hip': -5, 'r-hip': -45, 'l-knee': 35, 'r-knee': 110, 'l-shoulder': 0, 'r-shoulder': 0, 'l-elbow': -90, 'r-elbow': -90 }, '0.175초 — 오른 무릎 높이 들기'],
|
|
217
|
+
[0.35, { 'l-hip': 25, 'r-hip': -40, 'l-knee': 75, 'r-knee': 20, 'l-shoulder': -45, 'r-shoulder': 40, 'l-elbow': -95, 'r-elbow': -90 }, '0.35초 — 오른발 착지'],
|
|
218
|
+
[0.525, { 'l-hip': -45, 'r-hip': -5, 'l-knee': 110, 'r-knee': 35, 'l-shoulder': 0, 'r-shoulder': 0, 'l-elbow': -90, 'r-elbow': -90 }, '0.525초 — 왼 무릎 높이 들기']
|
|
219
|
+
]
|
|
220
|
+
for (const [at, values, words] of run) {
|
|
221
|
+
await h.say(words)
|
|
222
|
+
await pose(h, 'run', at, values)
|
|
223
|
+
}
|
|
224
|
+
await h.say('')
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: '08-preview',
|
|
229
|
+
title: '8. 미리보기에서 재생',
|
|
230
|
+
note: '동작 그룹에서 클립을 고르고 배속을 올립니다',
|
|
231
|
+
async run(h) {
|
|
232
|
+
await h.button('미리보기')
|
|
233
|
+
await h.wait(1500)
|
|
234
|
+
await h.say('「동작」 그룹: 클립 walk, 배속 1', '복사로 만든 그룹은 「동작」이라는 이름으로 생깁니다')
|
|
235
|
+
await h.click('walk clip', t => t.query('span[clip-choices] button').find(b => t.words(b) === 'walk'))
|
|
236
|
+
const speed = await h.locate('speed', t => t.query('input[type=range][max="3"]').pop())
|
|
237
|
+
await h.drag({ x: speed.x - speed.w / 2 + 2, y: speed.y }, { x: speed.x - speed.w / 2 + speed.w / 3, y: speed.y })
|
|
238
|
+
await h.wait(4000)
|
|
239
|
+
await h.say('클립 「run」 — 같은 배속으로 바뀌어 뜁니다', '조작값 줄은 「재생 중」으로 잠깁니다')
|
|
240
|
+
await h.click('run clip', t => t.query('span[clip-choices] button').find(b => t.words(b) === 'run'))
|
|
241
|
+
await h.wait(4000)
|
|
242
|
+
await h.say('')
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
export { addPart, attach, apply, joint, pose, openAttach, inDialog }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 녹화한 장(章) 영상을 교재에 싣는 형태로 바꾼다: WebM → MP4(H.264, 웹 재생용 faststart), 장의 마지막 화면 → 포스터 JPG.
|
|
3
|
+
*
|
|
4
|
+
* node tutorial/humanoid/encode.mjs <녹화 폴더> <교재 media 폴더>
|
|
5
|
+
*
|
|
6
|
+
* 교재 media 폴더는 operato-figure 의 helps/figure/media/humanoid 이다. ffmpeg 이 PATH 에 있어야 한다.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'node:child_process'
|
|
9
|
+
import { mkdirSync, readdirSync, statSync } from 'node:fs'
|
|
10
|
+
import { join, resolve } from 'node:path'
|
|
11
|
+
|
|
12
|
+
const [from, to] = process.argv.slice(2).map(p => p && resolve(p))
|
|
13
|
+
if (!from || !to) {
|
|
14
|
+
console.error('usage: node tutorial/humanoid/encode.mjs <recordings> <media folder>')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
mkdirSync(to, { recursive: true })
|
|
18
|
+
|
|
19
|
+
for (const name of readdirSync(from).filter(n => n.endsWith('.webm')).sort()) {
|
|
20
|
+
const id = name.slice(0, -5)
|
|
21
|
+
const mp4 = join(to, `${id}.mp4`)
|
|
22
|
+
/* 1280 wide keeps the panels' words legible; crf 28 and no sound keep a chapter to a few MB. */
|
|
23
|
+
execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', join(from, name), '-vf', 'scale=1280:-2,fps=30', '-c:v', 'libx264', '-preset', 'slow', '-crf', '28', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', mp4])
|
|
24
|
+
execFileSync('ffmpeg', ['-loglevel', 'error', '-y', '-i', join(from, `${id}.png`), '-vf', 'scale=1280:-2', '-q:v', '4', join(to, `${id}.jpg`)])
|
|
25
|
+
console.log(`${id}.mp4 ${(statSync(mp4).size / 1e6).toFixed(1)} MB`)
|
|
26
|
+
}
|