@things-factory/figure-ui 10.1.44 → 10.1.45

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.
Files changed (128) hide show
  1. package/client/modeller/figure-v3-links.ts +303 -0
  2. package/client/modeller/modeller-route.ts +6 -21
  3. package/client/modeller/v3-editor.ts +44 -3
  4. package/client/modeller/v3-gizmo.ts +13 -2
  5. package/client/modeller/v3-links.ts +138 -0
  6. package/client/pages/figure-v3-modeller-page.ts +2 -5
  7. package/client/route.ts +2 -6
  8. package/dist-client/modeller/figure-v3-links.d.ts +42 -0
  9. package/dist-client/modeller/figure-v3-links.js +306 -0
  10. package/dist-client/modeller/figure-v3-links.js.map +1 -0
  11. package/dist-client/modeller/modeller-route.d.ts +4 -11
  12. package/dist-client/modeller/modeller-route.js +5 -22
  13. package/dist-client/modeller/modeller-route.js.map +1 -1
  14. package/dist-client/modeller/v3-editor.d.ts +5 -0
  15. package/dist-client/modeller/v3-editor.js +46 -2
  16. package/dist-client/modeller/v3-editor.js.map +1 -1
  17. package/dist-client/modeller/v3-gizmo.js +11 -1
  18. package/dist-client/modeller/v3-gizmo.js.map +1 -1
  19. package/dist-client/modeller/v3-links.d.ts +54 -0
  20. package/dist-client/modeller/v3-links.js +110 -0
  21. package/dist-client/modeller/v3-links.js.map +1 -0
  22. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -5
  23. package/dist-client/pages/figure-v3-modeller-page.js +2 -5
  24. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  25. package/dist-client/route.d.ts +1 -1
  26. package/dist-client/route.js +2 -5
  27. package/dist-client/route.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +4 -4
  30. package/test/ai-proposal-contract.test.ts +0 -88
  31. package/test/figure-source.test.ts +0 -102
  32. package/test/i18n-prefix-guard.test.ts +0 -24
  33. package/test/modeller-route.test.ts +14 -25
  34. package/test/part-edits.test.ts +0 -7
  35. package/test/scene-budget.test.ts +0 -7
  36. package/test/v3-gizmo.test.ts +10 -0
  37. package/test/v3-links.test.ts +123 -0
  38. package/things-factory.config.js +2 -5
  39. package/translations/en.json +6 -0
  40. package/translations/ja.json +6 -0
  41. package/translations/ko.json +6 -0
  42. package/translations/ms.json +6 -0
  43. package/translations/zh.json +6 -0
  44. package/client/modeller/angle-dial.ts +0 -70
  45. package/client/modeller/base-box.ts +0 -175
  46. package/client/modeller/figure-animations.ts +0 -1249
  47. package/client/modeller/figure-canvas.ts +0 -567
  48. package/client/modeller/figure-capabilities.ts +0 -379
  49. package/client/modeller/figure-inspector.ts +0 -2208
  50. package/client/modeller/figure-parts.ts +0 -479
  51. package/client/modeller/figure-preview.ts +0 -598
  52. package/client/modeller/figure-report.ts +0 -966
  53. package/client/modeller/figure-settings.ts +0 -328
  54. package/client/modeller/figure-side.ts +0 -272
  55. package/client/modeller/figure-thumbnail.ts +0 -123
  56. package/client/modeller/figure-view.ts +0 -152
  57. package/client/modeller/joint-edits.ts +0 -262
  58. package/client/modeller/joint-gizmo.ts +0 -180
  59. package/client/modeller/maturity.ts +0 -288
  60. package/client/modeller/new-figure-entry.ts +0 -4
  61. package/client/modeller/pose-defaults.ts +0 -67
  62. package/client/pages/figure-modeller-page.ts +0 -2082
  63. package/dist-client/modeller/angle-dial.d.ts +0 -32
  64. package/dist-client/modeller/angle-dial.js +0 -41
  65. package/dist-client/modeller/angle-dial.js.map +0 -1
  66. package/dist-client/modeller/base-box.d.ts +0 -71
  67. package/dist-client/modeller/base-box.js +0 -110
  68. package/dist-client/modeller/base-box.js.map +0 -1
  69. package/dist-client/modeller/figure-animations.d.ts +0 -159
  70. package/dist-client/modeller/figure-animations.js +0 -1132
  71. package/dist-client/modeller/figure-animations.js.map +0 -1
  72. package/dist-client/modeller/figure-canvas.d.ts +0 -150
  73. package/dist-client/modeller/figure-canvas.js +0 -506
  74. package/dist-client/modeller/figure-canvas.js.map +0 -1
  75. package/dist-client/modeller/figure-capabilities.d.ts +0 -42
  76. package/dist-client/modeller/figure-capabilities.js +0 -349
  77. package/dist-client/modeller/figure-capabilities.js.map +0 -1
  78. package/dist-client/modeller/figure-inspector.d.ts +0 -329
  79. package/dist-client/modeller/figure-inspector.js +0 -2095
  80. package/dist-client/modeller/figure-inspector.js.map +0 -1
  81. package/dist-client/modeller/figure-parts.d.ts +0 -64
  82. package/dist-client/modeller/figure-parts.js +0 -478
  83. package/dist-client/modeller/figure-parts.js.map +0 -1
  84. package/dist-client/modeller/figure-preview.d.ts +0 -138
  85. package/dist-client/modeller/figure-preview.js +0 -549
  86. package/dist-client/modeller/figure-preview.js.map +0 -1
  87. package/dist-client/modeller/figure-report.d.ts +0 -165
  88. package/dist-client/modeller/figure-report.js +0 -952
  89. package/dist-client/modeller/figure-report.js.map +0 -1
  90. package/dist-client/modeller/figure-settings.d.ts +0 -39
  91. package/dist-client/modeller/figure-settings.js +0 -317
  92. package/dist-client/modeller/figure-settings.js.map +0 -1
  93. package/dist-client/modeller/figure-side.d.ts +0 -78
  94. package/dist-client/modeller/figure-side.js +0 -317
  95. package/dist-client/modeller/figure-side.js.map +0 -1
  96. package/dist-client/modeller/figure-thumbnail.d.ts +0 -3
  97. package/dist-client/modeller/figure-thumbnail.js +0 -95
  98. package/dist-client/modeller/figure-thumbnail.js.map +0 -1
  99. package/dist-client/modeller/figure-view.d.ts +0 -118
  100. package/dist-client/modeller/figure-view.js +0 -78
  101. package/dist-client/modeller/figure-view.js.map +0 -1
  102. package/dist-client/modeller/joint-edits.d.ts +0 -49
  103. package/dist-client/modeller/joint-edits.js +0 -223
  104. package/dist-client/modeller/joint-edits.js.map +0 -1
  105. package/dist-client/modeller/joint-gizmo.d.ts +0 -34
  106. package/dist-client/modeller/joint-gizmo.js +0 -158
  107. package/dist-client/modeller/joint-gizmo.js.map +0 -1
  108. package/dist-client/modeller/maturity.d.ts +0 -55
  109. package/dist-client/modeller/maturity.js +0 -212
  110. package/dist-client/modeller/maturity.js.map +0 -1
  111. package/dist-client/modeller/new-figure-entry.d.ts +0 -2
  112. package/dist-client/modeller/new-figure-entry.js +0 -5
  113. package/dist-client/modeller/new-figure-entry.js.map +0 -1
  114. package/dist-client/modeller/pose-defaults.d.ts +0 -9
  115. package/dist-client/modeller/pose-defaults.js +0 -35
  116. package/dist-client/modeller/pose-defaults.js.map +0 -1
  117. package/dist-client/pages/figure-modeller-page.d.ts +0 -419
  118. package/dist-client/pages/figure-modeller-page.js +0 -2060
  119. package/dist-client/pages/figure-modeller-page.js.map +0 -1
  120. package/test/angle-dial.test.ts +0 -78
  121. package/test/base-box.test.ts +0 -235
  122. package/test/joint-edits.test.ts +0 -165
  123. package/test/joint-gizmo.test.ts +0 -174
  124. package/test/joint-needle.test.ts +0 -162
  125. package/test/modeller-undo-redo.test.ts +0 -33
  126. package/test/new-figure-entry.test.ts +0 -15
  127. package/test/pose-defaults.test.ts +0 -71
  128. package/test/preview-board-depth.test.ts +0 -27
@@ -1 +0,0 @@
1
- {"version":3,"file":"figure-view.js","sourceRoot":"","sources":["../../client/modeller/figure-view.ts"],"names":[],"mappings":"AAiEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,QAAQ;IACR,WAAW;IACX,SAAS;IACT,QAAQ;IACR,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;CACC,CAAA;AAGV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,IAAkB;IAC1C,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IAClD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAU,CAAA;AAEvE,qCAAqC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAElC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,SAAS,EAAE,OAAO;IAClB,iDAAiD;IACjD,aAAa,EAAE,IAAI;IACnB,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,KAAK;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,SAAS;IACnB,iBAAiB,EAAE,GAAG;IACtB,oBAAoB,EAAE,IAAI;IAC1B,gBAAgB,EAAE,IAAI;IACtB,aAAa,EAAE,GAAG;CACnB,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAA2E,EAC3E,aAAqB,EACrB,MAAc;IAEd,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAA;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;IACzF,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAAA;AAChC,CAAC","sourcesContent":["/**\n * 저작 화면의 **보기 설정**.\n *\n * ## 저장되지 않는다\n *\n * 이것은 자산이 아니라 **작업대**다. 조명을 밝게 하고 바닥을 켠 것은 그 사람이 지금\n * 보기 편한 상태일 뿐, 그 자산의 성질이 아니다. 그래서 `FigureSource` 에 안 들어간다 —\n * `toFigureSource` 는 판에서 `width` · `height` · `depth` 와 몇 가지만 집어 가고 나머지는\n * 버린다.\n *\n * 다른 사람이 같은 자산을 열면 자기 설정으로 본다. 그것이 맞다.\n *\n * ## 씬이 아는 이름을 그대로 쓴다\n *\n * 여기 이름은 전부 `model-layer` 의 상태 이름이다. 옮겨 적는 표를 두지 않는다 — 표를\n * 두면 씬이 새 설정을 받아도 이쪽이 모른다.\n *\n * 값이 바뀌면 씬이 스스로 반영한다(`three-capability` 의 `onchange`). 다만 `grid` 는\n * 한동안 3D 를 세울 때만 읽혔다 — things-scene 10.1.5 에서 고쳤다.\n */\nexport interface ViewSettings {\n /**\n * 어떤 도면에 놓인 것으로 보나 — 씬 전체의 좌표 모드.\n *\n * **도형의 `placement` 와 다른 것이다.** 도형의 것은 「이 종류는 바닥에 서나, 천장에\n * 매달리나」이고, 이것은 「지금 보고 있는 도면이 어느 쪽을 기준으로 재나」다. 바닥\n * 기준 도면에 천정 기반 OHT 를 놓으면 도면은 안 뒤집히고 OHT 가 천장 높이에 매달린다.\n *\n * 한동안 도형의 `placement` 를 이 자리에 흘려 넣었다. 모델러에 도형이 하나뿐이라\n * 그럴듯해 보였지만, 그러면 도형의 성질이 씬 전체를 뒤집는다 — 도면에서는 성립할 수가\n * 없고(도면에는 도형이 여럿이다), 저작 중에는 배치를 고를 때마다 저작자가 적어 둔\n * 높이가 전부 다른 숫자로 보였다.\n *\n * 씬의 낱말 그대로다: `floor` 바닥 기준 · `space` 중심 기준(바닥 없음) · `inverted`\n * 천장 기준. **미리보기에만 먹인다** — 편집 캔버스의 `zPos` 는 저작자가 직접 적는\n * 숫자라 그 뜻이 보기 설정에 따라 바뀌면 안 된다.\n */\n placement: 'floor' | 'space' | 'inverted'\n /**\n * 천장 높이 (mm). 천정 기반 도형을 미리보기에서 어디에 매달 것인가.\n *\n * 도형은 제가 놓일 방의 높이를 모른다. 도면은 알지만 미리보기에는 도면이 없으므로,\n * 저작자가 가정할 값을 여기서 정한다.\n */\n ceilingHeight: number\n /** 좌표 바탕(축 가이드). 원점과 눈금을 보인다. */\n grid: boolean\n /** 축 이름표(X · Y · Z). 눈금만 두고 글자를 끌 수 있다. */\n gridLabels: boolean\n /** 판의 바닥면. 좌표 바탕과 겹치므로 기본은 끈다. */\n floor: boolean\n /** Environment — 반사(IBL). 금속 재질이 무엇을 비출지 정한다. */\n environment: EnvironmentName\n /** Sky Color — 배경색. */\n skyColor: string\n /** Key Light Intensity. */\n dirLightIntensity: number\n /** Key Light — Follow Camera. 끄면 그림자 방향이 고정된다. */\n dirLightFollowCamera: boolean\n /** Key Light — Cast Shadow. */\n dirShadowEnabled: boolean\n /** Hemisphere Light Intensity — 그늘진 면을 들어올린다. */\n hemiIntensity: number\n}\n\n/**\n * 환경 프리셋 — **`scene-manager` 의 `SkyPreset` 과 같은 여덟이다.**\n *\n * 한동안 다섯만 적었다. 씬은 여덟을 다 받는데 이쪽이 셋을 몰라서 고를 수 없었다.\n * operato-board 의 3D 씬 속성 패널도 같은 여덟을 보인다.\n */\nexport const ENVIRONMENTS = [\n 'studio',\n 'warehouse',\n 'factory',\n 'office',\n 'home',\n 'sunny',\n 'cloudy',\n 'rainy'\n] as const\nexport type EnvironmentName = (typeof ENVIRONMENTS)[number]\n\n/**\n * 씬에 그대로 넣는 보기 설정 — **좌표 기준과 천장 높이는 빼고.**\n *\n * 나머지는 전부 `model-layer` 의 상태 이름이라 그대로 넣으면 씬이 알아서 반응한다.\n * 그런데 `placement` 는 편집 캔버스에 들어가면 안 되고(거기는 늘 상자 기준이다),\n * `ceilingHeight` 는 씬이 모르는 이름이다.\n *\n * **rest spread 로 넘기지 않으려고 이 함수가 있다.** 캔버스가 한동안\n * `const { grid, ..., ...light } = view` 로 나머지를 통째로 넘겼는데, 그러면 설정을\n * 하나 더할 때마다 그것이 두 화면에 **말없이** 실린다. 새로 더한 좌표 기준이 바로\n * 그 길로 캔버스에 들어갈 뻔했다.\n */\nexport function sceneLook(view: ViewSettings): Record<string, unknown> {\n const { placement, ceilingHeight, ...look } = view\n return look\n}\n\n/**\n * 씬의 좌표 기준 — **씬의 낱말 그대로 셋이다.**\n *\n * 형식의 `floor` · `center` · `ceiling` 과 짝이 맞지만 철자가 다르다. 옮겨 적는 표를 두지\n * 않는 것이 이 파일의 규칙이므로 씬 쪽을 쓴다. 화면에는 둘 다 보인다 — 「천장 기준\n * (inverted)」.\n */\nexport const SCENE_REFERENCES = ['floor', 'space', 'inverted'] as const\n\n/** 배경색 기본값. 회색이라 부품 색이 제 색으로 보인다. */\nexport const WORKBENCH = '#7c7c7c'\n\n/**\n * 처음 값.\n *\n * **지금 저작 화면이 실제로 쓰던 값 그대로다.** 설정을 만들면서 화면이 달라지면\n * 무엇이 설정 때문이고 무엇이 원래 그랬는지 갈리지 않는다.\n */\nexport const DEFAULT_VIEW: ViewSettings = {\n placement: 'floor',\n /* 공장 천장의 흔한 높이. 반도체 fab 의 OHT 레일이 대체로 이 언저리다. */\n ceilingHeight: 4000,\n grid: true,\n gridLabels: true,\n floor: false,\n environment: 'studio',\n skyColor: WORKBENCH,\n dirLightIntensity: 2.0,\n dirLightFollowCamera: true,\n dirShadowEnabled: true,\n hemiIntensity: 4.0\n}\n\n/**\n * How tall the preview board is: enough room for the tallest instance where the figure actually stands.\n *\n * A floor or center figure stands on the floor, so the tallest instance is the whole height. Only a\n * ceiling figure needs the room up to the ceiling. The board used to reach the ceiling for every figure,\n * which made a 920 tall AGV preview 4400 tall.\n *\n * This is an envelope, not a placement rule: where each instance goes is still the scene's\n * (`FigureRealObject.effectiveZPos`), and the camera fits what the scene drew.\n */\nexport function previewBoardDepth(\n figure: { placement?: 'floor' | 'ceiling' | 'center'; base: { y: number } },\n ceilingHeight: number,\n factor: number\n): number {\n const tallest = figure.base.y * factor\n const reach = figure.placement === 'ceiling' ? Math.max(ceilingHeight, tallest) : tallest\n return Math.round(reach * 1.1)\n}\n"]}
@@ -1,49 +0,0 @@
1
- import type { FigureJoint, FigurePart, JointType, Vec3 } from '@hatiolab/figure-model';
2
- import type { FigureDraft, PartModel } from './figure-source.js';
3
- /** The joint whose child is this part, if any. */
4
- export declare function jointOf(draft: FigureDraft | undefined, part: string): FigureJoint | undefined;
5
- /**
6
- * Parts a part may be attached to: every other part except itself and anything attached below it, since
7
- * either would close a loop the format refuses (`parent-cycle`).
8
- */
9
- export declare function parentChoices(parts: readonly PartModel[], part: string): string[];
10
- /**
11
- * 새 관절이 설 자리 — **부품이 붙은 부품과 맞닿는 면의 한가운데.**
12
- *
13
- * 경첩은 두 부품이 닿는 자리에 있다. 팔꿈치는 윗팔과 앞팔이 만나는 곳이고, 문은 문틀에 닿는
14
- * 모서리에 있다. 그래서 처음 값은 그 자리여야 한다.
15
- *
16
- * 한동안 **부품 아랫면 한가운데**를 썼다. 위로 쌓는 부품에는 맞는 자리인데(로봇 팔이 그렇다),
17
- * 옆에 달린 부품에는 틀린다 — 캐비닛 옆에 달린 문의 축이 문 바닥에 생긴다. 저작자는 그것을
18
- * 화면에서 보고 손으로 옮겨야 했고, 어디로 옮길지는 두 부품의 치수를 머릿속에서 빼야 나왔다.
19
- *
20
- * **가장 벌어진 축이 맞닿는 축이다.** 두 상자가 겹치는 축에서는 뺀 값이 음수이고, 마주 보는
21
- * 축에서만 0 이나 양수가 된다. 나머지 두 축은 겹치는 구간의 한가운데를 쓴다 — 문의 축은 문
22
- * 높이의 한가운데에 있지 위아래 끝에 있지 않다.
23
- *
24
- * 붙은 부품이 없으면(Figure 틀에 직접 붙는다) 아랫면 한가운데다. 그때 맞닿는 것은 바닥이다.
25
- *
26
- * ⚠ 부품의 제 회전은 보지 않는다. 비스듬히 세운 부품에서는 축에 나란한 상자로 셈한 자리가
27
- * 나오고, 그것은 **처음 값**이다 — 저작자가 옮길 수 있고, 화면의 부채꼴이 어디인지 보여 준다.
28
- */
29
- export declare function seamOrigin(part: FigurePart, parent?: FigurePart): Vec3;
30
- /**
31
- * Sets how a part moves relative to its parent, or clears the joint with `undefined`.
32
- *
33
- * - From none: adds a joint at the seam with the parent (`seamOrigin`), with a vertical axis and starting
34
- * limits, and a parameter that drives it over those limits.
35
- * - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel
36
- * is removed too, because the format refuses an empty one.
37
- * - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and
38
- * moves a plain driving parameter along with them.
39
- */
40
- export declare function setJointType(draft: FigureDraft, part: FigurePart, type: JointType | undefined, parent?: FigurePart): FigureDraft;
41
- /** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */
42
- export declare function patchJoint(draft: FigureDraft, name: string, patch: {
43
- origin?: Vec3;
44
- axis?: Vec3;
45
- limits?: {
46
- min: number;
47
- max: number;
48
- };
49
- }): FigureDraft;
@@ -1,223 +0,0 @@
1
- /*
2
- * Copyright © HatioLab Inc. All rights reserved.
3
- */
4
- /** The limits a new joint starts with. Revolute in degrees, prismatic in millimetres. */
5
- const NEW_LIMITS = {
6
- revolute: { min: -90, max: 90 },
7
- prismatic: { min: 0, max: 500 }
8
- };
9
- /** The span a parameter driving a continuous joint offers: one turn either way. */
10
- const CONTINUOUS_SPAN = { min: -360, max: 360 };
11
- /** The joint whose child is this part, if any. */
12
- export function jointOf(draft, part) {
13
- return draft?.joints?.find(joint => joint.child === part);
14
- }
15
- /**
16
- * Parts a part may be attached to: every other part except itself and anything attached below it, since
17
- * either would close a loop the format refuses (`parent-cycle`).
18
- */
19
- export function parentChoices(parts, part) {
20
- const below = new Set([part]);
21
- let grew = true;
22
- while (grew) {
23
- grew = false;
24
- for (const one of parts) {
25
- if (one.parent !== undefined && below.has(one.parent) && !below.has(one.name)) {
26
- below.add(one.name);
27
- grew = true;
28
- }
29
- }
30
- }
31
- return parts.map(one => one.name).filter(name => !!name && !below.has(name));
32
- }
33
- /** 한 축에서 부품이 차지하는 구간. */
34
- const reachOf = (part, axis) => ({
35
- min: part.transform.position[axis] - part.transform.size[axis] / 2,
36
- max: part.transform.position[axis] + part.transform.size[axis] / 2
37
- });
38
- /**
39
- * 새 관절이 설 자리 — **부품이 붙은 부품과 맞닿는 면의 한가운데.**
40
- *
41
- * 경첩은 두 부품이 닿는 자리에 있다. 팔꿈치는 윗팔과 앞팔이 만나는 곳이고, 문은 문틀에 닿는
42
- * 모서리에 있다. 그래서 처음 값은 그 자리여야 한다.
43
- *
44
- * 한동안 **부품 아랫면 한가운데**를 썼다. 위로 쌓는 부품에는 맞는 자리인데(로봇 팔이 그렇다),
45
- * 옆에 달린 부품에는 틀린다 — 캐비닛 옆에 달린 문의 축이 문 바닥에 생긴다. 저작자는 그것을
46
- * 화면에서 보고 손으로 옮겨야 했고, 어디로 옮길지는 두 부품의 치수를 머릿속에서 빼야 나왔다.
47
- *
48
- * **가장 벌어진 축이 맞닿는 축이다.** 두 상자가 겹치는 축에서는 뺀 값이 음수이고, 마주 보는
49
- * 축에서만 0 이나 양수가 된다. 나머지 두 축은 겹치는 구간의 한가운데를 쓴다 — 문의 축은 문
50
- * 높이의 한가운데에 있지 위아래 끝에 있지 않다.
51
- *
52
- * 붙은 부품이 없으면(Figure 틀에 직접 붙는다) 아랫면 한가운데다. 그때 맞닿는 것은 바닥이다.
53
- *
54
- * ⚠ 부품의 제 회전은 보지 않는다. 비스듬히 세운 부품에서는 축에 나란한 상자로 셈한 자리가
55
- * 나오고, 그것은 **처음 값**이다 — 저작자가 옮길 수 있고, 화면의 부채꼴이 어디인지 보여 준다.
56
- */
57
- export function seamOrigin(part, parent) {
58
- const { position, size } = part.transform;
59
- if (!parent)
60
- return { x: position.x, y: position.y - size.y / 2, z: position.z };
61
- const axes = ['x', 'y', 'z'];
62
- const gaps = axes.map(axis => {
63
- const mine = reachOf(part, axis);
64
- const theirs = reachOf(parent, axis);
65
- return { axis, gap: Math.max(mine.min - theirs.max, theirs.min - mine.max), mine, theirs };
66
- });
67
- const meeting = gaps.reduce((widest, one) => (one.gap > widest.gap ? one : widest));
68
- const origin = { x: position.x, y: position.y, z: position.z };
69
- for (const { axis, mine, theirs } of gaps) {
70
- if (axis === meeting.axis) {
71
- /* 맞닿는 면은 **내 면**이다. 부모 쪽 면을 쓰면 둘 사이가 벌어져 있을 때 축이 허공에 선다. */
72
- origin[axis] = mine.min >= theirs.max ? mine.min : mine.max;
73
- continue;
74
- }
75
- const from = Math.max(mine.min, theirs.min);
76
- const to = Math.min(mine.max, theirs.max);
77
- origin[axis] = from <= to ? (from + to) / 2 : position[axis];
78
- }
79
- return { x: round4(origin.x), y: round4(origin.y), z: round4(origin.z) };
80
- }
81
- const round4 = (value) => Math.round(value * 10000) / 10000;
82
- function freeName(taken, stem) {
83
- const used = new Set(taken);
84
- if (!used.has(stem))
85
- return stem;
86
- for (let n = 2;; n++)
87
- if (!used.has(`${stem}-${n}`))
88
- return `${stem}-${n}`;
89
- }
90
- /** The span the driving parameter offers for a joint: its limits, or one turn either way when it has none. */
91
- function spanOf(joint) {
92
- return joint.limits ?? CONTINUOUS_SPAN;
93
- }
94
- function drivingParameter(joint) {
95
- const span = spanOf(joint);
96
- return {
97
- name: joint.name,
98
- range: { unit: joint.type === 'prismatic' ? 'mm' : 'deg', min: span.min, max: span.max },
99
- default: 0,
100
- clip: {
101
- channels: [
102
- {
103
- target: joint.name,
104
- keys: [
105
- { at: 0, value: span.min },
106
- { at: 1, value: span.max }
107
- ]
108
- }
109
- ]
110
- }
111
- };
112
- }
113
- /**
114
- * Whether a parameter is the plain one `setJointType` made for a joint: one channel on that joint, keys at
115
- * 0 and 1 spanning the given limits. Only such a parameter follows the joint when its limits or type change;
116
- * one the author reshaped is left as written.
117
- */
118
- function isPlainDriver(parameter, joint, span) {
119
- const channels = parameter.clip?.channels ?? [];
120
- if (channels.length !== 1)
121
- return false;
122
- const [channel] = channels;
123
- if (channel.target !== joint.name || channel.path !== undefined)
124
- return false;
125
- const keys = channel.keys;
126
- return (keys.length === 2 &&
127
- keys[0].at === 0 &&
128
- keys[1].at === 1 &&
129
- keys[0].value === span.min &&
130
- keys[1].value === span.max &&
131
- parameter.range?.min === span.min &&
132
- parameter.range?.max === span.max);
133
- }
134
- /**
135
- * Sets how a part moves relative to its parent, or clears the joint with `undefined`.
136
- *
137
- * - From none: adds a joint at the seam with the parent (`seamOrigin`), with a vertical axis and starting
138
- * limits, and a parameter that drives it over those limits.
139
- * - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel
140
- * is removed too, because the format refuses an empty one.
141
- * - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and
142
- * moves a plain driving parameter along with them.
143
- */
144
- export function setJointType(draft, part, type, parent) {
145
- const joints = draft.joints ?? [];
146
- const existing = joints.find(joint => joint.child === part.name);
147
- if (type === undefined) {
148
- if (!existing)
149
- return draft;
150
- return withoutJoint(draft, existing.name);
151
- }
152
- if (!existing) {
153
- const joint = {
154
- name: freeName([...joints.map(one => one.name)], `${part.name}-joint`),
155
- child: part.name,
156
- type,
157
- origin: seamOrigin(part, parent),
158
- axis: { x: 0, y: 1, z: 0 }
159
- };
160
- if (type !== 'continuous')
161
- joint.limits = { ...NEW_LIMITS[type] };
162
- const parameters = draft.parameters ?? [];
163
- const taken = [...parameters.map(one => one.name), ...(draft.animations ?? []).map(one => one.name)];
164
- const driver = drivingParameter(joint);
165
- driver.name = freeName(taken, joint.name);
166
- return { ...draft, joints: [...joints, joint], parameters: [...parameters, driver] };
167
- }
168
- if (existing.type === type)
169
- return draft;
170
- const next = { ...existing, type };
171
- if (type === 'continuous')
172
- delete next.limits;
173
- else if (existing.type === 'continuous' || (existing.type === 'prismatic') !== (type === 'prismatic')) {
174
- next.limits = { ...NEW_LIMITS[type] };
175
- }
176
- return replaceJoint(draft, existing, next);
177
- }
178
- /** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */
179
- export function patchJoint(draft, name, patch) {
180
- const existing = draft.joints?.find(joint => joint.name === name);
181
- if (!existing)
182
- return draft;
183
- const next = { ...existing, ...patch };
184
- if (existing.type === 'continuous')
185
- delete next.limits;
186
- return replaceJoint(draft, existing, next);
187
- }
188
- function replaceJoint(draft, before, after) {
189
- const joints = (draft.joints ?? []).map(joint => (joint === before ? after : joint));
190
- const was = spanOf(before);
191
- const unitChanged = (before.type === 'prismatic') !== (after.type === 'prismatic');
192
- const spanChanged = was.min !== spanOf(after).min || was.max !== spanOf(after).max;
193
- if (!unitChanged && !spanChanged)
194
- return { ...draft, joints };
195
- const parameters = (draft.parameters ?? []).map(parameter => {
196
- if (!isPlainDriver(parameter, before, was))
197
- return parameter;
198
- const driver = drivingParameter(after);
199
- return { ...parameter, range: driver.range, clip: { ...parameter.clip, channels: driver.clip.channels } };
200
- });
201
- return { ...draft, joints, parameters };
202
- }
203
- function withoutJoint(draft, name) {
204
- const keep = (one) => (one.channels ?? []).filter(channel => channel.target !== name);
205
- const parameters = (draft.parameters ?? [])
206
- .map(parameter => ({ ...parameter, clip: { ...parameter.clip, channels: keep(parameter.clip) } }))
207
- .filter(parameter => parameter.clip.channels.length > 0);
208
- const animations = (draft.animations ?? [])
209
- .map(clip => ({ ...clip, channels: keep(clip) }))
210
- .filter(clip => clip.channels.length > 0);
211
- const joints = (draft.joints ?? []).filter(joint => joint.name !== name);
212
- const next = { ...draft };
213
- if (joints.length > 0)
214
- next.joints = joints;
215
- else
216
- delete next.joints;
217
- if (draft.parameters !== undefined)
218
- next.parameters = parameters;
219
- if (draft.animations !== undefined)
220
- next.animations = animations;
221
- return next;
222
- }
223
- //# sourceMappingURL=joint-edits.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"joint-edits.js","sourceRoot":"","sources":["../../client/modeller/joint-edits.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureJoint, FigurePart, JointType, Vec3 } from '@hatiolab/figure-model'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * Editing joints and parents (ADR-0066). Pure functions over the draft and the part list, so the inspector\n * only renders and dispatches.\n *\n * A joint is always created together with the parameter that drives it. Values reach a joint only through\n * `parameters`, and the preview draws a slider per parameter, so a joint without one could be defined but\n * never seen moving.\n */\n\ntype Parameter = NonNullable<FigureDraft['parameters']>[number]\n\n/** The limits a new joint starts with. Revolute in degrees, prismatic in millimetres. */\nconst NEW_LIMITS: Record<Exclude<JointType, 'continuous'>, { min: number; max: number }> = {\n revolute: { min: -90, max: 90 },\n prismatic: { min: 0, max: 500 }\n}\n\n/** The span a parameter driving a continuous joint offers: one turn either way. */\nconst CONTINUOUS_SPAN = { min: -360, max: 360 }\n\n/** The joint whose child is this part, if any. */\nexport function jointOf(draft: FigureDraft | undefined, part: string): FigureJoint | undefined {\n return draft?.joints?.find(joint => joint.child === part)\n}\n\n/**\n * Parts a part may be attached to: every other part except itself and anything attached below it, since\n * either would close a loop the format refuses (`parent-cycle`).\n */\nexport function parentChoices(parts: readonly PartModel[], part: string): string[] {\n const below = new Set<string>([part])\n let grew = true\n while (grew) {\n grew = false\n for (const one of parts) {\n if (one.parent !== undefined && below.has(one.parent) && !below.has(one.name)) {\n below.add(one.name)\n grew = true\n }\n }\n }\n return parts.map(one => one.name).filter(name => !!name && !below.has(name))\n}\n\ntype Span = { min: number; max: number }\n\n/** 한 축에서 부품이 차지하는 구간. */\nconst reachOf = (part: FigurePart, axis: 'x' | 'y' | 'z'): Span => ({\n min: part.transform.position[axis] - part.transform.size[axis] / 2,\n max: part.transform.position[axis] + part.transform.size[axis] / 2\n})\n\n/**\n * 새 관절이 설 자리 — **부품이 붙은 부품과 맞닿는 면의 한가운데.**\n *\n * 경첩은 두 부품이 닿는 자리에 있다. 팔꿈치는 윗팔과 앞팔이 만나는 곳이고, 문은 문틀에 닿는\n * 모서리에 있다. 그래서 처음 값은 그 자리여야 한다.\n *\n * 한동안 **부품 아랫면 한가운데**를 썼다. 위로 쌓는 부품에는 맞는 자리인데(로봇 팔이 그렇다),\n * 옆에 달린 부품에는 틀린다 — 캐비닛 옆에 달린 문의 축이 문 바닥에 생긴다. 저작자는 그것을\n * 화면에서 보고 손으로 옮겨야 했고, 어디로 옮길지는 두 부품의 치수를 머릿속에서 빼야 나왔다.\n *\n * **가장 벌어진 축이 맞닿는 축이다.** 두 상자가 겹치는 축에서는 뺀 값이 음수이고, 마주 보는\n * 축에서만 0 이나 양수가 된다. 나머지 두 축은 겹치는 구간의 한가운데를 쓴다 — 문의 축은 문\n * 높이의 한가운데에 있지 위아래 끝에 있지 않다.\n *\n * 붙은 부품이 없으면(Figure 틀에 직접 붙는다) 아랫면 한가운데다. 그때 맞닿는 것은 바닥이다.\n *\n * ⚠ 부품의 제 회전은 보지 않는다. 비스듬히 세운 부품에서는 축에 나란한 상자로 셈한 자리가\n * 나오고, 그것은 **처음 값**이다 — 저작자가 옮길 수 있고, 화면의 부채꼴이 어디인지 보여 준다.\n */\nexport function seamOrigin(part: FigurePart, parent?: FigurePart): Vec3 {\n const { position, size } = part.transform\n if (!parent) return { x: position.x, y: position.y - size.y / 2, z: position.z }\n\n const axes = ['x', 'y', 'z'] as const\n const gaps = axes.map(axis => {\n const mine = reachOf(part, axis)\n const theirs = reachOf(parent, axis)\n return { axis, gap: Math.max(mine.min - theirs.max, theirs.min - mine.max), mine, theirs }\n })\n\n const meeting = gaps.reduce((widest, one) => (one.gap > widest.gap ? one : widest))\n const origin = { x: position.x, y: position.y, z: position.z }\n\n for (const { axis, mine, theirs } of gaps) {\n if (axis === meeting.axis) {\n /* 맞닿는 면은 **내 면**이다. 부모 쪽 면을 쓰면 둘 사이가 벌어져 있을 때 축이 허공에 선다. */\n origin[axis] = mine.min >= theirs.max ? mine.min : mine.max\n continue\n }\n const from = Math.max(mine.min, theirs.min)\n const to = Math.min(mine.max, theirs.max)\n origin[axis] = from <= to ? (from + to) / 2 : position[axis]\n }\n\n return { x: round4(origin.x), y: round4(origin.y), z: round4(origin.z) }\n}\n\nconst round4 = (value: number) => Math.round(value * 10000) / 10000\n\nfunction freeName(taken: Iterable<string>, stem: string): string {\n const used = new Set(taken)\n if (!used.has(stem)) return stem\n for (let n = 2; ; n++) if (!used.has(`${stem}-${n}`)) return `${stem}-${n}`\n}\n\n/** The span the driving parameter offers for a joint: its limits, or one turn either way when it has none. */\nfunction spanOf(joint: FigureJoint): { min: number; max: number } {\n return joint.limits ?? CONTINUOUS_SPAN\n}\n\nfunction drivingParameter(joint: FigureJoint): Parameter {\n const span = spanOf(joint)\n return {\n name: joint.name,\n range: { unit: joint.type === 'prismatic' ? 'mm' : 'deg', min: span.min, max: span.max },\n default: 0,\n clip: {\n channels: [\n {\n target: joint.name,\n keys: [\n { at: 0, value: span.min },\n { at: 1, value: span.max }\n ]\n }\n ]\n }\n }\n}\n\n/**\n * Whether a parameter is the plain one `setJointType` made for a joint: one channel on that joint, keys at\n * 0 and 1 spanning the given limits. Only such a parameter follows the joint when its limits or type change;\n * one the author reshaped is left as written.\n */\nfunction isPlainDriver(parameter: Parameter, joint: FigureJoint, span: { min: number; max: number }): boolean {\n const channels = parameter.clip?.channels ?? []\n if (channels.length !== 1) return false\n const [channel] = channels\n if (channel!.target !== joint.name || channel!.path !== undefined) return false\n const keys = channel!.keys as { at: number; value: number }[]\n return (\n keys.length === 2 &&\n keys[0]!.at === 0 &&\n keys[1]!.at === 1 &&\n keys[0]!.value === span.min &&\n keys[1]!.value === span.max &&\n parameter.range?.min === span.min &&\n parameter.range?.max === span.max\n )\n}\n\n/**\n * Sets how a part moves relative to its parent, or clears the joint with `undefined`.\n *\n * - From none: adds a joint at the seam with the parent (`seamOrigin`), with a vertical axis and starting\n * limits, and a parameter that drives it over those limits.\n * - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel\n * is removed too, because the format refuses an empty one.\n * - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and\n * moves a plain driving parameter along with them.\n */\nexport function setJointType(\n draft: FigureDraft,\n part: FigurePart,\n type: JointType | undefined,\n parent?: FigurePart\n): FigureDraft {\n const joints = draft.joints ?? []\n const existing = joints.find(joint => joint.child === part.name)\n\n if (type === undefined) {\n if (!existing) return draft\n return withoutJoint(draft, existing.name)\n }\n\n if (!existing) {\n const joint: FigureJoint = {\n name: freeName([...joints.map(one => one.name)], `${part.name}-joint`),\n child: part.name,\n type,\n origin: seamOrigin(part, parent),\n axis: { x: 0, y: 1, z: 0 }\n }\n if (type !== 'continuous') joint.limits = { ...NEW_LIMITS[type] }\n\n const parameters = draft.parameters ?? []\n const taken = [...parameters.map(one => one.name), ...(draft.animations ?? []).map(one => one.name)]\n const driver = drivingParameter(joint)\n driver.name = freeName(taken, joint.name)\n\n return { ...draft, joints: [...joints, joint], parameters: [...parameters, driver] }\n }\n\n if (existing.type === type) return draft\n\n const next: FigureJoint = { ...existing, type }\n if (type === 'continuous') delete next.limits\n else if (existing.type === 'continuous' || (existing.type === 'prismatic') !== (type === 'prismatic')) {\n next.limits = { ...NEW_LIMITS[type] }\n }\n\n return replaceJoint(draft, existing, next)\n}\n\n/** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */\nexport function patchJoint(\n draft: FigureDraft,\n name: string,\n patch: { origin?: Vec3; axis?: Vec3; limits?: { min: number; max: number } }\n): FigureDraft {\n const existing = draft.joints?.find(joint => joint.name === name)\n if (!existing) return draft\n const next: FigureJoint = { ...existing, ...patch }\n if (existing.type === 'continuous') delete next.limits\n return replaceJoint(draft, existing, next)\n}\n\nfunction replaceJoint(draft: FigureDraft, before: FigureJoint, after: FigureJoint): FigureDraft {\n const joints = (draft.joints ?? []).map(joint => (joint === before ? after : joint))\n const was = spanOf(before)\n const unitChanged = (before.type === 'prismatic') !== (after.type === 'prismatic')\n const spanChanged = was.min !== spanOf(after).min || was.max !== spanOf(after).max\n\n if (!unitChanged && !spanChanged) return { ...draft, joints }\n\n const parameters = (draft.parameters ?? []).map(parameter => {\n if (!isPlainDriver(parameter, before, was)) return parameter\n const driver = drivingParameter(after)\n return { ...parameter, range: driver.range, clip: { ...parameter.clip, channels: driver.clip.channels } }\n })\n return { ...draft, joints, parameters }\n}\n\nfunction withoutJoint(draft: FigureDraft, name: string): FigureDraft {\n const keep = <T extends { channels?: { target: string }[] }>(one: T) => (one.channels ?? []).filter(channel => channel.target !== name)\n\n const parameters = (draft.parameters ?? [])\n .map(parameter => ({ ...parameter, clip: { ...parameter.clip, channels: keep(parameter.clip) } }) as Parameter)\n .filter(parameter => parameter.clip.channels.length > 0)\n const animations = (draft.animations ?? [])\n .map(clip => ({ ...clip, channels: keep(clip) }) as NonNullable<FigureDraft['animations']>[number])\n .filter(clip => clip.channels.length > 0)\n const joints = (draft.joints ?? []).filter(joint => joint.name !== name)\n\n const next: FigureDraft = { ...draft }\n if (joints.length > 0) next.joints = joints\n else delete next.joints\n if (draft.parameters !== undefined) next.parameters = parameters\n if (draft.animations !== undefined) next.animations = animations\n return next\n}\n"]}
@@ -1,34 +0,0 @@
1
- import type { FigureJoint, FigurePart, FigureSource } from '@hatiolab/figure-model';
2
- import type { FigureDraft, PartModel } from './figure-source.js';
3
- /**
4
- * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.
5
- *
6
- * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은
7
- * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.
8
- * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.
9
- *
10
- * ## 부채꼴 하나다
11
- *
12
- * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,
13
- * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.
14
- *
15
- * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로
16
- * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지
17
- * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).
18
- */
19
- /** 저작면에만 있는 이름. 부품 목록에는 없다. */
20
- export declare const JOINT_GIZMO = "__joint-gizmo";
21
- /** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */
22
- export declare function axisName(axis: {
23
- x: number;
24
- y: number;
25
- z: number;
26
- }): string | undefined;
27
- /**
28
- * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,
29
- * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**
30
- */
31
- export declare function gizmoPart(joint: FigureJoint, child: PartModel | undefined, draft: FigureDraft): PartModel | undefined;
32
- /** 미리보기에만 있는 이름. 저장 형식에는 없다. */
33
- export declare const NEEDLE = "__needle-";
34
- export declare function jointNeedles(source: FigureSource): FigurePart[];
@@ -1,158 +0,0 @@
1
- /*
2
- * Copyright © HatioLab Inc. All rights reserved.
3
- */
4
- import { partFromFigure } from './figure-source.js';
5
- /**
6
- * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.
7
- *
8
- * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은
9
- * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.
10
- * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.
11
- *
12
- * ## 부채꼴 하나다
13
- *
14
- * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,
15
- * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.
16
- *
17
- * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로
18
- * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지
19
- * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).
20
- */
21
- /** 저작면에만 있는 이름. 부품 목록에는 없다. */
22
- export const JOINT_GIZMO = '__joint-gizmo';
23
- /** 부채꼴을 몇 조각으로 그리나. 한 조각이 6도쯤이면 원이 원으로 보인다. */
24
- const STEP = 6;
25
- /** 두께. 얇아야 부품을 가리지 않고, 너무 얇으면 옆에서 볼 때 사라진다. */
26
- const THICKNESS = 6;
27
- /**
28
- * 도는 평면과 두께 방향을 축에 맞춘다.
29
- *
30
- * 부채꼴은 단면이 눕고 **제 y 방향으로** 밀려 두꺼워지는 도형이라, 기본 상태의 축이 +y 다.
31
- * 다른 축은 부품 회전으로 눕힌다 — 부품이 제 회전을 지는 것과 같은 방법이고, 형식이 이미
32
- * 아는 길이다.
33
- *
34
- * `turn` 은 단면 안에서 각을 재는 방향이다. 씬의 회전과 단면의 각이 도는 쪽이 반대인 축이
35
- * 있어서, 그것을 여기 한 곳에 적어 둔다 — 부채꼴이 실제로 도는 쪽과 반대로 벌어지면
36
- * 가리키는 뜻이 뒤집힌다.
37
- */
38
- const AXIS_LAY = {
39
- '+y': { turn: -1 },
40
- '-y': { turn: 1 },
41
- '+x': { rotation: { z: 90 }, turn: 1 },
42
- '-x': { rotation: { z: 90 }, turn: -1 },
43
- '+z': { rotation: { x: 90 }, turn: -1 },
44
- '-z': { rotation: { x: 90 }, turn: 1 }
45
- };
46
- /** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */
47
- export function axisName(axis) {
48
- const named = [
49
- ['x', axis.x],
50
- ['y', axis.y],
51
- ['z', axis.z]
52
- ];
53
- const on = named.filter(([, value]) => value !== 0);
54
- if (on.length !== 1)
55
- return undefined;
56
- const [name, value] = on[0];
57
- return `${value > 0 ? '+' : '-'}${name}`;
58
- }
59
- /** 이 관절이 도는 범위. 무한 회전은 한 바퀴다. */
60
- function span(joint) {
61
- if (joint.type === 'continuous' || !joint.limits)
62
- return { from: -180, to: 180 };
63
- return { from: joint.limits.min, to: joint.limits.max };
64
- }
65
- /** 부채꼴의 단면. 점은 부품 중심 기준의 밀리미터다. */
66
- function sector(from, to, radius, turn) {
67
- const path = [];
68
- const full = Math.abs(to - from) >= 359.999;
69
- if (!full)
70
- path.push({ x: 0, y: 0 });
71
- const steps = Math.max(2, Math.ceil(Math.abs(to - from) / STEP));
72
- for (let i = 0; i <= steps; i++) {
73
- const degrees = from + ((to - from) * i) / steps;
74
- const rad = (degrees * Math.PI * turn) / 180;
75
- path.push({ x: +(Math.cos(rad) * radius).toFixed(3), y: +(Math.sin(rad) * radius).toFixed(3) });
76
- }
77
- return path;
78
- }
79
- /**
80
- * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,
81
- * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**
82
- */
83
- export function gizmoPart(joint, child, draft) {
84
- const lay = AXIS_LAY[axisName(joint.axis) ?? ''];
85
- if (!lay)
86
- return undefined;
87
- /* 반지름은 **도는 부품의 크기**에서 온다. 부채꼴이 그 부품보다 작으면 무엇이 도는지 안 읽힌다. */
88
- const reach = child ? Math.max(child.width, child.height, child.depth) : 0;
89
- const radius = Math.max(60, Math.round(reach * 0.6));
90
- const { from, to } = span(joint);
91
- const part = {
92
- name: JOINT_GIZMO,
93
- primitive: 'polygon',
94
- transform: {
95
- position: { ...joint.origin },
96
- size: { x: radius * 2, y: THICKNESS, z: radius * 2 },
97
- ...(lay.rotation ? { rotation: lay.rotation } : {})
98
- },
99
- shape: { path: sector(from, to, radius, lay.turn) },
100
- material: { token: 'palette.accent', transparent: true }
101
- };
102
- return { ...partFromFigure(part, draft), locked: true };
103
- }
104
- /*
105
- * 미리보기에서 **관절이 돌았다는 것**을 보이게 하는 바늘.
106
- *
107
- * 상자 위에 원기둥을 얹고 그 축으로 돌리면 화면이 그대로다 — 원기둥은 제 축에서 대칭이라
108
- * 돌아도 같은 그림이다. 저작자는 막대를 76도까지 밀고도 아무것도 못 보고 「관절이 안 먹는다」로
109
- * 읽는다. 값은 제대로 갔고 부품도 돌았는데 볼 것이 없었을 뿐이다.
110
- *
111
- * 그래서 관절이 모는 부품에 얇은 막대 하나를 **자식으로** 붙인다. 부모가 관절의 자식이라
112
- * 관절이 도는 만큼 같이 돌고, 축에서 비켜나 있어 대칭인 부품에서도 움직임이 보인다. 매 프레임
113
- * 무엇을 다시 그릴 일도, 막대를 밀 때마다 씬을 다시 세울 일도 없다.
114
- *
115
- * **미리보기에만 붙인다.** 저장 형식에도, 저작면에도, 카드 그림에도 안 들어간다 — 미리보기는
116
- * 크기 반응을 보는 작업대이고 거기서만 쓰는 표시다.
117
- */
118
- /** 미리보기에만 있는 이름. 저장 형식에는 없다. */
119
- export const NEEDLE = '__needle-';
120
- /**
121
- * 막대의 굵기 — **길이에 견주어** 잡는다.
122
- *
123
- * 8mm 로 고정했더니 미리보기에서 안 보였다. 미리보기는 여덟 대를 한 화면에 세우는 자리라
124
- * 100mm 짜리 부품이 화면에서 20 픽셀쯤이고, 그 위의 8mm 는 한 픽셀이 안 된다. 세어 보면
125
- * 막대는 분명히 서 있는데(부품 둘짜리 표본의 인스턴스에 mesh 셋 · 삼각형 72) 눈에 안 띈다.
126
- */
127
- const needleThick = (radius) => Math.max(6, Math.round(radius / 5));
128
- /** 축에서 비켜날 방향. 그 축과 직각이어야 도는 것이 보인다. */
129
- const ASIDE = { x: 'z', y: 'x', z: 'x' };
130
- export function jointNeedles(source) {
131
- const byName = new Map(source.parts.map(part => [part.name, part]));
132
- return (source.joints ?? []).flatMap(joint => {
133
- const axis = axisName(joint.axis);
134
- if (!axis)
135
- return [];
136
- const child = byName.get(joint.child);
137
- const size = child?.transform.size;
138
- const reach = size ? Math.max(size.x, size.y, size.z) : 0;
139
- const radius = Math.max(60, Math.round(reach * 0.6));
140
- const aside = ASIDE[axis.slice(1)];
141
- const thick = needleThick(radius);
142
- return [
143
- {
144
- name: `${NEEDLE}${joint.name}`,
145
- primitive: 'cube',
146
- transform: {
147
- position: { ...joint.origin, [aside]: joint.origin[aside] + radius / 2 },
148
- size: aside === 'x'
149
- ? { x: radius, y: thick, z: thick }
150
- : { x: thick, y: thick, z: radius }
151
- },
152
- material: { token: 'palette.accent' },
153
- parent: joint.child
154
- }
155
- ];
156
- });
157
- }
158
- //# sourceMappingURL=joint-gizmo.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"joint-gizmo.js","sourceRoot":"","sources":["../../client/modeller/joint-gizmo.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureJoint, FigurePart, FigureSource } from '@hatiolab/figure-model'\n\nimport { partFromFigure } from './figure-source.js'\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.\n *\n * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은\n * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.\n * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.\n *\n * ## 부채꼴 하나다\n *\n * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,\n * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.\n *\n * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로\n * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지\n * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).\n */\n\n/** 저작면에만 있는 이름. 부품 목록에는 없다. */\nexport const JOINT_GIZMO = '__joint-gizmo'\n\n/** 부채꼴을 몇 조각으로 그리나. 한 조각이 6도쯤이면 원이 원으로 보인다. */\nconst STEP = 6\n\n/** 두께. 얇아야 부품을 가리지 않고, 너무 얇으면 옆에서 볼 때 사라진다. */\nconst THICKNESS = 6\n\n/**\n * 도는 평면과 두께 방향을 축에 맞춘다.\n *\n * 부채꼴은 단면이 눕고 **제 y 방향으로** 밀려 두꺼워지는 도형이라, 기본 상태의 축이 +y 다.\n * 다른 축은 부품 회전으로 눕힌다 — 부품이 제 회전을 지는 것과 같은 방법이고, 형식이 이미\n * 아는 길이다.\n *\n * `turn` 은 단면 안에서 각을 재는 방향이다. 씬의 회전과 단면의 각이 도는 쪽이 반대인 축이\n * 있어서, 그것을 여기 한 곳에 적어 둔다 — 부채꼴이 실제로 도는 쪽과 반대로 벌어지면\n * 가리키는 뜻이 뒤집힌다.\n */\nconst AXIS_LAY: Record<string, { rotation?: FigurePart['transform']['rotation']; turn: 1 | -1 }> = {\n '+y': { turn: -1 },\n '-y': { turn: 1 },\n '+x': { rotation: { z: 90 }, turn: 1 },\n '-x': { rotation: { z: 90 }, turn: -1 },\n '+z': { rotation: { x: 90 }, turn: -1 },\n '-z': { rotation: { x: 90 }, turn: 1 }\n}\n\n/** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */\nexport function axisName(axis: { x: number; y: number; z: number }): string | undefined {\n const named = [\n ['x', axis.x],\n ['y', axis.y],\n ['z', axis.z]\n ] as const\n const on = named.filter(([, value]) => value !== 0)\n if (on.length !== 1) return undefined\n const [name, value] = on[0]!\n return `${value > 0 ? '+' : '-'}${name}`\n}\n\n/** 이 관절이 도는 범위. 무한 회전은 한 바퀴다. */\nfunction span(joint: FigureJoint): { from: number; to: number } {\n if (joint.type === 'continuous' || !joint.limits) return { from: -180, to: 180 }\n return { from: joint.limits.min, to: joint.limits.max }\n}\n\n/** 부채꼴의 단면. 점은 부품 중심 기준의 밀리미터다. */\nfunction sector(from: number, to: number, radius: number, turn: 1 | -1): { x: number; y: number }[] {\n const path: { x: number; y: number }[] = []\n const full = Math.abs(to - from) >= 359.999\n\n if (!full) path.push({ x: 0, y: 0 })\n\n const steps = Math.max(2, Math.ceil(Math.abs(to - from) / STEP))\n for (let i = 0; i <= steps; i++) {\n const degrees = from + ((to - from) * i) / steps\n const rad = (degrees * Math.PI * turn) / 180\n path.push({ x: +(Math.cos(rad) * radius).toFixed(3), y: +(Math.sin(rad) * radius).toFixed(3) })\n }\n\n return path\n}\n\n/**\n * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,\n * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**\n */\nexport function gizmoPart(joint: FigureJoint, child: PartModel | undefined, draft: FigureDraft): PartModel | undefined {\n const lay = AXIS_LAY[axisName(joint.axis) ?? '']\n if (!lay) return undefined\n\n /* 반지름은 **도는 부품의 크기**에서 온다. 부채꼴이 그 부품보다 작으면 무엇이 도는지 안 읽힌다. */\n const reach = child ? Math.max(child.width, child.height, child.depth) : 0\n const radius = Math.max(60, Math.round(reach * 0.6))\n\n const { from, to } = span(joint)\n const part: FigurePart = {\n name: JOINT_GIZMO,\n primitive: 'polygon',\n transform: {\n position: { ...joint.origin },\n size: { x: radius * 2, y: THICKNESS, z: radius * 2 },\n ...(lay.rotation ? { rotation: lay.rotation } : {})\n },\n shape: { path: sector(from, to, radius, lay.turn) },\n material: { token: 'palette.accent', transparent: true }\n }\n\n return { ...partFromFigure(part, draft), locked: true }\n}\n\n/*\n * 미리보기에서 **관절이 돌았다는 것**을 보이게 하는 바늘.\n *\n * 상자 위에 원기둥을 얹고 그 축으로 돌리면 화면이 그대로다 — 원기둥은 제 축에서 대칭이라\n * 돌아도 같은 그림이다. 저작자는 막대를 76도까지 밀고도 아무것도 못 보고 「관절이 안 먹는다」로\n * 읽는다. 값은 제대로 갔고 부품도 돌았는데 볼 것이 없었을 뿐이다.\n *\n * 그래서 관절이 모는 부품에 얇은 막대 하나를 **자식으로** 붙인다. 부모가 관절의 자식이라\n * 관절이 도는 만큼 같이 돌고, 축에서 비켜나 있어 대칭인 부품에서도 움직임이 보인다. 매 프레임\n * 무엇을 다시 그릴 일도, 막대를 밀 때마다 씬을 다시 세울 일도 없다.\n *\n * **미리보기에만 붙인다.** 저장 형식에도, 저작면에도, 카드 그림에도 안 들어간다 — 미리보기는\n * 크기 반응을 보는 작업대이고 거기서만 쓰는 표시다.\n */\n\n/** 미리보기에만 있는 이름. 저장 형식에는 없다. */\nexport const NEEDLE = '__needle-'\n\n/**\n * 막대의 굵기 — **길이에 견주어** 잡는다.\n *\n * 8mm 로 고정했더니 미리보기에서 안 보였다. 미리보기는 여덟 대를 한 화면에 세우는 자리라\n * 100mm 짜리 부품이 화면에서 20 픽셀쯤이고, 그 위의 8mm 는 한 픽셀이 안 된다. 세어 보면\n * 막대는 분명히 서 있는데(부품 둘짜리 표본의 인스턴스에 mesh 셋 · 삼각형 72) 눈에 안 띈다.\n */\nconst needleThick = (radius: number) => Math.max(6, Math.round(radius / 5))\n\n/** 축에서 비켜날 방향. 그 축과 직각이어야 도는 것이 보인다. */\nconst ASIDE: Record<string, 'x' | 'z'> = { x: 'z', y: 'x', z: 'x' }\n\nexport function jointNeedles(source: FigureSource): FigurePart[] {\n const byName = new Map(source.parts.map(part => [part.name, part]))\n\n return (source.joints ?? []).flatMap(joint => {\n const axis = axisName(joint.axis)\n if (!axis) return []\n\n const child = byName.get(joint.child)\n const size = child?.transform.size\n const reach = size ? Math.max(size.x, size.y, size.z) : 0\n const radius = Math.max(60, Math.round(reach * 0.6))\n const aside = ASIDE[axis.slice(1)]!\n const thick = needleThick(radius)\n\n return [\n {\n name: `${NEEDLE}${joint.name}`,\n primitive: 'cube' as const,\n transform: {\n position: { ...joint.origin, [aside]: joint.origin[aside] + radius / 2 },\n size:\n aside === 'x'\n ? { x: radius, y: thick, z: thick }\n : { x: thick, y: thick, z: radius }\n },\n material: { token: 'palette.accent' },\n parent: joint.child\n }\n ]\n })\n}\n"]}
@@ -1,55 +0,0 @@
1
- import type { FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model';
2
- /**
3
- * 성숙도 — 이 형상이 얼마나 잘 만들어졌나.
4
- *
5
- * ## 축이 여럿이어야 하는 이유
6
- *
7
- * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리
8
- * 상자도 그 축에서는 만점이다. 잘 만들어졌다는 것은 **여러 물음에 각각 답하는 것**이고,
9
- * 그것을 한 숫자로 뭉개면 무엇을 고쳐야 할지 알 수 없다.
10
- *
11
- * ## 축을 지어내지 않았다
12
- *
13
- * `docs/design/component-authoring-tool.md` 의 「인정받을 수 있는 최소 품질 — 기준선」
14
- * 다섯이 이미 있다. 그것을 잴 수 있는 형태로 옮기고, 이미 셈이 있는 것(규모·되쓰기)을
15
- * 더했다.
16
- *
17
- * 기준선 1 실루엣으로 알아본다 → readable **아직 측정하지 못한다**
18
- * 기준선 2 상태가 색으로 구분된다 → reactive **아직 측정하지 못한다**
19
- * 기준선 3 다른 자산과 한 세트로 보인다 → consistency
20
- * 기준선 4 확대해도 안 깨진다 → consistency (텍스처 0)
21
- * 기준선 5 라벨이 읽힌다 → reactive
22
- *
23
- * ## 못 재는 축을 지어내지 않는다
24
- *
25
- * 두 축은 지금 잴 수 없다. 하나는 그리기가 필요하고(실루엣), 하나는 형식에 길이
26
- * 없다(상태 바인딩 — figure-model#2). **그것을 0점으로도 만점으로도 두지 않는다.**
27
- * 「아직 측정하지 못한다」고 알리고 이유를 붙인다. 측정하지 못한 것을 점수로 만들면 그 점수가 거짓말을
28
- * 한다.
29
- *
30
- * ## 이 파일은 임시다
31
- *
32
- * 셈은 figure-model 것이어야 한다 — 거기가 `costOf` · `scoreOf` 를 갖고 있다.
33
- * 형식 쪽 결정(#2 · #4)이 서면 이 파일을 지우고 그쪽으로 옮긴다.
34
- */
35
- export type AxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable';
36
- export interface MaturityAxis {
37
- key: AxisKey;
38
- /** 0~1. 못 재면 없다. */
39
- ratio?: number;
40
- /** 지금 이 축의 값. 그대로 보여 준다. */
41
- reading?: string;
42
- /** 값이 숫자가 아니라 말일 때의 문구 키. `reading` 과 함께 쓰지 않는다. */
43
- readingKey?: string;
44
- /** 왜 이 값인가. */
45
- because?: string;
46
- /** 못 재면 그 이유. 있으면 `ratio` 가 없다. */
47
- blocked?: string;
48
- }
49
- /**
50
- * 여섯 축을 측정한다.
51
- *
52
- * 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다 — 그것이 이
53
- * 리포트의 존재 이유다.
54
- */
55
- export declare function maturityOf(source: FigureSource, cost: FigureCost, score: FigureScore): MaturityAxis[];