@things-factory/figure-ui 10.1.210 → 10.1.212

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 (92) hide show
  1. package/client/graphql/index.ts +1 -1
  2. package/client/modeller/angle-dial.ts +1 -1
  3. package/client/modeller/canvas.ts +1 -1
  4. package/client/modeller/editor.ts +24 -32
  5. package/client/modeller/figure-history-panel.ts +1 -1
  6. package/client/modeller/figure-links.ts +61 -37
  7. package/client/modeller/figure-parts.ts +3 -3
  8. package/client/modeller/figure-settings.ts +1 -1
  9. package/client/modeller/figure-size-editor.ts +41 -4
  10. package/client/modeller/links.ts +63 -24
  11. package/client/modeller/live.ts +33 -0
  12. package/client/modeller/scene-budget.ts +3 -3
  13. package/client/modeller/thumbnail.ts +1 -1
  14. package/client/pages/figure-list-page.ts +2 -2
  15. package/client/pages/figure-modeller-page.ts +4 -4
  16. package/client/types.ts +2 -2
  17. package/client/viewparts/figure-thumb.ts +1 -1
  18. package/dist-client/graphql/index.d.ts +1 -1
  19. package/dist-client/graphql/index.js +1 -1
  20. package/dist-client/graphql/index.js.map +1 -1
  21. package/dist-client/modeller/angle-dial.d.ts +1 -1
  22. package/dist-client/modeller/angle-dial.js.map +1 -1
  23. package/dist-client/modeller/canvas.js +1 -1
  24. package/dist-client/modeller/canvas.js.map +1 -1
  25. package/dist-client/modeller/editor.d.ts +5 -0
  26. package/dist-client/modeller/editor.js +24 -32
  27. package/dist-client/modeller/editor.js.map +1 -1
  28. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  29. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  30. package/dist-client/modeller/figure-links.d.ts +2 -0
  31. package/dist-client/modeller/figure-links.js +66 -39
  32. package/dist-client/modeller/figure-links.js.map +1 -1
  33. package/dist-client/modeller/figure-parts.d.ts +3 -3
  34. package/dist-client/modeller/figure-parts.js.map +1 -1
  35. package/dist-client/modeller/figure-settings.js +1 -1
  36. package/dist-client/modeller/figure-settings.js.map +1 -1
  37. package/dist-client/modeller/figure-size-editor.d.ts +3 -1
  38. package/dist-client/modeller/figure-size-editor.js +45 -4
  39. package/dist-client/modeller/figure-size-editor.js.map +1 -1
  40. package/dist-client/modeller/links.d.ts +15 -6
  41. package/dist-client/modeller/links.js +63 -25
  42. package/dist-client/modeller/links.js.map +1 -1
  43. package/dist-client/modeller/live.d.ts +14 -0
  44. package/dist-client/modeller/live.js +31 -0
  45. package/dist-client/modeller/live.js.map +1 -1
  46. package/dist-client/modeller/scene-budget.d.ts +3 -3
  47. package/dist-client/modeller/scene-budget.js +3 -3
  48. package/dist-client/modeller/scene-budget.js.map +1 -1
  49. package/dist-client/modeller/thumbnail.js +1 -1
  50. package/dist-client/modeller/thumbnail.js.map +1 -1
  51. package/dist-client/pages/figure-list-page.d.ts +1 -1
  52. package/dist-client/pages/figure-list-page.js +2 -2
  53. package/dist-client/pages/figure-list-page.js.map +1 -1
  54. package/dist-client/pages/figure-modeller-page.d.ts +3 -3
  55. package/dist-client/pages/figure-modeller-page.js +4 -4
  56. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  57. package/dist-client/route.d.ts +1 -1
  58. package/dist-client/tsconfig.tsbuildinfo +1 -1
  59. package/dist-client/types.d.ts +2 -2
  60. package/dist-client/types.js +1 -1
  61. package/dist-client/types.js.map +1 -1
  62. package/dist-client/viewparts/figure-thumb.js +1 -1
  63. package/dist-client/viewparts/figure-thumb.js.map +1 -1
  64. package/docs/multipart-response-review-2026-10-24.md +1 -1
  65. package/docs/response-authoring-2026-10-24.md +4 -4
  66. package/package.json +6 -6
  67. package/test/ai-proposal.test.ts +1 -1
  68. package/test/align.test.ts +1 -1
  69. package/test/browser/editing.check.mjs +38 -0
  70. package/test/browser/modeller.check.mjs +55 -0
  71. package/test/command-preview.test.ts +3 -3
  72. package/test/fields.test.ts +2 -2
  73. package/test/first-minute.test.ts +5 -5
  74. package/test/first-save.test.ts +3 -3
  75. package/test/gizmo.test.ts +3 -3
  76. package/test/keys.test.ts +2 -2
  77. package/test/layout.test.ts +1 -1
  78. package/test/links.test.ts +53 -14
  79. package/test/live.test.ts +4 -4
  80. package/test/material.test.ts +1 -1
  81. package/test/properties.test.ts +1 -1
  82. package/test/proposal-record.test.ts +1 -1
  83. package/test/refusals.test.ts +1 -1
  84. package/test/resize-with-placement.test.ts +1 -1
  85. package/test/scene-budget.test.ts +4 -4
  86. package/test/verdict.test.ts +1 -1
  87. package/translations/en.json +3 -0
  88. package/translations/es.json +3 -0
  89. package/translations/ja.json +3 -0
  90. package/translations/ko.json +109 -106
  91. package/translations/ms.json +3 -0
  92. package/translations/zh.json +3 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 화면이 다루는 모양. 도형 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.
2
+ * 화면이 다루는 모양. 피규어 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.
3
3
  */
4
4
  /** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */
5
5
  export interface Figure {
@@ -26,7 +26,7 @@ export interface Figure {
26
26
  id: string;
27
27
  name: string;
28
28
  };
29
- /** 도형 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */
29
+ /** 피규어 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */
30
30
  source?: string;
31
31
  properties?: string;
32
32
  version?: number;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 화면이 다루는 모양. 도형 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.
2
+ * 화면이 다루는 모양. 피규어 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.
3
3
  */
4
4
  export {};
5
5
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * 화면이 다루는 모양. 도형 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.\n */\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n /** Namespace of the full name {namespace}/{type} (ADR-0114). */\n namespace?: string | null\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** 도형 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n /** Retired: still released and served, no longer offered for new use. */\n deprecated?: boolean\n /** The type to use instead of a retired figure, in the same domain. */\n supersededBy?: string | null\n /** Public: other domains read its releases and draw it by full name (LV figure-server design G2). */\n public?: boolean\n /** The domain it belongs to; carried by another domain's public figure, so a screen says whose it is. */\n domain?: { id: string; name?: string; subdomain?: string } | null\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * 화면이 다루는 모양. 피규어 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.\n */\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n /** Namespace of the full name {namespace}/{type} (ADR-0114). */\n namespace?: string | null\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** 피규어 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n /** Retired: still released and served, no longer offered for new use. */\n deprecated?: boolean\n /** The type to use instead of a retired figure, in the same domain. */\n supersededBy?: string | null\n /** Public: other domains read its releases and draw it by full name (LV figure-server design G2). */\n public?: boolean\n /** The domain it belongs to; carried by another domain's public figure, so a screen says whose it is. */\n domain?: { id: string; name?: string; subdomain?: string } | null\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
@@ -62,7 +62,7 @@ let FigureThumb = class FigureThumb extends localize(i18next)(LitElement) {
62
62
  태그 — **그림 위에 얹는다.**
63
63
 
64
64
  카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.
65
- 태그는 도형마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가
65
+ 태그는 피규어마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가
66
66
  잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.
67
67
 
68
68
  아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.
@@ -1 +1 @@
1
- {"version":3,"file":"figure-thumb.js","sourceRoot":"","sources":["../../client/viewparts/figure-thumb.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AAEnC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,sCAAsC,CAAA;AAC7C,OAAO,gEAAgE,CAAA;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAG5C,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD;;;;;;;;;GASG;AACH,SAAS,YAAY,CAAC,MAAc;IAClC,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;QAC/B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IACtD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,OAAO,EAAE,CAAA;IAE3D,OAAO,GAAG,WAAW,qBAAqB,MAAM,CAAC,EAAE,MAAM,KAAK,EAAE,CAAA;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AAEI,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAAvD;;QAkGL,2FAA2F;QAC9D,aAAQ,GAAG,KAAK,CAAA;IAmD/C,CAAC;aArJQ,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8FlB,AA9FY,CA8FZ;IAMD,MAAM;QACJ,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,IAAI,CAAA,EAAE,CAAA;QACf,CAAC;QAED,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;QAEhC,OAAO,IAAI,CAAA;;cAED,GAAG,IAAI,EAAE;cACT,MAAM,CAAC,IAAI,IAAI,EAAE;gBACf,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;;iDAErI,OAAO,CAAC,CAAC,CAAC,oCAAoC,CAAC;QACxF,MAAM,CAAC,IAAI,EAAE,MAAM;YACnB,CAAC,CAAC,IAAI,CAAA;cACA,MAAM,CAAC,IAAI,CAAC,GAAG,CACf,GAAG,CAAC,EAAE,CAAC,IAAI,CAAA;;0BAEC,OAAO,CAAC,CAAC,CAAC,8BAA8B,CAAC;2BACxC,CAAC,CAAQ,EAAE,EAAE;gBACpB,CAAC,CAAC,eAAe,EAAE,CAAA;gBACnB,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAClF,CAAA;YACH,CAAC;;oBAEC,GAAG;;eAER,CACF;iBACI;YACT,CAAC,CAAC,EAAE;;QAEJ,IAAI,CAAC,QAAQ,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;;gBAE5C,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;iBACvC,CAAC,CAAQ,EAAE,EAAE;YACpB,sCAAsC;YACtC,CAAC,CAAC,eAAe,EAAE,CAAA;YACnB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAA;QAC/C,CAAC;;;gBAGO;;KAEX,CAAA;IACH,CAAC;;AApD2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;2CAAgB;AAEd;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;;6CAAiB;AAnGlC,WAAW;IADvB,aAAa,CAAC,cAAc,CAAC;GACjB,WAAW,CAsJvB","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport '@operato/data-grist/ox-card-thumb.js'\nimport '@things-factory/figure-viewer/dist-client/figure-thumb-mark.js'\nimport { getPathInfo } from '@operato/utils'\n\nimport type { Figure } from '../types.js'\nimport { hasBeenReleased } from '../figure-released.js'\n\n/**\n * 그림을 부르는 주소.\n *\n * **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를\n * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 판을\n * 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.\n *\n * 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를\n * 손으로 짜맞추면 도메인이 바뀌는 배치에서 조용히 틀린다.\n */\nfunction thumbnailSrc(figure: Figure): string | undefined {\n if (!figure.thumbnailUpdatedAt) {\n return undefined\n }\n\n const { contextPath } = getPathInfo(location.pathname)\n const stamp = new Date(figure.thumbnailUpdatedAt).getTime()\n\n return `${contextPath}/figure-thumbnail/${figure.id}?v=${stamp}`\n}\n\n/**\n * 카드의 그림 칸. 그림 · 그림 없음 · 초안 표시는 모든 카드 카탈로그의 `ox-card-thumb`(@operato/data-grist)가\n * 그리고, 여기서는 그 위에 이 카탈로그의 것(태그 · 삭제)만 얹는다(ADR-0117 개정 2026-10-13).\n *\n * ## 왜 컴포넌트인가\n *\n * 카탈로그는 `ox-grist` 의 CARD 모드로 그린다. 격자에게 그림 칸은 컬럼 하나이고, 그 칸을 그리는\n * 것은 컬럼의 `record.renderer` 다. 그림 위에 얹히는 것들(초안 표시, 삭제)은 **그림 칸 안에**\n * 있어야 하므로 renderer 가 그려야 하는데, renderer 의 결과는 `ox-card-field` 의 shadow root\n * 안으로 들어간다 — 화면의 `static styles` 가 닿지 않고, 인라인 style 로는 `:hover` 를 쓸 수 없다.\n *\n * 그래서 그림 칸만 컴포넌트로 둔다. 자기 스타일을 갖고 있으니 hover 도 테마도 정상으로 돈다.\n * 격자로 옮기기 전 손으로 만들었던 카드의 위쪽 절반이 그대로 이 자리다.\n *\n * ## 삭제가 왜 여기 있나\n *\n * 격자의 gutter 줄에 두면 카드 아래에 단추 줄이 하나 생긴다. 감춰도 자리가 남고, 자리를 없애면\n * 눌릴 곳이 사라진다. 되돌릴 수 없는 조작은 **고르는 눈길에 걸리지 않아야** 하므로 그림 오른쪽\n * 위, 손이 갔을 때만 나오는 자리에 둔다.\n */\n@customElement('figure-thumb')\nexport class FigureThumb extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n /*\n 태그 — **그림 위에 얹는다.**\n\n 카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.\n 태그는 도형마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가\n 잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.\n\n 아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.\n\n **투명한 알약이다.** 면을 꽉 채우면 그림 위에 흰 띠가 생겨 형상을 가린다 — 카탈로그에서\n 먼저 보아야 하는 것은 형상이다. 그래서 바탕은 거의 비우고, 읽히는 것은 뒤를 흐리는 것과\n 실낱 테두리가 맡는다. 흐림이 안 되는 환경에서도 테두리가 남아 글자가 형상에 묻히지 않는다.\n */\n div[tags] {\n position: absolute;\n right: 6px;\n bottom: 6px;\n left: 6px;\n display: flex;\n flex-wrap: wrap-reverse;\n justify-content: flex-end;\n gap: 3px;\n /* 태그가 많아도 그림을 덮지 않는다 — 넘치는 것은 위로 잘린다 */\n max-height: 44px;\n overflow: hidden;\n /* 알약 사이의 빈 자리는 그림이 받는다 — 거기서 누르면 저작면이 열린다 */\n pointer-events: none;\n }\n /*\n **알약은 누를 수 있다.** 누르면 그 태그로 목록을 거른다.\n\n 태그를 눈에 보이게 한 다음의 물음은 「그래서 이걸로 뭘 하나」다. 같은 태그가 붙은 것을\n 보려고 태그를 눈으로 읽어 검색창에 옮겨 적게 두면, 보여 준 뜻이 절반만 산다.\n\n 카드 자체가 저작면을 여는 자리이므로 알약에서 그 클릭을 멈춘다 — 안 멈추면 태그를 누른\n 사람이 편집기로 끌려간다.\n */\n div[tags] button {\n pointer-events: auto;\n cursor: pointer;\n padding: 0 7px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n line-height: 1.7;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 34%, transparent);\n border: 1px solid color-mix(in srgb, var(--md-sys-color-outline, #888) 42%, transparent);\n backdrop-filter: blur(4px);\n border-radius: 999em;\n white-space: nowrap;\n }\n div[tags] button:hover,\n div[tags] button:focus-visible {\n color: var(--md-sys-color-on-surface);\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 72%, transparent);\n border-color: var(--md-sys-color-primary);\n outline: none;\n }\n\n button[remove] {\n position: absolute;\n top: 5px;\n right: 5px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n /* 그림이 밝을 수도 어두울 수도 있다 — 옅은 면을 깔아 아이콘이 어디서나 읽히게 한다 */\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 82%, transparent);\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease-out;\n }\n button[remove] md-icon {\n --md-icon-size: 18px;\n }\n :host(:hover) button[remove],\n button[remove]:focus-visible {\n opacity: 1;\n }\n button[remove]:hover {\n color: var(--md-sys-color-on-error, #fff);\n background-color: var(--md-sys-color-error);\n }\n `\n\n @property({ type: Object }) figure!: Figure\n /** Another domain's public figure (G2): its card says whose it is and offers no delete. */\n @property({ type: Boolean }) readonly = false\n\n render() {\n const figure = this.figure\n if (!figure) {\n return html``\n }\n\n const src = thumbnailSrc(figure)\n\n return html`\n <ox-card-thumb\n src=${src || ''}\n alt=${figure.name || ''}\n badge=${this.readonly ? figure.domain?.name ?? '' : figure.deprecated ? i18next.t('figure.label.retired') : figure.state === 'released' ? '' : i18next.t('figure.label.draft')}\n >\n <figure-thumb-mark slot=\"empty\" .label=${i18next.t('figure.text.thumbnail-not-made-yet')}></figure-thumb-mark>\n ${figure.tags?.length\n ? html`<div tags>\n ${figure.tags.map(\n tag => html`\n <button\n title=${i18next.t('figure.text.find-by-this-tag')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('search-tag', { detail: { tag }, bubbles: true, composed: true })\n )\n }}\n >\n ${tag}\n </button>\n `\n )}\n </div>`\n : ''}\n\n ${this.readonly || hasBeenReleased(figure) ? '' : html`<button\n remove\n title=${i18next.t('figure.button.delete-figure')}\n @click=${(e: Event) => {\n /* 카드 클릭(=저작면 열기)까지 올라가지 않게 여기서 멈춘다 */\n e.stopPropagation()\n this.dispatchEvent(new CustomEvent('remove'))\n }}\n >\n <md-icon>delete</md-icon>\n </button>`}\n </ox-card-thumb>\n `\n }\n}\n"]}
1
+ {"version":3,"file":"figure-thumb.js","sourceRoot":"","sources":["../../client/viewparts/figure-thumb.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport '@operato/data-grist/ox-card-thumb.js'\nimport '@things-factory/figure-viewer/dist-client/figure-thumb-mark.js'\nimport { getPathInfo } from '@operato/utils'\n\nimport type { Figure } from '../types.js'\nimport { hasBeenReleased } from '../figure-released.js'\n\n/**\n * 그림을 부르는 주소.\n *\n * **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를\n * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 판을\n * 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.\n *\n * 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를\n * 손으로 짜맞추면 도메인이 바뀌는 배치에서 조용히 틀린다.\n */\nfunction thumbnailSrc(figure: Figure): string | undefined {\n if (!figure.thumbnailUpdatedAt) {\n return undefined\n }\n\n const { contextPath } = getPathInfo(location.pathname)\n const stamp = new Date(figure.thumbnailUpdatedAt).getTime()\n\n return `${contextPath}/figure-thumbnail/${figure.id}?v=${stamp}`\n}\n\n/**\n * 카드의 그림 칸. 그림 · 그림 없음 · 초안 표시는 모든 카드 카탈로그의 `ox-card-thumb`(@operato/data-grist)가\n * 그리고, 여기서는 그 위에 이 카탈로그의 것(태그 · 삭제)만 얹는다(ADR-0117 개정 2026-10-13).\n *\n * ## 왜 컴포넌트인가\n *\n * 카탈로그는 `ox-grist` 의 CARD 모드로 그린다. 격자에게 그림 칸은 컬럼 하나이고, 그 칸을 그리는\n * 것은 컬럼의 `record.renderer` 다. 그림 위에 얹히는 것들(초안 표시, 삭제)은 **그림 칸 안에**\n * 있어야 하므로 renderer 가 그려야 하는데, renderer 의 결과는 `ox-card-field` 의 shadow root\n * 안으로 들어간다 — 화면의 `static styles` 가 닿지 않고, 인라인 style 로는 `:hover` 를 쓸 수 없다.\n *\n * 그래서 그림 칸만 컴포넌트로 둔다. 자기 스타일을 갖고 있으니 hover 도 테마도 정상으로 돈다.\n * 격자로 옮기기 전 손으로 만들었던 카드의 위쪽 절반이 그대로 이 자리다.\n *\n * ## 삭제가 왜 여기 있나\n *\n * 격자의 gutter 줄에 두면 카드 아래에 단추 줄이 하나 생긴다. 감춰도 자리가 남고, 자리를 없애면\n * 눌릴 곳이 사라진다. 되돌릴 수 없는 조작은 **고르는 눈길에 걸리지 않아야** 하므로 그림 오른쪽\n * 위, 손이 갔을 때만 나오는 자리에 둔다.\n */\n@customElement('figure-thumb')\nexport class FigureThumb extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n /*\n 태그 — **그림 위에 얹는다.**\n\n 카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.\n 태그는 피규어마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가\n 잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.\n\n 아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.\n\n **투명한 알약이다.** 면을 꽉 채우면 그림 위에 흰 띠가 생겨 형상을 가린다 — 카탈로그에서\n 먼저 보아야 하는 것은 형상이다. 그래서 바탕은 거의 비우고, 읽히는 것은 뒤를 흐리는 것과\n 실낱 테두리가 맡는다. 흐림이 안 되는 환경에서도 테두리가 남아 글자가 형상에 묻히지 않는다.\n */\n div[tags] {\n position: absolute;\n right: 6px;\n bottom: 6px;\n left: 6px;\n display: flex;\n flex-wrap: wrap-reverse;\n justify-content: flex-end;\n gap: 3px;\n /* 태그가 많아도 그림을 덮지 않는다 — 넘치는 것은 위로 잘린다 */\n max-height: 44px;\n overflow: hidden;\n /* 알약 사이의 빈 자리는 그림이 받는다 — 거기서 누르면 저작면이 열린다 */\n pointer-events: none;\n }\n /*\n **알약은 누를 수 있다.** 누르면 그 태그로 목록을 거른다.\n\n 태그를 눈에 보이게 한 다음의 물음은 「그래서 이걸로 뭘 하나」다. 같은 태그가 붙은 것을\n 보려고 태그를 눈으로 읽어 검색창에 옮겨 적게 두면, 보여 준 뜻이 절반만 산다.\n\n 카드 자체가 저작면을 여는 자리이므로 알약에서 그 클릭을 멈춘다 — 안 멈추면 태그를 누른\n 사람이 편집기로 끌려간다.\n */\n div[tags] button {\n pointer-events: auto;\n cursor: pointer;\n padding: 0 7px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n line-height: 1.7;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 34%, transparent);\n border: 1px solid color-mix(in srgb, var(--md-sys-color-outline, #888) 42%, transparent);\n backdrop-filter: blur(4px);\n border-radius: 999em;\n white-space: nowrap;\n }\n div[tags] button:hover,\n div[tags] button:focus-visible {\n color: var(--md-sys-color-on-surface);\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 72%, transparent);\n border-color: var(--md-sys-color-primary);\n outline: none;\n }\n\n button[remove] {\n position: absolute;\n top: 5px;\n right: 5px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n /* 그림이 밝을 수도 어두울 수도 있다 — 옅은 면을 깔아 아이콘이 어디서나 읽히게 한다 */\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 82%, transparent);\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease-out;\n }\n button[remove] md-icon {\n --md-icon-size: 18px;\n }\n :host(:hover) button[remove],\n button[remove]:focus-visible {\n opacity: 1;\n }\n button[remove]:hover {\n color: var(--md-sys-color-on-error, #fff);\n background-color: var(--md-sys-color-error);\n }\n `\n\n @property({ type: Object }) figure!: Figure\n /** Another domain's public figure (G2): its card says whose it is and offers no delete. */\n @property({ type: Boolean }) readonly = false\n\n render() {\n const figure = this.figure\n if (!figure) {\n return html``\n }\n\n const src = thumbnailSrc(figure)\n\n return html`\n <ox-card-thumb\n src=${src || ''}\n alt=${figure.name || ''}\n badge=${this.readonly ? figure.domain?.name ?? '' : figure.deprecated ? i18next.t('figure.label.retired') : figure.state === 'released' ? '' : i18next.t('figure.label.draft')}\n >\n <figure-thumb-mark slot=\"empty\" .label=${i18next.t('figure.text.thumbnail-not-made-yet')}></figure-thumb-mark>\n ${figure.tags?.length\n ? html`<div tags>\n ${figure.tags.map(\n tag => html`\n <button\n title=${i18next.t('figure.text.find-by-this-tag')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('search-tag', { detail: { tag }, bubbles: true, composed: true })\n )\n }}\n >\n ${tag}\n </button>\n `\n )}\n </div>`\n : ''}\n\n ${this.readonly || hasBeenReleased(figure) ? '' : html`<button\n remove\n title=${i18next.t('figure.button.delete-figure')}\n @click=${(e: Event) => {\n /* 카드 클릭(=저작면 열기)까지 올라가지 않게 여기서 멈춘다 */\n e.stopPropagation()\n this.dispatchEvent(new CustomEvent('remove'))\n }}\n >\n <md-icon>delete</md-icon>\n </button>`}\n </ox-card-thumb>\n `\n }\n}\n"]}
@@ -29,7 +29,7 @@ figure-model 0.10.0, things-scene 10.1.99는 operato-application 설치본을
29
29
 
30
30
  ### 2. 전체 적용이 기존 관계를 덮어쓴다
31
31
 
32
- 보강대에서 기존 ‘도형 크기에 맞추기’를 클릭했다. 화면은 너비를 피규어 너비의 86.7%로, 높이를 피규어 높이의 75%로, 깊이를 피규어 깊이의 6.7%로 변경했고, -200 입력은 0이 됐다. 실행 취소로 원래 관계를 복원했다.
32
+ 보강대에서 기존 ‘피규어 크기에 맞추기’를 클릭했다. 화면은 너비를 피규어 너비의 86.7%로, 높이를 피규어 높이의 75%로, 깊이를 피규어 깊이의 6.7%로 변경했고, -200 입력은 0이 됐다. 실행 취소로 원래 관계를 복원했다.
33
33
 
34
34
  `figure-links.ts`가 axes 없는 follow-size를 보내고, figure-model `authoring-actions.ts`의 followSize가 replace:true로 모든 편집 치수를 연결한다. 의도된 명령 동작이지만 UI에서 참조 대상 교체와 기존 규칙 제거가 분명하지 않다. 피규어 너비 3000에서 원래 보강대는 2800, 버튼 적용 뒤는 약 2600이다. 따라서 단순히 ‘같은 설정을 한 번에 하는 중복 버튼’이라고 설명한 이전 판단은 불충분했다.
35
35
 
@@ -4,22 +4,22 @@
4
4
 
5
5
  ## 확인한 문제와 코드 근거
6
6
 
7
- figure.hatiolab.com의 새 Figure에서 상자를 직접 추가하고 1500 × 80 × 600 mm로 편집했다. 반응형 → 크기 반응 정하기를 먼저 열었을 때, 도형의 size 입력이 아직 없으면 축별 선택지는 고정뿐이었다. 별도의 ‘도형 크기에 맞추기’를 사용하면 모든 축이 함께 연결되어, 너비만 반응시키려는 작업에도 불필요한 해제가 필요했다.
7
+ figure.hatiolab.com의 새 Figure에서 상자를 직접 추가하고 1500 × 80 × 600 mm로 편집했다. 반응형 → 크기 반응 정하기를 먼저 열었을 때, 피규어의 size 입력이 아직 없으면 축별 선택지는 고정뿐이었다. 별도의 ‘피규어 크기에 맞추기’를 사용하면 모든 축이 함께 연결되어, 너비만 반응시키려는 작업에도 불필요한 해제가 필요했다.
8
8
 
9
9
  - `client/modeller/figure-links.ts`: 기존 renderDimensions는 모두 고정이면 치수 요약을 숨겼고, 전체 축 follow-size만 노출했다. 고급 선택지는 hasInstanceSize로 제한했다.
10
- - figure-model `src/authoring-actions.ts`의 followSize: 이미 axes를 지원한다. 필요한 size 입력을 선언하고 선택한 축의 현재 치수/도형 크기 비율로 link-dimension을 만든다. 따라서 새 모델 표현이나 렌더링 구현은 필요하지 않았다.
10
+ - figure-model `src/authoring-actions.ts`의 followSize: 이미 axes를 지원한다. 필요한 size 입력을 선언하고 선택한 축의 현재 치수/피규어 크기 비율로 link-dimension을 만든다. 따라서 새 모델 표현이나 렌더링 구현은 필요하지 않았다.
11
11
  - 브라우저 자동화의 fill로 입력한 숫자가 실제 모델에 반영되지 않은 현상은 native 키 입력에서는 재현되지 않았다. 제품 입력 결함으로 판단하지 않았다.
12
12
 
13
13
  ## 반영한 변경
14
14
 
15
15
  실제 figure-ui 편집기의 반응형 패널에 너비·높이·깊이의 현재 치수와 고정 여부를 처음부터 표시한다. 각 고정 치수에 ‘비례 변경’을 제공하고 기존 follow-size 명령에 해당 축만 전달한다. 설정 직후 치수와 위치를 유지하며 한 번의 undo로 복구된다. 기존 전체 축 설정, 고급 수식/다른 부품 연결, 고정으로 되돌리기는 유지한다. 잠긴 모델과 uniform 크기 방식에서는 새 설정 버튼도 비활성화한다. 여섯 언어의 문구를 추가했다.
16
16
 
17
- 비례 변경은 해당 도형 축을 따라가는 치수 규칙이다. 부품을 면에 붙이거나 위치를 비례 이동시키는 설정은 아니다. 원형 부품은 기존 provider가 제공하는 편집 치수를 사용하며, 회전된 부품의 방향 대응을 새로 추정하지 않는다.
17
+ 비례 변경은 해당 피규어 축을 따라가는 치수 규칙이다. 부품을 면에 붙이거나 위치를 비례 이동시키는 설정은 아니다. 원형 부품은 기존 provider가 제공하는 편집 치수를 사용하며, 회전된 부품의 방향 대응을 새로 추정하지 않는다.
18
18
 
19
19
  ## 검증 결과
20
20
 
21
21
  - 관련 단위 테스트 56개 통과: 기존 설치 figure-model 0.9.6 및 앱의 0.10.0 각각 실행.
22
- - 추가 회귀 검증: 새 1500 × 80 × 600 mm 프레임에 X만 설정. 설정 직후 geometry/pose 동일. 도형 세 축 입력을 모두 두 배로 주면 geometry는 3000 × 80 × 600 mm. 한 번의 undo로 size 선언까지 복구, redo 동일.
22
+ - 추가 회귀 검증: 새 1500 × 80 × 600 mm 프레임에 X만 설정. 설정 직후 geometry/pose 동일. 피규어 세 축 입력을 모두 두 배로 주면 geometry는 3000 × 80 × 600 mm. 한 번의 undo로 size 선언까지 복구, redo 동일.
23
23
  - 실제 editor 소스를 빌드한 로컬 fixture를 Chrome에서 조작: 너비 비례 변경 → 미리보기 너비 3000 → 편집 → undo → redo 확인. 별도 샘플 모델러가 아닌 제품 editor와 things-scene을 사용한다. fixture의 저장소는 메모리이며 운영 저장·재접속 검증은 하지 않았다.
24
24
  - 캡처: `evidence/2026-10-24/width-preview.png`. 왼쪽 3000은 미리보기 입력, 오른쪽 1500은 편집 기준 치수다.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.210",
3
+ "version": "10.1.212",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -42,11 +42,11 @@
42
42
  "@operato/slot-layout-editor": "^10.29.43",
43
43
  "@operato/styles": "^10.0.0",
44
44
  "@operato/utils": "^10.29.43",
45
- "@things-factory/ai-assistant": "^10.1.210",
46
- "@things-factory/figure-service": "^10.1.210",
47
- "@things-factory/figure-viewer": "^10.1.210",
48
- "@things-factory/setting-base": "^10.1.208",
45
+ "@things-factory/ai-assistant": "^10.1.212",
46
+ "@things-factory/figure-service": "^10.1.212",
47
+ "@things-factory/figure-viewer": "^10.1.212",
48
+ "@things-factory/setting-base": "^10.1.212",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "89fbda12d43d8532ba4e95ba6d6d7e05e2da2502"
51
+ "gitHead": "1e1cdb79fca2915580429af451b6cf1dae653a93"
52
52
  }
@@ -94,7 +94,7 @@ test('a revision carries the figure it was made for, and only a revision does (a
94
94
  test('refused: a stale version is said as what happened; any other refusal carries its reason', async () => {
95
95
  const stale = await runFigureProposal({ steps }, taker({ applied: false, code: 'REVISION_CONFLICT', error: 'editor changed' }), say)
96
96
  assert.equal(stale.verdict, 'refused')
97
- assert.match(stale.note!, /모델이 바뀌어/)
97
+ assert.match(stale.note!, /피규어가 바뀌어/)
98
98
  const other = await runFigureProposal({ steps }, taker({ applied: false, code: 'ATTACH_FACE', error: 'faces along x' }), say)
99
99
  /* Said from the code in the reader's words, as the modeller says an edit it refuses -- never the kernel's English (audit N6). */
100
100
  assert.match(other.note!, /적용하지 않았습니다: 이 편집은 할 수 없습니다\. \(ATTACH_FACE\)/)
@@ -36,7 +36,7 @@ test('가운데 X · Z 는 부품 상자의 가운데를 0 에 둔다', () => {
36
36
  })
37
37
 
38
38
  /*
39
- * 기준면에 맞추기는 도형의 배치를 따른다 — figure-model 의 한 규칙(`figurePlaneAlignChanges`, 설계자 2026-09-27). 바닥은
39
+ * 기준면에 맞추기는 피규어의 배치를 따른다 — figure-model 의 한 규칙(`figurePlaneAlignChanges`, 설계자 2026-09-27). 바닥은
40
40
  * 아랫면을 y 0 에, 천장은 윗면을 y 0 에. 한 번 옮길 뿐 붙이지 않는다. 돌린 부품은 돈 부피로.
41
41
  */
42
42
  test('기준면 맞추기 — 배치를 따르고, 한 번 옮길 뿐 붙이지 않는다', async () => {
@@ -374,6 +374,44 @@ describe('values in hand on the properties tab: drawn at once, written on change
374
374
  })
375
375
  })
376
376
 
377
+ /* A size of the part's own on the 「배치」 tab's size editor is a value in hand as any field is (user 2026-09-24). */
378
+ describe('a size in hand on the arrangement tab: drawn while typed or dragged, written once', () => {
379
+ const WIDTH = 'box X figure.label.size-measure'
380
+ const wide = async () => Math.round((await look('box')).size[0])
381
+ let was
382
+ before(async () => {
383
+ await mount('box')
384
+ await pick('box')
385
+ await click(await read(() => probe.tab('figure.label.tab-arrangement')))
386
+ })
387
+
388
+ test('a width typed is drawn while it is typed, and written once on Enter', async () => {
389
+ was = await wide()
390
+ const start = await revision()
391
+ await type(WIDTH, '700')
392
+ assert.equal(await wide(), 700, 'drawn 700 wide while typed')
393
+ assert.equal(await revision(), start, 'nothing written yet')
394
+ await page().keyboard.press('Enter')
395
+ await sleep(400)
396
+ assert.equal(await revision(), start + 1)
397
+ assert.equal(await read(() => probe.value('box.size.x')), 700)
398
+ await press(...UNDO)
399
+ assert.equal(await wide(), was, 'undone, drawn as it stands')
400
+ })
401
+
402
+ test('its label dragged sideways scrubs the width: drawn while held, one step on release', async () => {
403
+ const start = await revision()
404
+ const label = await read(id => editing.fieldLabel(id), WIDTH)
405
+ assert.ok(label, 'the X of the size editor')
406
+ const held = await drag(label, 60, async () => ({ drawn: await wide(), shown: await read(id => editing.fieldValue(id), WIDTH), revision: await revision() }))
407
+ assert.notEqual(held.drawn, was, 'drawn while held')
408
+ assert.equal(String(held.drawn), held.shown, 'the field follows what is drawn')
409
+ assert.equal(held.revision, start, 'nothing written while held')
410
+ assert.equal(await revision(), start + 1, 'the release is one step')
411
+ assert.equal(await read(() => probe.value('box.size.x')), held.drawn)
412
+ })
413
+ })
414
+
377
415
  describe('the pose sliders: drawn while dragged, one step on release; the pose kept as the default (user 2026-09-24)', () => {
378
416
  before(async () => {
379
417
  await mount('six-axis')
@@ -630,6 +630,61 @@ describe('a motion is changed where it is (2026-09-25)', () => {
630
630
  assert.equal(now.motion[0].upperLimit, 45, 'the new limit')
631
631
  assert.equal(now.id, was.id, 'the same joint')
632
632
  })
633
+
634
+ /* A joint is a list of axes (ADR-0159): one more is added where the motion is said, and taken away again. */
635
+ const jointOf = part => read(part => (JSON.parse(document.querySelector('figure-editor').session.source()).joints ?? []).find(j => j.body1.part === part) ?? null, part)
636
+ const button = async (text, attribute) =>
637
+ page().evaluate(
638
+ (text, attribute) => {
639
+ const deep = (root = document) => [...root.querySelectorAll('button')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
640
+ const b = deep().find(x => x.textContent.includes(text) && (!attribute || x.hasAttribute(attribute)))
641
+ if (!b) return null
642
+ b.scrollIntoView({ block: 'center' })
643
+ const r = b.getBoundingClientRect()
644
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
645
+ },
646
+ text,
647
+ attribute
648
+ )
649
+ const rows = () =>
650
+ page().evaluate(() => {
651
+ const deep = (root = document) => [...root.querySelectorAll('div[motion-axis]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
652
+ return deep().length
653
+ })
654
+
655
+ test('an axis added to the shoulder: one step, after the first, on an axis the first does not use, with a control of its own', async () => {
656
+ const was = await jointOf('upper-arm')
657
+ await click(await button('figure.button.add-axis', 'add-axis'), 'add an axis')
658
+ const start = await state()
659
+ await click(await button('figure.button.add-axis'), 'the form\'s add-axis')
660
+ const after = await state()
661
+ assert.equal(after.error, '', 'not refused')
662
+ assert.equal(after.revision, start.revision + 1, 'one step')
663
+ const now = await jointOf('upper-arm')
664
+ assert.equal(now.motion.length, 2)
665
+ assert.deepEqual(now.motion[0], was.motion[0], 'the first axis as it was')
666
+ assert.notEqual(now.motion[1].axis, now.motion[0].axis, 'another axis')
667
+ assert.equal(now.motion[1].state.id, 'upper-arm-turn-2')
668
+ assert.equal(await rows(), 2, 'a row an axis')
669
+ })
670
+
671
+ test('the second axis taken away: one step, the first as it was', async () => {
672
+ const was = await jointOf('upper-arm')
673
+ const start = await state()
674
+ const removes = await page().evaluate(() => {
675
+ const deep = (root = document) => [...root.querySelectorAll('button[remove-axis]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
676
+ const b = deep()[1]
677
+ if (!b) return null
678
+ b.scrollIntoView({ block: 'center' })
679
+ const r = b.getBoundingClientRect()
680
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
681
+ })
682
+ await click(removes, 'remove the second axis')
683
+ assert.equal((await state()).revision, start.revision + 1, 'one step')
684
+ const now = await jointOf('upper-arm')
685
+ assert.deepEqual(now.motion, [was.motion[0]], 'the first axis alone, as it was')
686
+ assert.equal(await rows(), 1)
687
+ })
633
688
  })
634
689
 
635
690
  describe('the modeller made easier to find and to keep (2026-09-26 baseline)', () => {
@@ -49,12 +49,12 @@ test('seen, not done: the session\'s preview writes nothing, and names the versi
49
49
  /* Said: how far the part goes, and what goes with it. */
50
50
  const words = previewWordsOf(held, 'top', say)
51
51
  /* Attached with no alignment, it is centred on the leg too: said, not hidden. */
52
- assert.deepEqual(words, ['도형 기준으로 X −400 · Y −180 mm 옮겨집니다.', '함께 움직이는 부품: lamp'])
52
+ assert.deepEqual(words, ['피규어 기준으로 X −400 · Y −180 mm 옮겨집니다.', '함께 움직이는 부품: lamp'])
53
53
  })
54
54
 
55
55
  test('a turn is said as one axis and one signed angle; a refusal clears the result and says why', () => {
56
56
  const turned: HeldPreview = { context: 'c', revision: 0, key: 'k', moves: [{ body: 'box', part: 'box', translate: [0, 0, 0], turn: { axis: 'y', degrees: -30 } }] }
57
- assert.deepEqual(previewWordsOf(turned, 'box', say), ['도형 기준 Y축 둘레로 −30° 돕니다.'])
57
+ assert.deepEqual(previewWordsOf(turned, 'box', say), ['피규어 기준 Y축 둘레로 −30° 돕니다.'])
58
58
  const slanted: HeldPreview = { context: 'c', revision: 0, key: 'k', moves: [{ body: 'box', part: 'box', translate: [0, 0, 0], turn: { axis: [0.577, 0.577, 0.577], degrees: 120 } }] }
59
59
  assert.match(previewWordsOf(slanted, 'box', say)[0]!, /기울어진 축\(0\.58, 0\.58, 0\.58\) 둘레로 \+120°/)
60
60
  const refused: HeldPreview = { context: 'c', revision: 0, key: 'k', refused: '거꾸로 붙일 수 없습니다', geometry: [{}], moves: turned.moves }
@@ -177,7 +177,7 @@ test('a motion is named by its parts, and drawn at both ends of its range -- onl
177
177
 
178
178
  /* ADR-0097 §5.5, Q3: past the face's edge is said beside the fastening, with the way to change it; nothing moves. */
179
179
  test('past the edge of its face is said beside the fastening, with the way to change alignment and gap', () => {
180
- assert.equal(say('figure.text.overhang-out', { on: 'top', face: '윗면', sides: say('figure.text.overhang-side-high', { axis: 'X', mm: '100' }) }), 'top의 윗면 가장자리 밖으로 나와 있습니다: X 높은 쪽 100 mm. 모델의 점유 공간과는 따로 봅니다.')
180
+ assert.equal(say('figure.text.overhang-out', { on: 'top', face: '윗면', sides: say('figure.text.overhang-side-high', { axis: 'X', mm: '100' }) }), 'top의 윗면 가장자리 밖으로 나와 있습니다: X 높은 쪽 100 mm. 피규어의 점유 공간과는 따로 봅니다.')
181
181
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
182
182
  const t = JSON.parse(read(`../translations/${lang}.json`))
183
183
  for (const key of ['figure.text.overhang-out', 'figure.text.overhang-possible', 'figure.text.overhang-unknown', 'figure.text.overhang-held', 'figure.text.overhang-side-low', 'figure.text.overhang-side-high', 'figure.button.fix-alignment']) assert.ok(t[key], `${lang} ${key}`)
@@ -20,10 +20,10 @@ test('부품의 위치 · 회전 · 크기는 세 칸 한 줄씩, 나머지는
20
20
  ]
21
21
  const { vectors, singles } = groupFields(fields, 'arm')
22
22
  assert.deepEqual(vectors.map(g => g.kind === 'vector' && g.name), ['position', 'rotation'])
23
- assert.deepEqual(singles.map(g => g.kind === 'single' && g.name), ['radius', 'length'], '다른 부품 · 도형의 값은 섞이지 않는다')
23
+ assert.deepEqual(singles.map(g => g.kind === 'single' && g.name), ['radius', 'length'], '다른 부품 · 피규어의 값은 섞이지 않는다')
24
24
  })
25
25
 
26
- test('도형은 크기(size.x · y · z)가 세 칸 한 줄', () => {
26
+ test('피규어는 크기(size.x · y · z)가 세 칸 한 줄', () => {
27
27
  const { vectors, singles } = groupFields([f('size.x'), f('size.y'), f('size.z'), f('opening'), f('arm.position.x')])
28
28
  assert.deepEqual(vectors.map(g => g.kind === 'vector' && g.name), ['size'])
29
29
  assert.deepEqual(singles.map(g => g.kind === 'single' && g.name), ['opening'])
@@ -22,16 +22,16 @@ test('a selected part at the head: its kind, and what its size follows only when
22
22
  /* As usual -- a size of its own, loose: nothing but what it is. */
23
23
  assert.deepEqual(partSummaryOf(a, 'box', say), { kind: 'figure.label.primitive-cube', size: '' })
24
24
  const only = applyFigureProposal(a, planFigureResize(a, { part: 'box', axes: ['x'] }).steps)
25
- assert.equal(partSummaryOf(only, 'box', sayKo).size, '너비는 도형 크기를 따라 바뀝니다.')
25
+ assert.equal(partSummaryOf(only, 'box', sayKo).size, '너비는 피규어 크기를 따라 바뀝니다.')
26
26
  /* Every side following the figure's own: one sentence, not one per side (user 2026-10-16). */
27
27
  const every = applyFigureProposal(a, planFigureResize(a, { part: 'box', axes: ['x', 'y', 'z'] }).steps)
28
- assert.equal(partSummaryOf(every, 'box', sayKo).size, '너비 · 높이 · 깊이는 도형 크기를 따라 바뀝니다.')
28
+ assert.equal(partSummaryOf(every, 'box', sayKo).size, '너비 · 높이 · 깊이는 피규어 크기를 따라 바뀝니다.')
29
29
  /* Another part's measure, said by name -- not 「규칙을 따름」. */
30
30
  let two = addFigureBox(a, { id: 'bed', size: { x: 800, y: 50, z: 400 }, position: { x: 0, y: 25, z: 0 } })
31
31
  two = applyFigureAuthoring(two, { kind: 'link-dimension', part: 'box', axis: 'y', source: { of: 'part', part: 'bed', axis: 'x' } } as never)
32
32
  assert.equal(partSummaryOf(two, 'box', sayKo).size, '높이는 bed의 너비와 같습니다.')
33
33
  assert.deepEqual([particlesOf('지름', 'ko'), particlesOf('높이', 'ko')], [{ topic: '지름은', with: '지름과' }, { topic: '높이는', with: '높이와' }])
34
- assert.ok(!/규칙을 따름|규칙으로 결정됨|규칙이 정하는|도형을 따라감|크기 고정|붙임 없음/.test(JSON.stringify(ko)), 'the words nobody knew are gone')
34
+ assert.ok(!/규칙을 따름|규칙으로 결정됨|규칙이 정하는|피규어를 따라감|크기 고정|붙임 없음/.test(JSON.stringify(ko)), 'the words nobody knew are gone')
35
35
  })
36
36
 
37
37
  test('the side tabs say their names, in every language', () => {
@@ -65,8 +65,8 @@ test('a measure that follows something says what, where the row once said 「다
65
65
  const ko = JSON.parse(read('../translations/ko.json')) as Record<string, string>
66
66
  const sayKo = (key: string, params: Record<string, string> = {}) => Object.entries(params).reduce((t, [k, v]) => t.split(`{${k}}`).join(v), ko[key] ?? key)
67
67
  assert.equal(followWordsOf({ of: 'part', part: 'bed', axis: 'x', times: 1 } as never, sayKo), 'bed의 X')
68
- assert.equal(followWordsOf({ of: 'instance', axis: 'y', times: 0.5 } as never, sayKo), '도형 Y 크기에 비례')
69
- assert.equal(followWordsOf({ of: 'instance', axis: 'z', times: 1 } as never, sayKo), '도형 Z 크기와 같게')
68
+ assert.equal(followWordsOf({ of: 'instance', axis: 'y', times: 0.5 } as never, sayKo), '피규어 Y 크기에 비례')
69
+ assert.equal(followWordsOf({ of: 'instance', axis: 'z', times: 1 } as never, sayKo), '피규어 Z 크기와 같게')
70
70
  assert.ok(!Object.values(ko).some(v => /\((floor|ceiling|center|wall|inverted|space)\)/.test(v)), 'no placement keeps its English name in brackets')
71
71
  assert.ok(!/AGV_LIFTER|대문자/.test(ko['figure.text.type-name-will-not-release']!), 'the identifier rule is the format\'s: lower case and hyphens')
72
72
  })
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **처음 저장은 타입과 이름을 묻는다** — 묻지 않을 때 새 도형이 기계 이름으로 저장됐다(2026-09-25).
2
+ * **처음 저장은 타입과 이름을 묻는다** — 묻지 않을 때 새 피규어가 기계 이름으로 저장됐다(2026-09-25).
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
@@ -44,7 +44,7 @@ function recording() {
44
44
  return { store, requests }
45
45
  }
46
46
 
47
- test('타입은 새 도형을 만드는 저장에만 실린다 — 이미 있는 도형의 저장은 타입을 보내지 않는다', async () => {
47
+ test('타입은 새 피규어를 만드는 저장에만 실린다 — 이미 있는 피규어의 저장은 타입을 보내지 않는다', async () => {
48
48
  const { store, requests } = recording()
49
49
  const session = createFigureProductSession(store)
50
50
  session.startNew()
@@ -55,7 +55,7 @@ test('타입은 새 도형을 만드는 저장에만 실린다 — 이미 있는
55
55
  session.command({ kind: 'rename', name: '리프터' })
56
56
  await session.save({ type: 'other' })
57
57
  assert.equal(requests[1]!.id, 'new-id')
58
- assert.equal('type' in requests[1]!, false, '있는 도형에는 보내지 않는다')
58
+ assert.equal('type' in requests[1]!, false, '있는 피규어에는 보내지 않는다')
59
59
  })
60
60
 
61
61
  test('타입을 주지 않으면 싣지 않는다 — 서버가 임시 이름을 붙인다', async () => {
@@ -39,7 +39,7 @@ test('만든 상자는 셋 다, 원기둥도 셋 다 — 원기둥의 크기는
39
39
  assert.equal(drum.round?.along.length, 1)
40
40
  })
41
41
 
42
- test('편집기의 도형 팔레트로 만든 상자 · 둥근 상자도 크기를 낸다 — 입력 이름이 dimension.* 이다', () => {
42
+ test('편집기의 부품 팔레트로 만든 상자 · 둥근 상자도 크기를 낸다 — 입력 이름이 dimension.* 이다', () => {
43
43
  let asset = createFigureAsset('Probe')
44
44
  asset = addFigurePrimitive(asset, { id: 'box-1', kind: 'box', dimensions: { width: 100, height: 100, depth: 100 }, position: { x: 0, y: 50, z: 0 } })
45
45
  asset = addFigurePrimitive(asset, { id: 'rb-1', kind: 'rounded-box', dimensions: { width: 100, height: 60, depth: 80, round: 10 }, position: { x: 0, y: 50, z: 0 } })
@@ -148,10 +148,10 @@ test('크기 칸은 가운데를, 손잡이는 반대쪽을 — 배치와 상관
148
148
  assert.equal(centreY(grow(asset, 'post', postH, 2200, { keep: 'opposite', axis: 'y', side: -1 } as never), 'post') + 2200 / 2, 100, '아래 손잡이: 윗면이 그 자리')
149
149
  const rollerLength = gizmoReachOf(asset, 'roller').round!.along[0]!
150
150
  assert.deepEqual(figureSizeEditOf(asset, 'roller', [set(rollerLength, 800)], { keep: 'centre' }).changes, [set(rollerLength, 800)], '누운 롤러의 길이: 가운데가 원점이라 자리는 그대로')
151
- /* 천장 도형도 같다 — 배치가 지키는 면을 따로 두지 않는다(ADR-0140 4.1 이 ADR-0097 §4.3 을 대신함). */
151
+ /* 천장 피규어도 같다 — 배치가 지키는 면을 따로 두지 않는다(ADR-0140 4.1 이 ADR-0097 §4.3 을 대신함). */
152
152
  let hung = addFigureBox(createFigureAsset('Lamp'), { id: 'lamp', size: { x: 100, y: 100, z: 100 }, position: { x: 0, y: -50, z: 0 } })
153
153
  hung = applyFigureAuthoring(hung, { kind: 'set-placement', placement: 'ceiling' })
154
- assert.equal(centreY(grow(hung, 'lamp', gizmoReachOf(hung, 'lamp').sizes![1], 300, { keep: 'centre' } as never), 'lamp'), -50, '천장 도형: 숫자 칸도 가운데')
154
+ assert.equal(centreY(grow(hung, 'lamp', gizmoReachOf(hung, 'lamp').sizes![1], 300, { keep: 'centre' } as never), 'lamp'), -50, '천장 피규어: 숫자 칸도 가운데')
155
155
  })
156
156
 
157
157
  /* 손잡이는 키를 몰라도 찾는다 — 도구 막대에 옮기기 · 돌리기 · 크기, 제 숫자로 놓인 부품에만 (09-26). */
package/test/keys.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **모델러의 키** — Ctrl+Z · Ctrl+Y 와 복제 · 지우기 · 도형 전체로. 칸에 치는 키는 칸의 것이다.
2
+ * **모델러의 키** — Ctrl+Z · Ctrl+Y 와 복제 · 지우기 · 피규어 전체로. 칸에 치는 키는 칸의 것이다.
3
3
  */
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
@@ -12,7 +12,7 @@ test('되돌리기 · 다시 하기 — Ctrl 과 Cmd 모두, 부품을 고르지
12
12
  assert.equal(keyActionOf({ key: 'y', ctrlKey: true }, ''), 'redo')
13
13
  })
14
14
 
15
- test('고른 부품에만 — 복제 · 지우기 · 도형 전체로', () => {
15
+ test('고른 부품에만 — 복제 · 지우기 · 피규어 전체로', () => {
16
16
  assert.equal(keyActionOf({ key: 'd', ctrlKey: true }, 'arm'), 'duplicate')
17
17
  assert.equal(keyActionOf({ key: 'Delete' }, 'arm'), 'remove')
18
18
  assert.equal(keyActionOf({ key: 'Backspace' }, 'arm'), 'remove')
@@ -37,7 +37,7 @@ test('맞추기는 글자 단추로 — 옮기는 넷(가운데 X·Z, 면에 대
37
37
  assert.ok(!ko['figure.text.center-on-x']!.includes('기준 상자'), 'says what the centre is')
38
38
  })
39
39
 
40
- test('불러오는 동안 빈 무대에 「도형을 추가해…」가 깜빡이지 않고, 좁은 화면 알림은 이 브라우저에서 한 번', () => {
40
+ test('불러오는 동안 빈 무대의 안내가 깜빡이지 않고, 좁은 화면 알림은 이 브라우저에서 한 번', () => {
41
41
  /* A new figure's start card says how to begin; an empty stage otherwise is one loading, or with every part hidden. The bare stage and the notice said once are the browser check's (test/browser/screen.check.mjs). */
42
42
  for (const lang of ['ko', 'en', 'ja', 'zh', 'ms', 'es'])
43
43
  assert.equal((wordsOf(lang) as Record<string, string>)['figure.text.add-a-shape-to-start'], undefined, lang)