@things-factory/figure-ui 10.1.211 → 10.1.213

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 (68) 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/figure-history-panel.ts +1 -1
  5. package/client/modeller/figure-parts.ts +3 -3
  6. package/client/modeller/figure-settings.ts +1 -1
  7. package/client/modeller/links.ts +2 -2
  8. package/client/modeller/scene-budget.ts +3 -3
  9. package/client/modeller/thumbnail.ts +1 -1
  10. package/client/pages/figure-list-page.ts +2 -2
  11. package/client/pages/figure-modeller-page.ts +4 -4
  12. package/client/types.ts +2 -2
  13. package/client/viewparts/figure-thumb.ts +1 -1
  14. package/dist-client/graphql/index.d.ts +1 -1
  15. package/dist-client/graphql/index.js +1 -1
  16. package/dist-client/graphql/index.js.map +1 -1
  17. package/dist-client/modeller/angle-dial.d.ts +1 -1
  18. package/dist-client/modeller/angle-dial.js.map +1 -1
  19. package/dist-client/modeller/canvas.js +1 -1
  20. package/dist-client/modeller/canvas.js.map +1 -1
  21. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  22. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  23. package/dist-client/modeller/figure-parts.d.ts +3 -3
  24. package/dist-client/modeller/figure-parts.js.map +1 -1
  25. package/dist-client/modeller/figure-settings.js +1 -1
  26. package/dist-client/modeller/figure-settings.js.map +1 -1
  27. package/dist-client/modeller/links.d.ts +1 -1
  28. package/dist-client/modeller/links.js +2 -2
  29. package/dist-client/modeller/links.js.map +1 -1
  30. package/dist-client/modeller/scene-budget.d.ts +3 -3
  31. package/dist-client/modeller/scene-budget.js +3 -3
  32. package/dist-client/modeller/scene-budget.js.map +1 -1
  33. package/dist-client/modeller/thumbnail.js +1 -1
  34. package/dist-client/modeller/thumbnail.js.map +1 -1
  35. package/dist-client/pages/figure-list-page.d.ts +1 -1
  36. package/dist-client/pages/figure-list-page.js +2 -2
  37. package/dist-client/pages/figure-list-page.js.map +1 -1
  38. package/dist-client/pages/figure-modeller-page.d.ts +3 -3
  39. package/dist-client/pages/figure-modeller-page.js +4 -4
  40. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  41. package/dist-client/tsconfig.tsbuildinfo +1 -1
  42. package/dist-client/types.d.ts +2 -2
  43. package/dist-client/types.js +1 -1
  44. package/dist-client/types.js.map +1 -1
  45. package/dist-client/viewparts/figure-thumb.js +1 -1
  46. package/dist-client/viewparts/figure-thumb.js.map +1 -1
  47. package/docs/multipart-response-review-2026-10-24.md +1 -1
  48. package/docs/response-authoring-2026-10-24.md +4 -4
  49. package/package.json +6 -6
  50. package/test/ai-proposal.test.ts +1 -1
  51. package/test/align.test.ts +1 -1
  52. package/test/command-preview.test.ts +3 -3
  53. package/test/fields.test.ts +2 -2
  54. package/test/first-minute.test.ts +5 -5
  55. package/test/first-save.test.ts +3 -3
  56. package/test/gizmo.test.ts +3 -3
  57. package/test/keys.test.ts +2 -2
  58. package/test/layout.test.ts +1 -1
  59. package/test/links.test.ts +14 -14
  60. package/test/live.test.ts +4 -4
  61. package/test/material.test.ts +1 -1
  62. package/test/properties.test.ts +1 -1
  63. package/test/proposal-record.test.ts +1 -1
  64. package/test/refusals.test.ts +1 -1
  65. package/test/resize-with-placement.test.ts +1 -1
  66. package/test/scene-budget.test.ts +4 -4
  67. package/test/verdict.test.ts +1 -1
  68. package/translations/ko.json +106 -106
@@ -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":"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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"]}
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.211",
3
+ "version": "10.1.213",
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.213",
46
+ "@things-factory/figure-service": "^10.1.213",
47
+ "@things-factory/figure-viewer": "^10.1.213",
48
+ "@things-factory/setting-base": "^10.1.213",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "1b781d2bbb963380eda73ab7020091987393de37"
51
+ "gitHead": "81f23aa18cb584c7d8185e81cca0d688f509cf8c"
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 () => {
@@ -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)
@@ -96,7 +96,7 @@ test('세 칸의 말은 다섯 말 모두에 있다', () => {
96
96
  }
97
97
  })
98
98
 
99
- test('도형 크기를 따라 움직이는 자리를 가려 읽고, 여기에 고정하면 사라진다 — 드라이룸 램프의 길', () => {
99
+ test('피규어 크기를 따라 움직이는 자리를 가려 읽고, 여기에 고정하면 사라진다 — 드라이룸 램프의 길', () => {
100
100
  let asset = pair()
101
101
  asset = addFigureBox(asset, { id: 'lamp', size: { x: 40, y: 20, z: 40 }, position: { x: 0, y: 10, z: 0 } })
102
102
  const model = asset.document.model
@@ -125,7 +125,7 @@ test('자리 — 부품의 면에 둔 집는 자리·놓는 자리를 그 부품
125
125
  assert.deepEqual(placesOn(asset, 'plate'), [], '다른 부품의 자리는 안 섞인다')
126
126
  })
127
127
 
128
- test('담는 능력이 없는 도형은 자리가 있어도 보드가 아무것도 넘기지 않는다 — 화면이 그것을 말한다', () => {
128
+ test('담는 능력이 없는 피규어는 자리가 있어도 보드가 아무것도 넘기지 않는다 — 화면이 그것을 말한다', () => {
129
129
  const asset = pair()
130
130
  assert.equal(holdsThings(asset), false)
131
131
  asset.document.capabilities.names = ['Capacity', 'Holdable']
@@ -221,7 +221,7 @@ const openSides = () => {
221
221
  }
222
222
 
223
223
  test('도면에서의 크기 — 좁히지 않은 끝은 부품에서 계산하고, 좁힌 끝은 작성자의 값이다', () => {
224
- assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 도형에는 범위가 없다')
224
+ assert.equal(instanceRangeOf(createFigureAsset('Probe')), undefined, '크기를 선언하지 않은 피규어에는 범위가 없다')
225
225
  const asset = openSides()
226
226
  const y = asset.document.model.inputs.find(i => i.id === 'size.y')!
227
227
  y.min = 90
@@ -245,7 +245,7 @@ test('도면에서의 크기 — 숫자를 치면 그 끝을 좁히고, 칸을
245
245
  )
246
246
  })
247
247
 
248
- test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
248
+ test('한 비율로 커지는 피규어 — 자기 숫자인 길이를 세고, 한 번에 따라가게 하며, 무엇이 남는지 이름을 댄다(아키텍트 판정 09-25)', () => {
249
249
  let asset = createFigureAsset('Probe')
250
250
  asset = addFigureBox(asset, { id: 'base', size: { x: 400, y: 100, z: 400 }, position: { x: 0, y: 0, z: 0 } })
251
251
  asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 60 }, position: { x: 0, y: 0, z: 0 } })
@@ -262,7 +262,7 @@ test('한 비율로 커지는 도형 — 자기 숫자인 길이를 세고, 한
262
262
  assert.equal(Math.round(valueNow(revisionOf(asset), 'lamp.dimension.x.value')! * 1e6) / 1e6, 60, '따르게 해도 지금 크기는 그대로')
263
263
  })
264
264
 
265
- test('관절이 있는 도형에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
265
+ test('관절이 있는 피규어에는 한 비율로 키우기를 권하고, 선언하면 규칙으로 보인다(수석 아키텍트 판정 09-24)', () => {
266
266
  let asset = sizedPair()
267
267
  assert.equal(turnsAboutAJoint(asset), false)
268
268
  asset = moving(asset, 'flange', { kind: 'turn', axis: 'y', id: 'flange.turn', min: 0, max: 90 })
@@ -372,7 +372,7 @@ test('따라 움직이는 관절 — 같은 방식으로 움직이고 제 조작
372
372
  assert.deepEqual(motionOf(asset, 'right')?.follows, { sum: [{ joint: 'left', multiplier: -1 }], offset: 0 }, '화면은 무엇을 따르는지 읽는다')
373
373
  })
374
374
 
375
- test('면은 도형이 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없고 곡면, 원뿔대는 위아래뿐, 원기둥 옆은 면이 아님 (2026-09-25, ADR-0140 3.9)', async () => {
375
+ test('면은 피규어가 정한다 — 문틀(portal)은 바깥 여섯에 개구부 셋, 공은 없고 곡면, 원뿔대는 위아래뿐, 원기둥 옆은 면이 아님 (2026-09-25, ADR-0140 3.9)', async () => {
376
376
  const { facesOf, faceAxisOf, canFasten, alignAxesOf, surfaceOf } = await import('../client/modeller/links.ts')
377
377
  let asset = createFigureAsset('Doorway')
378
378
  asset = addFigurePrimitive(asset, {
@@ -431,20 +431,20 @@ test('돌려 놓인 부품에도 모서리 맞춤을 내놓는다 — 지금 자
431
431
  void compileFigureAsset
432
432
  })
433
433
 
434
- test('모델러에서 만든 도형은 부품에서 재어 도형 크기를 선언한다 — 그 뒤에야 부품이 도형 크기를 따를 수 있다 (사용자 09-25)', async () => {
434
+ test('모델러에서 만든 피규어는 부품에서 재어 피규어 크기를 선언한다 — 그 뒤에야 부품이 피규어 크기를 따를 수 있다 (사용자 09-25)', async () => {
435
435
  const { instanceSizeToDeclare, instanceRangeOf, hasInstanceSize } = await import('../client/modeller/links.ts')
436
436
  const { compileFigureAsset } = await import('@hatiolab/figure-model')
437
437
  let asset = createFigureAsset('Conveyor')
438
438
  assert.equal(instanceSizeToDeclare(asset), undefined, '부품이 없으면 잴 것이 없다')
439
439
  asset = addFigureBox(asset, { id: 'frame', size: { x: 2000, y: 100, z: 600 }, position: { x: 0, y: 0, z: 0 } })
440
440
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'frame', to: { plane: 'mounting-plane' } })
441
- assert.equal(hasInstanceSize(asset, 'x'), false, '새 도형에는 도형 크기가 없다')
441
+ assert.equal(hasInstanceSize(asset, 'x'), false, '새 피규어에는 피규어 크기가 없다')
442
442
  const toDeclare = instanceSizeToDeclare(asset)!
443
443
  assert.deepEqual(toDeclare.at, { x: 2000, y: 100, z: 600 }, '지금 부품의 크기가 선언할 크기')
444
444
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', at: toDeclare.at })
445
445
  assert.ok(instanceRangeOf(asset), '선언 뒤에는 도면에서의 크기가 보인다')
446
446
  assert.equal(instanceSizeToDeclare(asset), undefined, '이미 있으면 다시 권하지 않는다')
447
- assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 도형 크기를 따르는 선택이 열린다')
447
+ assert.equal(hasInstanceSize(asset, 'x'), true, '부품이 피규어 크기를 따르는 선택이 열린다')
448
448
  asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'frame', axis: 'x', source: { of: 'instance', axis: 'x' } } as never)
449
449
  const at = (size: number) => (compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'frame')!.shape.dimensions as Record<string, number>)['size.x']
450
450
  assert.equal(at(3000), 3000, '보드가 3000 으로 놓으면 틀도 3000')
@@ -490,7 +490,7 @@ test('미끄러짐의 이동 거리가 부품 길이를 따른다 — 레일이
490
490
  assert.equal(end(4500), 0, '레일이 길어져도 끝까지')
491
491
  })
492
492
 
493
- test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
493
+ test('면 위 거리를 피규어 크기 비율로 — 피규어가 커지면 그 자리도 비율로 옮긴다 (사이징, 2026-09-25)', async () => {
494
494
  const { alignOf } = await import('../client/modeller/links.ts')
495
495
  const { compileFigureAsset } = await import('@hatiolab/figure-model')
496
496
  let asset = createFigureAsset('Panel')
@@ -498,7 +498,7 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
498
498
  asset = addFigureBox(asset, { id: 'lamp', size: { x: 60, y: 60, z: 30 }, position: { x: 0, y: 0, z: 0 } })
499
499
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'panel', to: { plane: 'mounting-plane' } })
500
500
  const front = { part: 'panel', face: 'front' }
501
- assert.deepEqual(alignOf(asset, front, { x: 'share' }, { x: 300 }), undefined, '도형 크기가 없으면 비율을 보낼 수 없다')
501
+ assert.deepEqual(alignOf(asset, front, { x: 'share' }, { x: 300 }), undefined, '피규어 크기가 없으면 비율을 보낼 수 없다')
502
502
  asset = applyFigureAuthoring(asset, { kind: 'declare-instance-size', range: { x: { min: 200, max: 5000 }, y: { min: 200, max: 5000 }, z: { min: 10, max: 500 } }, at: { x: 1000, y: 800, z: 40 } })
503
503
  asset = applyFigureAuthoring(asset, { kind: 'link-dimension', part: 'panel', axis: 'x', source: { of: 'instance', axis: 'x' } })
504
504
  const align = alignOf(asset, front, { x: 'share' }, { x: 300 })!
@@ -506,7 +506,7 @@ test('면 위 거리를 도형 크기 비율로 — 도형이 커지면 그 자
506
506
  asset = applyFigureAuthoring(asset, { kind: 'attach', part: 'lamp', to: front, align })
507
507
  const x = (size: number) => Math.round(compileFigureAsset({ ...asset, designInputs: { ...asset.designInputs, 'size.x': size } }).evaluate().geometry.find(g => g.id === 'lamp')!.pose.t[0]!)
508
508
  assert.equal(x(1000), 300)
509
- assert.equal(x(2000), 600, '도형이 두 배면 거리도 두 배')
509
+ assert.equal(x(2000), 600, '피규어가 두 배면 거리도 두 배')
510
510
  })
511
511
 
512
512
  test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이 도는 자리까지 재지 않는다 (2026-09-25)', async () => {
@@ -521,7 +521,7 @@ test('선언할 크기는 부품이 쉬고 있을 때의 크기 — 문짝이
521
521
  assert.deepEqual(instanceSizeToDeclare(asset)!.at, { x: 995, y: 2100, z: 150 })
522
522
  })
523
523
 
524
- test('바닥에 선 부품도 도형 크기를 선언한 도형이면 도형의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
524
+ test('바닥에 선 부품도 피규어 크기를 선언한 피규어면 피규어의 옆면에 맞출 수 있다 (2026-09-25)', async () => {
525
525
  const { alignAxesOf, alignOf } = await import('../client/modeller/links.ts')
526
526
  let asset = addFigureBox(createFigureAsset('Gate'), { id: 'post', size: { x: 100, y: 2000, z: 150 }, position: { x: 0, y: 0, z: 0 } })
527
527
  assert.deepEqual(alignAxesOf(asset, 'plane'), [{ axis: 'x', edges: false }, { axis: 'z', edges: false }], '크기가 없으면 옆면도 없다')
@@ -616,7 +616,7 @@ test('팔레트의 문틀은 문 크기로 서고, 세 축 모두 크기를 따
616
616
  })
617
617
 
618
618
  /* 크기를 따라감 한 번 — 치수 칸의 단추 하나, 크기를 대신 정했으면 한 줄로 말한다 (ADR 판정 Q1, 09-26). */
619
- test('치수 칸 — 「도형 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
619
+ test('치수 칸 — 「피규어 크기를 따라감」 단추는 follow-size 한 번, 다 따르면 사라진다; 크기를 대신 정하면 한 줄', () => {
620
620
  const ko = wordsOf('ko')
621
621
  assert.match(ko['figure.text.size-declared-to-follow'], /\{x\} × \{y\} × \{z\} mm/)
622
622
  })
package/test/live.test.ts CHANGED
@@ -17,7 +17,7 @@ const slider = () => {
17
17
  return moving(asset, 'block', { kind: 'slide', axis: 'x', id: 'shift', min: 0, max: 60, start: 0 })
18
18
  }
19
19
 
20
- test('끄는 값으로 그 자세를 바로 그린다 — 도형은 그대로', () => {
20
+ test('끄는 값으로 그 자세를 바로 그린다 — 피규어는 그대로', () => {
21
21
  const asset = slider()
22
22
  const before = JSON.stringify(asset)
23
23
  const parts = livePartsOf(asset, { values: { shift: 45 } })!
@@ -26,7 +26,7 @@ test('끄는 값으로 그 자세를 바로 그린다 — 도형은 그대로',
26
26
  assert.equal(livePartsOf(asset, NOTHING_LIVE), null, '끄는 것이 없으면 평소대로')
27
27
  })
28
28
 
29
- test('속성창의 설계 값도 치는 동안 그린다 — 크기가 따라 변하고 도형은 그대로', () => {
29
+ test('속성창의 설계 값도 치는 동안 그린다 — 크기가 따라 변하고 피규어는 그대로', () => {
30
30
  const asset = slider()
31
31
  const before = JSON.stringify(asset)
32
32
  const size = (parts: { id: string; shape?: unknown }[]) => JSON.stringify((parts.find(p => p.id === 'block') as { shape: unknown }).shape)
@@ -40,7 +40,7 @@ test('설계 값과 자세를 가른다 — 모르는 이름은 버린다', () =
40
40
  assert.deepEqual(liveOverridesOf(slider(), { shift: 10, 'block.size.x': 5, nothing: 1 }), { design: { 'block.size.x': 5 }, state: { shift: 10 } })
41
41
  })
42
42
 
43
- test('치는 도중 도형이 못 받는 값이면 지금 모습을 그린다 — 지어낸 도형을 보이지 않는다', () => {
43
+ test('치는 도중 피규어가 못 받는 값이면 지금 모습을 그린다 — 지어낸 피규어를 보이지 않는다', () => {
44
44
  assert.equal(livePartsOf(slider(), { values: { 'block.size.x': -5 } }), null)
45
45
  })
46
46
 
@@ -51,7 +51,7 @@ test('재질의 끄는 값(발광)도 그린다 — 편집 하나를 기록 없
51
51
  edit: { kind: 'set-material', target: 'block', material: { token: 'palette.primary', emissive: { token: 'palette.primary', intensity: 0.5, on: true } } }
52
52
  })!
53
53
  assert.equal(glowing.find(p => p.id === 'block')!.material.emissive?.intensity, 0.5)
54
- assert.equal(asset.appearance[0]!.material.emissive, undefined, '도형은 그대로')
54
+ assert.equal(asset.appearance[0]!.material.emissive, undefined, '피규어는 그대로')
55
55
  })
56
56
 
57
57
  test('이름표를 끌면 값이 크기에 맞는 걸음으로 움직인다 — 각도 1°, 길이는 자릿수에 따라, Shift 는 열 배 곱게', () => {
@@ -57,7 +57,7 @@ test('곡면 분할은 둥근 몸체에만 내고, 고르면 삼각형 수가
57
57
  assert.ok(fine > coarse, `48 분할이 8 분할보다 삼각형이 많다 (${coarse} → ${fine})`)
58
58
  })
59
59
 
60
- test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 도형의 팔레트는 기본색 하나다', () => {
60
+ test('자산에 없는 색 역할은 그 색을 함께 보낸다 — 새 피규어의 팔레트는 기본색 하나다', () => {
61
61
  const asset = probe()
62
62
  const offered = offeredPaletteOf({ 'palette.primary': '#000000', 'palette.accent': '#f28c28', 'palette.dark': '#2b2b2b' }, asset.palette)
63
63
  assert.equal(offered['palette.primary'], asset.palette['palette.primary'], '자산의 색이 앱의 색 위에 선다')
@@ -1,5 +1,5 @@
1
1
  /*
2
- * **도형의 속성을 고친다** (2026-09-24).
2
+ * **피규어의 속성을 고친다** (2026-09-24).
3
3
  *
4
4
  * 속성 탭은 이름 · 배치 기준 · 차지하는 공간 · 분류 · 태그를 맨 위에 둔다. 전에는 타입 칸 하나뿐이었다.
5
5
  * 두 축을 본다: 탭이 내는 명령이 figure-model 에서 **정말로** 그 일을 하는가, 그리고 화면이 명령과 행 쓰기를 잇는가.
@@ -115,7 +115,7 @@ test('the dock checks the result from the server: saved is accepted and named, n
115
115
  const { proposalOutcomeFrom } = await import('../client/modeller/ai-proposal.js')
116
116
  const words = JSON.parse(read('../translations/ko.json'))
117
117
  const sayKo = (key: string, params: Record<string, string> = {}) => words[key].replace(/\{(\w+)\}/g, (_: string, k: string) => params[k] ?? '')
118
- assert.deepEqual(proposalOutcomeFrom([{ figureId: 'x', type: 'LIDDED', name: '뚜껑 상자' }], sayKo), { verdict: 'accepted', note: '저장된 도형에 들어 있습니다: 뚜껑 상자' })
118
+ assert.deepEqual(proposalOutcomeFrom([{ figureId: 'x', type: 'LIDDED', name: '뚜껑 상자' }], sayKo), { verdict: 'accepted', note: '저장된 피규어에 들어 있습니다: 뚜껑 상자' })
119
119
  assert.equal(proposalOutcomeFrom([], sayKo), undefined, 'not saved is not known, never refused')
120
120
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) assert.ok(JSON.parse(read(`../translations/${lang}.json`))['figure.text.proposal-saved-into'], lang)
121
121
  })
@@ -271,7 +271,7 @@ test('단면 · 속 공간 — 모서리, 벽, 막힌 끝, 붙은 면을 거절
271
271
  assert.match(editRefusalOf(refusalOf(() => act(wedge, { kind: 'set-section', part: 'wedge', points: crossed })), keyed), /^figure\.text\.edit-refused .*"code":"SECTION_INVALID"/)
272
272
  })
273
273
 
274
- test('도형의 한도 — 노드 수, 저장 크기, 중첩 깊이를 숫자로 말한다 (「노드 한도」로 잘못 말하던 결함)', () => {
274
+ test('피규어의 한도 — 노드 수, 저장 크기, 중첩 깊이를 숫자로 말한다 (「노드 한도」로 잘못 말하던 결함)', () => {
275
275
  const doc = structuredClone(box().document) as { model: { nodes: unknown[] } }
276
276
  for (let i = 0; i <= fm.FIGURE_DOCUMENT_NODE_LIMIT; i++) doc.model.nodes.push({ id: `k${i}`, op: 'const@1', args: [], outputs: {} })
277
277
  const nodes = editRefusalOf(refusalOf(() => fm.compileFigureDocument(doc as never)), say)
@@ -94,7 +94,7 @@ test('fresh, width only, and all set back to fixed are three different answers',
94
94
  assert.deepEqual(resizeAxesOf(following, 'box'), { axes: ['y'], basis: 'chosen' })
95
95
  assert.deepEqual(resizeAxesOf(following, 'box', ['x']), { axes: ['x', 'y'], basis: 'chosen' })
96
96
  const ko = JSON.parse(readFileSync(new URL('../translations/ko.json', import.meta.url), 'utf-8'))
97
- assert.equal(ko['figure.button.follow-size'], '도형 크기에 맞추기')
97
+ assert.equal(ko['figure.button.follow-size'], '피규어 크기에 맞추기')
98
98
  })
99
99
 
100
100
  test('a part off the floor is not moved by choosing its size response', () => {
@@ -45,9 +45,9 @@ describe('한 화면이 감당하는 양', () => {
45
45
  })
46
46
 
47
47
  /*
48
- 실측한 두 도형이 이 분모로 되돌아오는지 본다. 분모를 누가 고치면 이 둘이 먼저 어긋난다.
48
+ 실측한 두 피규어가 이 분모로 되돌아오는지 본다. 분모를 누가 고치면 이 둘이 먼저 어긋난다.
49
49
  */
50
- it('잰 두 도형이 잰 수와 맞다', () => {
50
+ it('잰 두 피규어가 잰 수와 맞다', () => {
51
51
  /* 묶음 9 짜리 6 축 로봇: 600 대에서 59.5fps, 700 대에서 56.7fps 였다. */
52
52
  assert.ok(capacityFrom(9, 200)!.instances <= 600, '잰 값보다 많이 놓으라고 하면 안 된다')
53
53
  /* 묶음 2 짜리 컨베이어: 1,600 대에서 59.3fps, 2,500 대에서 36.8fps 였다. */
@@ -60,8 +60,8 @@ describe('한 화면이 감당하는 양', () => {
60
60
  * 놓는 개수로 값을 말하지 않는다 — ADR-0051 ③.
61
61
  *
62
62
  * 보고서에 「100개 기준 draw call」 · 「1만개 기준」이 오래 남아 있었다. 놓는 개수는 값을 치르지
63
- * 않는다(가만히 선 도형은 프레임 비용 0, 카메라 밖은 컬링). 치르는 것은 한 화면에 보이는 개수 ×
64
- * 그 도형의 묶음 수이고, 그 답은 위의 실측 분모가 한다.
63
+ * 않는다(가만히 선 피규어는 프레임 비용 0, 카메라 밖은 컬링). 치르는 것은 한 화면에 보이는 개수 ×
64
+ * 그 피규어의 묶음 수이고, 그 답은 위의 실측 분모가 한다.
65
65
  */
66
66
  describe('놓는 개수 기준 문구가 화면에 없다', () => {
67
67
  const HERE = fileURLToPath(new URL('.', import.meta.url))