@things-factory/figure-ui 10.1.26 → 10.1.28

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 (83) hide show
  1. package/client/graphql/index.ts +23 -0
  2. package/client/modeller/base-box.ts +104 -0
  3. package/client/modeller/figure-animations.ts +253 -32
  4. package/client/modeller/figure-canvas.ts +18 -3
  5. package/client/modeller/figure-inspector.ts +292 -3
  6. package/client/modeller/figure-preview.ts +57 -2
  7. package/client/modeller/figure-report.ts +9 -4
  8. package/client/modeller/figure-side.ts +9 -1
  9. package/client/modeller/figure-source.ts +24 -4
  10. package/client/modeller/figure-view.ts +20 -0
  11. package/client/modeller/joint-edits.ts +262 -0
  12. package/client/modeller/joint-gizmo.ts +118 -0
  13. package/client/modeller/pose-defaults.ts +67 -0
  14. package/client/modeller/proposal.ts +9 -3
  15. package/client/modeller/scene-budget.ts +31 -40
  16. package/client/pages/figure-list-page.ts +4 -9
  17. package/client/pages/figure-modeller-page.ts +124 -41
  18. package/dist-client/graphql/index.d.ts +7 -0
  19. package/dist-client/graphql/index.js +21 -0
  20. package/dist-client/graphql/index.js.map +1 -1
  21. package/dist-client/modeller/base-box.d.ts +47 -0
  22. package/dist-client/modeller/base-box.js +52 -0
  23. package/dist-client/modeller/base-box.js.map +1 -0
  24. package/dist-client/modeller/figure-animations.d.ts +29 -0
  25. package/dist-client/modeller/figure-animations.js +228 -25
  26. package/dist-client/modeller/figure-animations.js.map +1 -1
  27. package/dist-client/modeller/figure-canvas.d.ts +9 -0
  28. package/dist-client/modeller/figure-canvas.js +14 -3
  29. package/dist-client/modeller/figure-canvas.js.map +1 -1
  30. package/dist-client/modeller/figure-inspector.d.ts +37 -0
  31. package/dist-client/modeller/figure-inspector.js +253 -2
  32. package/dist-client/modeller/figure-inspector.js.map +1 -1
  33. package/dist-client/modeller/figure-preview.d.ts +15 -0
  34. package/dist-client/modeller/figure-preview.js +48 -2
  35. package/dist-client/modeller/figure-preview.js.map +1 -1
  36. package/dist-client/modeller/figure-report.d.ts +3 -2
  37. package/dist-client/modeller/figure-report.js +9 -4
  38. package/dist-client/modeller/figure-report.js.map +1 -1
  39. package/dist-client/modeller/figure-side.d.ts +5 -1
  40. package/dist-client/modeller/figure-side.js +12 -0
  41. package/dist-client/modeller/figure-side.js.map +1 -1
  42. package/dist-client/modeller/figure-source.d.ts +15 -0
  43. package/dist-client/modeller/figure-source.js +8 -4
  44. package/dist-client/modeller/figure-source.js.map +1 -1
  45. package/dist-client/modeller/figure-view.d.ts +16 -0
  46. package/dist-client/modeller/figure-view.js +15 -0
  47. package/dist-client/modeller/figure-view.js.map +1 -1
  48. package/dist-client/modeller/joint-edits.d.ts +49 -0
  49. package/dist-client/modeller/joint-edits.js +223 -0
  50. package/dist-client/modeller/joint-edits.js.map +1 -0
  51. package/dist-client/modeller/joint-gizmo.d.ts +31 -0
  52. package/dist-client/modeller/joint-gizmo.js +104 -0
  53. package/dist-client/modeller/joint-gizmo.js.map +1 -0
  54. package/dist-client/modeller/pose-defaults.d.ts +9 -0
  55. package/dist-client/modeller/pose-defaults.js +35 -0
  56. package/dist-client/modeller/pose-defaults.js.map +1 -0
  57. package/dist-client/modeller/proposal.d.ts +1 -1
  58. package/dist-client/modeller/proposal.js +8 -3
  59. package/dist-client/modeller/proposal.js.map +1 -1
  60. package/dist-client/modeller/scene-budget.d.ts +31 -34
  61. package/dist-client/modeller/scene-budget.js +28 -37
  62. package/dist-client/modeller/scene-budget.js.map +1 -1
  63. package/dist-client/pages/figure-list-page.js +4 -9
  64. package/dist-client/pages/figure-list-page.js.map +1 -1
  65. package/dist-client/pages/figure-modeller-page.d.ts +43 -0
  66. package/dist-client/pages/figure-modeller-page.js +121 -40
  67. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  68. package/dist-client/tsconfig.tsbuildinfo +1 -1
  69. package/dist-server/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +3 -3
  71. package/test/ai-proposal-contract.test.ts +16 -11
  72. package/test/base-box.test.ts +154 -0
  73. package/test/figure-source.test.ts +32 -0
  74. package/test/joint-edits.test.ts +165 -0
  75. package/test/joint-gizmo.test.ts +174 -0
  76. package/test/pose-defaults.test.ts +71 -0
  77. package/test/preview-board-depth.test.ts +27 -0
  78. package/test/scene-budget.test.ts +53 -0
  79. package/translations/en.json +37 -2
  80. package/translations/ja.json +37 -2
  81. package/translations/ko.json +37 -2
  82. package/translations/ms.json +37 -2
  83. package/translations/zh.json +37 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joint-edits.js","sourceRoot":"","sources":["../../client/modeller/joint-edits.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureJoint, FigurePart, JointType, Vec3 } from '@hatiolab/figure-model'\n\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * Editing joints and parents (ADR-0066). Pure functions over the draft and the part list, so the inspector\n * only renders and dispatches.\n *\n * A joint is always created together with the parameter that drives it. Values reach a joint only through\n * `parameters`, and the preview draws a slider per parameter, so a joint without one could be defined but\n * never seen moving.\n */\n\ntype Parameter = NonNullable<FigureDraft['parameters']>[number]\n\n/** The limits a new joint starts with. Revolute in degrees, prismatic in millimetres. */\nconst NEW_LIMITS: Record<Exclude<JointType, 'continuous'>, { min: number; max: number }> = {\n revolute: { min: -90, max: 90 },\n prismatic: { min: 0, max: 500 }\n}\n\n/** The span a parameter driving a continuous joint offers: one turn either way. */\nconst CONTINUOUS_SPAN = { min: -360, max: 360 }\n\n/** The joint whose child is this part, if any. */\nexport function jointOf(draft: FigureDraft | undefined, part: string): FigureJoint | undefined {\n return draft?.joints?.find(joint => joint.child === part)\n}\n\n/**\n * Parts a part may be attached to: every other part except itself and anything attached below it, since\n * either would close a loop the format refuses (`parent-cycle`).\n */\nexport function parentChoices(parts: readonly PartModel[], part: string): string[] {\n const below = new Set<string>([part])\n let grew = true\n while (grew) {\n grew = false\n for (const one of parts) {\n if (one.parent !== undefined && below.has(one.parent) && !below.has(one.name)) {\n below.add(one.name)\n grew = true\n }\n }\n }\n return parts.map(one => one.name).filter(name => !!name && !below.has(name))\n}\n\ntype Span = { min: number; max: number }\n\n/** 한 축에서 부품이 차지하는 구간. */\nconst reachOf = (part: FigurePart, axis: 'x' | 'y' | 'z'): Span => ({\n min: part.transform.position[axis] - part.transform.size[axis] / 2,\n max: part.transform.position[axis] + part.transform.size[axis] / 2\n})\n\n/**\n * 새 관절이 설 자리 — **부품이 붙은 부품과 맞닿는 면의 한가운데.**\n *\n * 경첩은 두 부품이 닿는 자리에 있다. 팔꿈치는 윗팔과 앞팔이 만나는 곳이고, 문은 문틀에 닿는\n * 모서리에 있다. 그래서 처음 값은 그 자리여야 한다.\n *\n * 한동안 **부품 아랫면 한가운데**를 썼다. 위로 쌓는 부품에는 맞는 자리인데(로봇 팔이 그렇다),\n * 옆에 달린 부품에는 틀린다 — 캐비닛 옆에 달린 문의 축이 문 바닥에 생긴다. 저작자는 그것을\n * 화면에서 보고 손으로 옮겨야 했고, 어디로 옮길지는 두 부품의 치수를 머릿속에서 빼야 나왔다.\n *\n * **가장 벌어진 축이 맞닿는 축이다.** 두 상자가 겹치는 축에서는 뺀 값이 음수이고, 마주 보는\n * 축에서만 0 이나 양수가 된다. 나머지 두 축은 겹치는 구간의 한가운데를 쓴다 — 문의 축은 문\n * 높이의 한가운데에 있지 위아래 끝에 있지 않다.\n *\n * 붙은 부품이 없으면(Figure 틀에 직접 붙는다) 아랫면 한가운데다. 그때 맞닿는 것은 바닥이다.\n *\n * ⚠ 부품의 제 회전은 보지 않는다. 비스듬히 세운 부품에서는 축에 나란한 상자로 셈한 자리가\n * 나오고, 그것은 **처음 값**이다 — 저작자가 옮길 수 있고, 화면의 부채꼴이 어디인지 보여 준다.\n */\nexport function seamOrigin(part: FigurePart, parent?: FigurePart): Vec3 {\n const { position, size } = part.transform\n if (!parent) return { x: position.x, y: position.y - size.y / 2, z: position.z }\n\n const axes = ['x', 'y', 'z'] as const\n const gaps = axes.map(axis => {\n const mine = reachOf(part, axis)\n const theirs = reachOf(parent, axis)\n return { axis, gap: Math.max(mine.min - theirs.max, theirs.min - mine.max), mine, theirs }\n })\n\n const meeting = gaps.reduce((widest, one) => (one.gap > widest.gap ? one : widest))\n const origin = { x: position.x, y: position.y, z: position.z }\n\n for (const { axis, mine, theirs } of gaps) {\n if (axis === meeting.axis) {\n /* 맞닿는 면은 **내 면**이다. 부모 쪽 면을 쓰면 둘 사이가 벌어져 있을 때 축이 허공에 선다. */\n origin[axis] = mine.min >= theirs.max ? mine.min : mine.max\n continue\n }\n const from = Math.max(mine.min, theirs.min)\n const to = Math.min(mine.max, theirs.max)\n origin[axis] = from <= to ? (from + to) / 2 : position[axis]\n }\n\n return { x: round4(origin.x), y: round4(origin.y), z: round4(origin.z) }\n}\n\nconst round4 = (value: number) => Math.round(value * 10000) / 10000\n\nfunction freeName(taken: Iterable<string>, stem: string): string {\n const used = new Set(taken)\n if (!used.has(stem)) return stem\n for (let n = 2; ; n++) if (!used.has(`${stem}-${n}`)) return `${stem}-${n}`\n}\n\n/** The span the driving parameter offers for a joint: its limits, or one turn either way when it has none. */\nfunction spanOf(joint: FigureJoint): { min: number; max: number } {\n return joint.limits ?? CONTINUOUS_SPAN\n}\n\nfunction drivingParameter(joint: FigureJoint): Parameter {\n const span = spanOf(joint)\n return {\n name: joint.name,\n range: { unit: joint.type === 'prismatic' ? 'mm' : 'deg', min: span.min, max: span.max },\n default: 0,\n clip: {\n channels: [\n {\n target: joint.name,\n keys: [\n { at: 0, value: span.min },\n { at: 1, value: span.max }\n ]\n }\n ]\n }\n }\n}\n\n/**\n * Whether a parameter is the plain one `setJointType` made for a joint: one channel on that joint, keys at\n * 0 and 1 spanning the given limits. Only such a parameter follows the joint when its limits or type change;\n * one the author reshaped is left as written.\n */\nfunction isPlainDriver(parameter: Parameter, joint: FigureJoint, span: { min: number; max: number }): boolean {\n const channels = parameter.clip?.channels ?? []\n if (channels.length !== 1) return false\n const [channel] = channels\n if (channel!.target !== joint.name || channel!.path !== undefined) return false\n const keys = channel!.keys as { at: number; value: number }[]\n return (\n keys.length === 2 &&\n keys[0]!.at === 0 &&\n keys[1]!.at === 1 &&\n keys[0]!.value === span.min &&\n keys[1]!.value === span.max &&\n parameter.range?.min === span.min &&\n parameter.range?.max === span.max\n )\n}\n\n/**\n * Sets how a part moves relative to its parent, or clears the joint with `undefined`.\n *\n * - From none: adds a joint at the seam with the parent (`seamOrigin`), with a vertical axis and starting\n * limits, and a parameter that drives it over those limits.\n * - To none: removes the joint and every channel that drove it; a parameter or clip left with no channel\n * is removed too, because the format refuses an empty one.\n * - Between types: keeps the origin and axis, sets limits for the new type (none for continuous), and\n * moves a plain driving parameter along with them.\n */\nexport function setJointType(\n draft: FigureDraft,\n part: FigurePart,\n type: JointType | undefined,\n parent?: FigurePart\n): FigureDraft {\n const joints = draft.joints ?? []\n const existing = joints.find(joint => joint.child === part.name)\n\n if (type === undefined) {\n if (!existing) return draft\n return withoutJoint(draft, existing.name)\n }\n\n if (!existing) {\n const joint: FigureJoint = {\n name: freeName([...joints.map(one => one.name)], `${part.name}-joint`),\n child: part.name,\n type,\n origin: seamOrigin(part, parent),\n axis: { x: 0, y: 1, z: 0 }\n }\n if (type !== 'continuous') joint.limits = { ...NEW_LIMITS[type] }\n\n const parameters = draft.parameters ?? []\n const taken = [...parameters.map(one => one.name), ...(draft.animations ?? []).map(one => one.name)]\n const driver = drivingParameter(joint)\n driver.name = freeName(taken, joint.name)\n\n return { ...draft, joints: [...joints, joint], parameters: [...parameters, driver] }\n }\n\n if (existing.type === type) return draft\n\n const next: FigureJoint = { ...existing, type }\n if (type === 'continuous') delete next.limits\n else if (existing.type === 'continuous' || (existing.type === 'prismatic') !== (type === 'prismatic')) {\n next.limits = { ...NEW_LIMITS[type] }\n }\n\n return replaceJoint(draft, existing, next)\n}\n\n/** Changes a joint's origin, axis or limits. A plain driving parameter follows new limits. */\nexport function patchJoint(\n draft: FigureDraft,\n name: string,\n patch: { origin?: Vec3; axis?: Vec3; limits?: { min: number; max: number } }\n): FigureDraft {\n const existing = draft.joints?.find(joint => joint.name === name)\n if (!existing) return draft\n const next: FigureJoint = { ...existing, ...patch }\n if (existing.type === 'continuous') delete next.limits\n return replaceJoint(draft, existing, next)\n}\n\nfunction replaceJoint(draft: FigureDraft, before: FigureJoint, after: FigureJoint): FigureDraft {\n const joints = (draft.joints ?? []).map(joint => (joint === before ? after : joint))\n const was = spanOf(before)\n const unitChanged = (before.type === 'prismatic') !== (after.type === 'prismatic')\n const spanChanged = was.min !== spanOf(after).min || was.max !== spanOf(after).max\n\n if (!unitChanged && !spanChanged) return { ...draft, joints }\n\n const parameters = (draft.parameters ?? []).map(parameter => {\n if (!isPlainDriver(parameter, before, was)) return parameter\n const driver = drivingParameter(after)\n return { ...parameter, range: driver.range, clip: { ...parameter.clip, channels: driver.clip.channels } }\n })\n return { ...draft, joints, parameters }\n}\n\nfunction withoutJoint(draft: FigureDraft, name: string): FigureDraft {\n const keep = <T extends { channels?: { target: string }[] }>(one: T) => (one.channels ?? []).filter(channel => channel.target !== name)\n\n const parameters = (draft.parameters ?? [])\n .map(parameter => ({ ...parameter, clip: { ...parameter.clip, channels: keep(parameter.clip) } }) as Parameter)\n .filter(parameter => parameter.clip.channels.length > 0)\n const animations = (draft.animations ?? [])\n .map(clip => ({ ...clip, channels: keep(clip) }) as NonNullable<FigureDraft['animations']>[number])\n .filter(clip => clip.channels.length > 0)\n const joints = (draft.joints ?? []).filter(joint => joint.name !== name)\n\n const next: FigureDraft = { ...draft }\n if (joints.length > 0) next.joints = joints\n else delete next.joints\n if (draft.parameters !== undefined) next.parameters = parameters\n if (draft.animations !== undefined) next.animations = animations\n return next\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { FigureJoint } from '@hatiolab/figure-model';
2
+ import type { FigureDraft, PartModel } from './figure-source.js';
3
+ /**
4
+ * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.
5
+ *
6
+ * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은
7
+ * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.
8
+ * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.
9
+ *
10
+ * ## 부채꼴 하나다
11
+ *
12
+ * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,
13
+ * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.
14
+ *
15
+ * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로
16
+ * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지
17
+ * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).
18
+ */
19
+ /** 저작면에만 있는 이름. 부품 목록에는 없다. */
20
+ export declare const JOINT_GIZMO = "__joint-gizmo";
21
+ /** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */
22
+ export declare function axisName(axis: {
23
+ x: number;
24
+ y: number;
25
+ z: number;
26
+ }): string | undefined;
27
+ /**
28
+ * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,
29
+ * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**
30
+ */
31
+ export declare function gizmoPart(joint: FigureJoint, child: PartModel | undefined, draft: FigureDraft): PartModel | undefined;
@@ -0,0 +1,104 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ import { partFromFigure } from './figure-source.js';
5
+ /**
6
+ * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.
7
+ *
8
+ * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은
9
+ * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.
10
+ * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.
11
+ *
12
+ * ## 부채꼴 하나다
13
+ *
14
+ * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,
15
+ * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.
16
+ *
17
+ * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로
18
+ * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지
19
+ * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).
20
+ */
21
+ /** 저작면에만 있는 이름. 부품 목록에는 없다. */
22
+ export const JOINT_GIZMO = '__joint-gizmo';
23
+ /** 부채꼴을 몇 조각으로 그리나. 한 조각이 6도쯤이면 원이 원으로 보인다. */
24
+ const STEP = 6;
25
+ /** 두께. 얇아야 부품을 가리지 않고, 너무 얇으면 옆에서 볼 때 사라진다. */
26
+ const THICKNESS = 6;
27
+ /**
28
+ * 도는 평면과 두께 방향을 축에 맞춘다.
29
+ *
30
+ * 부채꼴은 단면이 눕고 **제 y 방향으로** 밀려 두꺼워지는 도형이라, 기본 상태의 축이 +y 다.
31
+ * 다른 축은 부품 회전으로 눕힌다 — 부품이 제 회전을 지는 것과 같은 방법이고, 형식이 이미
32
+ * 아는 길이다.
33
+ *
34
+ * `turn` 은 단면 안에서 각을 재는 방향이다. 씬의 회전과 단면의 각이 도는 쪽이 반대인 축이
35
+ * 있어서, 그것을 여기 한 곳에 적어 둔다 — 부채꼴이 실제로 도는 쪽과 반대로 벌어지면
36
+ * 가리키는 뜻이 뒤집힌다.
37
+ */
38
+ const AXIS_LAY = {
39
+ '+y': { turn: -1 },
40
+ '-y': { turn: 1 },
41
+ '+x': { rotation: { z: 90 }, turn: 1 },
42
+ '-x': { rotation: { z: 90 }, turn: -1 },
43
+ '+z': { rotation: { x: 90 }, turn: -1 },
44
+ '-z': { rotation: { x: 90 }, turn: 1 }
45
+ };
46
+ /** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */
47
+ export function axisName(axis) {
48
+ const named = [
49
+ ['x', axis.x],
50
+ ['y', axis.y],
51
+ ['z', axis.z]
52
+ ];
53
+ const on = named.filter(([, value]) => value !== 0);
54
+ if (on.length !== 1)
55
+ return undefined;
56
+ const [name, value] = on[0];
57
+ return `${value > 0 ? '+' : '-'}${name}`;
58
+ }
59
+ /** 이 관절이 도는 범위. 무한 회전은 한 바퀴다. */
60
+ function span(joint) {
61
+ if (joint.type === 'continuous' || !joint.limits)
62
+ return { from: -180, to: 180 };
63
+ return { from: joint.limits.min, to: joint.limits.max };
64
+ }
65
+ /** 부채꼴의 단면. 점은 부품 중심 기준의 밀리미터다. */
66
+ function sector(from, to, radius, turn) {
67
+ const path = [];
68
+ const full = Math.abs(to - from) >= 359.999;
69
+ if (!full)
70
+ path.push({ x: 0, y: 0 });
71
+ const steps = Math.max(2, Math.ceil(Math.abs(to - from) / STEP));
72
+ for (let i = 0; i <= steps; i++) {
73
+ const degrees = from + ((to - from) * i) / steps;
74
+ const rad = (degrees * Math.PI * turn) / 180;
75
+ path.push({ x: +(Math.cos(rad) * radius).toFixed(3), y: +(Math.sin(rad) * radius).toFixed(3) });
76
+ }
77
+ return path;
78
+ }
79
+ /**
80
+ * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,
81
+ * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**
82
+ */
83
+ export function gizmoPart(joint, child, draft) {
84
+ const lay = AXIS_LAY[axisName(joint.axis) ?? ''];
85
+ if (!lay)
86
+ return undefined;
87
+ /* 반지름은 **도는 부품의 크기**에서 온다. 부채꼴이 그 부품보다 작으면 무엇이 도는지 안 읽힌다. */
88
+ const reach = child ? Math.max(child.width, child.height, child.depth) : 0;
89
+ const radius = Math.max(60, Math.round(reach * 0.6));
90
+ const { from, to } = span(joint);
91
+ const part = {
92
+ name: JOINT_GIZMO,
93
+ primitive: 'polygon',
94
+ transform: {
95
+ position: { ...joint.origin },
96
+ size: { x: radius * 2, y: THICKNESS, z: radius * 2 },
97
+ ...(lay.rotation ? { rotation: lay.rotation } : {})
98
+ },
99
+ shape: { path: sector(from, to, radius, lay.turn) },
100
+ material: { token: 'palette.accent', transparent: true }
101
+ };
102
+ return { ...partFromFigure(part, draft), locked: true };
103
+ }
104
+ //# sourceMappingURL=joint-gizmo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joint-gizmo.js","sourceRoot":"","sources":["../../client/modeller/joint-gizmo.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureJoint, FigurePart } from '@hatiolab/figure-model'\n\nimport { partFromFigure } from './figure-source.js'\nimport type { FigureDraft, PartModel } from './figure-source.js'\n\n/**\n * 관절이 **어느 자리에서 어느 쪽으로 얼마나 도는지**를 저작면에 그린다.\n *\n * 축은 `+x · -x · +y …` 중에서 고른다. 이름만 보고 어느 쪽으로 도는지 아는 사람은 없고, 지금은\n * 미리보기로 건너가 막대를 끝까지 밀어 봐야 안다 — 여섯 축짜리 로봇에서는 그 왕복이 여섯 번이다.\n * 도는 자리를 그 자리에 그려 두면 고르는 순간 보인다.\n *\n * ## 부채꼴 하나다\n *\n * 관절의 기준점에 **가동 범위만큼 벌어진 얇은 부채꼴**을 눕힌다. 부채꼴의 면이 도는 평면이고,\n * 두께 방향이 축이며, 벌어진 각이 `limits` 다. 무한 회전(`continuous`)은 온전한 원이다.\n *\n * 부품 하나로 그린다 — 씬에 겹쳐 그리는 길을 따로 뚫지 않는다. 저작면은 부품을 컴포넌트로\n * 세우는 자리이고, 이 부채꼴도 그 규칙을 그대로 따른다. `locked` 라 클릭·마퀴·기즈모가 집지\n * 않고, 저장 형식으로는 나가지 않는다(저작면 안에서만 만들어 붙인다).\n */\n\n/** 저작면에만 있는 이름. 부품 목록에는 없다. */\nexport const JOINT_GIZMO = '__joint-gizmo'\n\n/** 부채꼴을 몇 조각으로 그리나. 한 조각이 6도쯤이면 원이 원으로 보인다. */\nconst STEP = 6\n\n/** 두께. 얇아야 부품을 가리지 않고, 너무 얇으면 옆에서 볼 때 사라진다. */\nconst THICKNESS = 6\n\n/**\n * 도는 평면과 두께 방향을 축에 맞춘다.\n *\n * 부채꼴은 단면이 눕고 **제 y 방향으로** 밀려 두꺼워지는 도형이라, 기본 상태의 축이 +y 다.\n * 다른 축은 부품 회전으로 눕힌다 — 부품이 제 회전을 지는 것과 같은 방법이고, 형식이 이미\n * 아는 길이다.\n *\n * `turn` 은 단면 안에서 각을 재는 방향이다. 씬의 회전과 단면의 각이 도는 쪽이 반대인 축이\n * 있어서, 그것을 여기 한 곳에 적어 둔다 — 부채꼴이 실제로 도는 쪽과 반대로 벌어지면\n * 가리키는 뜻이 뒤집힌다.\n */\nconst AXIS_LAY: Record<string, { rotation?: FigurePart['transform']['rotation']; turn: 1 | -1 }> = {\n '+y': { turn: -1 },\n '-y': { turn: 1 },\n '+x': { rotation: { z: 90 }, turn: 1 },\n '-x': { rotation: { z: 90 }, turn: -1 },\n '+z': { rotation: { x: 90 }, turn: -1 },\n '-z': { rotation: { x: 90 }, turn: 1 }\n}\n\n/** 축 벡터를 `+x` 꼴의 이름으로. 형식은 단위 벡터를 쓰고 저작면은 여섯 중 하나를 고른다. */\nexport function axisName(axis: { x: number; y: number; z: number }): string | undefined {\n const named = [\n ['x', axis.x],\n ['y', axis.y],\n ['z', axis.z]\n ] as const\n const on = named.filter(([, value]) => value !== 0)\n if (on.length !== 1) return undefined\n const [name, value] = on[0]!\n return `${value > 0 ? '+' : '-'}${name}`\n}\n\n/** 이 관절이 도는 범위. 무한 회전은 한 바퀴다. */\nfunction span(joint: FigureJoint): { from: number; to: number } {\n if (joint.type === 'continuous' || !joint.limits) return { from: -180, to: 180 }\n return { from: joint.limits.min, to: joint.limits.max }\n}\n\n/** 부채꼴의 단면. 점은 부품 중심 기준의 밀리미터다. */\nfunction sector(from: number, to: number, radius: number, turn: 1 | -1): { x: number; y: number }[] {\n const path: { x: number; y: number }[] = []\n const full = Math.abs(to - from) >= 359.999\n\n if (!full) path.push({ x: 0, y: 0 })\n\n const steps = Math.max(2, Math.ceil(Math.abs(to - from) / STEP))\n for (let i = 0; i <= steps; i++) {\n const degrees = from + ((to - from) * i) / steps\n const rad = (degrees * Math.PI * turn) / 180\n path.push({ x: +(Math.cos(rad) * radius).toFixed(3), y: +(Math.sin(rad) * radius).toFixed(3) })\n }\n\n return path\n}\n\n/**\n * 관절 하나를 그리는 부품. 그릴 수 없으면 `undefined` — 축이 비스듬하면 부채꼴로 그릴 수 없고,\n * 그때 아무 쪽이나 골라 그리면 **틀린 방향을 가리킨다.**\n */\nexport function gizmoPart(joint: FigureJoint, child: PartModel | undefined, draft: FigureDraft): PartModel | undefined {\n const lay = AXIS_LAY[axisName(joint.axis) ?? '']\n if (!lay) return undefined\n\n /* 반지름은 **도는 부품의 크기**에서 온다. 부채꼴이 그 부품보다 작으면 무엇이 도는지 안 읽힌다. */\n const reach = child ? Math.max(child.width, child.height, child.depth) : 0\n const radius = Math.max(60, Math.round(reach * 0.6))\n\n const { from, to } = span(joint)\n const part: FigurePart = {\n name: JOINT_GIZMO,\n primitive: 'polygon',\n transform: {\n position: { ...joint.origin },\n size: { x: radius * 2, y: THICKNESS, z: radius * 2 },\n ...(lay.rotation ? { rotation: lay.rotation } : {})\n },\n shape: { path: sector(from, to, radius, lay.turn) },\n material: { token: 'palette.accent', transparent: true }\n }\n\n return { ...partFromFigure(part, draft), locked: true }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { FigureDraft } from './figure-source.js';
2
+ /** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */
3
+ export declare function poseChanges(draft: FigureDraft | undefined, values: Record<string, number>): {
4
+ name: string;
5
+ from: number;
6
+ to: number;
7
+ }[];
8
+ /** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */
9
+ export declare function poseAsDefaults(draft: FigureDraft, values: Record<string, number>): FigureDraft;
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ */
4
+ /** 지금 이 파라미터가 서는 값. 형식의 `valueOf` 와 같은 순서로 읽는다. */
5
+ function standing(parameter) {
6
+ return parameter.default ?? parameter.range?.min ?? 0;
7
+ }
8
+ /** 범위 밖은 끝에서 문다. 막대는 범위 안에서만 움직이지만, 값은 다른 길로도 들어온다. */
9
+ function settled(value, parameter) {
10
+ const range = parameter.range;
11
+ const bound = range ? Math.min(Math.max(value, range.min), range.max) : value;
12
+ return Math.round(bound * 10000) / 10000;
13
+ }
14
+ /** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */
15
+ export function poseChanges(draft, values) {
16
+ return (draft?.parameters ?? []).flatMap(parameter => {
17
+ const given = values[parameter.name];
18
+ if (given === undefined || !Number.isFinite(given))
19
+ return [];
20
+ const to = settled(given, parameter);
21
+ const from = standing(parameter);
22
+ return to === from ? [] : [{ name: parameter.name, from, to }];
23
+ });
24
+ }
25
+ /** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */
26
+ export function poseAsDefaults(draft, values) {
27
+ const changes = new Map(poseChanges(draft, values).map(change => [change.name, change.to]));
28
+ if (changes.size === 0)
29
+ return draft;
30
+ return {
31
+ ...draft,
32
+ parameters: (draft.parameters ?? []).map(parameter => changes.has(parameter.name) ? { ...parameter, default: changes.get(parameter.name) } : parameter)
33
+ };
34
+ }
35
+ //# sourceMappingURL=pose-defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pose-defaults.js","sourceRoot":"","sources":["../../client/modeller/pose-defaults.ts"],"names":[],"mappings":"AAAA;;GAEG;AA0BH,mDAAmD;AACnD,SAAS,QAAQ,CAAC,SAAoB;IACpC,OAAO,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAA;AACvD,CAAC;AAED,uDAAuD;AACvD,SAAS,OAAO,CAAC,KAAa,EAAE,SAAoB;IAClD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAA;IAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAC7E,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAA;AAC1C,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,WAAW,CACzB,KAA8B,EAC9B,MAA8B;IAE9B,OAAO,CAAC,KAAK,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAA;QAE7D,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;QACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAA;QAChC,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,cAAc,CAAC,KAAkB,EAAE,MAA8B;IAC/E,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC3F,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEpC,OAAO;QACL,GAAG,KAAK;QACR,UAAU,EAAE,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CACnD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAClG;KACF,CAAA;AACH,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n */\n\nimport type { FigureDraft } from './figure-source.js'\n\n/**\n * 지금 세워 둔 자세를 **기본값으로 앉힌다.**\n *\n * ## 왜 필요한가\n *\n * 관절의 휴지 자세는 값이 0 인 자세다(ADR-0066). 팔이 여섯 마디면 그 자세는 곧게 선 기둥이라,\n * 목록에서도 도면에서도 로봇으로 안 보인다. 로봇다운 자세는 **파라미터의 기본값**이 만든다 —\n * 어깨 -35, 팔꿈치 70 을 적어 두면 아무도 값을 주지 않을 때 그 자세로 선다.\n *\n * 그것을 손으로 적으려면 미리보기에서 막대를 맞춘 뒤 그 수를 읽어 파라미터 칸마다 옮겨야 한다.\n * 여섯 축이면 여섯 번이고, 옮기는 동안 화면의 자세와 적은 수가 갈린다. 여기서 하는 일은 그\n * 옮겨 적기 하나다.\n *\n * ## 무엇을 건드리나\n *\n * **막대를 민 파라미터만** 건드린다. 안 민 것은 이미 제 기본값에 서 있으므로 적을 것이 없다.\n * 값이 지금 기본값과 같으면 그 파라미터는 **그대로 둔다** — 새 객체로 바꾸면 고친 것이 없는데도\n * 「저장 안 됨」이 붙는다. 하나도 안 바뀌면 받은 초안을 그대로 돌려준다.\n */\n\ntype Parameter = NonNullable<FigureDraft['parameters']>[number]\n\n/** 지금 이 파라미터가 서는 값. 형식의 `valueOf` 와 같은 순서로 읽는다. */\nfunction standing(parameter: Parameter): number {\n return parameter.default ?? parameter.range?.min ?? 0\n}\n\n/** 범위 밖은 끝에서 문다. 막대는 범위 안에서만 움직이지만, 값은 다른 길로도 들어온다. */\nfunction settled(value: number, parameter: Parameter): number {\n const range = parameter.range\n const bound = range ? Math.min(Math.max(value, range.min), range.max) : value\n return Math.round(bound * 10000) / 10000\n}\n\n/** 무엇이 달라지나 — 이름과 바뀔 값. 비어 있으면 앉힐 것이 없다. */\nexport function poseChanges(\n draft: FigureDraft | undefined,\n values: Record<string, number>\n): { name: string; from: number; to: number }[] {\n return (draft?.parameters ?? []).flatMap(parameter => {\n const given = values[parameter.name]\n if (given === undefined || !Number.isFinite(given)) return []\n\n const to = settled(given, parameter)\n const from = standing(parameter)\n return to === from ? [] : [{ name: parameter.name, from, to }]\n })\n}\n\n/** 지금 자세를 기본값으로 앉힌 초안. 앉힐 것이 없으면 **받은 초안 그대로**다. */\nexport function poseAsDefaults(draft: FigureDraft, values: Record<string, number>): FigureDraft {\n const changes = new Map(poseChanges(draft, values).map(change => [change.name, change.to]))\n if (changes.size === 0) return draft\n\n return {\n ...draft,\n parameters: (draft.parameters ?? []).map(parameter =>\n changes.has(parameter.name) ? { ...parameter, default: changes.get(parameter.name)! } : parameter\n )\n }\n}\n"]}
@@ -19,7 +19,7 @@ import type { FigurePart, FigureSource } from '@hatiolab/figure-model';
19
19
  * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준
20
20
  * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.
21
21
  */
22
- export type ChangeKind = 'added' | 'changed' | 'removed' | 'base' | 'placement' | 'detail' | 'styleKit' | 'animations' | 'parameters' | 'capabilities';
22
+ export type ChangeKind = 'added' | 'changed' | 'removed' | 'base' | 'placement' | 'detail' | 'styleKit' | 'animations' | 'parameters' | 'capabilities' | 'joints';
23
23
  export interface ProposalChange {
24
24
  kind: ChangeKind;
25
25
  /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */
@@ -10,11 +10,15 @@
10
10
  * 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)
11
11
  * 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.
12
12
  *
13
- * 형식의 `FigureSource` `type` · `parts` 빼면 여섯이 전부다. `type` 저장되는
13
+ * 같은 일이 `joints` 났다(2026-09-18). 형식에 관절이 서고 저작 화면도 지었는데
14
+ * 목록만 그대로여서, 관절을 더해 온 후보는 변경 목록에 안 뜨고 받아도 관절이 사라졌다. 형식의
15
+ * 칸을 세는 시험이 그것을 잡았다.
16
+ *
17
+ * 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 일곱이 전부다. `type` 은 저장되는
14
18
  * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이
15
19
  * 아니라 저장할 때 서버가 찍는다.
16
20
  */
17
- const FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities'];
21
+ const FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities', 'joints'];
18
22
  const FIELD_KIND = {
19
23
  base: 'base',
20
24
  placement: 'placement',
@@ -22,7 +26,8 @@ const FIELD_KIND = {
22
26
  styleKit: 'styleKit',
23
27
  animations: 'animations',
24
28
  parameters: 'parameters',
25
- capabilities: 'capabilities'
29
+ capabilities: 'capabilities',
30
+ joints: 'joints'
26
31
  };
27
32
  /** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */
28
33
  function samePart(a, b) {
@@ -1 +1 @@
1
- {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../client/modeller/proposal.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigurePart, FigureSource } from '@hatiolab/figure-model'\n\n/**\n * AI 후보를 지금 것과 비교하고, 고른 것만 받아들인다.\n *\n * 비교는 **저장 형식**으로 한다. 편집 중의 데이터는 씬 모델이지만, 후보는 서버에서\n * FigureSource 로 오고 기준 치수·디테일 등급처럼 부품 밖의 값도 함께 봐야 한다.\n *\n * 부품 편집 규칙은 여기 없다 — `part-edits.ts` 가 씬 모델 용어로 갖고 있다.\n */\n\n/**\n * 후보가 무엇을 바꾸자는 것인가.\n *\n * 후보를 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 —\n * 그게 가장 흔한 반응이다. 그래서 후보를 **변경 목록**으로 본다.\n *\n * 부품은 이름으로 짝짓는다. `FigurePart.name` 이 저장되는 식별자이고 바인딩이\n * 그 이름을 가리키므로, 이름이 같으면 같은 부품이다.\n *\n * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준\n * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.\n */\nexport type ChangeKind =\n | 'added'\n | 'changed'\n | 'removed'\n | 'base'\n | 'placement'\n | 'detail'\n | 'styleKit'\n | 'animations'\n | 'parameters'\n | 'capabilities'\n\n/**\n * 부품 밖에서 바뀔 수 있는 것 전부.\n *\n * 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도\n * 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.\n *\n * 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.\n * 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는\n * 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만\n * 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)\n * 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.\n *\n * 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 여섯이 전부다. `type` 은 저장되는\n * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이\n * 아니라 저장할 때 서버가 찍는다.\n */\nconst FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities'] as const\nconst FIELD_KIND: Record<(typeof FIGURE_FIELDS)[number], ChangeKind> = {\n base: 'base',\n placement: 'placement',\n detailLevel: 'detail',\n styleKit: 'styleKit',\n animations: 'animations',\n parameters: 'parameters',\n capabilities: 'capabilities'\n}\n\nexport interface ProposalChange {\n kind: ChangeKind\n /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */\n key: string\n /** 부품 변경일 때 후보 쪽 부품. */\n part?: FigurePart\n}\n\n/** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */\nfunction samePart(a: FigurePart, b: FigurePart): boolean {\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\n/** 키 순서를 맞춰 견줄 수 있게 한다. */\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as object).sort()) {\n out[key] = sortKeys((value as Record<string, unknown>)[key])\n }\n return out\n }\n return value\n}\n\nexport function diffProposal(current: FigureSource | undefined, proposal: FigureSource): ProposalChange[] {\n const changes: ProposalChange[] = []\n const mine = new Map((current?.parts ?? []).map(part => [part.name, part]))\n const theirs = new Map(proposal.parts.map(part => [part.name, part]))\n\n for (const field of FIGURE_FIELDS) {\n if (!current) break\n const before = JSON.stringify(sortKeys(current[field]))\n const after = JSON.stringify(sortKeys(proposal[field]))\n if (before !== after) {\n changes.push({ kind: FIELD_KIND[field], key: field })\n }\n }\n\n for (const part of proposal.parts) {\n const old = mine.get(part.name)\n if (!old) changes.push({ kind: 'added', key: part.name, part })\n else if (!samePart(old, part)) changes.push({ kind: 'changed', key: part.name, part })\n }\n for (const part of current?.parts ?? []) {\n if (!theirs.has(part.name)) changes.push({ kind: 'removed', key: part.name })\n }\n\n return changes\n}\n\n/**\n * 고른 변경만 받는다.\n *\n * **타입 이름은 지금 것을 지킨다.** 만든 뒤에는 고칠 수 없는 저장 식별자이고,\n * 배치된 인스턴스가 그 이름으로 청사진을 찾는다. 후보가 다른 이름을 지어 왔어도\n * 그것 때문에 이미 놓인 것이 빈 자리가 되면 안 된다.\n *\n * `picked` 를 주지 않으면 전부 받는다 — 그것이 보통의 길이다.\n *\n * 더해지는 부품은 **뒤에 붙인다.** 후보가 놓은 자리에 끼워 넣으면, 일부만 골랐을\n * 때 순서가 어디서 온 것인지 알 수 없게 된다.\n */\nexport function applyProposal(\n current: FigureSource | undefined,\n proposal: FigureSource,\n picked?: Set<string>\n): FigureSource {\n const takes = (change: ProposalChange) => !picked || picked.has(change.key)\n const changes = diffProposal(current, proposal)\n\n if (!current) {\n return { ...proposal }\n }\n\n const removed = new Set(changes.filter(c => c.kind === 'removed' && takes(c)).map(c => c.key))\n const replaced = new Map(\n changes.filter(c => c.kind === 'changed' && takes(c)).map(c => [c.key, c.part as FigurePart])\n )\n const added = changes.filter(c => c.kind === 'added' && takes(c)).map(c => c.part as FigurePart)\n\n const parts = [\n ...current.parts.filter(part => !removed.has(part.name)).map(part => replaced.get(part.name) ?? part),\n ...added\n ]\n\n // 고른 필드만 후보 값으로 바꾼다. 안 고른 것은 지금 값이 남는다.\n const fields: Record<string, unknown> = {}\n for (const field of FIGURE_FIELDS) {\n const change = changes.find(c => c.key === field)\n if (change && takes(change)) {\n fields[field] = proposal[field]\n }\n }\n\n return { ...current, ...fields, parts } as FigureSource\n}\n\n/** 전부 받는다. */\nexport function takeProposal(current: FigureSource | undefined, proposal: FigureSource): FigureSource {\n return applyProposal(current, proposal)\n}\n"]}
1
+ {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../client/modeller/proposal.ts"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,QAAQ,CAAU,CAAA;AACrI,MAAM,UAAU,GAAuD;IACrE,IAAI,EAAE,MAAM;IACZ,SAAS,EAAE,WAAW;IACtB,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,UAAU;IACpB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,YAAY,EAAE,cAAc;IAC5B,MAAM,EAAE,QAAQ;CACjB,CAAA;AAUD,0DAA0D;AAC1D,SAAS,QAAQ,CAAC,CAAa,EAAE,CAAa;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;AACpE,CAAC;AAED,2BAA2B;AAC3B,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,GAAG,GAA4B,EAAE,CAAA;QACvC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACtD,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAA;QAC9D,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAiC,EAAE,QAAsB;IACpF,MAAM,OAAO,GAAqB,EAAE,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;IAErE,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,CAAC,OAAO;YAAE,MAAK;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QACvD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QACvD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/B,IAAI,CAAC,GAAG;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;aAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IACxF,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;QACxC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAC/E,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAiC,EACjC,QAAsB,EACtB,MAAoB;IAEpB,MAAM,KAAK,GAAG,CAAC,MAAsB,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC3E,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAE/C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAA;IACxB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9F,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAkB,CAAC,CAAC,CAC9F,CAAA;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAkB,CAAC,CAAA;IAEhG,MAAM,KAAK,GAAG;QACZ,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;QACrG,GAAG,KAAK;KACT,CAAA;IAED,wCAAwC;IACxC,MAAM,MAAM,GAA4B,EAAE,CAAA;IAC1C,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAA;QACjD,IAAI,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAED,OAAO,EAAE,GAAG,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAkB,CAAA;AACzD,CAAC;AAED,cAAc;AACd,MAAM,UAAU,YAAY,CAAC,OAAiC,EAAE,QAAsB;IACpF,OAAO,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;AACzC,CAAC","sourcesContent":["import type { FigurePart, FigureSource } from '@hatiolab/figure-model'\n\n/**\n * AI 후보를 지금 것과 비교하고, 고른 것만 받아들인다.\n *\n * 비교는 **저장 형식**으로 한다. 편집 중의 데이터는 씬 모델이지만, 후보는 서버에서\n * FigureSource 로 오고 기준 치수·디테일 등급처럼 부품 밖의 값도 함께 봐야 한다.\n *\n * 부품 편집 규칙은 여기 없다 — `part-edits.ts` 가 씬 모델 용어로 갖고 있다.\n */\n\n/**\n * 후보가 무엇을 바꾸자는 것인가.\n *\n * 후보를 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 —\n * 그게 가장 흔한 반응이다. 그래서 후보를 **변경 목록**으로 본다.\n *\n * 부품은 이름으로 짝짓는다. `FigurePart.name` 이 저장되는 식별자이고 바인딩이\n * 그 이름을 가리키므로, 이름이 같으면 같은 부품이다.\n *\n * 기준 치수와 디테일 등급도 목록에 넣는다. 부품만 골라 받으면 새 부품이 옛 기준\n * 상자 밖으로 나가 **검증에 걸릴 수 있다** — 무엇을 함께 받아야 하는지 보여야 한다.\n */\nexport type ChangeKind =\n | 'added'\n | 'changed'\n | 'removed'\n | 'base'\n | 'placement'\n | 'detail'\n | 'styleKit'\n | 'animations'\n | 'parameters'\n | 'capabilities'\n | 'joints'\n\n/**\n * 부품 밖에서 바뀔 수 있는 것 전부.\n *\n * 여기 없는 필드는 **조용히 버려진다** — 후보가 바꿔 왔어도 목록에 안 뜨고 받아도\n * 반영되지 않는다. 형식에 필드가 늘면 여기에 더해야 한다.\n *\n * 그 일이 실제로 났다(2026-09-15). `animations` · `capabilities` · `placement` 셋이 빠져 있었다.\n * 후보가 애니메이션만 더해 오면 변경 목록이 **비어서** 「n건 반영」 단추가 잠기고, 저작자는\n * 후보를 받지도 버리지도 못한 채 멈춘다. 다른 변경과 섞여 오면 단추는 눌리는데 애니메이션만\n * 말없이 사라진다. 그리고 `anchor` 는 도형 수준 필드였다가 형식에서 없어진 것인데(2026-09-10)\n * 여기 남아 있었다 — 이 목록이 형식을 따라가지 않은 것이 양쪽으로 드러난 셈이다.\n *\n * 같은 일이 `joints` 로 한 번 더 났다(2026-09-18). 형식에 관절이 서고 저작 화면도 지었는데 이\n * 목록만 그대로여서, 관절을 더해 온 후보는 변경 목록에 안 뜨고 받아도 관절이 사라졌다. 형식의\n * 칸을 세는 시험이 그것을 잡았다.\n *\n * 형식의 `FigureSource` 는 `type` · `parts` 를 빼면 이 일곱이 전부다. `type` 은 저장되는\n * 식별자라 후보가 바꿔 와도 받지 않고(`applyProposal` 참고), `version` 은 저작자가 고를 것이\n * 아니라 저장할 때 서버가 찍는다.\n */\nconst FIGURE_FIELDS = ['base', 'placement', 'detailLevel', 'styleKit', 'animations', 'parameters', 'capabilities', 'joints'] as const\nconst FIELD_KIND: Record<(typeof FIGURE_FIELDS)[number], ChangeKind> = {\n base: 'base',\n placement: 'placement',\n detailLevel: 'detail',\n styleKit: 'styleKit',\n animations: 'animations',\n parameters: 'parameters',\n capabilities: 'capabilities',\n joints: 'joints'\n}\n\nexport interface ProposalChange {\n kind: ChangeKind\n /** 부품 변경이면 부품 이름. 그 밖이면 무엇이 바뀌는지. */\n key: string\n /** 부품 변경일 때 후보 쪽 부품. */\n part?: FigurePart\n}\n\n/** 부품 둘이 같은가. 순서까지 같은 JSON 으로 견준다 — 필드 순서는 만드는 쪽이 정한다. */\nfunction samePart(a: FigurePart, b: FigurePart): boolean {\n return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b))\n}\n\n/** 키 순서를 맞춰 견줄 수 있게 한다. */\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as object).sort()) {\n out[key] = sortKeys((value as Record<string, unknown>)[key])\n }\n return out\n }\n return value\n}\n\nexport function diffProposal(current: FigureSource | undefined, proposal: FigureSource): ProposalChange[] {\n const changes: ProposalChange[] = []\n const mine = new Map((current?.parts ?? []).map(part => [part.name, part]))\n const theirs = new Map(proposal.parts.map(part => [part.name, part]))\n\n for (const field of FIGURE_FIELDS) {\n if (!current) break\n const before = JSON.stringify(sortKeys(current[field]))\n const after = JSON.stringify(sortKeys(proposal[field]))\n if (before !== after) {\n changes.push({ kind: FIELD_KIND[field], key: field })\n }\n }\n\n for (const part of proposal.parts) {\n const old = mine.get(part.name)\n if (!old) changes.push({ kind: 'added', key: part.name, part })\n else if (!samePart(old, part)) changes.push({ kind: 'changed', key: part.name, part })\n }\n for (const part of current?.parts ?? []) {\n if (!theirs.has(part.name)) changes.push({ kind: 'removed', key: part.name })\n }\n\n return changes\n}\n\n/**\n * 고른 변경만 받는다.\n *\n * **타입 이름은 지금 것을 지킨다.** 만든 뒤에는 고칠 수 없는 저장 식별자이고,\n * 배치된 인스턴스가 그 이름으로 청사진을 찾는다. 후보가 다른 이름을 지어 왔어도\n * 그것 때문에 이미 놓인 것이 빈 자리가 되면 안 된다.\n *\n * `picked` 를 주지 않으면 전부 받는다 — 그것이 보통의 길이다.\n *\n * 더해지는 부품은 **뒤에 붙인다.** 후보가 놓은 자리에 끼워 넣으면, 일부만 골랐을\n * 때 순서가 어디서 온 것인지 알 수 없게 된다.\n */\nexport function applyProposal(\n current: FigureSource | undefined,\n proposal: FigureSource,\n picked?: Set<string>\n): FigureSource {\n const takes = (change: ProposalChange) => !picked || picked.has(change.key)\n const changes = diffProposal(current, proposal)\n\n if (!current) {\n return { ...proposal }\n }\n\n const removed = new Set(changes.filter(c => c.kind === 'removed' && takes(c)).map(c => c.key))\n const replaced = new Map(\n changes.filter(c => c.kind === 'changed' && takes(c)).map(c => [c.key, c.part as FigurePart])\n )\n const added = changes.filter(c => c.kind === 'added' && takes(c)).map(c => c.part as FigurePart)\n\n const parts = [\n ...current.parts.filter(part => !removed.has(part.name)).map(part => replaced.get(part.name) ?? part),\n ...added\n ]\n\n // 고른 필드만 후보 값으로 바꾼다. 안 고른 것은 지금 값이 남는다.\n const fields: Record<string, unknown> = {}\n for (const field of FIGURE_FIELDS) {\n const change = changes.find(c => c.key === field)\n if (change && takes(change)) {\n fields[field] = proposal[field]\n }\n }\n\n return { ...current, ...fields, parts } as FigureSource\n}\n\n/** 전부 받는다. */\nexport function takeProposal(current: FigureSource | undefined, proposal: FigureSource): FigureSource {\n return applyProposal(current, proposal)\n}\n"]}
@@ -1,54 +1,51 @@
1
1
  import type { FigureCost } from '@hatiolab/figure-model';
2
2
  /**
3
- * 한 도면이 감당하는 양 — 「몇 개까지 편한가」의 분모.
3
+ * 한 화면이 감당하는 양 — 「몇 개까지 편한가」의 분모.
4
4
  *
5
- * ## 값은 figure-model 것이어야 한다
5
+ * ## 놓은 개수가 아니라 **보이는 개수**다
6
6
  *
7
- * figure-model `LIMITS` 전부 **Figure 하나짜리** 한도다(묶음 3 · 애니메이션 2 …).
8
- * 「몇 개를 놓을 있나」는 도면이 감당하는 양을 알아야 나오는데,값은 아직
9
- * 어디에도 선언돼 있지 않다. 여기 두는 것은 임시다 figure-model 선언하면
10
- * 파일을 지운다. 두 벌이 되면 갈린다.
7
+ * 3D 요청이 있어야 다시 그린다. 가만히 있는 도형은 프레임마다 아무 값도 치르지 않는다 —
8
+ * 900 대를 놓아 두어도 60fps 다. 값을 치르는 것은 **카메라 안에 들어 있고 프레임에 다시
9
+ * 그려지는** 것뿐이다. 그래서 파일의 분모는 「한 도면에 놓는 수」가 아니라 「한 화면에 보이는
10
+ * 부품 묶음 수」다.
11
11
  *
12
- * ## 지어낸 수가 아니다
12
+ * ## 수는 잰 값이다
13
13
  *
14
- * `docs/design/component-authoring-tool.md` §4.15 「목표 수치」에 적힌 값이다.
14
+ * 전에는 설계 문서의 목표치(도면당 draw call 2,500)를 썼고, 그 문서가 스스로 「측정 없이 정한
15
+ * 목표」라고 적어 두었다. 2026-09-18 에 실기에서 쟀다
16
+ * (`things-scene/test/browser/measure-figure-frames.mjs`, Apple M5, 저작 화면과 같은 조명 · 그림자):
15
17
  *
16
- * 도면 자산 500
17
- * draw call ≤ 2500
18
- * 삼각형 ≤ 50 만
18
+ * 묶음 9 짜리 6 축 로봇 600 대 보이면 59.5 fps · 700 대에서 56.7 fps
19
+ * 묶음 2 짜리 컨베이어 1,600 대 보이면 59.3 fps · 2,500 대에서 36.8 fps
19
20
  *
20
- * 문서가 스스로 이렇게 적어 두었다 **「측정 없이 정한 목표다. 저작도구가 서면
21
- * 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
22
- * 숫자만 크게 내걸면 재 본 값처럼 읽힌다.
23
- */
24
- /**
25
- * 그림자 패스가 draw call 을 두 배로 만든다.
21
+ * 도형의 꺾이는 지점이 **인스턴스 × 묶음 = 3,600 ~ 5,400** 들어온다. 보수적으로 4,000 을
22
+ * 쓴다. 도형이 달라도 곱이 비슷한 데서 꺾이므로, 분모는 묶음 수로 둔다.
26
23
  *
27
- * 실측이다(2026-09-05, Apple M5). 자산 500~5,500 어디서나 실측이 figure-model 예측의
28
- * **정확히 2.00 배**로 나왔고, 그림자 맵을 끄면 예측과 같아졌다. 그림자 맵은 장면을
29
- * 한 번 더 그리기 때문이다. figure-model 의 `at.drawCalls` 는 색 패스만 센다.
24
+ * 수는 **이 기계에서 씬으로** 값이다. 더 낮은 GPU 나 브라우저에서는 더 일찍 꺾인다.
25
+ * 화면에는 기계와 날짜를 함께 적는다 적지 않으면 어디서나 참인 수처럼 읽힌다.
30
26
  *
31
- * ## 등급 S 그림자를 진다
27
+ * ## 그림자는 이미 들어 있다
32
28
  *
33
- * 설계 문서 §4.15 「그림자는 등급 M 이상만」이라고 정했다. 그래서 처음에는 등급이
34
- * S 곱을 빼려 했다. **그런데 등급 S 세워 보니 draw call 이 똑같이 두 배로
35
- * 나왔다** 규칙은 문서에만 있고 제품에는 없다.
36
- *
37
- * 그래서 등급을 보지 않고 언제나 곱한다. 없는 규칙을 믿고 셈하면 화면이 「S 로 하면
38
- * 두 배 놓을 수 있다」고 거짓말을 한다. things-scene 이 그 규칙을 구현하면 그때
39
- * 등급을 보면 된다 — 그쪽에 알릴 것.
29
+ * 2026-09-05 에는 draw call figure-model 예측과 견주며 그림자 패스가 두 배로 만든다고
30
+ * 쟀다. 지금 값은 **그림자를 프레임 시간을 직접 것**이라 배수가 이미 안에 있다.
31
+ * 다시 곱하면 세는 것이다.
40
32
  */
41
- export declare const SHADOW_PASS_FACTOR = 2;
42
33
  export declare const SCENE_BUDGET: {
43
- /** 한 도면의 draw call. 로우폴리에서 먼저 차는 쪽이 대개 이것이다. */
44
- readonly drawCalls: 2500;
45
- /** 한 도면의 삼각형. */
34
+ /** 한 화면에 보이는 부품 묶음. 60fps 꺾이는 지점, 실측. */
35
+ readonly groupsOnScreen: 4000;
36
+ /** 한 화면의 삼각형. **이 수는 아직 안 쟀다** — 설계 문서의 목표치다. */
46
37
  readonly triangles: 500000;
47
38
  };
39
+ /** 어디서 언제 잰 값인가. 화면이 함께 보여 준다 — 적지 않으면 어디서나 참인 수로 읽힌다. */
40
+ export declare const MEASURED: {
41
+ readonly gpu: "Apple M5";
42
+ readonly on: "2026-09-18";
43
+ readonly fps: 60;
44
+ };
48
45
  /** 무엇이 먼저 차나. */
49
- export type BindingBudget = 'drawCalls' | 'triangles';
46
+ export type BindingBudget = 'groups' | 'triangles';
50
47
  export interface FigureCapacity {
51
- /** 이 형상을개까지 놓을 있나. */
48
+ /** 이 형상이 **한 화면에 보일 때까지** 60fps 로 도나. */
52
49
  instances: number;
53
50
  /** 먼저 차는 예산. **여기부터 고치면 된다.** */
54
51
  bound: BindingBudget;
@@ -1,49 +1,42 @@
1
1
  /**
2
- * 한 도면이 감당하는 양 — 「몇 개까지 편한가」의 분모.
2
+ * 한 화면이 감당하는 양 — 「몇 개까지 편한가」의 분모.
3
3
  *
4
- * ## 값은 figure-model 것이어야 한다
4
+ * ## 놓은 개수가 아니라 **보이는 개수**다
5
5
  *
6
- * figure-model `LIMITS` 전부 **Figure 하나짜리** 한도다(묶음 3 · 애니메이션 2 …).
7
- * 「몇 개를 놓을 있나」는 도면이 감당하는 양을 알아야 나오는데,값은 아직
8
- * 어디에도 선언돼 있지 않다. 여기 두는 것은 임시다 figure-model 선언하면
9
- * 파일을 지운다. 두 벌이 되면 갈린다.
6
+ * 3D 요청이 있어야 다시 그린다. 가만히 있는 도형은 프레임마다 아무 값도 치르지 않는다 —
7
+ * 900 대를 놓아 두어도 60fps 다. 값을 치르는 것은 **카메라 안에 들어 있고 프레임에 다시
8
+ * 그려지는** 것뿐이다. 그래서 파일의 분모는 「한 도면에 놓는 수」가 아니라 「한 화면에 보이는
9
+ * 부품 묶음 수」다.
10
10
  *
11
- * ## 지어낸 수가 아니다
11
+ * ## 수는 잰 값이다
12
12
  *
13
- * `docs/design/component-authoring-tool.md` §4.15 「목표 수치」에 적힌 값이다.
13
+ * 전에는 설계 문서의 목표치(도면당 draw call 2,500)를 썼고, 그 문서가 스스로 「측정 없이 정한
14
+ * 목표」라고 적어 두었다. 2026-09-18 에 실기에서 쟀다
15
+ * (`things-scene/test/browser/measure-figure-frames.mjs`, Apple M5, 저작 화면과 같은 조명 · 그림자):
14
16
  *
15
- * 도면 자산 500
16
- * draw call ≤ 2500
17
- * 삼각형 ≤ 50 만
17
+ * 묶음 9 짜리 6 축 로봇 600 대 보이면 59.5 fps · 700 대에서 56.7 fps
18
+ * 묶음 2 짜리 컨베이어 1,600 대 보이면 59.3 fps · 2,500 대에서 36.8 fps
18
19
  *
19
- * 문서가 스스로 이렇게 적어 두었다 **「측정 없이 정한 목표다. 저작도구가 서면
20
- * 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.
21
- * 숫자만 크게 내걸면 재 본 값처럼 읽힌다.
22
- */
23
- /**
24
- * 그림자 패스가 draw call 을 두 배로 만든다.
25
- *
26
- * 실측이다(2026-09-05, Apple M5). 자산 500~5,500 어디서나 실측이 figure-model 예측의
27
- * **정확히 2.00 배**로 나왔고, 그림자 맵을 끄면 예측과 같아졌다. 그림자 맵은 장면을
28
- * 한 번 더 그리기 때문이다. figure-model 의 `at.drawCalls` 는 색 패스만 센다.
20
+ * 도형의 꺾이는 지점이 **인스턴스 × 묶음 = 3,600 ~ 5,400** 들어온다. 보수적으로 4,000 을
21
+ * 쓴다. 도형이 달라도 곱이 비슷한 데서 꺾이므로, 분모는 묶음 수로 둔다.
29
22
  *
30
- * ## 등급 S 그림자를 진다
23
+ * 수는 **이 기계에서 이 씬으로** 잰 값이다. 더 낮은 GPU 나 브라우저에서는 더 일찍 꺾인다.
24
+ * 화면에는 잰 기계와 날짜를 함께 적는다 — 적지 않으면 어디서나 참인 수처럼 읽힌다.
31
25
  *
32
- * 설계 문서 §4.15 「그림자는 등급 M 이상만」이라고 정했다. 그래서 처음에는 등급이
33
- * S 면 이 곱을 빼려 했다. **그런데 등급 S 로 세워 재 보니 draw call 이 똑같이 두 배로
34
- * 나왔다** — 그 규칙은 문서에만 있고 제품에는 없다.
26
+ * ## 그림자는 이미 들어 있다
35
27
  *
36
- * 그래서 등급을 보지 않고 언제나 곱한다. 없는 규칙을 믿고 셈하면 화면이 「S 하면
37
- * 놓을 있다」고 거짓말을 한다. things-scene 규칙을 구현하면 그때
38
- * 등급을 보면 된다 그쪽에 알릴 것.
28
+ * 2026-09-05 에는 draw call figure-model 예측과 견주며 그림자 패스가 배로 만든다고
29
+ * 쟀다. 지금 값은 **그림자를 프레임 시간을 직접 잰 것**이라 배수가 이미 안에 있다.
30
+ * 다시 곱하면 세는 것이다.
39
31
  */
40
- export const SHADOW_PASS_FACTOR = 2;
41
32
  export const SCENE_BUDGET = {
42
- /** 한 도면의 draw call. 로우폴리에서 먼저 차는 쪽이 대개 이것이다. */
43
- drawCalls: 2500,
44
- /** 한 도면의 삼각형. */
33
+ /** 한 화면에 보이는 부품 묶음. 60fps 꺾이는 지점, 실측. */
34
+ groupsOnScreen: 4000,
35
+ /** 한 화면의 삼각형. **이 수는 아직 안 쟀다** — 설계 문서의 목표치다. */
45
36
  triangles: 500_000
46
37
  };
38
+ /** 어디서 언제 잰 값인가. 화면이 함께 보여 준다 — 적지 않으면 어디서나 참인 수로 읽힌다. */
39
+ export const MEASURED = { gpu: 'Apple M5', on: '2026-09-18', fps: 60 };
47
40
  /**
48
41
  * 몇 개까지 놓을 수 있나.
49
42
  *
@@ -69,12 +62,10 @@ export function capacityFrom(groups, triangles) {
69
62
  if (groups <= 0 && triangles <= 0) {
70
63
  return undefined;
71
64
  }
72
- // 화면이 실제로 내는 draw call. 그림자 패스가 장면을 더 그린다 — 등급과 무관하게.
73
- const calls = groups * SHADOW_PASS_FACTOR;
74
- const byDrawCalls = calls > 0 ? Math.floor(SCENE_BUDGET.drawCalls / calls) : Infinity;
65
+ const byGroups = groups > 0 ? Math.floor(SCENE_BUDGET.groupsOnScreen / groups) : Infinity;
75
66
  const byTriangles = triangles > 0 ? Math.floor(SCENE_BUDGET.triangles / triangles) : Infinity;
76
- return byDrawCalls <= byTriangles
77
- ? { instances: byDrawCalls, bound: 'drawCalls', per: calls, budget: SCENE_BUDGET.drawCalls }
67
+ return byGroups <= byTriangles
68
+ ? { instances: byGroups, bound: 'groups', per: groups, budget: SCENE_BUDGET.groupsOnScreen }
78
69
  : { instances: byTriangles, bound: 'triangles', per: triangles, budget: SCENE_BUDGET.triangles };
79
70
  }
80
71
  //# sourceMappingURL=scene-budget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-budget.js","sourceRoot":"","sources":["../../client/modeller/scene-budget.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,gDAAgD;IAChD,SAAS,EAAE,IAAI;IACf,iBAAiB;IACjB,SAAS,EAAE,OAAO;CACV,CAAA;AAgBV;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,SAAiB;IAC5D,2CAA2C;IAC3C,IAAI,MAAM,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,0DAA0D;IAC1D,MAAM,KAAK,GAAG,MAAM,GAAG,kBAAkB,CAAA;IAEzC,MAAM,WAAW,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IACrF,MAAM,WAAW,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAE7F,OAAO,WAAW,IAAI,WAAW;QAC/B,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE;QAC5F,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE,CAAA;AACpG,CAAC","sourcesContent":["import type { FigureCost } from '@hatiolab/figure-model'\n\n/**\n * 한 도면이 감당하는 양 — 「몇 개까지 편한가」의 분모.\n *\n * ## 값은 figure-model 것이어야 한다\n *\n * figure-model 의 `LIMITS` 전부 **Figure 하나짜리** 한도다(묶음 3 · 애니메이션 2 …).\n * 「몇 개를 놓을 있나」는 도면이 감당하는 양을 알아야 나오는데, 값은 아직\n * 어디에도 선언돼 있지 않다. 여기 두는 것은 임시다 figure-model선언하면 이\n * 파일을 지운다. 벌이 되면 갈린다.\n *\n * ## 지어낸 수가 아니다\n *\n * `docs/design/component-authoring-tool.md` §4.15 「목표 수치」에 적힌 값이다.\n *\n * 도면 자산 500 개\n * draw call 2500\n * 삼각형 ≤ 50 만\n *\n * 그 문서가 스스로 이렇게 적어 두었다 **「측정 없이 정한 목표다. 저작도구가 서면\n * 실제 도면으로 재고 조정한다」**(미결 D-20). 그래서 화면에도 근거를 함께 띄운다.\n * 숫자만 크게 내걸면 값처럼 읽힌다.\n */\n/**\n * 그림자 패스가 draw call 배로 만든다.\n *\n * 실측이다(2026-09-05, Apple M5). 자산 500~5,500 어디서나 실측이 figure-model 예측의\n * **정확히 2.00 배**로 나왔고, 그림자 맵을 끄면 예측과 같아졌다. 그림자 맵은 장면을\n * 그리기 때문이다. figure-model `at.drawCalls` 패스만 센다.\n *\n * ## 등급 S 그림자를 진다\n *\n * 설계 문서 §4.15 「그림자는 등급 M 이상만」이라고 정했다. 그래서 처음에는 등급이\n * S 곱을 빼려 했다. **그런데 등급 S 세워 보니 draw call 이 똑같이 두 배로\n * 나왔다** 규칙은 문서에만 있고 제품에는 없다.\n *\n * 그래서 등급을 보지 않고 언제나 곱한다. 없는 규칙을 믿고 셈하면 화면이 「S 하면\n * 놓을 있다」고 거짓말을 한다. things-scene 규칙을 구현하면 그때\n * 등급을 보면 된다 그쪽에 알릴 것.\n */\nexport const SHADOW_PASS_FACTOR = 2\n\nexport const SCENE_BUDGET = {\n /** 한 도면의 draw call. 로우폴리에서 먼저 차는 쪽이 대개 이것이다. */\n drawCalls: 2500,\n /** 한 도면의 삼각형. */\n triangles: 500_000\n} as const\n\n/** 무엇이 먼저 차나. */\nexport type BindingBudget = 'drawCalls' | 'triangles'\n\nexport interface FigureCapacity {\n /** 이 형상을개까지 놓을 있나. */\n instances: number\n /** 먼저 차는 예산. **여기부터 고치면 된다.** */\n bound: BindingBudget\n /** 인스턴스 하나가 무는 값. */\n per: number\n /** 그 예산의 한 도면 총량. */\n budget: number\n}\n\n/**\n * 몇 개까지 놓을 수 있나.\n *\n * 두 예산 중 **먼저 차는 쪽**이 답이다. 평균이나 곱을 내지 않는다 — 하나만 꽉 차도\n * 그것이 병목이고, 저작자가 고쳐야 할 곳은 그 하나다.\n *\n * 셈은 여기서 하지만 재료는 전부 figure-model 이 낸 것이다. `cost.groups` 가 인스턴스\n * 하나당 draw call 이고 `cost.triangles` 가 인스턴스 하나의 삼각형 수다.\n */\nexport function capacityOf(cost: FigureCost): FigureCapacity | undefined {\n return capacityFrom(cost.groups, cost.triangles)\n}\n\n/**\n * 저장된 값만으로 셈한다.\n *\n * 목록은 정본을 받아 오지 않는다 — 카드 수십 장마다 컴파일하면 목록이 무거워진다.\n * 대신 서버가 저장해 둔 `groups` · `triangles` 를 쓴다. **목록과 저작면이 같은 함수를\n * 쓰는 것**이 요점이다. 카드가 제 나름대로 셈하면 같은 형상이 두 화면에서 다른 개수로\n * 나온다.\n */\nexport function capacityFrom(groups: number, triangles: number): FigureCapacity | undefined {\n // 부품이 없으면 「무한히 놓을 수 있다」가 아니라 **답할 것이 없다**.\n if (groups <= 0 && triangles <= 0) {\n return undefined\n }\n\n // 화면이 실제로 내는 draw call. 그림자 패스가 장면을 한 번 더 그린다 — 등급과 무관하게.\n const calls = groups * SHADOW_PASS_FACTOR\n\n const byDrawCalls = calls > 0 ? Math.floor(SCENE_BUDGET.drawCalls / calls) : Infinity\n const byTriangles = triangles > 0 ? Math.floor(SCENE_BUDGET.triangles / triangles) : Infinity\n\n return byDrawCalls <= byTriangles\n ? { instances: byDrawCalls, bound: 'drawCalls', per: calls, budget: SCENE_BUDGET.drawCalls }\n : { instances: byTriangles, bound: 'triangles', per: triangles, budget: SCENE_BUDGET.triangles }\n}\n\n"]}
1
+ {"version":3,"file":"scene-budget.js","sourceRoot":"","sources":["../../client/modeller/scene-budget.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,2CAA2C;IAC3C,cAAc,EAAE,IAAI;IACpB,iDAAiD;IACjD,SAAS,EAAE,OAAO;CACV,CAAA;AAEV,0DAA0D;AAC1D,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,EAAE,EAAW,CAAA;AAgB/E;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,SAAiB;IAC5D,2CAA2C;IAC3C,IAAI,MAAM,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QAClC,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IACzF,MAAM,WAAW,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAE7F,OAAO,QAAQ,IAAI,WAAW;QAC5B,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,cAAc,EAAE;QAC5F,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,CAAC,SAAS,EAAE,CAAA;AACpG,CAAC","sourcesContent":["import type { FigureCost } from '@hatiolab/figure-model'\n\n/**\n * 한 화면이 감당하는 양 — 「몇 개까지 편한가」의 분모.\n *\n * ## 놓은 개수가 아니라 **보이는 개수**다\n *\n * 3D요청이 있어야 다시 그린다. 가만히 있는 도형은 프레임마다 아무 값도 치르지 않는다 —\n * 900 대를 놓아 두어도 60fps 다. 값을 치르는 것은 **카메라 안에 들어 있고 프레임에 다시\n * 그려지는** 것뿐이다. 그래서파일의 분모는 「한 도면에 놓는 수」가 아니라 「한 화면에 보이는\n * 부품 묶음 수」다.\n *\n * ## 수는 값이다\n *\n * 전에는 설계 문서의 목표치(도면당 draw call 2,500)를 썼고, 그 문서가 스스로 「측정 없이 정한\n * 목표」라고 적어 두었다. 2026-09-18 실기에서 쟀다\n * (`things-scene/test/browser/measure-figure-frames.mjs`, Apple M5, 저작 화면과 같은 조명 · 그림자):\n *\n * 묶음 9 짜리 6 로봇 600 보이면 59.5 fps · 700 대에서 56.7 fps\n * 묶음 2 짜리 컨베이어 1,600 보이면 59.3 fps · 2,500 대에서 36.8 fps\n *\n * 도형의 꺾이는 지점이 **인스턴스 × 묶음 = 3,600 ~ 5,400** 에 들어온다. 보수적으로 4,000 을\n * 쓴다. 도형이 달라도 곱이 비슷한 데서 꺾이므로, 분모는 묶음 수로 둔다.\n *\n * 수는 **이 기계에서 씬으로** 값이다. 낮은 GPU 브라우저에서는 일찍 꺾인다.\n * 화면에는 기계와 날짜를 함께 적는다 적지 않으면 어디서나 참인 수처럼 읽힌다.\n *\n * ## 그림자는 이미 들어 있다\n *\n * 2026-09-05 에는 draw call figure-model 예측과 견주며 그림자 패스가 배로 만든다고\n * 쟀다. 지금 값은 **그림자를 프레임 시간을 직접 잰 것**이라 배수가 이미 안에 있다.\n * 다시 곱하면 세는 것이다.\n */\n\nexport const SCENE_BUDGET = {\n /** 한 화면에 보이는 부품 묶음. 60fps 꺾이는 지점, 실측. */\n groupsOnScreen: 4000,\n /** 한 화면의 삼각형. **이 수는 아직 안 쟀다** — 설계 문서의 목표치다. */\n triangles: 500_000\n} as const\n\n/** 어디서 언제 잰 값인가. 화면이 함께 보여 준다 — 적지 않으면 어디서나 참인 수로 읽힌다. */\nexport const MEASURED = { gpu: 'Apple M5', on: '2026-09-18', fps: 60 } as const\n\n/** 무엇이 먼저 차나. */\nexport type BindingBudget = 'groups' | 'triangles'\n\nexport interface FigureCapacity {\n /** 이 형상이 **한 화면에 보일 때까지** 60fps 로 도나. */\n instances: number\n /** 먼저 차는 예산. **여기부터 고치면 된다.** */\n bound: BindingBudget\n /** 인스턴스 하나가 무는 값. */\n per: number\n /** 그 예산의 한 도면 총량. */\n budget: number\n}\n\n/**\n * 몇 개까지 놓을 수 있나.\n *\n * 두 예산 중 **먼저 차는 쪽**이 답이다. 평균이나 곱을 내지 않는다 — 하나만 꽉 차도\n * 그것이 병목이고, 저작자가 고쳐야 할 곳은 그 하나다.\n *\n * 셈은 여기서 하지만 재료는 전부 figure-model 이 낸 것이다. `cost.groups` 가 인스턴스\n * 하나당 draw call 이고 `cost.triangles` 가 인스턴스 하나의 삼각형 수다.\n */\nexport function capacityOf(cost: FigureCost): FigureCapacity | undefined {\n return capacityFrom(cost.groups, cost.triangles)\n}\n\n/**\n * 저장된 값만으로 셈한다.\n *\n * 목록은 정본을 받아 오지 않는다 — 카드 수십 장마다 컴파일하면 목록이 무거워진다.\n * 대신 서버가 저장해 둔 `groups` · `triangles` 를 쓴다. **목록과 저작면이 같은 함수를\n * 쓰는 것**이 요점이다. 카드가 제 나름대로 셈하면 같은 형상이 두 화면에서 다른 개수로\n * 나온다.\n */\nexport function capacityFrom(groups: number, triangles: number): FigureCapacity | undefined {\n // 부품이 없으면 「무한히 놓을 수 있다」가 아니라 **답할 것이 없다**.\n if (groups <= 0 && triangles <= 0) {\n return undefined\n }\n\n const byGroups = groups > 0 ? Math.floor(SCENE_BUDGET.groupsOnScreen / groups) : Infinity\n const byTriangles = triangles > 0 ? Math.floor(SCENE_BUDGET.triangles / triangles) : Infinity\n\n return byGroups <= byTriangles\n ? { instances: byGroups, bound: 'groups', per: groups, budget: SCENE_BUDGET.groupsOnScreen }\n : { instances: byTriangles, bound: 'triangles', per: triangles, budget: SCENE_BUDGET.triangles }\n}\n\n"]}