@things-factory/figure-ui 10.1.5 → 10.1.6

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 (75) hide show
  1. package/client/graphql/index.ts +35 -3
  2. package/client/modeller/figure-ask.ts +5 -5
  3. package/client/modeller/figure-canvas.ts +1 -1
  4. package/client/modeller/figure-capabilities.ts +372 -0
  5. package/client/modeller/figure-history-panel.ts +171 -18
  6. package/client/modeller/figure-inspector.ts +392 -76
  7. package/client/modeller/figure-parts.ts +15 -15
  8. package/client/modeller/figure-preview.ts +2 -2
  9. package/client/modeller/figure-report.ts +102 -37
  10. package/client/modeller/figure-settings.ts +20 -20
  11. package/client/modeller/figure-side.ts +37 -9
  12. package/client/modeller/figure-source.test.ts +15 -1
  13. package/client/modeller/figure-source.ts +31 -15
  14. package/client/modeller/figure-thumbnail.ts +49 -4
  15. package/client/modeller/maturity.ts +10 -10
  16. package/client/pages/figure-list-page.ts +152 -66
  17. package/client/pages/figure-modeller-page.ts +260 -43
  18. package/client/types.ts +25 -0
  19. package/client/viewparts/figure-thumb.ts +172 -0
  20. package/dist-client/graphql/index.d.ts +8 -1
  21. package/dist-client/graphql/index.js +32 -3
  22. package/dist-client/graphql/index.js.map +1 -1
  23. package/dist-client/modeller/figure-ask.js +5 -5
  24. package/dist-client/modeller/figure-ask.js.map +1 -1
  25. package/dist-client/modeller/figure-canvas.js +1 -1
  26. package/dist-client/modeller/figure-canvas.js.map +1 -1
  27. package/dist-client/modeller/figure-capabilities.d.ts +42 -0
  28. package/dist-client/modeller/figure-capabilities.js +344 -0
  29. package/dist-client/modeller/figure-capabilities.js.map +1 -0
  30. package/dist-client/modeller/figure-history-panel.d.ts +15 -1
  31. package/dist-client/modeller/figure-history-panel.js +165 -18
  32. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  33. package/dist-client/modeller/figure-inspector.d.ts +84 -0
  34. package/dist-client/modeller/figure-inspector.js +379 -75
  35. package/dist-client/modeller/figure-inspector.js.map +1 -1
  36. package/dist-client/modeller/figure-parts.js +15 -15
  37. package/dist-client/modeller/figure-parts.js.map +1 -1
  38. package/dist-client/modeller/figure-preview.js +2 -2
  39. package/dist-client/modeller/figure-preview.js.map +1 -1
  40. package/dist-client/modeller/figure-report.d.ts +18 -1
  41. package/dist-client/modeller/figure-report.js +105 -37
  42. package/dist-client/modeller/figure-report.js.map +1 -1
  43. package/dist-client/modeller/figure-settings.js +19 -19
  44. package/dist-client/modeller/figure-settings.js.map +1 -1
  45. package/dist-client/modeller/figure-side.d.ts +1 -0
  46. package/dist-client/modeller/figure-side.js +36 -8
  47. package/dist-client/modeller/figure-side.js.map +1 -1
  48. package/dist-client/modeller/figure-source.d.ts +15 -8
  49. package/dist-client/modeller/figure-source.js +16 -7
  50. package/dist-client/modeller/figure-source.js.map +1 -1
  51. package/dist-client/modeller/figure-thumbnail.js +40 -4
  52. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  53. package/dist-client/modeller/maturity.js +10 -10
  54. package/dist-client/modeller/maturity.js.map +1 -1
  55. package/dist-client/pages/figure-list-page.d.ts +1 -4
  56. package/dist-client/pages/figure-list-page.js +150 -63
  57. package/dist-client/pages/figure-list-page.js.map +1 -1
  58. package/dist-client/pages/figure-modeller-page.d.ts +45 -0
  59. package/dist-client/pages/figure-modeller-page.js +259 -43
  60. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  61. package/dist-client/tsconfig.tsbuildinfo +1 -1
  62. package/dist-client/types.d.ts +23 -0
  63. package/dist-client/types.js.map +1 -1
  64. package/dist-client/viewparts/figure-thumb.d.ts +29 -0
  65. package/dist-client/viewparts/figure-thumb.js +168 -0
  66. package/dist-client/viewparts/figure-thumb.js.map +1 -0
  67. package/dist-server/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +7 -5
  69. package/test/i18n-prefix-guard.test.ts +129 -0
  70. package/test/korean-register-guard.test.ts +83 -0
  71. package/translations/en.json +307 -264
  72. package/translations/ja.json +275 -264
  73. package/translations/ko.json +307 -264
  74. package/translations/ms.json +275 -264
  75. package/translations/zh.json +275 -264
@@ -50,11 +50,11 @@ export function toFigureSource(board, parts) {
50
50
  return withoutEmpty({
51
51
  type: board.figureType ?? '',
52
52
  base: { x: board.width, y: board.depth, z: board.height },
53
- anchor: board.anchor,
54
53
  detailLevel: board.detailLevel,
55
54
  styleKit: board.styleKit,
56
55
  parts: parts.map(part => partTo(part, half)),
57
- animations: board.animations
56
+ animations: board.animations,
57
+ capabilities: board.capabilities
58
58
  });
59
59
  }
60
60
  function partTo(part, half) {
@@ -64,6 +64,13 @@ function partTo(part, half) {
64
64
  z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined
65
65
  });
66
66
  const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow });
67
+ /*
68
+ 재질이 하나도 없으면 **칸 자체를 안 적는다.**
69
+
70
+ 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어
71
+ `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이
72
+ 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.
73
+ */
67
74
  const material = withoutEmpty({
68
75
  token: part.token,
69
76
  preset: part.preset,
@@ -86,13 +93,14 @@ function partTo(part, half) {
86
93
  }),
87
94
  segments: part.segments,
88
95
  shape: Object.keys(shape).length > 0 ? shape : undefined,
89
- material,
96
+ material: Object.keys(material).length > 0 ? material : undefined,
90
97
  materialSlot: part.materialSlot,
91
98
  keepRound: part.keepRound,
92
99
  sizing: part.sizing,
93
100
  anchor: part.anchor,
94
101
  repeat: part.repeat,
95
- label: part.label
102
+ label: part.label,
103
+ capability: part.capability
96
104
  });
97
105
  }
98
106
  /** 형식을 씬 모델로 편다. */
@@ -105,8 +113,8 @@ export function fromFigureSource(source) {
105
113
  figureType: source.type,
106
114
  detailLevel: source.detailLevel,
107
115
  styleKit: source.styleKit,
108
- anchor: source.anchor,
109
- animations: source.animations
116
+ animations: source.animations,
117
+ capabilities: source.capabilities
110
118
  });
111
119
  return { board, parts: source.parts.map(part => partFrom(part, half)) };
112
120
  }
@@ -147,7 +155,8 @@ function partFrom(part, half) {
147
155
  sizing: part.sizing,
148
156
  anchor: part.anchor,
149
157
  repeat: part.repeat,
150
- label: part.label
158
+ label: part.label,
159
+ capability: part.capability
151
160
  });
152
161
  }
153
162
  //# sourceMappingURL=figure-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DetailLevel, FigurePart, FigureSource, Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a board. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The board is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x board width\n * base.y board depth <- height\n * base.z board height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - board width/2\n * position.y zPos + depth/2 - board depth/2 <- zPos is the underside\n * position.z top + height/2 - board height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 보드의 상태. 기준 상자이자 원본 데이터의 뼈대다. */\nexport interface BoardModel {\n /** 기준 상자. 씬의 용어 그대로다. */\n width: number\n height: number\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 형상 전체의 기준점.\n *\n * **부품의 `anchor` 와 다른 것이다.** 이쪽은 점(`Vec3`)이고 저쪽은 축별로 어느 면을\n * 붙잡느냐다. 한 형식 안에 같은 낱말이 둘인 것은 좋지 않으나, 이름을 바꾸면 저장된\n * 원본이 따라와야 하므로 여기서 정하지 않는다.\n */\n anchor?: Vec3\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n token?: string\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nfunction round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(board: BoardModel): Vec3 {\n return { x: board.width / 2, y: board.depth / 2, z: board.height / 2 }\n}\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function boardExtent(board: BoardModel, axis: PlaneAxis): number {\n return axis === 'x' ? board.width : axis === 'y' ? board.depth : board.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, board: BoardModel): FigurePart {\n return partTo(part, halfOf(board))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, board: BoardModel): PartModel {\n return partFrom(part, halfOf(board))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSource {\n const half = halfOf(board)\n\n return withoutEmpty({\n type: board.figureType ?? '',\n base: { x: board.width, y: board.depth, z: board.height },\n anchor: board.anchor,\n detailLevel: board.detailLevel,\n styleKit: board.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: board.animations\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { board: BoardModel; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const board = withoutEmpty({\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n anchor: source.anchor,\n animations: source.animations\n }) as BoardModel\n\n return { board, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label\n }) as PartModel\n}\n"]}
1
+ {"version":3,"file":"figure-source.js","sourceRoot":"","sources":["../../client/modeller/figure-source.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DetailLevel, FigurePart, FigureSource, Vec3 } from '@hatiolab/figure-model'\n\n/**\n * Scene model <-> `FigureSource`.\n *\n * ## Why this is the only place that converts\n *\n * While authoring, the source of truth is the **scene model** -- part\n * components sitting on a board. `FigureSource` is produced from it when the\n * asset is saved or scored.\n *\n * The two use different vocabularies. The scene inherited 2D canvas terms\n * (`width`/`height` in plan, `depth` for height, `left`/`top`/`zPos` for\n * position); the format uses the axes 3D tools use (`x` right, `y` up, `z`\n * front, position at the part's **centre**).\n *\n * **Convert in one place.** Two places drift, and this format has already had\n * one incident caused by axes disagreeing.\n *\n * ## It was called `fold`, and that was wrong\n *\n * `fold` is an established term -- it reduces a structure to a summary, and\n * this repo already uses it correctly (`foldKpi`, `foldProbeResults`). Nothing\n * is reduced here: the same information comes out in a different shape, and it\n * goes back unchanged. A reader who learned `fold` from `foldKpi` reads this\n * one as reducing something, and the user did exactly that.\n *\n * ## The mapping\n *\n * The board is the base box; its centre is the format's origin.\n *\n * format scene\n * ──────────────── ──────────────────────────────\n * base.x board width\n * base.y board depth <- height\n * base.z board height\n *\n * size.x width\n * size.y depth\n * size.z height\n *\n * position.x left + width/2 - board width/2\n * position.y zPos + depth/2 - board depth/2 <- zPos is the underside\n * position.z top + height/2 - board height/2\n *\n * rotation.x rotationX (scene radians, format degrees)\n * rotation.y -rotation\n * rotation.z rotationY\n *\n * The rotation names are crossed because of the scene: `rotationY` becomes\n * three's z (`real-object.ts`, `get rotation()`). That stays inside this table.\n *\n * ## Fields nothing reads yet still travel\n *\n * `materialSlot`, `keepRound`, `sizing`, `anchor`, `repeat` and `label` exist in the format\n * and the editor ignores them. They still have to survive a round trip. When a\n * source proposed by AI is opened, edited and written back, a field that\n * disappears **without an error** is one the author cannot know about.\n *\n * **Five have gone that way here**: `ports`, `animations`, `anchor`,\n * `shape.hollow` and `keepRound`. The last one the test caught first, which\n * is the guard below actually earning its keep. The third showed up as a conveyor motor that stayed on the\n * floor no matter what the sample said; the fourth as a hollowed tray that\n * came out a solid block.\n *\n * All four passed the round-trip test, because the fixture it uses did not\n * carry the field. So the test now reads the **field list the format declares**\n * rather than trusting the fixture.\n *\n * `shape` is unpacked into `round`, `path` and `hollow` rather than carried\n * whole, because the editor holds those as separate state. Every field added\n * under `shape` costs a line here, and forgetting that line was the fourth\n * of the four.\n */\n/** 보드의 상태. 기준 상자이자 원본 데이터의 뼈대다. */\nexport interface BoardModel {\n /** 기준 상자. 씬의 용어 그대로다. */\n width: number\n height: number\n depth: number\n /** 컴포넌트 타입 이름. **저장되는 식별자다.** */\n figureType?: string\n detailLevel?: DetailLevel\n styleKit?: string\n /**\n * 움직임. **부품 바깥에 있다** — clip 하나가 부품 여럿을 함께 움직이기 때문이다.\n *\n * 편집면이 아직 이것을 만지지 않는다. 그래도 여기 실어 두는 것은, 안 실으면 열었다\n * 저장하는 것만으로 **조용히 지워지기** 때문이다.\n */\n animations?: FigureSource['animations']\n /**\n * 이 도형이 지는 능력. 씬의 capability mixin 이름이다.\n *\n * **부품이 아니라 도형 전체의 것이다** — 「이 컨베이어는 흐름의 마디다」는 부품 하나가\n * 아니라 도형이 지는 성질이다. 부품 쪽 `capability` 는 그 능력을 **어느 부품이 받쳐\n * 주나**를 말하는 다른 칸이다.\n */\n capabilities?: FigureSource['capabilities']\n}\n\n/** 부품 컴포넌트의 상태. 씬의 용어 그대로다. */\nexport interface PartModel {\n type: 'figure-part'\n /** 부품 이름. **저장되는 식별자다.** 씬은 이 값을 검색에도 쓴다. */\n name: string\n primitive: FigurePart['primitive']\n\n left: number\n top: number\n width: number\n height: number\n /** 부피의 **밑면** 높이. 씬의 'floor' 배치에서 그렇게 정해져 있다. */\n zPos: number\n depth: number\n\n /** 라디안. 씬이 그렇게 쓴다. */\n rotationX?: number\n rotation?: number\n rotationY?: number\n\n segments?: number\n round?: number\n path?: { x: number; y: number }[]\n /** 속을 판 단면. 편집면이 아직 안 만지지만 잃어버리면 통이 통짜 상자가 된다. */\n hollow?: NonNullable<FigurePart['shape']>['hollow']\n\n /**\n * 편집 중에만 쓰는 상태 — **저장 형식으로 넘어가지 않는다.**\n *\n * 씬이 이미 갖고 있는 속성이다. 편집 모드에서는 `faint`(반투명 + 색을 버리고 회색으로\n * 수렴)로 보이고, 뷰 모드에서는 아예 안 그려진다. 판단은 씬의 `modeling-faint` 한 곳에\n * 있고 2D 캔버스·3D 재질·DOM 세 채널이 같이 쓴다.\n *\n * 숨김은 \"지금 이 뒤를 만지려고\" 하는 것이지 그 부품의 성질이 아니다. 그래서\n * `toFigureSource` 가 싣지 않고, 다시 열면 전부 보이는 상태로 돌아온다.\n */\n hidden?: boolean\n\n token?: string\n preset?: FigurePart['material']['preset']\n flatShading?: boolean\n transparent?: boolean\n /**\n * 자체발광. 없으면 안 빛난다.\n *\n * 바깥의 `token` 이 **꺼진 모습**이고 이 안의 `token` 이 **켜진 모습**이다 — 꺼진\n * 전구는 빨갛지 않고 어두운 플라스틱이다.\n */\n emissive?: FigurePart['material']['emissive']\n surface?: FigurePart['material']['surface']\n\n /* 아직 아무 일도 안 하지만 잃어버리지 않는다. */\n materialSlot?: FigurePart['materialSlot']\n /** 단면을 원으로 지킬까. 곡면만 뜻이 있고 기본은 지키는 것이다. */\n keepRound?: FigurePart['keepRound']\n sizing?: FigurePart['sizing']\n /** 축마다 어느 면을 붙잡나. `sizing` 이 정한 것을 덮어쓴다. */\n anchor?: FigurePart['anchor']\n repeat?: FigurePart['repeat']\n label?: FigurePart['label']\n /**\n * 이 부품이 능력의 어느 자리인가 — 물건이 앉는 자리(slot)이거나 드나드는 문(port)이다.\n *\n * 이 칸이 있는 부품은 **그려지지 않는다.** 저작면에서는 어디인지 보여야 하니 그리지만,\n * 청사진에서는 `groups` 가 아니라 `anchors` 로 간다.\n */\n capability?: FigurePart['capability']\n}\n\nconst RAD_TO_DEG = 180 / Math.PI\nconst DEG_TO_RAD = Math.PI / 180\n\n/** 값이 있는 것만 남긴다. 없는 필드를 `undefined` 로 두면 접었다 편 것이 원본과 달라진다. */\nfunction withoutEmpty<T extends object>(value: T): T {\n const out = {} as Record<string, unknown>\n for (const [key, item] of Object.entries(value)) {\n if (item !== undefined) out[key] = item\n }\n return out as T\n}\n\n/** 소수점 넷째 자리까지. 라디안↔도를 오가며 생기는 꼬리를 자른다. */\nfunction round4(value: number): number {\n return Math.round(value * 1e4) / 1e4\n}\n\n/**\n * 판의 절반. 형식의 원점이 판 중심이라 이 값이 늘 함께 다닌다.\n *\n * 축 이름은 **형식의 것**이다 — x 가로 · y 높이 · z 세로.\n */\nexport function halfOf(board: BoardModel): Vec3 {\n return { x: board.width / 2, y: board.depth / 2, z: board.height / 2 }\n}\n\n/**\n * 저작자가 보는 축이 씬의 어느 용어인가.\n *\n * 씬은 2D 캔버스에서 온 용어를 쓰므로 축 이름이 그대로 없다. 이 표가 **유일한** 대응이다 —\n * 편집 규칙이 저마다 자기 표를 들면 한 곳을 고칠 때 다른 곳이 남는다.\n */\nexport const AXIS_KEYS = {\n x: { at: 'left', size: 'width' },\n y: { at: 'zPos', size: 'depth' },\n z: { at: 'top', size: 'height' }\n} as const\n\nexport type PlaneAxis = keyof typeof AXIS_KEYS\n\n/** 판이 그 축으로 얼마나 넓은가. */\nexport function boardExtent(board: BoardModel, axis: PlaneAxis): number {\n return axis === 'x' ? board.width : axis === 'y' ? board.depth : board.height\n}\n\n/** 부품 하나를 저장 형식으로 변환한다. 저작 화면이 저작자의 어휘로 보일 때 쓴다. */\nexport function partToFigure(part: PartModel, board: BoardModel): FigurePart {\n return partTo(part, halfOf(board))\n}\n\n/** 부품 하나를 씬 용어로 되돌린다. */\nexport function partFromFigure(part: FigurePart, board: BoardModel): PartModel {\n return partFrom(part, halfOf(board))\n}\n\n/** 씬 모델을 저장 형식으로 변환한다. */\nexport function toFigureSource(board: BoardModel, parts: PartModel[]): FigureSource {\n const half = halfOf(board)\n\n return withoutEmpty({\n type: board.figureType ?? '',\n base: { x: board.width, y: board.depth, z: board.height },\n detailLevel: board.detailLevel,\n styleKit: board.styleKit,\n parts: parts.map(part => partTo(part, half)),\n animations: board.animations,\n capabilities: board.capabilities\n }) as FigureSource\n}\n\nfunction partTo(part: PartModel, half: Vec3): FigurePart {\n const rotation = withoutEmpty({\n x: part.rotationX ? round4(part.rotationX * RAD_TO_DEG) : undefined,\n y: part.rotation ? round4(-part.rotation * RAD_TO_DEG) : undefined,\n z: part.rotationY ? round4(part.rotationY * RAD_TO_DEG) : undefined\n })\n\n const shape = withoutEmpty({ round: part.round, path: part.path, hollow: part.hollow })\n /*\n 재질이 하나도 없으면 **칸 자체를 안 적는다.**\n\n 빈 객체를 적으면 형식이 「재질은 있는데 무엇으로 칠할지 모른다」로 읽어\n `material-unresolvable` 을 낸다. 그리지 않는 자리(`capability`)에는 재질이 없는 것이\n 정상이고, 그 부품이 조용히 오류를 달고 다니게 된다.\n */\n const material = withoutEmpty({\n token: part.token,\n preset: part.preset,\n flatShading: part.flatShading,\n transparent: part.transparent,\n emissive: part.emissive,\n surface: part.surface\n })\n\n return withoutEmpty({\n name: part.name,\n primitive: part.primitive,\n transform: withoutEmpty({\n position: {\n x: round4(part.left + part.width / 2 - half.x),\n y: round4(part.zPos + part.depth / 2 - half.y),\n z: round4(part.top + part.height / 2 - half.z)\n },\n size: { x: part.width, y: part.depth, z: part.height },\n rotation: Object.keys(rotation).length > 0 ? rotation : undefined\n }),\n segments: part.segments,\n shape: Object.keys(shape).length > 0 ? shape : undefined,\n material: Object.keys(material).length > 0 ? material : undefined,\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as FigurePart\n}\n\n/** 형식을 씬 모델로 편다. */\nexport function fromFigureSource(source: FigureSource): { board: BoardModel; parts: PartModel[] } {\n const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }\n\n const board = withoutEmpty({\n width: source.base.x,\n height: source.base.z,\n depth: source.base.y,\n figureType: source.type,\n detailLevel: source.detailLevel,\n styleKit: source.styleKit,\n animations: source.animations,\n capabilities: source.capabilities\n }) as BoardModel\n\n return { board, parts: source.parts.map(part => partFrom(part, half)) }\n}\n\nfunction partFrom(part: FigurePart, half: Vec3): PartModel {\n const { position, size, rotation } = part.transform\n const material = part.material ?? {}\n\n return withoutEmpty({\n type: 'figure-part',\n name: part.name,\n primitive: part.primitive,\n\n left: round4(half.x + position.x - size.x / 2),\n top: round4(half.z + position.z - size.z / 2),\n width: size.x,\n height: size.z,\n zPos: round4(half.y + position.y - size.y / 2),\n depth: size.y,\n\n /*\n 라디안은 자르지 않는다.\n\n 소수 넷째에서 잘랐더니 도로 되돌릴 때 57 배로 커져 45 도가 45.0001 이 됐다.\n 자르는 것은 **사람이 읽는 쪽**(도)에서만 한다.\n */\n rotationX: rotation?.x ? rotation.x * DEG_TO_RAD : undefined,\n rotation: rotation?.y ? -rotation.y * DEG_TO_RAD : undefined,\n rotationY: rotation?.z ? rotation.z * DEG_TO_RAD : undefined,\n\n segments: part.segments,\n round: part.shape?.round,\n path: part.shape?.path,\n hollow: part.shape?.hollow,\n\n token: material.token,\n preset: material.preset,\n flatShading: material.flatShading,\n transparent: material.transparent,\n emissive: material.emissive,\n surface: material.surface,\n\n materialSlot: part.materialSlot,\n keepRound: part.keepRound,\n sizing: part.sizing,\n anchor: part.anchor,\n repeat: part.repeat,\n label: part.label,\n capability: part.capability\n }) as PartModel\n}\n"]}
@@ -22,9 +22,34 @@ import './figure-canvas.js';
22
22
  /** 카드는 4:3 이다. */
23
23
  const WIDTH = 480;
24
24
  const HEIGHT = 360;
25
- /** 프레임 두 장 뒤에 한 번 더 쉰다 — 씬이 서고 카메라가 자리를 잡을 틈. 씬에 그 신호가 없다. */
25
+ /**
26
+ * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 그 신호가 없다.
27
+ *
28
+ * **rAF 만 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면
29
+ * 이 약속이 영원히 안 풀린다. 그 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,
30
+ * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.
31
+ *
32
+ * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래
33
+ * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**
34
+ */
26
35
  function settle() {
27
- return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150))));
36
+ const frames = new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150))));
37
+ return Promise.race([frames, delay(600)]);
38
+ }
39
+ function delay(ms) {
40
+ return new Promise(resolve => setTimeout(resolve, ms));
41
+ }
42
+ /**
43
+ * 그림을 기다리는 시간의 한도.
44
+ *
45
+ * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가
46
+ * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,
47
+ * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).
48
+ */
49
+ const PATIENCE = 5000;
50
+ /** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */
51
+ function within(work, ms) {
52
+ return Promise.race([work, delay(ms).then(() => undefined)]);
28
53
  }
29
54
  export async function captureThumbnail(board, parts) {
30
55
  if (!board || !parts.length) {
@@ -45,8 +70,19 @@ export async function captureThumbnail(board, parts) {
45
70
  shooter.style.cssText = 'display: block; width: 100%; height: 100%;';
46
71
  try {
47
72
  host.appendChild(shooter);
48
- await settle();
49
- return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined;
73
+ /*
74
+ **한도 안에 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을
75
+ 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속
76
+ 안 생기면 그 줄이 단서가 된다.
77
+ */
78
+ const shot = await within((async () => {
79
+ await settle();
80
+ return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined;
81
+ })(), PATIENCE);
82
+ if (!shot) {
83
+ console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`);
84
+ }
85
+ return shot;
50
86
  }
51
87
  catch (e) {
52
88
  console.warn(`[thumbnail] 세우지 못했습니다 — ${e.message}`);
@@ -1 +1 @@
1
- {"version":3,"file":"figure-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/figure-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AASlB,8DAA8D;AAC9D,SAAS,MAAM;IACb,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAC3B,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA6B,EAC7B,KAAkB;IAElB,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAY,CAAA;IAClE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QACzB,MAAM,MAAM,EAAE,CAAA;QAEd,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC7E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './figure-canvas.js'\n\nimport type { BoardModel, PartModel } from './figure-source.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있고, 칸의 비율도 창 크기와 접힌 패널에 따라\n * 다르다. 그것을 찍으면 카드마다 프레임이 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이\n * 된다. 목록은 견주는 자리이므로 그림은 **정해진 크기와 정해진 카메라**로 찍는다.\n *\n * ## 왜 씬을 여기서 세우지 않고 부품을 세우나\n *\n * 씬 값(환경 · 하늘 · 조명 · 픽셀 비율 · 좌표 바탕)은 `figure-canvas` 가 든다. 여기서 다시 적으면\n * 두 벌이 되고, 그러면 **카드에 보이는 그림과 저작면에서 보던 것이 아무 오류 없이 갈린다.** 그래서\n * 같은 부품을 `viewOnly` 로 하나 더 세워 그 부품이 자기 씬을 세우게 하고, 찍은 뒤 버린다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 보드는 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주\n * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\ninterface Shooter extends HTMLElement {\n board?: BoardModel\n parts?: PartModel[]\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n}\n\n/** 프레임 두 장 뒤에 한 번 더 쉰다 씬이 서고 카메라가 자리를 잡을 틈. 씬에신호가 없다. */\nfunction settle(): Promise<void> {\n return new Promise(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n}\n\nexport async function captureThumbnail(\n board: BoardModel | undefined,\n parts: PartModel[]\n): Promise<string | undefined> {\n if (!board || !parts.length) {\n return undefined\n }\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-canvas') as Shooter\n shooter.viewOnly = true\n shooter.board = board\n shooter.parts = parts\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n await settle()\n\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
1
+ {"version":3,"file":"figure-thumbnail.js","sourceRoot":"","sources":["../../client/modeller/figure-thumbnail.ts"],"names":[],"mappings":"AAAA,OAAO,oBAAoB,CAAA;AAI3B;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,kBAAkB;AAClB,MAAM,KAAK,GAAG,GAAG,CAAA;AACjB,MAAM,MAAM,GAAG,GAAG,CAAA;AASlB;;;;;;;;;GASG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CACzC,qBAAqB,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CACnF,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,wDAAwD;AACxD,SAAS,MAAM,CAAI,IAAgB,EAAE,EAAU;IAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAA6B,EAC7B,KAAkB;IAElB,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;MAGE;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,CAAC,CAAA;IAC/C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,mDAAmD,KAAK,eAAe,MAAM,2BAA2B,CAAA;IAC7H,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;IAE/B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAY,CAAA;IAClE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAA;IAEpE,IAAI,CAAC;QACH,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAEzB;;;;UAIE;QACF,MAAM,IAAI,GAAG,MAAM,MAAM,CACvB,CAAC,KAAK,IAAI,EAAE;YACV,MAAM,MAAM,EAAE,CAAA;YACd,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAC7E,CAAC,CAAC,EAAE,EACJ,QAAQ,CACT,CAAA;QAED,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,IAAI,CAAC,eAAe,QAAQ,6BAA6B,CAAC,CAAA;QACpE,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,2BAA4B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/D,OAAO,SAAS,CAAA;IAClB,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;AACH,CAAC","sourcesContent":["import './figure-canvas.js'\n\nimport type { BoardModel, PartModel } from './figure-source.js'\n\n/**\n * 카드에 쓸 그림을 찍는다 — **숨은 자리에 저작 캔버스를 하나 더 세워서.**\n *\n * ## 왜 보이는 캔버스를 찍지 않나\n *\n * 저작면의 캔버스는 저작자가 돌려 둔 각도와 확대로 서 있고, 칸의 비율도 창 크기와 접힌 패널에 따라\n * 다르다. 그것을 찍으면 카드마다 프레임이 달라지고, 방금 확대해 보던 나사 하나가 그 자산의 얼굴이\n * 된다. 목록은 견주는 자리이므로 그림은 **정해진 크기와 정해진 카메라**로 찍는다.\n *\n * ## 왜 씬을 여기서 세우지 않고 부품을 세우나\n *\n * 씬 값(환경 · 하늘 · 조명 · 픽셀 비율 · 좌표 바탕)은 `figure-canvas` 가 든다. 여기서 다시 적으면\n * 두 벌이 되고, 그러면 **카드에 보이는 그림과 저작면에서 보던 것이 아무 오류 없이 갈린다.** 그래서\n * 같은 부품을 `viewOnly` 로 하나 더 세워 그 부품이 자기 씬을 세우게 하고, 찍은 뒤 버린다.\n *\n * ## 왜 서버가 아니라 브라우저인가\n *\n * 보드는 저장할 때 서버가 찍는데, 백엔드에 GPU 가 없는 배포가 많아 헤드리스로 3D 를 찍는 것이 자주\n * 실패한다. 도형을 그릴 GPU 는 이미 보는 사람 손에 있다.\n */\n\n/** 카드는 4:3 이다. */\nconst WIDTH = 480\nconst HEIGHT = 360\n\ninterface Shooter extends HTMLElement {\n board?: BoardModel\n parts?: PartModel[]\n viewOnly?: boolean\n snapshot?: (width?: number, height?: number) => Promise<string | undefined>\n}\n\n/**\n * 씬이 서고 카메라가 자리를 잡을 틈 — 프레임 두 장 뒤에 한 번 더 쉰다. 씬에 신호가 없다.\n *\n * **rAF 기다리면 안 된다.** 탭이 뒤로 가거나 창이 가려지면 브라우저가 rAF 를 멈추고, 그러면\n * 이 약속이 영원히 안 풀린다. 위에 저장이 얹혀 있어서 저장이 통째로 멈췄다 — 단추는 잠긴 채로,\n * 아무 말도 없이. [확인함] 2026-09-10 포커스 없는 탭에서 `saving: true` 로 고정.\n *\n * 그래서 시계와 함께 경주시킨다. 늦게 깨어난 쪽은 버려지고, 화면이 안 그려지는 상황이면 아래\n * `snapshot` 이 빈 것을 돌려주거나 시간이 다 된다 — 어느 쪽이든 **저장은 계속 간다.**\n */\nfunction settle(): Promise<void> {\n const frames = new Promise<void>(resolve =>\n requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(resolve, 150)))\n )\n\n return Promise.race([frames, delay(600)])\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * 그림을 기다리는 시간의 한도.\n *\n * 3D 를 세워 한 장 찍는 데 걸리는 시간은 형상과 기계에 따라 다르지만, 이 한도의 목적은 빠르기가\n * 아니라 **저장이 멈추지 않는 것**이다. 넘으면 그림 없이 저장한다 — 정본이 그림보다 중하고,\n * 그림은 다음에 저작면을 열 때 다시 채운다(`figure-modeller-page` 의 fillThumbnail).\n */\nconst PATIENCE = 5000\n\n/** 시간이 다 되면 없는 것으로 답한다. 던지지 않는다 — 그림이 없는 것은 실패가 아니다. */\nfunction within<T>(work: Promise<T>, ms: number): Promise<T | undefined> {\n return Promise.race([work, delay(ms).then(() => undefined)])\n}\n\nexport async function captureThumbnail(\n board: BoardModel | undefined,\n parts: PartModel[]\n): Promise<string | undefined> {\n if (!board || !parts.length) {\n return undefined\n }\n\n /*\n 화면 밖에 둔다. `display: none` 이면 캔버스가 크기를 못 얻어 아무것도 그리지 못한다.\n WebGL context 는 브라우저가 8~16개에서 끊으므로 찍은 뒤 반드시 버린다(아래 finally).\n */\n const host = document.createElement('div')\n host.setAttribute('figure-thumbnail-stage', '')\n host.style.cssText = `position: fixed; left: -10000px; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px; pointer-events: none;`\n document.body.appendChild(host)\n\n const shooter = document.createElement('figure-canvas') as Shooter\n shooter.viewOnly = true\n shooter.board = board\n shooter.parts = parts\n shooter.style.cssText = 'display: block; width: 100%; height: 100%;'\n\n try {\n host.appendChild(shooter)\n\n /*\n **한도 안에 못 찍으면 그림 없이 간다.** 이 함수를 부르는 자리가 저장이고, 저장은 그림을\n 기다리다 멈춰서는 안 된다. 못 찍은 것은 조용히 넘기지 않고 콘솔에 남긴다 — 그림이 계속\n 안 생기면 그 줄이 단서가 된다.\n */\n const shot = await within(\n (async () => {\n await settle()\n return shooter.snapshot ? await shooter.snapshot(WIDTH, HEIGHT) : undefined\n })(),\n PATIENCE\n )\n\n if (!shot) {\n console.warn(`[thumbnail] ${PATIENCE}ms 안에 못 찍었습니다 — 그림 없이 저장합니다`)\n }\n\n return shot\n } catch (e) {\n console.warn(`[thumbnail] 세우지 못했습니다 — ${(e as Error).message}`)\n return undefined\n } finally {\n host.remove()\n }\n}\n"]}
@@ -15,7 +15,7 @@ export function maturityOf(source, cost, score) {
15
15
  function scale(cost) {
16
16
  const capacity = capacityFrom(cost.groups, cost.triangles);
17
17
  if (!capacity) {
18
- return { key: 'scale', blocked: 'text.maturity-blocked-no-parts' };
18
+ return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' };
19
19
  }
20
20
  return {
21
21
  key: 'scale',
@@ -23,7 +23,7 @@ function scale(cost) {
23
23
  // 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.
24
24
  ratio: Math.min(1, capacity.instances / BOARD_ASSETS),
25
25
  reading: String(capacity.instances),
26
- because: 'text.maturity-scale-because'
26
+ because: 'figure.text.maturity-scale-because'
27
27
  };
28
28
  }
29
29
  /**
@@ -40,8 +40,8 @@ function consistency(source, cost) {
40
40
  return {
41
41
  key: 'consistency',
42
42
  ratio: (3 - faults) / 3,
43
- readingKey: faults === 0 ? 'text.maturity-consistency-clean' : 'text.maturity-consistency-faults',
44
- because: 'text.maturity-consistency-because'
43
+ readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',
44
+ because: 'figure.text.maturity-consistency-because'
45
45
  };
46
46
  }
47
47
  /**
@@ -54,14 +54,14 @@ function consistency(source, cost) {
54
54
  function reuse(score) {
55
55
  const { distinct, parts, saved } = score.reuse;
56
56
  if (parts === 0) {
57
- return { key: 'reuse', blocked: 'text.maturity-blocked-no-parts' };
57
+ return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' };
58
58
  }
59
59
  return {
60
60
  key: 'reuse',
61
61
  // ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다
62
62
  ratio: 1 - distinct / parts,
63
63
  reading: String(saved),
64
- because: 'text.maturity-reuse-because'
64
+ because: 'figure.text.maturity-reuse-because'
65
65
  };
66
66
  }
67
67
  /**
@@ -82,7 +82,7 @@ function detail(source, cost) {
82
82
  // 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.
83
83
  ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),
84
84
  reading: `${cost.parts} / ${limit}`,
85
- because: 'text.maturity-detail-because'
85
+ because: 'figure.text.maturity-detail-because'
86
86
  };
87
87
  }
88
88
  /**
@@ -95,7 +95,7 @@ function detail(source, cost) {
95
95
  * **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.
96
96
  */
97
97
  function reactive(source) {
98
- return { key: 'reactive', blocked: 'text.maturity-blocked-no-binding' };
98
+ return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' };
99
99
  }
100
100
  /**
101
101
  * 식별성 — 기준선 1. 위에서 볼 때 무엇인지 알아보나.
@@ -137,7 +137,7 @@ function reactive(source) {
137
137
  function readable(source, cost) {
138
138
  const regions = topViewRegions(source);
139
139
  if (regions.total === 0) {
140
- return { key: 'readable', blocked: 'text.maturity-blocked-no-parts' };
140
+ return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' };
141
141
  }
142
142
  // 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.
143
143
  // Vázquez 의 「열린 장면」 형태와 같다(최대가 log(면 수 + 1)).
@@ -150,7 +150,7 @@ function readable(source, cost) {
150
150
  key: 'readable',
151
151
  ratio: Math.min(1, entropy / ceiling),
152
152
  reading: `${regions.byMaterial.size}`,
153
- because: 'text.maturity-readable-because'
153
+ because: 'figure.text.maturity-readable-because'
154
154
  };
155
155
  }
156
156
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../client/modeller/maturity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAG3F,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAoDhD,oDAAoD;AACpD,MAAM,YAAY,GAAG,GAAG,CAAA;AAExB;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,MAAoB,EAAE,IAAgB,EAAE,KAAkB;IACnF,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;AAC/H,CAAC;AAED,8BAA8B;AAC9B,SAAS,KAAK,CAAC,IAAgB;IAC7B,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IAC1D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAA;IACpE,CAAC;IAED,OAAO;QACL,GAAG,EAAE,OAAO;QACZ,yDAAyD;QACzD,mCAAmC;QACnC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,SAAS,GAAG,YAAY,CAAC;QACrD,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QACnC,OAAO,EAAE,6BAA6B;KACvC,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,MAAoB,EAAE,IAAgB;IACzD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CACnC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAChF,CAAC,MAAM,CAAA;IAER,MAAM,MAAM,GACV,CAAC,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACvB,CAAC,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAE1D,OAAO;QACL,GAAG,EAAE,aAAa;QAClB,KAAK,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC;QACvB,UAAU,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,kCAAkC;QACjG,OAAO,EAAE,mCAAmC;KAC7C,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,KAAK,CAAC,KAAkB;IAC/B,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC,KAAK,CAAA;IAC9C,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAA;IACpE,CAAC;IAED,OAAO;QACL,GAAG,EAAE,OAAO;QACZ,0CAA0C;QAC1C,KAAK,EAAE,CAAC,GAAG,QAAQ,GAAG,KAAK;QAC3B,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC;QACtB,OAAO,EAAE,6BAA6B;KACvC,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,MAAM,CAAC,MAAoB,EAAE,IAAgB;IACpD,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,GAAG,CAAgB,CAAA;IACxD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IAE/B,OAAO;QACL,GAAG,EAAE,QAAQ;QACb,2CAA2C;QAC3C,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC;QAC/D,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,MAAM,KAAK,EAAE;QACnC,OAAO,EAAE,8BAA8B;KACxC,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,QAAQ,CAAC,MAAoB;IACpC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAA;AACzE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,SAAS,QAAQ,CAAC,MAAoB,EAAE,IAAgB;IACtD,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;IACtC,IAAI,OAAO,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAA;IACvE,CAAC;IAED,0CAA0C;IAC1C,8CAA8C;IAC9C,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,UAAU,CAAC;SAChE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC;SACrC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IAE7B,MAAM,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,GAAG,CAAC,CAAC,CAAA;IAEnD,OAAO;QACL,GAAG,EAAE,UAAU;QACf,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;QACrC,OAAO,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE;QACrC,OAAO,EAAE,gCAAgC;KAC1C,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,cAAc,CAAC,MAAoB;IAC1C,MAAM,IAAI,GAAG,EAAE,CAAA;IACf,MAAM,KAAK,GAAG,IAAI,KAAK,CAAgB,IAAI,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IAE5C,4CAA4C;IAC5C,kCAAkC;IAClC,MAAM,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/B,MAAM,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAE/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAA;QACzC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;QACnC,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;QAE/B,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACtF,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACxF,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACtF,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QAExF,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC7B,0CAA0C;gBAC1C,IAAI,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC7C,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;oBAC5D,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;oBAC5D,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;wBAAE,SAAQ;gBACzC,CAAC;gBACD,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAA;gBACtB,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;oBACzC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAA;oBACd,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAA;gBACjB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC5C,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,IAAI;YAAE,SAAQ;QAC1B,OAAO,EAAE,CAAA;QACT,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACrD,CAAC;IAED,OAAO;QACL,UAAU;QACV,KAAK,EAAE,OAAO;QACd,UAAU,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO;QACjC,SAAS,EAAE,IAAI,GAAG,IAAI;KACvB,CAAA;AACH,CAAC;AAED,kCAAkC;AAClC,MAAM,eAAe,GAAa,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA","sourcesContent":["import { LIMITS, PART_LIMIT, SEGMENT_PRESETS, materialKeyOf } from '@hatiolab/figure-model'\nimport type { DetailLevel, FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'\n\nimport { capacityFrom } from './scene-budget.js'\n\n/**\n * 성숙도 — 이 형상이 얼마나 잘 만들어졌나.\n *\n * ## 축이 여럿이어야 하는 이유\n *\n * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리\n * 상자도 그 축에서는 만점이다. 잘 만들어졌다는 것은 **여러 물음에 각각 답하는 것**이고,\n * 그것을 한 숫자로 뭉개면 무엇을 고쳐야 할지 알 수 없다.\n *\n * ## 축을 지어내지 않았다\n *\n * `docs/design/component-authoring-tool.md` 의 「인정받을 수 있는 최소 품질 — 기준선」\n * 다섯이 이미 있다. 그것을 잴 수 있는 형태로 옮기고, 이미 셈이 있는 것(규모·되쓰기)을\n * 더했다.\n *\n * 기준선 1 실루엣으로 알아본다 → readable **아직 측정하지 못한다**\n * 기준선 2 상태가 색으로 구분된다 → reactive **아직 측정하지 못한다**\n * 기준선 3 다른 자산과 한 세트로 보인다 → consistency\n * 기준선 4 확대해도 안 깨진다 → consistency (텍스처 0)\n * 기준선 5 라벨이 읽힌다 → reactive\n *\n * ## 못 재는 축을 지어내지 않는다\n *\n * 두 축은 지금 잴 수 없다. 하나는 그리기가 필요하고(실루엣), 하나는 형식에 길이\n * 없다(상태 바인딩 — figure-model#2). **그것을 0점으로도 만점으로도 두지 않는다.**\n * 「아직 측정하지 못한다」고 알리고 이유를 붙인다. 측정하지 못한 것을 점수로 만들면 그 점수가 거짓말을\n * 한다.\n *\n * ## 이 파일은 임시다\n *\n * 셈은 figure-model 것이어야 한다 — 거기가 `costOf` · `scoreOf` 를 갖고 있다.\n * 형식 쪽 결정(#2 · #4)이 서면 이 파일을 지우고 그쪽으로 옮긴다.\n */\n\nexport type AxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable'\n\nexport interface MaturityAxis {\n key: AxisKey\n /** 0~1. 못 재면 없다. */\n ratio?: number\n /** 지금 이 축의 값. 그대로 보여 준다. */\n reading?: string\n /** 값이 숫자가 아니라 말일 때의 문구 키. `reading` 과 함께 쓰지 않는다. */\n readingKey?: string\n /** 왜 이 값인가. */\n because?: string\n /** 못 재면 그 이유. 있으면 `ratio` 가 없다. */\n blocked?: string\n}\n\n/** 한 도면에 이만큼 놓는 것을 본다. 설계 문서 §4.15 의 「자산 500 개」. */\nconst BOARD_ASSETS = 500\n\n/**\n * 여섯 축을 측정한다.\n *\n * 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다 — 그것이 이\n * 리포트의 존재 이유다.\n */\nexport function maturityOf(source: FigureSource, cost: FigureCost, score: FigureScore): MaturityAxis[] {\n return [scale(cost), consistency(source, cost), reuse(score), detail(source, cost), reactive(source), readable(source, cost)]\n}\n\n/** 규모 — 한 도면에 얼마나 놓을 수 있나. */\nfunction scale(cost: FigureCost): MaturityAxis {\n const capacity = capacityFrom(cost.groups, cost.triangles)\n if (!capacity) {\n return { key: 'scale', blocked: 'text.maturity-blocked-no-parts' }\n }\n\n return {\n key: 'scale',\n // 한 도면 목표치(500)를 채우면 1 이다. 그보다 많이 놓을 수 있어도 1 을 넘기지 않는다 —\n // 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.\n ratio: Math.min(1, capacity.instances / BOARD_ASSETS),\n reading: String(capacity.instances),\n because: 'text.maturity-scale-because'\n }\n}\n\n/**\n * 한 세트로 보이나. 기준선 3·4.\n *\n * 셋을 본다. 팔레트 밖 표면이 없나(텍스처 0 — 확대해도 안 깨진다), 분할 수가\n * 프리셋인가, 투명 재질을 아끼나. 셋 다 이미 형식이 위반으로 세고 있는 것들이다.\n */\nfunction consistency(source: FigureSource, cost: FigureCost): MaturityAxis {\n const offPreset = source.parts.filter(\n part => part.segments !== undefined && !SEGMENT_PRESETS.includes(part.segments)\n ).length\n\n const faults =\n (cost.surfaceTextures > 0 ? 1 : 0) +\n (offPreset > 0 ? 1 : 0) +\n (cost.distinctMaterials > LIMITS.materialGroups ? 1 : 0)\n\n return {\n key: 'consistency',\n ratio: (3 - faults) / 3,\n readingKey: faults === 0 ? 'text.maturity-consistency-clean' : 'text.maturity-consistency-faults',\n because: 'text.maturity-consistency-because'\n }\n}\n\n/**\n * 되쓰나 — 같은 형상을 몇 번 다시 쓰나.\n *\n * `scoreOf` 가 이미 재고 있으나 **점수에는 넣지 않는다**고 선언돼 있다(한도가 아니라\n * 덤이다). 축으로는 뜻이 있다 — 되쓰기가 많은 형상이 잘 짜인 형상이다. 다만 부품마다\n * 형상이 달라도 잘못이 아니므로 **낮다고 나무라지 않는다.**\n */\nfunction reuse(score: FigureScore): MaturityAxis {\n const { distinct, parts, saved } = score.reuse\n if (parts === 0) {\n return { key: 'reuse', blocked: 'text.maturity-blocked-no-parts' }\n }\n\n return {\n key: 'reuse',\n // ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다\n ratio: 1 - distinct / parts,\n reading: String(saved),\n because: 'text.maturity-reuse-because'\n }\n}\n\n/**\n * 정밀도 — 밝힌 등급에 걸맞게 만들어졌나.\n *\n * 등급(S·M·L)은 「어느 거리에서 읽히도록 만든 것인가」를 밝히는 것이다. 그런데 등급만\n * 올려 놓고 부품 셋으로 끝내면 그 약속을 안 지킨 것이다. **한도를 얼마나 썼나**로 본다.\n *\n * 이 축은 「많이 쓸수록 좋다」가 아니다. 등급에 맞게 채웠나를 본다 — 그래서 넘치면\n * 다시 내려간다.\n */\nfunction detail(source: FigureSource, cost: FigureCost): MaturityAxis {\n const level = (source.detailLevel ?? 'M') as DetailLevel\n const limit = PART_LIMIT[level]\n const used = cost.parts / limit\n\n return {\n key: 'detail',\n // 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.\n ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),\n reading: `${cost.parts} / ${limit}`,\n because: 'text.maturity-detail-because'\n }\n}\n\n/**\n * 살아 있나 — 기준선 2·5. 상태가 색으로 구분되고 라벨이 읽히나.\n *\n * **지금 잴 수 없다.** 형식에 `slot` 이 있지만 그리는 쪽이 읽지 않고(선언 한 줄이\n * 전부다), 상태를 부품에 잇는 길 자체가 아직 정해지지 않았다(figure-model#2).\n *\n * 0 점으로 두지 않는다. 0 은 「이 형상이 상태를 안 보여 준다」는 뜻이 되는데, 사실은\n * **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.\n */\nfunction reactive(source: FigureSource): MaturityAxis {\n return { key: 'reactive', blocked: 'text.maturity-blocked-no-binding' }\n}\n\n/**\n * 식별성 — 기준선 1. 위에서 볼 때 무엇인지 알아보나.\n *\n * ## 시점 엔트로피를 빌린다\n *\n * 「어느 시점에서 얼마나 알아볼 수 있나」는 이미 연구된 물음이다. Vázquez 등의\n * **viewpoint entropy** 는 시점에서 보이는 각 면의 투영 면적 비율로 섀넌 엔트로피를\n * 낸다. 모든 면이 같은 면적으로 보일 때가 최대이고, 한 면이 화면을 다 덮으면 0 이다.\n *\n * H = -Σ (Ai / At) · log(Ai / At)\n *\n * ## 다만 면으로 세지 않는다\n *\n * 그 문헌이 스스로 밝히는 한계가 **폴리곤 분할에 좌우된다**는 것이다 — 같은 형상도\n * 잘게 쪼개면 엔트로피가 올라간다. 우리에게는 그 한계가 치명적이다. 분할 수를 8 에서\n * 16 으로 올리는 것은 형상을 알아보기 쉽게 만드는 일이 아닌데, 면으로 세면 점수가\n * 오른다.\n *\n * 그래서 **면이 아니라 색 구역으로 센다.** 눈이 구별하는 것은 삼각형이 아니라 색이\n * 다른 덩어리다. 같은 토큰을 쓰는 부품 둘이 붙어 있으면 눈에는 한 덩어리다.\n * 이렇게 하면 분할 수에 좌우되지 않으면서 원래 물음에 답한다.\n *\n * ## 기준값도 지어내지 않는다\n *\n * 정규화 분모를 `log(재질 group 한도 + 1)` 로 둔다. 이 형식이 허용하는 재질 group 이\n * 셋이고 거기에 배경을 더하면 넷이므로, **이 형식에서 도달 가능한 최대**가 곧\n * 기준이다. 임의로 고른 수가 아니다.\n *\n * ## 그리지 않고 셈으로 낸다\n *\n * 저작 중에 매번 렌더링할 수 없다. 대신 부품의 발자국(x·y·너비·높이)을 위에서\n * 겹쳐 본다 — 위에 있는 것이 아래를 가린다. 축에 정렬된 상자이므로 격자에 찍어\n * 세면 된다.\n *\n * 이 근사가 실제 렌더링과 맞는지는 하네스가 따로 확인한다\n * (`test/browser/check-silhouette.mjs`).\n */\nfunction readable(source: FigureSource, cost: FigureCost): MaturityAxis {\n const regions = topViewRegions(source)\n if (regions.total === 0) {\n return { key: 'readable', blocked: 'text.maturity-blocked-no-parts' }\n }\n\n // 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.\n // Vázquez 의 「열린 장면」 형태와 같다(최대가 log(면 수 + 1)).\n const shares = [...regions.byMaterial.values(), regions.background]\n .map(area => area / regions.footprint)\n .filter(share => share > 0)\n\n const entropy = -shares.reduce((sum, p) => sum + p * Math.log(p), 0)\n const ceiling = Math.log(LIMITS.materialGroups + 1)\n\n return {\n key: 'readable',\n ratio: Math.min(1, entropy / ceiling),\n reading: `${regions.byMaterial.size}`,\n because: 'text.maturity-readable-because'\n }\n}\n\n/**\n * 위에서 내려다볼 때 어느 재질이 얼마나 보이나.\n *\n * 부품 발자국을 격자에 찍되 **높은 것이 이긴다.** 축에 정렬된 상자만 다루므로\n * 이것으로 충분하다 — 회전한 부품은 발자국을 크게 잡아 보수적으로 본다.\n */\nfunction topViewRegions(source: FigureSource) {\n const GRID = 64\n const cells = new Array<string | null>(GRID * GRID).fill(null)\n const height = new Float32Array(GRID * GRID)\n\n // 톱뷰의 두 축은 x 와 z 다. y 는 높이라 위에서 보면 보이지 않는다.\n // 원점은 기준 상자의 중심이므로 격자의 가운데가 0 이다.\n const sx = GRID / source.base.x\n const sz = GRID / source.base.z\n\n for (const part of source.parts) {\n const { position, size } = part.transform\n const top = position.y + size.y / 2\n const key = materialKeyOf(part)\n\n const x0 = Math.max(0, Math.floor((position.x - size.x / 2 + source.base.x / 2) * sx))\n const x1 = Math.min(GRID, Math.ceil((position.x + size.x / 2 + source.base.x / 2) * sx))\n const y0 = Math.max(0, Math.floor((position.z - size.z / 2 + source.base.z / 2) * sz))\n const y1 = Math.min(GRID, Math.ceil((position.z + size.z / 2 + source.base.z / 2) * sz))\n\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n // 원기둥·구는 발자국 상자를 다 채우지 않는다. 모서리를 깎아 근사한다.\n if (ROUND_FOOTPRINT.includes(part.primitive)) {\n const cxp = (x + 0.5 - (x0 + x1) / 2) / ((x1 - x0) / 2 || 1)\n const cyp = (y + 0.5 - (y0 + y1) / 2) / ((y1 - y0) / 2 || 1)\n if (cxp * cxp + cyp * cyp > 1) continue\n }\n const i = y * GRID + x\n if (cells[i] === null || top > height[i]) {\n cells[i] = key\n height[i] = top\n }\n }\n }\n }\n\n const byMaterial = new Map<string, number>()\n let covered = 0\n for (const key of cells) {\n if (key === null) continue\n covered++\n byMaterial.set(key, (byMaterial.get(key) ?? 0) + 1)\n }\n\n return {\n byMaterial,\n total: covered,\n background: GRID * GRID - covered,\n footprint: GRID * GRID\n }\n}\n\n/** 위에서 보면 발자국 상자를 다 채우지 않는 도형. */\nconst ROUND_FOOTPRINT: string[] = ['cylinder', 'sphere']\n"]}
1
+ {"version":3,"file":"maturity.js","sourceRoot":"","sources":["../../client/modeller/maturity.ts"],"names":[],"mappings":"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","sourcesContent":["import { LIMITS, PART_LIMIT, SEGMENT_PRESETS, materialKeyOf } from '@hatiolab/figure-model'\nimport type { DetailLevel, FigureCost, FigureScore, FigureSource } from '@hatiolab/figure-model'\n\nimport { capacityFrom } from './scene-budget.js'\n\n/**\n * 성숙도 — 이 형상이 얼마나 잘 만들어졌나.\n *\n * ## 축이 여럿이어야 하는 이유\n *\n * 「몇 개까지 편한가」 하나로는 좋은 형상과 나쁜 형상이 구분되지 않는다. 부품 하나짜리\n * 상자도 그 축에서는 만점이다. 잘 만들어졌다는 것은 **여러 물음에 각각 답하는 것**이고,\n * 그것을 한 숫자로 뭉개면 무엇을 고쳐야 할지 알 수 없다.\n *\n * ## 축을 지어내지 않았다\n *\n * `docs/design/component-authoring-tool.md` 의 「인정받을 수 있는 최소 품질 — 기준선」\n * 다섯이 이미 있다. 그것을 잴 수 있는 형태로 옮기고, 이미 셈이 있는 것(규모·되쓰기)을\n * 더했다.\n *\n * 기준선 1 실루엣으로 알아본다 → readable **아직 측정하지 못한다**\n * 기준선 2 상태가 색으로 구분된다 → reactive **아직 측정하지 못한다**\n * 기준선 3 다른 자산과 한 세트로 보인다 → consistency\n * 기준선 4 확대해도 안 깨진다 → consistency (텍스처 0)\n * 기준선 5 라벨이 읽힌다 → reactive\n *\n * ## 못 재는 축을 지어내지 않는다\n *\n * 두 축은 지금 잴 수 없다. 하나는 그리기가 필요하고(실루엣), 하나는 형식에 길이\n * 없다(상태 바인딩 — figure-model#2). **그것을 0점으로도 만점으로도 두지 않는다.**\n * 「아직 측정하지 못한다」고 알리고 이유를 붙인다. 측정하지 못한 것을 점수로 만들면 그 점수가 거짓말을\n * 한다.\n *\n * ## 이 파일은 임시다\n *\n * 셈은 figure-model 것이어야 한다 — 거기가 `costOf` · `scoreOf` 를 갖고 있다.\n * 형식 쪽 결정(#2 · #4)이 서면 이 파일을 지우고 그쪽으로 옮긴다.\n */\n\nexport type AxisKey = 'scale' | 'consistency' | 'reuse' | 'detail' | 'reactive' | 'readable'\n\nexport interface MaturityAxis {\n key: AxisKey\n /** 0~1. 못 재면 없다. */\n ratio?: number\n /** 지금 이 축의 값. 그대로 보여 준다. */\n reading?: string\n /** 값이 숫자가 아니라 말일 때의 문구 키. `reading` 과 함께 쓰지 않는다. */\n readingKey?: string\n /** 왜 이 값인가. */\n because?: string\n /** 못 재면 그 이유. 있으면 `ratio` 가 없다. */\n blocked?: string\n}\n\n/** 한 도면에 이만큼 놓는 것을 본다. 설계 문서 §4.15 의 「자산 500 개」. */\nconst BOARD_ASSETS = 500\n\n/**\n * 여섯 축을 측정한다.\n *\n * 한 숫자로 합치지 않는다. 합치면 「무엇을 고쳐야 하나」가 사라진다 — 그것이 이\n * 리포트의 존재 이유다.\n */\nexport function maturityOf(source: FigureSource, cost: FigureCost, score: FigureScore): MaturityAxis[] {\n return [scale(cost), consistency(source, cost), reuse(score), detail(source, cost), reactive(source), readable(source, cost)]\n}\n\n/** 규모 — 한 도면에 얼마나 놓을 수 있나. */\nfunction scale(cost: FigureCost): MaturityAxis {\n const capacity = capacityFrom(cost.groups, cost.triangles)\n if (!capacity) {\n return { key: 'scale', blocked: 'figure.text.maturity-blocked-no-parts' }\n }\n\n return {\n key: 'scale',\n // 한 도면 목표치(500)를 채우면 1 이다. 그보다 많이 놓을 수 있어도 1 을 넘기지 않는다 —\n // 목표를 넘긴 여유는 다른 축을 고치는 데 쓰는 편이 낫다.\n ratio: Math.min(1, capacity.instances / BOARD_ASSETS),\n reading: String(capacity.instances),\n because: 'figure.text.maturity-scale-because'\n }\n}\n\n/**\n * 한 세트로 보이나. 기준선 3·4.\n *\n * 셋을 본다. 팔레트 밖 표면이 없나(텍스처 0 — 확대해도 안 깨진다), 분할 수가\n * 프리셋인가, 투명 재질을 아끼나. 셋 다 이미 형식이 위반으로 세고 있는 것들이다.\n */\nfunction consistency(source: FigureSource, cost: FigureCost): MaturityAxis {\n const offPreset = source.parts.filter(\n part => part.segments !== undefined && !SEGMENT_PRESETS.includes(part.segments)\n ).length\n\n const faults =\n (cost.surfaceTextures > 0 ? 1 : 0) +\n (offPreset > 0 ? 1 : 0) +\n (cost.distinctMaterials > LIMITS.materialGroups ? 1 : 0)\n\n return {\n key: 'consistency',\n ratio: (3 - faults) / 3,\n readingKey: faults === 0 ? 'figure.text.maturity-consistency-clean' : 'figure.text.maturity-consistency-faults',\n because: 'figure.text.maturity-consistency-because'\n }\n}\n\n/**\n * 되쓰나 — 같은 형상을 몇 번 다시 쓰나.\n *\n * `scoreOf` 가 이미 재고 있으나 **점수에는 넣지 않는다**고 선언돼 있다(한도가 아니라\n * 덤이다). 축으로는 뜻이 있다 — 되쓰기가 많은 형상이 잘 짜인 형상이다. 다만 부품마다\n * 형상이 달라도 잘못이 아니므로 **낮다고 나무라지 않는다.**\n */\nfunction reuse(score: FigureScore): MaturityAxis {\n const { distinct, parts, saved } = score.reuse\n if (parts === 0) {\n return { key: 'reuse', blocked: 'figure.text.maturity-blocked-no-parts' }\n }\n\n return {\n key: 'reuse',\n // ratio 가 낮을수록 많이 되쓴다 — 뒤집어서 높은 쪽이 좋게 만든다\n ratio: 1 - distinct / parts,\n reading: String(saved),\n because: 'figure.text.maturity-reuse-because'\n }\n}\n\n/**\n * 정밀도 — 밝힌 등급에 걸맞게 만들어졌나.\n *\n * 등급(S·M·L)은 「어느 거리에서 읽히도록 만든 것인가」를 밝히는 것이다. 그런데 등급만\n * 올려 놓고 부품 셋으로 끝내면 그 약속을 안 지킨 것이다. **한도를 얼마나 썼나**로 본다.\n *\n * 이 축은 「많이 쓸수록 좋다」가 아니다. 등급에 맞게 채웠나를 본다 — 그래서 넘치면\n * 다시 내려간다.\n */\nfunction detail(source: FigureSource, cost: FigureCost): MaturityAxis {\n const level = (source.detailLevel ?? 'M') as DetailLevel\n const limit = PART_LIMIT[level]\n const used = cost.parts / limit\n\n return {\n key: 'detail',\n // 절반쯤 쓴 것을 제일 좋게 본다. 텅 비어도, 넘쳐도 등급과 안 맞는다.\n ratio: used > 1 ? Math.max(0, 2 - used) : Math.min(1, used * 2),\n reading: `${cost.parts} / ${limit}`,\n because: 'figure.text.maturity-detail-because'\n }\n}\n\n/**\n * 살아 있나 — 기준선 2·5. 상태가 색으로 구분되고 라벨이 읽히나.\n *\n * **지금 잴 수 없다.** 형식에 `slot` 이 있지만 그리는 쪽이 읽지 않고(선언 한 줄이\n * 전부다), 상태를 부품에 잇는 길 자체가 아직 정해지지 않았다(figure-model#2).\n *\n * 0 점으로 두지 않는다. 0 은 「이 형상이 상태를 안 보여 준다」는 뜻이 되는데, 사실은\n * **아무 형상도 보여 줄 수 없는 상태**다. 축 자체가 아직 없는 것이다.\n */\nfunction reactive(source: FigureSource): MaturityAxis {\n return { key: 'reactive', blocked: 'figure.text.maturity-blocked-no-binding' }\n}\n\n/**\n * 식별성 — 기준선 1. 위에서 볼 때 무엇인지 알아보나.\n *\n * ## 시점 엔트로피를 빌린다\n *\n * 「어느 시점에서 얼마나 알아볼 수 있나」는 이미 연구된 물음이다. Vázquez 등의\n * **viewpoint entropy** 는 시점에서 보이는 각 면의 투영 면적 비율로 섀넌 엔트로피를\n * 낸다. 모든 면이 같은 면적으로 보일 때가 최대이고, 한 면이 화면을 다 덮으면 0 이다.\n *\n * H = -Σ (Ai / At) · log(Ai / At)\n *\n * ## 다만 면으로 세지 않는다\n *\n * 그 문헌이 스스로 밝히는 한계가 **폴리곤 분할에 좌우된다**는 것이다 — 같은 형상도\n * 잘게 쪼개면 엔트로피가 올라간다. 우리에게는 그 한계가 치명적이다. 분할 수를 8 에서\n * 16 으로 올리는 것은 형상을 알아보기 쉽게 만드는 일이 아닌데, 면으로 세면 점수가\n * 오른다.\n *\n * 그래서 **면이 아니라 색 구역으로 센다.** 눈이 구별하는 것은 삼각형이 아니라 색이\n * 다른 덩어리다. 같은 토큰을 쓰는 부품 둘이 붙어 있으면 눈에는 한 덩어리다.\n * 이렇게 하면 분할 수에 좌우되지 않으면서 원래 물음에 답한다.\n *\n * ## 기준값도 지어내지 않는다\n *\n * 정규화 분모를 `log(재질 group 한도 + 1)` 로 둔다. 이 형식이 허용하는 재질 group 이\n * 셋이고 거기에 배경을 더하면 넷이므로, **이 형식에서 도달 가능한 최대**가 곧\n * 기준이다. 임의로 고른 수가 아니다.\n *\n * ## 그리지 않고 셈으로 낸다\n *\n * 저작 중에 매번 렌더링할 수 없다. 대신 부품의 발자국(x·y·너비·높이)을 위에서\n * 겹쳐 본다 — 위에 있는 것이 아래를 가린다. 축에 정렬된 상자이므로 격자에 찍어\n * 세면 된다.\n *\n * 이 근사가 실제 렌더링과 맞는지는 하네스가 따로 확인한다\n * (`test/browser/check-silhouette.mjs`).\n */\nfunction readable(source: FigureSource, cost: FigureCost): MaturityAxis {\n const regions = topViewRegions(source)\n if (regions.total === 0) {\n return { key: 'readable', blocked: 'figure.text.maturity-blocked-no-parts' }\n }\n\n // 배경도 한 구역으로 센다 — 발자국을 얼마나 채우나가 식별성의 일부다.\n // Vázquez 의 「열린 장면」 형태와 같다(최대가 log(면 수 + 1)).\n const shares = [...regions.byMaterial.values(), regions.background]\n .map(area => area / regions.footprint)\n .filter(share => share > 0)\n\n const entropy = -shares.reduce((sum, p) => sum + p * Math.log(p), 0)\n const ceiling = Math.log(LIMITS.materialGroups + 1)\n\n return {\n key: 'readable',\n ratio: Math.min(1, entropy / ceiling),\n reading: `${regions.byMaterial.size}`,\n because: 'figure.text.maturity-readable-because'\n }\n}\n\n/**\n * 위에서 내려다볼 때 어느 재질이 얼마나 보이나.\n *\n * 부품 발자국을 격자에 찍되 **높은 것이 이긴다.** 축에 정렬된 상자만 다루므로\n * 이것으로 충분하다 — 회전한 부품은 발자국을 크게 잡아 보수적으로 본다.\n */\nfunction topViewRegions(source: FigureSource) {\n const GRID = 64\n const cells = new Array<string | null>(GRID * GRID).fill(null)\n const height = new Float32Array(GRID * GRID)\n\n // 톱뷰의 두 축은 x 와 z 다. y 는 높이라 위에서 보면 보이지 않는다.\n // 원점은 기준 상자의 중심이므로 격자의 가운데가 0 이다.\n const sx = GRID / source.base.x\n const sz = GRID / source.base.z\n\n for (const part of source.parts) {\n const { position, size } = part.transform\n const top = position.y + size.y / 2\n const key = materialKeyOf(part)\n\n const x0 = Math.max(0, Math.floor((position.x - size.x / 2 + source.base.x / 2) * sx))\n const x1 = Math.min(GRID, Math.ceil((position.x + size.x / 2 + source.base.x / 2) * sx))\n const y0 = Math.max(0, Math.floor((position.z - size.z / 2 + source.base.z / 2) * sz))\n const y1 = Math.min(GRID, Math.ceil((position.z + size.z / 2 + source.base.z / 2) * sz))\n\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n // 원기둥·구는 발자국 상자를 다 채우지 않는다. 모서리를 깎아 근사한다.\n if (ROUND_FOOTPRINT.includes(part.primitive)) {\n const cxp = (x + 0.5 - (x0 + x1) / 2) / ((x1 - x0) / 2 || 1)\n const cyp = (y + 0.5 - (y0 + y1) / 2) / ((y1 - y0) / 2 || 1)\n if (cxp * cxp + cyp * cyp > 1) continue\n }\n const i = y * GRID + x\n if (cells[i] === null || top > height[i]) {\n cells[i] = key\n height[i] = top\n }\n }\n }\n }\n\n const byMaterial = new Map<string, number>()\n let covered = 0\n for (const key of cells) {\n if (key === null) continue\n covered++\n byMaterial.set(key, (byMaterial.get(key) ?? 0) + 1)\n }\n\n return {\n byMaterial,\n total: covered,\n background: GRID * GRID - covered,\n footprint: GRID * GRID\n }\n}\n\n/** 위에서 보면 발자국 상자를 다 채우지 않는 도형. */\nconst ROUND_FOOTPRINT: string[] = ['cylinder', 'sphere']\n"]}
@@ -1,5 +1,6 @@
1
1
  import '@operato/data-grist';
2
2
  import '@material/web/icon/icon.js';
3
+ import '../viewparts/figure-thumb.js';
3
4
  import type { FetchOption } from '@operato/data-grist';
4
5
  import { PageView } from '@operato/shell';
5
6
  import type { Figure } from '../types.js';
@@ -41,10 +42,6 @@ export declare class FigureListPage extends FigureListPageBase {
41
42
  filter: {
42
43
  handler: () => void;
43
44
  };
44
- actions: {
45
- title: string;
46
- action: () => void;
47
- }[];
48
45
  };
49
46
  render(): import("lit-html").TemplateResult<1>;
50
47
  pageInitialized(): Promise<void>;