@things-factory/figure-ui 10.1.270 → 10.1.272

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 (113) hide show
  1. package/client/graphql/index.ts +2 -0
  2. package/client/modeller/canvas.ts +4 -2
  3. package/client/modeller/clips.ts +74 -4
  4. package/client/modeller/data-interface.ts +14 -0
  5. package/client/modeller/editor-styles.ts +11 -0
  6. package/client/modeller/editor.ts +39 -11
  7. package/client/modeller/fasten-form.ts +73 -3
  8. package/client/modeller/fasten-geometry.ts +11 -1
  9. package/client/modeller/figure-cavity-drawing.ts +1 -3
  10. package/client/modeller/figure-clips.ts +101 -8
  11. package/client/modeller/figure-fasten-dialog.ts +41 -30
  12. package/client/modeller/figure-fasten-drawing.ts +5 -4
  13. package/client/modeller/figure-first-save.ts +4 -3
  14. package/client/modeller/figure-links.ts +85 -28
  15. package/client/modeller/figure-preview.ts +50 -4
  16. package/client/modeller/figure-size-editor.ts +7 -2
  17. package/client/modeller/first-save.ts +8 -0
  18. package/client/modeller/focus-on-render.ts +4 -0
  19. package/client/modeller/palette.ts +27 -0
  20. package/client/modeller/pivot.ts +76 -0
  21. package/client/modeller/product-session.ts +5 -1
  22. package/client/modeller/size-response.ts +14 -0
  23. package/client/modeller/stage-model.ts +50 -6
  24. package/client/types.ts +2 -0
  25. package/dist-client/graphql/index.js +2 -0
  26. package/dist-client/graphql/index.js.map +1 -1
  27. package/dist-client/modeller/canvas.d.ts +1 -0
  28. package/dist-client/modeller/canvas.js +3 -2
  29. package/dist-client/modeller/canvas.js.map +1 -1
  30. package/dist-client/modeller/clips.d.ts +23 -6
  31. package/dist-client/modeller/clips.js +69 -4
  32. package/dist-client/modeller/clips.js.map +1 -1
  33. package/dist-client/modeller/data-interface.d.ts +5 -0
  34. package/dist-client/modeller/data-interface.js +15 -0
  35. package/dist-client/modeller/data-interface.js.map +1 -1
  36. package/dist-client/modeller/editor-styles.js +11 -0
  37. package/dist-client/modeller/editor-styles.js.map +1 -1
  38. package/dist-client/modeller/editor.d.ts +6 -0
  39. package/dist-client/modeller/editor.js +35 -11
  40. package/dist-client/modeller/editor.js.map +1 -1
  41. package/dist-client/modeller/fasten-form.d.ts +8 -0
  42. package/dist-client/modeller/fasten-form.js +68 -5
  43. package/dist-client/modeller/fasten-form.js.map +1 -1
  44. package/dist-client/modeller/fasten-geometry.d.ts +6 -0
  45. package/dist-client/modeller/fasten-geometry.js +10 -1
  46. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  47. package/dist-client/modeller/figure-cavity-drawing.js +1 -3
  48. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  49. package/dist-client/modeller/figure-clips.d.ts +9 -0
  50. package/dist-client/modeller/figure-clips.js +104 -8
  51. package/dist-client/modeller/figure-clips.js.map +1 -1
  52. package/dist-client/modeller/figure-fasten-dialog.js +39 -24
  53. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  54. package/dist-client/modeller/figure-fasten-drawing.js +5 -4
  55. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  56. package/dist-client/modeller/figure-first-save.js +4 -3
  57. package/dist-client/modeller/figure-first-save.js.map +1 -1
  58. package/dist-client/modeller/figure-links.d.ts +9 -0
  59. package/dist-client/modeller/figure-links.js +89 -35
  60. package/dist-client/modeller/figure-links.js.map +1 -1
  61. package/dist-client/modeller/figure-preview.js +48 -4
  62. package/dist-client/modeller/figure-preview.js.map +1 -1
  63. package/dist-client/modeller/figure-size-editor.js +7 -3
  64. package/dist-client/modeller/figure-size-editor.js.map +1 -1
  65. package/dist-client/modeller/first-save.d.ts +5 -0
  66. package/dist-client/modeller/first-save.js +7 -0
  67. package/dist-client/modeller/first-save.js.map +1 -1
  68. package/dist-client/modeller/focus-on-render.d.ts +2 -0
  69. package/dist-client/modeller/focus-on-render.js +4 -0
  70. package/dist-client/modeller/focus-on-render.js.map +1 -0
  71. package/dist-client/modeller/links.d.ts +1 -1
  72. package/dist-client/modeller/palette.d.ts +14 -0
  73. package/dist-client/modeller/palette.js +23 -0
  74. package/dist-client/modeller/palette.js.map +1 -1
  75. package/dist-client/modeller/pivot.d.ts +12 -0
  76. package/dist-client/modeller/pivot.js +59 -0
  77. package/dist-client/modeller/pivot.js.map +1 -0
  78. package/dist-client/modeller/product-session.js +6 -2
  79. package/dist-client/modeller/product-session.js.map +1 -1
  80. package/dist-client/modeller/size-response.d.ts +6 -0
  81. package/dist-client/modeller/size-response.js +14 -0
  82. package/dist-client/modeller/size-response.js.map +1 -1
  83. package/dist-client/modeller/stage-model.d.ts +17 -3
  84. package/dist-client/modeller/stage-model.js +44 -6
  85. package/dist-client/modeller/stage-model.js.map +1 -1
  86. package/dist-client/route.d.ts +1 -1
  87. package/dist-client/tsconfig.tsbuildinfo +1 -1
  88. package/dist-client/types.d.ts +2 -0
  89. package/dist-client/types.js.map +1 -1
  90. package/package.json +5 -5
  91. package/test/browser/editing.check.mjs +6 -5
  92. package/test/browser/section.check.mjs +21 -0
  93. package/test/clips.test.ts +49 -4
  94. package/test/data-motion.test.ts +8 -0
  95. package/test/fasten-form.test.ts +57 -0
  96. package/test/fasten-geometry.test.ts +25 -1
  97. package/test/first-part-on-plane.test.ts +52 -0
  98. package/test/first-save.test.ts +11 -1
  99. package/test/pivot.test.ts +57 -0
  100. package/test/save-landed.test.ts +20 -1
  101. package/test/size-response.test.ts +10 -2
  102. package/test/stage-model.test.ts +23 -1
  103. package/translations/en.json +9 -0
  104. package/translations/es.json +9 -0
  105. package/translations/ja.json +9 -0
  106. package/translations/ko.json +9 -0
  107. package/translations/ms.json +9 -0
  108. package/translations/zh.json +9 -0
  109. package/tutorial/humanoid/README.md +22 -0
  110. package/tutorial/humanoid/chapters.mjs +247 -0
  111. package/tutorial/humanoid/encode.mjs +26 -0
  112. package/tutorial/humanoid/lib.mjs +197 -0
  113. package/tutorial/humanoid/record.mjs +65 -0
@@ -227,7 +227,7 @@ export declare function placeKeptOf(revision: FigureRevision, part: string, targ
227
227
  export declare function alignOf(asset: FigureAsset, target: {
228
228
  part: string;
229
229
  face: string;
230
- } | 'plane', how: Partial<Record<Axis, Alignment>>, mm: Partial<Record<Axis, number>>, inputs?: Partial<Record<Axis, string>>): Partial<Record<Axis, "max" | "min" | {
230
+ } | 'plane', how: Partial<Record<Axis, Alignment>>, mm: Partial<Record<Axis, number>>, inputs?: Partial<Record<Axis, string>>): Partial<Record<Axis, "min" | "max" | {
231
231
  mm: number | {
232
232
  from: {
233
233
  of: "instance";
@@ -5,3 +5,17 @@ import type { FigurePaletteKind } from './figure-parts.js';
5
5
  * point at y 0) where the provider's frame is centred; a portal's frame already rises from its bottom.
6
6
  */
7
7
  export declare function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimitiveCreation;
8
+ /**
9
+ * Whether a part added now is the figure's base, added fastened to its mounting plane: the figure has no part yet and
10
+ * stands on a plane -- the floor (also when not said), the ceiling or a wall. A centred figure has no plane to stand on.
11
+ */
12
+ export declare function firstPartFastensToPlane(drawn: readonly unknown[], placement: string | undefined): boolean;
13
+ /**
14
+ * Whether a drawn part still stands where the palette put it -- unturned, at one of the places a part is added at. Such a
15
+ * part was not placed by the author: fastening it has no place of theirs to keep (tutorial 2026-11-11, a mast added
16
+ * inside the body was fastened 550 mm deep in it).
17
+ */
18
+ export declare function standsWhereAdded(pose: {
19
+ r: number[][];
20
+ t: number[];
21
+ } | undefined): boolean;
@@ -65,4 +65,27 @@ export function paletteRecipe(kind, id) {
65
65
  };
66
66
  }
67
67
  }
68
+ /**
69
+ * Whether a part added now is the figure's base, added fastened to its mounting plane: the figure has no part yet and
70
+ * stands on a plane -- the floor (also when not said), the ceiling or a wall. A centred figure has no plane to stand on.
71
+ */
72
+ export function firstPartFastensToPlane(drawn, placement) {
73
+ return drawn.length === 0 && placement !== 'center';
74
+ }
75
+ const PALETTE_KINDS = ['box', 'cylinder', 'rounded-box', 'sphere', 'frustum', 'polygon', 'pyramid', 'pipe', 'plate', 'wall', 'hollow-box', 'portal'];
76
+ /**
77
+ * Whether a drawn part still stands where the palette put it -- unturned, at one of the places a part is added at. Such a
78
+ * part was not placed by the author: fastening it has no place of theirs to keep (tutorial 2026-11-11, a mast added
79
+ * inside the body was fastened 550 mm deep in it).
80
+ */
81
+ export function standsWhereAdded(pose) {
82
+ if (!pose)
83
+ return false;
84
+ const unturned = pose.r.every((row, i) => row.every((v, j) => Math.abs(v - (i === j ? 1 : 0)) < 1e-9));
85
+ return (unturned &&
86
+ PALETTE_KINDS.some(kind => {
87
+ const { x, y, z } = paletteRecipe(kind, 'p').position;
88
+ return [x, y, z].every((v, i) => Math.abs(v - pose.t[i]) < 1e-6);
89
+ }));
90
+ }
68
91
  //# sourceMappingURL=palette.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"palette.js","sourceRoot":"","sources":["../../client/modeller/palette.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigurePrimitiveCreation } from '@hatiolab/figure-model'\n\nimport type { FigurePaletteKind } from './figure-parts.js'\n\n/**\n * What a palette button makes: the part at a starting size a person then changes. Standing on the floor (its lowest\n * point at y 0) where the provider's frame is centred; a portal's frame already rises from its bottom.\n */\nexport function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimitiveCreation {\n const standing = (height: number) => ({ id, position: { x: 0, y: height / 2, z: 0 } })\n switch (kind) {\n case 'box':\n return { ...standing(100), kind, dimensions: { width: 100, height: 100, depth: 100 } }\n case 'cylinder':\n return { ...standing(100), kind, dimensions: { radius: 50, length: 100 } }\n case 'rounded-box':\n return { ...standing(100), kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }\n case 'sphere':\n return { ...standing(100), kind, dimensions: { radius: 50, radiusY: 50 } }\n case 'frustum':\n return { ...standing(100), kind, dimensions: { radiusTop: 0, radiusBottom: 50, height: 100 } }\n case 'polygon':\n return {\n ...standing(100),\n kind,\n dimensions: { height: 100 },\n points: [\n { x: -50, z: -43 },\n { x: 50, z: -43 },\n { x: 0, z: 43 }\n ]\n }\n case 'pyramid':\n /* A tetrahedron: the drawn triangle's, drawn up to one point over its middle. */\n return {\n ...standing(100),\n kind,\n dimensions: { height: 100 },\n points: [\n { x: -50, z: -43 },\n { x: 50, z: -43 },\n { x: 0, z: 43 }\n ],\n apex: { x: 0, z: 0 }\n }\n case 'pipe':\n /* An elbow: up, a quarter bend toward +X, along. Its section and pieces are changed in its own panel. */\n return {\n id,\n position: { x: 0, y: 0, z: 0 },\n kind: 'pipe',\n dimensions: { radius: 25, wall: 3 },\n path: [{ line: 200 }, { bend: { radius: 100, angle: 90, toward: 90 } }, { line: 200 }]\n }\n case 'plate':\n return { ...standing(10), kind: 'box', dimensions: { width: 600, height: 10, depth: 400 } }\n case 'wall':\n return { ...standing(2400), kind: 'box', dimensions: { width: 1000, height: 2400, depth: 100 } }\n case 'hollow-box':\n /* A tote at a tote's size: walls and floor that keep their thickness when it grows. */\n return { ...standing(300), kind: 'hollow-box', dimensions: { width: 600, height: 300, depth: 400, roundX: 20, roundZ: 20, wallX: 10, wallZ: 10, floor: 15 } }\n case 'portal':\n return {\n id,\n position: { x: 0, y: 0, z: 0 },\n kind: 'portal',\n /* A door frame at a door's size, as a wall is a wall's: its width, height, depth and the jambs' and head's thickness. */\n dimensions: { width: 1000, height: 2200, depth: 150, jamb: 100, head: 100 }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"palette.js","sourceRoot":"","sources":["../../client/modeller/palette.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FigurePrimitiveCreation } from '@hatiolab/figure-model'\n\nimport type { FigurePaletteKind } from './figure-parts.js'\n\n/**\n * What a palette button makes: the part at a starting size a person then changes. Standing on the floor (its lowest\n * point at y 0) where the provider's frame is centred; a portal's frame already rises from its bottom.\n */\nexport function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimitiveCreation {\n const standing = (height: number) => ({ id, position: { x: 0, y: height / 2, z: 0 } })\n switch (kind) {\n case 'box':\n return { ...standing(100), kind, dimensions: { width: 100, height: 100, depth: 100 } }\n case 'cylinder':\n return { ...standing(100), kind, dimensions: { radius: 50, length: 100 } }\n case 'rounded-box':\n return { ...standing(100), kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }\n case 'sphere':\n return { ...standing(100), kind, dimensions: { radius: 50, radiusY: 50 } }\n case 'frustum':\n return { ...standing(100), kind, dimensions: { radiusTop: 0, radiusBottom: 50, height: 100 } }\n case 'polygon':\n return {\n ...standing(100),\n kind,\n dimensions: { height: 100 },\n points: [\n { x: -50, z: -43 },\n { x: 50, z: -43 },\n { x: 0, z: 43 }\n ]\n }\n case 'pyramid':\n /* A tetrahedron: the drawn triangle's, drawn up to one point over its middle. */\n return {\n ...standing(100),\n kind,\n dimensions: { height: 100 },\n points: [\n { x: -50, z: -43 },\n { x: 50, z: -43 },\n { x: 0, z: 43 }\n ],\n apex: { x: 0, z: 0 }\n }\n case 'pipe':\n /* An elbow: up, a quarter bend toward +X, along. Its section and pieces are changed in its own panel. */\n return {\n id,\n position: { x: 0, y: 0, z: 0 },\n kind: 'pipe',\n dimensions: { radius: 25, wall: 3 },\n path: [{ line: 200 }, { bend: { radius: 100, angle: 90, toward: 90 } }, { line: 200 }]\n }\n case 'plate':\n return { ...standing(10), kind: 'box', dimensions: { width: 600, height: 10, depth: 400 } }\n case 'wall':\n return { ...standing(2400), kind: 'box', dimensions: { width: 1000, height: 2400, depth: 100 } }\n case 'hollow-box':\n /* A tote at a tote's size: walls and floor that keep their thickness when it grows. */\n return { ...standing(300), kind: 'hollow-box', dimensions: { width: 600, height: 300, depth: 400, roundX: 20, roundZ: 20, wallX: 10, wallZ: 10, floor: 15 } }\n case 'portal':\n return {\n id,\n position: { x: 0, y: 0, z: 0 },\n kind: 'portal',\n /* A door frame at a door's size, as a wall is a wall's: its width, height, depth and the jambs' and head's thickness. */\n dimensions: { width: 1000, height: 2200, depth: 150, jamb: 100, head: 100 }\n }\n }\n}\n\n/**\n * Whether a part added now is the figure's base, added fastened to its mounting plane: the figure has no part yet and\n * stands on a plane -- the floor (also when not said), the ceiling or a wall. A centred figure has no plane to stand on.\n */\nexport function firstPartFastensToPlane(drawn: readonly unknown[], placement: string | undefined): boolean {\n return drawn.length === 0 && placement !== 'center'\n}\n\nconst PALETTE_KINDS: FigurePaletteKind[] = ['box', 'cylinder', 'rounded-box', 'sphere', 'frustum', 'polygon', 'pyramid', 'pipe', 'plate', 'wall', 'hollow-box', 'portal']\n\n/**\n * Whether a drawn part still stands where the palette put it -- unturned, at one of the places a part is added at. Such a\n * part was not placed by the author: fastening it has no place of theirs to keep (tutorial 2026-11-11, a mast added\n * inside the body was fastened 550 mm deep in it).\n */\nexport function standsWhereAdded(pose: { r: number[][]; t: number[] } | undefined): boolean {\n if (!pose) return false\n const unturned = pose.r.every((row, i) => row.every((v, j) => Math.abs(v - (i === j ? 1 : 0)) < 1e-9))\n return (\n unturned &&\n PALETTE_KINDS.some(kind => {\n const { x, y, z } = paletteRecipe(kind, 'p').position\n return [x, y, z].every((v, i) => Math.abs(v - pose.t[i]!) < 1e-6)\n })\n )\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import type { Axis } from './links.js';
2
+ import type { FigureRevision } from './revision.js';
3
+ export type PivotPoint = 'top' | 'middle' | 'bottom';
4
+ export declare const PIVOT_POINTS: readonly PivotPoint[];
5
+ /**
6
+ * The `through` that puts a turn's axis through each named point of `part`, in the joint's frame: null when the part
7
+ * has no joint to another part, or its shape has no faces to measure. A distance along the turn's own axis (`along`)
8
+ * moves nothing and is left out: the shoulder's points read Y alone, not an X the person would wonder about.
9
+ */
10
+ export declare function pivotPointsOf(revision: FigureRevision, part: string, along?: Axis): Record<PivotPoint, Partial<Record<Axis, number>>> | null;
11
+ /** Which named point a `through` is, when it is one: the form marks the button it matches. */
12
+ export declare function pivotPointOf(points: Record<PivotPoint, Partial<Record<Axis, number>>> | null, through: Partial<Record<Axis, number>> | undefined, along?: Axis): PivotPoint | undefined;
@@ -0,0 +1,59 @@
1
+ import { compileFigureGraph } from '@hatiolab/figure-model';
2
+ export const PIVOT_POINTS = ['top', 'middle', 'bottom'];
3
+ /**
4
+ * The `through` that puts a turn's axis through each named point of `part`, in the joint's frame: null when the part
5
+ * has no joint to another part, or its shape has no faces to measure. A distance along the turn's own axis (`along`)
6
+ * moves nothing and is left out: the shoulder's points read Y alone, not an X the person would wonder about.
7
+ */
8
+ export function pivotPointsOf(revision, part, along) {
9
+ const joint = revision.asset.joints?.find(j => j.body1.part === part);
10
+ if (!joint || !('part' in joint.body0))
11
+ return null;
12
+ const faces = revision.geometry.find(g => g.id === part)?.shape?.faces;
13
+ const mid = (lo, hi) => (faces?.[lo] && faces?.[hi] ? (faces[lo].at + faces[hi].at) / 2 : undefined);
14
+ const [x, z] = [mid('left', 'right'), mid('back', 'front')];
15
+ if (x === undefined || z === undefined || !faces?.top || !faces?.bottom)
16
+ return null;
17
+ const pose = restingPoseOf(revision, part);
18
+ if (!pose)
19
+ return null;
20
+ /* The pose carries the part's own frame into the joint's: a point of the part, said in the joint's frame. */
21
+ const inJoint = (p) => {
22
+ const v = [0, 1, 2].map(i => pose.r[i][0] * p[0] + pose.r[i][1] * p[1] + pose.r[i][2] * p[2] + pose.t[i]);
23
+ const out = {};
24
+ ['x', 'y', 'z'].forEach((a, i) => {
25
+ const mm = Math.round(v[i] * 10) / 10;
26
+ if (a !== along && Math.abs(mm) >= 0.05)
27
+ out[a] = mm;
28
+ });
29
+ return out;
30
+ };
31
+ return {
32
+ top: inJoint([x, faces.top.at, z]),
33
+ middle: inJoint([x, (faces.top.at + faces.bottom.at) / 2, z]),
34
+ bottom: inJoint([x, faces.bottom.at, z])
35
+ };
36
+ }
37
+ /** The part's pose in its joint's frame with every control at its default: the joint at rest, not as the sliders left it. */
38
+ function restingPoseOf(revision, part) {
39
+ const m = revision.asset.document.model;
40
+ const node = m.nodes.find(n => n.id === `${part}.pose`);
41
+ if (!node)
42
+ return null;
43
+ try {
44
+ const values = compileFigureGraph(m).evaluate({ ...revision.inputs, ...(revision.asset.stateDefaults ?? {}) }).values;
45
+ const pose = values[Object.values(node.outputs)[0]];
46
+ return pose?.r && pose?.t ? pose : null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /** Which named point a `through` is, when it is one: the form marks the button it matches. */
53
+ export function pivotPointOf(points, through, along) {
54
+ if (!points)
55
+ return undefined;
56
+ const same = (a, b = {}) => ['x', 'y', 'z'].every(k => k === along || Math.abs((a[k] ?? 0) - (b[k] ?? 0)) < 0.5);
57
+ return PIVOT_POINTS.find(p => same(points[p], through));
58
+ }
59
+ //# sourceMappingURL=pivot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pivot.js","sourceRoot":"","sources":["../../client/modeller/pivot.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureGraph } from '@hatiolab/figure-model'\n\nimport type { Axis } from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * Where a turn's axis may be put by name: the part's top end, its middle, its bottom end. The motion form's 「축이\n * 지나는 점」 is measured from the joint's origin along the joint's own axes, which a person cannot see -- in the\n * humanoid lesson the shoulder was given Y 120 to lift the pivot \"to the shoulder\" when the origin already stood at the\n * arm's top end (attached level with the torso's top), and the arm turned about a point 120 mm above it (tutorial run\n * 2026-11-12). A named point is worked out here from the part as it rests, so the number is right whatever the face,\n * the alignment or the gap.\n */\n\nexport type PivotPoint = 'top' | 'middle' | 'bottom'\nexport const PIVOT_POINTS: readonly PivotPoint[] = ['top', 'middle', 'bottom']\n\ntype Pose = { r: number[][]; t: number[] }\ntype Faces = Partial<Record<'left' | 'right' | 'bottom' | 'top' | 'back' | 'front', { at: number }>>\n\n/**\n * The `through` that puts a turn's axis through each named point of `part`, in the joint's frame: null when the part\n * has no joint to another part, or its shape has no faces to measure. A distance along the turn's own axis (`along`)\n * moves nothing and is left out: the shoulder's points read Y alone, not an X the person would wonder about.\n */\nexport function pivotPointsOf(revision: FigureRevision, part: string, along?: Axis): Record<PivotPoint, Partial<Record<Axis, number>>> | null {\n const joint = revision.asset.joints?.find(j => j.body1.part === part)\n if (!joint || !('part' in joint.body0)) return null\n const faces = (revision.geometry.find(g => (g as { id: string }).id === part) as { shape?: { faces?: Faces } } | undefined)?.shape?.faces\n const mid = (lo: keyof Faces, hi: keyof Faces) => (faces?.[lo] && faces?.[hi] ? (faces[lo]!.at + faces[hi]!.at) / 2 : undefined)\n const [x, z] = [mid('left', 'right'), mid('back', 'front')]\n if (x === undefined || z === undefined || !faces?.top || !faces?.bottom) return null\n const pose = restingPoseOf(revision, part)\n if (!pose) return null\n /* The pose carries the part's own frame into the joint's: a point of the part, said in the joint's frame. */\n const inJoint = (p: number[]): Partial<Record<Axis, number>> => {\n const v = [0, 1, 2].map(i => pose.r[i]![0]! * p[0]! + pose.r[i]![1]! * p[1]! + pose.r[i]![2]! * p[2]! + pose.t[i]!)\n const out: Partial<Record<Axis, number>> = {}\n ;(['x', 'y', 'z'] as const).forEach((a, i) => {\n const mm = Math.round(v[i]! * 10) / 10\n if (a !== along && Math.abs(mm) >= 0.05) out[a] = mm\n })\n return out\n }\n return {\n top: inJoint([x, faces.top.at, z]),\n middle: inJoint([x, (faces.top.at + faces.bottom.at) / 2, z]),\n bottom: inJoint([x, faces.bottom.at, z])\n }\n}\n\n/** The part's pose in its joint's frame with every control at its default: the joint at rest, not as the sliders left it. */\nfunction restingPoseOf(revision: FigureRevision, part: string): Pose | null {\n const m = revision.asset.document.model as unknown as { nodes: { id: string; outputs: Record<string, string> }[] }\n const node = m.nodes.find(n => n.id === `${part}.pose`)\n if (!node) return null\n try {\n const values = compileFigureGraph(m as never).evaluate({ ...revision.inputs, ...(revision.asset.stateDefaults ?? {}) }).values as Record<string, unknown>\n const pose = values[Object.values(node.outputs)[0]!] as Pose | undefined\n return pose?.r && pose?.t ? pose : null\n } catch {\n return null\n }\n}\n\n/** Which named point a `through` is, when it is one: the form marks the button it matches. */\nexport function pivotPointOf(\n points: Record<PivotPoint, Partial<Record<Axis, number>>> | null,\n through: Partial<Record<Axis, number>> | undefined,\n along?: Axis\n): PivotPoint | undefined {\n if (!points) return undefined\n const same = (a: Partial<Record<Axis, number>>, b: Partial<Record<Axis, number>> = {}) =>\n (['x', 'y', 'z'] as const).every(k => k === along || Math.abs((a[k] ?? 0) - (b[k] ?? 0)) < 0.5)\n return PIVOT_POINTS.find(p => same(points[p], through))\n}\n"]}
@@ -203,8 +203,12 @@ export function createFigureProductSession(store) {
203
203
  detachAsCopy() {
204
204
  if (saving)
205
205
  throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.');
206
- if (!conflict)
207
- throw refusal('SESSION_NO_CONFLICT', '충돌한 저장이 없습니다.');
206
+ /*
207
+ After a conflict, or to release edits that break the released contract as a new figure type (the release gate's
208
+ way out, tutorial 2026-11-11): either way the figure it came from stays as the server has it.
209
+ */
210
+ if (!conflict && !id)
211
+ throw refusal('SESSION_NOT_SAVED', '저장된 피규어가 아닙니다.');
208
212
  id = undefined;
209
213
  baseline = undefined;
210
214
  creationId = crypto.randomUUID();
@@ -1 +1 @@
1
- {"version":3,"file":"product-session.js","sourceRoot":"","sources":["../../client/modeller/product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { thumbnailHashOf } from './thumbnail-hash.js'\nimport { revisionFromView, type FigureRevision } from './revision.js'\nimport {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\n figureSizeRangeFitsOf,\n type FigureSizeRangeFit,\n type FigureInputChange,\n type FigurePrimitiveCreation,\n type FigurePartEdit,\n type FigureAuthoringAction,\n type FigureProposalStep,\n type FigureClipConsent\n} from '@hatiolab/figure-model'\n\n/** Consent to let clips go, and the figure and version it was given on. */\n\n/**\n * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the\n * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).\n */\nfunction refusal(code: string, message: string): Error & { code: string } {\n return Object.assign(Error(message), { code })\n}\n\nexport interface FigureClipConsentFor extends FigureClipConsent {\n context: string\n revision: number\n}\n\nexport interface FigureProductRecord {\n id: string\n source: string\n}\n\n/**\n * The row's catalogue fields -- the type (before release), the category, the tags. Kept on the row, not in the asset,\n * and saved with it by 「저장」: written at once on change, they stayed after 「저장하지 않고 떠나기」 and the save\n * button never lit for them (2026-11-01 audit item 5).\n */\nexport interface FigureCatalogPatch {\n type?: string\n category?: string\n tags?: string[]\n}\n\n/** A catalogue field as the editor saw it before changing it; null when it was empty. */\nexport type FigureCatalogSeen = { [K in keyof FigureCatalogPatch]?: FigureCatalogPatch[K] | null }\n\n/** Another editor changed a catalogue field first: which, and what the row holds now. */\nexport interface FigureCatalogConflict {\n field: keyof FigureCatalogPatch\n now: string | string[] | null\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface FigureProductStore {\n load(id: string): Promise<FigureProductRecord>\n /** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */\n readBack?(id: string): Promise<FigureProductRecord & { thumbnailHash?: string; type?: string }>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n /** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */\n namespace?: string\n /** Keys of the AI proposals whose steps the source holds: the server records them with the save. */\n proposalKeys?: string[]\n }): Promise<FigureProductRecord>\n /**\n * Writes the row's catalogue fields of a saved figure, only while the row still holds them as `seen` says -- else it\n * is refused with FIGURE_CATALOG_CONFLICT and the fields the row holds now.\n */\n updateCatalog(id: string, patch: FigureCatalogPatch, seen: FigureCatalogSeen): Promise<unknown>\n}\n\nconst sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n/**\n * A catalogue value as the server compares it (figure-service catalogConflictsOf): an empty category or type, or an\n * empty tag list, is no value -- null; tags keep their order, which is part of the value.\n */\nconst catalogValue = (value: unknown) => (value === '' || (Array.isArray(value) && !value.length) ? null : (value ?? null))\nconst sameCatalogValue = (a: unknown, b: unknown) => sameValue(catalogValue(a), catalogValue(b))\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createFigureProductSession(store: FigureProductStore) {\n const workspace = createFigureEditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n /*\n The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the\n workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --\n undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.\n */\n let done: Array<string | undefined> = [],\n undone: Array<string | undefined> = []\n /*\n A save refused because the server holds another source than the one opened (FIGURE_SOURCE_CONFLICT, not one of\n this session's own sends): the edits stay, and the server's latest is kept here to compare with and, if the person\n says so, to write over (2026-11-01 audit item 6). Undefined source: the latest could not be read.\n */\n let conflict: { source?: string } | undefined\n /* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */\n let catalog: FigureCatalogPatch = {}\n /* The catalogue fields a save is writing now: what the row will hold, not what it held, until it is done. */\n let catalogSending: FigureCatalogPatch | undefined\n /*\n The catalogue fields the last save wrote, known to be on the row from the moment it answered: a screen compares\n with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.\n */\n let catalogWritten: FigureCatalogPatch = {}\n /*\n What the row held for each field held in `catalog`, as the editor saw it when it first changed it: sent with the\n save, which goes on only while the row still holds it (outside review 2026-11-09: a later tab wrote over another's\n category without a word). After a conflict it is what the row holds then, so saving again writes over it knowingly.\n */\n let catalogSeen: FigureCatalogSeen = {}\n const catalogHeld = () => Object.keys(catalog).length > 0\n /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */\n let fitted: FigureSizeRangeFit[] = []\n function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {\n const was = workspace.read()\n const result = run()\n const now = workspace.read()\n if (now.revision === was.revision) return result\n fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []\n if (kind === 'do') {\n done.push(tag)\n undone = []\n } else if (kind === 'undo') undone.push(done.pop())\n else done.push(undone.pop())\n return result\n }\n const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]\n /*\n The creation id is read with the rest: a figure not saved yet is named by it (ADR-0061 decision 3) -- its AI\n conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.\n */\n function read() {\n const view = workspace.read()\n return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, catalogWritten: { ...catalogWritten }, conflict: conflict ? { latest: conflict.source } : undefined, id, creationId, loading, saving, error }\n }\n /*\n The version on screen, one object for as long as it is the version: the workspace hands out one asset per version\n (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.\n */\n let shown: FigureRevision | undefined\n function revisionNow(): FigureRevision | undefined {\n const view = workspace.read()\n if (!view.context || view.revision === null) return (shown = undefined)\n const context = view.context\n if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)\n return shown\n }\n /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */\n const unconfirmed = new Map<string, { thumbnail?: string }>()\n /*\n Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under\n it takes it; an edit on another figure or another version is refused rather than let clips go that were never named\n for it (designer 2026-09-27).\n */\n let consent: FigureClipConsentFor | undefined\n const consented = () => {\n if (!consent) return {}\n const given = consent\n consent = undefined\n const view = workspace.read()\n if (view.context !== given.context || view.revision !== given.revision) {\n const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })\n throw stale\n }\n return { dropClips: given.dropClips }\n }\n /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */\n function readable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function writable() {\n const open = readable()\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n return open\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n catalog = {}\n catalogWritten = {}\n catalogSeen = {}\n conflict = undefined\n done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if ((workspace.read().dirty || catalogHeld()) && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */\n revisionNow,\n /**\n * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`\n * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.\n */\n confirmedSource: () => (id ? baseline : undefined),\n /**\n * A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no\n * change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save\n * is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save\n * is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;\n * the save done lets go only of a value it wrote (2026-11-01 second re-check).\n */\n editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch) {\n if (!id) throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.')\n const next = { ...catalog }\n const seen = { ...catalogSeen }\n for (const key of Object.keys(patch) as (keyof FigureCatalogPatch)[]) {\n if (!(catalogSending && key in catalogSending) && sameCatalogValue(patch[key], saved[key])) {\n delete next[key]\n delete seen[key]\n } else {\n (next as Record<string, unknown>)[key] = patch[key]\n if (!(key in seen)) (seen as Record<string, unknown>)[key] = catalogValue(saved[key])\n }\n }\n catalog = next\n catalogSeen = seen\n return read()\n },\n /**\n * After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes\n * through only while the server still holds it -- a third change meanwhile is a conflict again.\n */\n overwriteConflict() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict?.source) throw refusal('SESSION_NO_CONFLICT', '덮어쓸 최신본을 읽지 못했습니다.')\n baseline = conflict.source\n conflict = undefined\n unconfirmed.clear()\n return read()\n },\n /**\n * After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation\n * id, saved by the first save's question; the figure they came from is left as the server holds it.\n */\n detachAsCopy() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict) throw refusal('SESSION_NO_CONFLICT', '충돌한 저장이 없습니다.')\n id = undefined\n baseline = undefined\n creationId = crypto.randomUUID()\n conflict = undefined\n /* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */\n const { type: _type, ...kept } = catalog\n catalog = kept\n /* A copy has no row yet: nothing it could have been changed on. */\n catalogSeen = {}\n unconfirmed.clear()\n return read()\n },\n /** A new figure, named in the screen's language by the page: the session has no words of its own. */\n startNew({ discard = false, name }: { discard?: boolean; name: string }) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset(name)))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return tracked('do', () =>\n workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n )\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {\n writable()\n /* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */\n return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)\n },\n /**\n * A new figure opened for an AI proposal from another screen is created under the proposal's key (ADR-0097 §5.6\n * item 5). Only before the modeller names the figure to the AI dock: once a conversation is anchored on the creation\n * id it stays (ADR-0061 decision 3), and the save's record of the proposal keeps one proposal one figure. Only on a\n * new figure not yet touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')\n creationId = key\n },\n /** The AI proposals whose steps are in the model now, each once. */\n proposalsInEffect: inEffect,\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly FigureProposalStep[]) {\n const at = writable()\n return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))\n },\n add(part: FigurePrimitiveCreation) {\n return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))\n },\n edit(edit: FigurePartEdit) {\n return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))\n },\n /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */\n previewCommand(action: FigureAuthoringAction) {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return workspace.previewCommand({ context: view.context, action })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: FigureAuthoringAction) {\n return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')\n },\n /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */\n takeSizeRangeFits(): FigureSizeRangeFit[] {\n const out = fitted\n fitted = []\n return out\n },\n /**\n * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and\n * the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that\n * would stop a clip is refused and asked about.\n */\n withConsent<T>(given: FigureClipConsentFor, run: () => T): T {\n consent = given\n try {\n return run()\n } finally {\n consent = undefined\n }\n },\n undo() {\n return tracked('undo', () => workspace.undo(writable()))\n },\n redo() {\n return tracked('redo', () => workspace.redo(writable()))\n },\n source() {\n const { context } = readable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `thumbnail.ts`).\n */\n async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n /* Read with the text it goes with: an edit while saving is not what this save holds. */\n const proposalKeys = inEffect()\n const sentCatalog = { ...catalog }\n saving = true\n catalogSending = sentCatalog\n catalogWritten = {}\n try {\n /* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */\n if (id === undefined || workspace.read().dirty) await saveSource()\n if (requestEpoch !== epoch) return read()\n if (id !== undefined && Object.keys(sentCatalog).length) {\n const seen = Object.fromEntries(Object.keys(sentCatalog).map(key => [key, catalogSeen[key as keyof FigureCatalogPatch] ?? null])) as FigureCatalogSeen\n try {\n await store.updateCatalog(id, sentCatalog, seen)\n } catch (failure) {\n /* The fields stay held, as typed; what the row holds now is what the next save expects -- it writes over it. */\n const fields = (failure as { extensions?: { fields?: FigureCatalogConflict[] } })?.extensions?.fields\n if ((failure as { code?: string })?.code === 'FIGURE_CATALOG_CONFLICT' && requestEpoch === epoch && fields)\n for (const { field, now } of fields) if (field in catalog) (catalogSeen as Record<string, unknown>)[field] = now\n throw failure\n }\n if (requestEpoch !== epoch) return read()\n catalogWritten = sentCatalog\n /* The row holds what was sent: a field changed again on the way is next compared with that. */\n for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[])\n if (sameValue(catalog[key], sentCatalog[key])) delete catalogSeen[key]\n else (catalogSeen as Record<string, unknown>)[key] = catalogValue(sentCatalog[key])\n /* What was sent is saved; a field changed again while it was on its way stays held. */\n const next = { ...catalog }\n for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[]) if (sameValue(next[key], sentCatalog[key])) delete next[key]\n catalog = next\n }\n } finally {\n if (requestEpoch === epoch) saving = false\n catalogSending = undefined\n }\n return read()\n\n async function saveSource() {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } catch (error) {\n /*\n A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second\n try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only\n those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row\n holds. Read back, every field the save writes must be what was sent: the source, the picture, the type\n (designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source\n alone say it was).\n */\n const code = (error as { code?: string })?.code\n const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'\n const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined\n const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined\n /* The picture is compared by the name the server keeps it under: the hash of its bytes (ADR-0104). */\n const holds = async (sent?: string) => sent === undefined || (!!landed && landed.thumbnailHash !== undefined && landed.thumbnailHash === (await thumbnailHashOf(sent)))\n const same = !!landed && held === exported.text && (await holds(thumbnail)) && (type === undefined || landed.type === type)\n if (same) record = landed!\n else if (\n code === 'FIGURE_SOURCE_CONFLICT' &&\n landed &&\n held !== undefined &&\n unconfirmed.has(held) &&\n /* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */\n (await holds(unconfirmed.get(held)!.thumbnail))\n ) {\n /*\n The row holds a text this session sent and never heard back about -- edited further since, the new text\n is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that\n server text as the expected source (designer 2026-09-27: \"the sent content is confirmed on the server\").\n */\n baseline = landed.source\n unconfirmed.clear()\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\n if (code === 'FIGURE_SOURCE_CONFLICT' && requestEpoch === epoch) conflict = { source: landed?.source }\n throw error\n }\n }\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n /* The server has it: the next save expects it, whatever happened on screen meanwhile. */\n id = record.id\n baseline = record.source\n unconfirmed.clear()\n conflict = undefined\n /*\n Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays\n unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.\n */\n if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)\n }\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}
1
+ {"version":3,"file":"product-session.js","sourceRoot":"","sources":["../../client/modeller/product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { thumbnailHashOf } from './thumbnail-hash.js'\nimport { revisionFromView, type FigureRevision } from './revision.js'\nimport {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\n figureSizeRangeFitsOf,\n type FigureSizeRangeFit,\n type FigureInputChange,\n type FigurePrimitiveCreation,\n type FigurePartEdit,\n type FigureAuthoringAction,\n type FigureProposalStep,\n type FigureClipConsent\n} from '@hatiolab/figure-model'\n\n/** Consent to let clips go, and the figure and version it was given on. */\n\n/**\n * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the\n * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).\n */\nfunction refusal(code: string, message: string): Error & { code: string } {\n return Object.assign(Error(message), { code })\n}\n\nexport interface FigureClipConsentFor extends FigureClipConsent {\n context: string\n revision: number\n}\n\nexport interface FigureProductRecord {\n id: string\n source: string\n}\n\n/**\n * The row's catalogue fields -- the type (before release), the category, the tags. Kept on the row, not in the asset,\n * and saved with it by 「저장」: written at once on change, they stayed after 「저장하지 않고 떠나기」 and the save\n * button never lit for them (2026-11-01 audit item 5).\n */\nexport interface FigureCatalogPatch {\n type?: string\n category?: string\n tags?: string[]\n}\n\n/** A catalogue field as the editor saw it before changing it; null when it was empty. */\nexport type FigureCatalogSeen = { [K in keyof FigureCatalogPatch]?: FigureCatalogPatch[K] | null }\n\n/** Another editor changed a catalogue field first: which, and what the row holds now. */\nexport interface FigureCatalogConflict {\n field: keyof FigureCatalogPatch\n now: string | string[] | null\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface FigureProductStore {\n load(id: string): Promise<FigureProductRecord>\n /** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */\n readBack?(id: string): Promise<FigureProductRecord & { thumbnailHash?: string; type?: string }>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n /** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */\n namespace?: string\n /** Keys of the AI proposals whose steps the source holds: the server records them with the save. */\n proposalKeys?: string[]\n }): Promise<FigureProductRecord>\n /**\n * Writes the row's catalogue fields of a saved figure, only while the row still holds them as `seen` says -- else it\n * is refused with FIGURE_CATALOG_CONFLICT and the fields the row holds now.\n */\n updateCatalog(id: string, patch: FigureCatalogPatch, seen: FigureCatalogSeen): Promise<unknown>\n}\n\nconst sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n/**\n * A catalogue value as the server compares it (figure-service catalogConflictsOf): an empty category or type, or an\n * empty tag list, is no value -- null; tags keep their order, which is part of the value.\n */\nconst catalogValue = (value: unknown) => (value === '' || (Array.isArray(value) && !value.length) ? null : (value ?? null))\nconst sameCatalogValue = (a: unknown, b: unknown) => sameValue(catalogValue(a), catalogValue(b))\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createFigureProductSession(store: FigureProductStore) {\n const workspace = createFigureEditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n /*\n The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the\n workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --\n undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.\n */\n let done: Array<string | undefined> = [],\n undone: Array<string | undefined> = []\n /*\n A save refused because the server holds another source than the one opened (FIGURE_SOURCE_CONFLICT, not one of\n this session's own sends): the edits stay, and the server's latest is kept here to compare with and, if the person\n says so, to write over (2026-11-01 audit item 6). Undefined source: the latest could not be read.\n */\n let conflict: { source?: string } | undefined\n /* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */\n let catalog: FigureCatalogPatch = {}\n /* The catalogue fields a save is writing now: what the row will hold, not what it held, until it is done. */\n let catalogSending: FigureCatalogPatch | undefined\n /*\n The catalogue fields the last save wrote, known to be on the row from the moment it answered: a screen compares\n with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.\n */\n let catalogWritten: FigureCatalogPatch = {}\n /*\n What the row held for each field held in `catalog`, as the editor saw it when it first changed it: sent with the\n save, which goes on only while the row still holds it (outside review 2026-11-09: a later tab wrote over another's\n category without a word). After a conflict it is what the row holds then, so saving again writes over it knowingly.\n */\n let catalogSeen: FigureCatalogSeen = {}\n const catalogHeld = () => Object.keys(catalog).length > 0\n /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */\n let fitted: FigureSizeRangeFit[] = []\n function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {\n const was = workspace.read()\n const result = run()\n const now = workspace.read()\n if (now.revision === was.revision) return result\n fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []\n if (kind === 'do') {\n done.push(tag)\n undone = []\n } else if (kind === 'undo') undone.push(done.pop())\n else done.push(undone.pop())\n return result\n }\n const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]\n /*\n The creation id is read with the rest: a figure not saved yet is named by it (ADR-0061 decision 3) -- its AI\n conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.\n */\n function read() {\n const view = workspace.read()\n return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, catalogWritten: { ...catalogWritten }, conflict: conflict ? { latest: conflict.source } : undefined, id, creationId, loading, saving, error }\n }\n /*\n The version on screen, one object for as long as it is the version: the workspace hands out one asset per version\n (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.\n */\n let shown: FigureRevision | undefined\n function revisionNow(): FigureRevision | undefined {\n const view = workspace.read()\n if (!view.context || view.revision === null) return (shown = undefined)\n const context = view.context\n if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)\n return shown\n }\n /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */\n const unconfirmed = new Map<string, { thumbnail?: string }>()\n /*\n Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under\n it takes it; an edit on another figure or another version is refused rather than let clips go that were never named\n for it (designer 2026-09-27).\n */\n let consent: FigureClipConsentFor | undefined\n const consented = () => {\n if (!consent) return {}\n const given = consent\n consent = undefined\n const view = workspace.read()\n if (view.context !== given.context || view.revision !== given.revision) {\n const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })\n throw stale\n }\n return { dropClips: given.dropClips }\n }\n /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */\n function readable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function writable() {\n const open = readable()\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n return open\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n catalog = {}\n catalogWritten = {}\n catalogSeen = {}\n conflict = undefined\n done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if ((workspace.read().dirty || catalogHeld()) && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */\n revisionNow,\n /**\n * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`\n * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.\n */\n confirmedSource: () => (id ? baseline : undefined),\n /**\n * A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no\n * change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save\n * is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save\n * is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;\n * the save done lets go only of a value it wrote (2026-11-01 second re-check).\n */\n editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch) {\n if (!id) throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.')\n const next = { ...catalog }\n const seen = { ...catalogSeen }\n for (const key of Object.keys(patch) as (keyof FigureCatalogPatch)[]) {\n if (!(catalogSending && key in catalogSending) && sameCatalogValue(patch[key], saved[key])) {\n delete next[key]\n delete seen[key]\n } else {\n (next as Record<string, unknown>)[key] = patch[key]\n if (!(key in seen)) (seen as Record<string, unknown>)[key] = catalogValue(saved[key])\n }\n }\n catalog = next\n catalogSeen = seen\n return read()\n },\n /**\n * After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes\n * through only while the server still holds it -- a third change meanwhile is a conflict again.\n */\n overwriteConflict() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict?.source) throw refusal('SESSION_NO_CONFLICT', '덮어쓸 최신본을 읽지 못했습니다.')\n baseline = conflict.source\n conflict = undefined\n unconfirmed.clear()\n return read()\n },\n /**\n * After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation\n * id, saved by the first save's question; the figure they came from is left as the server holds it.\n */\n detachAsCopy() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n /*\n After a conflict, or to release edits that break the released contract as a new figure type (the release gate's\n way out, tutorial 2026-11-11): either way the figure it came from stays as the server has it.\n */\n if (!conflict && !id) throw refusal('SESSION_NOT_SAVED', '저장된 피규어가 아닙니다.')\n id = undefined\n baseline = undefined\n creationId = crypto.randomUUID()\n conflict = undefined\n /* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */\n const { type: _type, ...kept } = catalog\n catalog = kept\n /* A copy has no row yet: nothing it could have been changed on. */\n catalogSeen = {}\n unconfirmed.clear()\n return read()\n },\n /** A new figure, named in the screen's language by the page: the session has no words of its own. */\n startNew({ discard = false, name }: { discard?: boolean; name: string }) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset(name)))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return tracked('do', () =>\n workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n )\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {\n writable()\n /* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */\n return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)\n },\n /**\n * A new figure opened for an AI proposal from another screen is created under the proposal's key (ADR-0097 §5.6\n * item 5). Only before the modeller names the figure to the AI dock: once a conversation is anchored on the creation\n * id it stays (ADR-0061 decision 3), and the save's record of the proposal keeps one proposal one figure. Only on a\n * new figure not yet touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')\n creationId = key\n },\n /** The AI proposals whose steps are in the model now, each once. */\n proposalsInEffect: inEffect,\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly FigureProposalStep[]) {\n const at = writable()\n return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))\n },\n add(part: FigurePrimitiveCreation) {\n return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))\n },\n edit(edit: FigurePartEdit) {\n return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))\n },\n /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */\n previewCommand(action: FigureAuthoringAction) {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return workspace.previewCommand({ context: view.context, action })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: FigureAuthoringAction) {\n return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')\n },\n /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */\n takeSizeRangeFits(): FigureSizeRangeFit[] {\n const out = fitted\n fitted = []\n return out\n },\n /**\n * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and\n * the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that\n * would stop a clip is refused and asked about.\n */\n withConsent<T>(given: FigureClipConsentFor, run: () => T): T {\n consent = given\n try {\n return run()\n } finally {\n consent = undefined\n }\n },\n undo() {\n return tracked('undo', () => workspace.undo(writable()))\n },\n redo() {\n return tracked('redo', () => workspace.redo(writable()))\n },\n source() {\n const { context } = readable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `thumbnail.ts`).\n */\n async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n /* Read with the text it goes with: an edit while saving is not what this save holds. */\n const proposalKeys = inEffect()\n const sentCatalog = { ...catalog }\n saving = true\n catalogSending = sentCatalog\n catalogWritten = {}\n try {\n /* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */\n if (id === undefined || workspace.read().dirty) await saveSource()\n if (requestEpoch !== epoch) return read()\n if (id !== undefined && Object.keys(sentCatalog).length) {\n const seen = Object.fromEntries(Object.keys(sentCatalog).map(key => [key, catalogSeen[key as keyof FigureCatalogPatch] ?? null])) as FigureCatalogSeen\n try {\n await store.updateCatalog(id, sentCatalog, seen)\n } catch (failure) {\n /* The fields stay held, as typed; what the row holds now is what the next save expects -- it writes over it. */\n const fields = (failure as { extensions?: { fields?: FigureCatalogConflict[] } })?.extensions?.fields\n if ((failure as { code?: string })?.code === 'FIGURE_CATALOG_CONFLICT' && requestEpoch === epoch && fields)\n for (const { field, now } of fields) if (field in catalog) (catalogSeen as Record<string, unknown>)[field] = now\n throw failure\n }\n if (requestEpoch !== epoch) return read()\n catalogWritten = sentCatalog\n /* The row holds what was sent: a field changed again on the way is next compared with that. */\n for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[])\n if (sameValue(catalog[key], sentCatalog[key])) delete catalogSeen[key]\n else (catalogSeen as Record<string, unknown>)[key] = catalogValue(sentCatalog[key])\n /* What was sent is saved; a field changed again while it was on its way stays held. */\n const next = { ...catalog }\n for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[]) if (sameValue(next[key], sentCatalog[key])) delete next[key]\n catalog = next\n }\n } finally {\n if (requestEpoch === epoch) saving = false\n catalogSending = undefined\n }\n return read()\n\n async function saveSource() {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } catch (error) {\n /*\n A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second\n try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only\n those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row\n holds. Read back, every field the save writes must be what was sent: the source, the picture, the type\n (designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source\n alone say it was).\n */\n const code = (error as { code?: string })?.code\n const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'\n const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined\n const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined\n /* The picture is compared by the name the server keeps it under: the hash of its bytes (ADR-0104). */\n const holds = async (sent?: string) => sent === undefined || (!!landed && landed.thumbnailHash !== undefined && landed.thumbnailHash === (await thumbnailHashOf(sent)))\n const same = !!landed && held === exported.text && (await holds(thumbnail)) && (type === undefined || landed.type === type)\n if (same) record = landed!\n else if (\n code === 'FIGURE_SOURCE_CONFLICT' &&\n landed &&\n held !== undefined &&\n unconfirmed.has(held) &&\n /* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */\n (await holds(unconfirmed.get(held)!.thumbnail))\n ) {\n /*\n The row holds a text this session sent and never heard back about -- edited further since, the new text\n is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that\n server text as the expected source (designer 2026-09-27: \"the sent content is confirmed on the server\").\n */\n baseline = landed.source\n unconfirmed.clear()\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\n if (code === 'FIGURE_SOURCE_CONFLICT' && requestEpoch === epoch) conflict = { source: landed?.source }\n throw error\n }\n }\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n /* The server has it: the next save expects it, whatever happened on screen meanwhile. */\n id = record.id\n baseline = record.source\n unconfirmed.clear()\n conflict = undefined\n /*\n Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays\n unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.\n */\n if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)\n }\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}
@@ -6,6 +6,12 @@ export type SizeSource = Extract<FigureDimensionSource, {
6
6
  of: 'instance' | 'part';
7
7
  }>;
8
8
  export declare const measureFactor: (asset: FigureAsset, part: string, axis: Axis) => number;
9
+ /**
10
+ * A size row's name: the axis, and 「지름」 for a measure set by its radius. A round body's one diameter across sets the
11
+ * axis it has no row of its own for as well, and says so -- a sphere read 「X · 지름」 and 「Y · 지름」, and a person
12
+ * looked for its Z (tutorial run 2026-11-12); now 「X·Z · 지름」 across and 「Y · 지름」 up.
13
+ */
14
+ export declare function sizeAxisWord(asset: FigureAsset, part: string, axis: Axis, diameter: string): string;
9
15
  /** Only expose direct own inputs (including the canonical identity wrapper). Never overwrite an arbitrary expression. */
10
16
  export declare function sizeInputOf(asset: FigureAsset, part: string, axis: Axis): string | undefined;
11
17
  export declare function sizeRelationOf(revision: FigureRevision, part: string, axis: Axis): {
@@ -6,6 +6,20 @@ function editOf(asset, part, axis) {
6
6
  return shape && edit && !('refused' in edit) ? { shape, edit } : undefined;
7
7
  }
8
8
  export const measureFactor = (asset, part, axis) => editOf(asset, part, axis)?.edit.k ?? 1;
9
+ /**
10
+ * A size row's name: the axis, and 「지름」 for a measure set by its radius. A round body's one diameter across sets the
11
+ * axis it has no row of its own for as well, and says so -- a sphere read 「X · 지름」 and 「Y · 지름」, and a person
12
+ * looked for its Z (tutorial run 2026-11-12); now 「X·Z · 지름」 across and 「Y · 지름」 up.
13
+ */
14
+ export function sizeAxisWord(asset, part, axis, diameter) {
15
+ if (measureFactor(asset, part, axis) !== 2)
16
+ return axis.toUpperCase();
17
+ const axes = followableAxes(asset, part);
18
+ const across = ['x', 'y', 'z'].filter(a => !axes.includes(a));
19
+ const first = axes.find(a => measureFactor(asset, part, a) === 2);
20
+ const named = first === axis && across.length ? [axis, ...across].map(a => a.toUpperCase()).join('·') : axis.toUpperCase();
21
+ return `${named} · ${diameter}`;
22
+ }
9
23
  /** Only expose direct own inputs (including the canonical identity wrapper). Never overwrite an arbitrary expression. */
10
24
  export function sizeInputOf(asset, part, axis) {
11
25
  const e = editOf(asset, part, axis);
@@ -1 +1 @@
1
- {"version":3,"file":"size-response.js","sourceRoot":"","sources":["../../client/modeller/size-response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAGtG,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,UAAU,EAAa,MAAM,YAAY,CAAA;AAK1F,SAAS,MAAM,CAAC,KAAkB,EAAE,IAAY,EAAE,IAAU;IAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,QAAQ,CAAC,CAAA;IAC5E,MAAM,IAAI,GAAG,KAAK,IAAI,qBAAqB,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9D,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAC5E,CAAC;AACD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAkB,EAAE,IAAY,EAAE,IAAU,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAA;AAErH,yHAAyH;AACzH,MAAM,UAAU,WAAW,CAAC,KAAkB,EAAE,IAAY,EAAE,IAAU;IACtE,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IACnC,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAA;IACxB,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAA;IAClC,IAAI,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC;YAAE,OAAO,GAAG,CAAA;QAC5H,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,GAAI,CAAC,CAAC,CAAA;QAC7E,IAAI,MAAM,EAAE,EAAE,KAAK,OAAO,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC;YAAE,OAAO,SAAS,CAAA;QAC/G,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACtB,CAAC;AACH,CAAC;AACD,MAAM,UAAU,cAAc,CAAC,QAAwB,EAAE,IAAY,EAAE,IAAU;IAC/E,MAAM,CAAC,GAAG,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IAC9C,IAAI,CAAC,CAAC,EAAE,KAAK,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;IAC7D,IAAI,CAAC,CAAC,EAAE,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,KAAK,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;IACrF,MAAM,MAAM,GAAe,CAAC,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAC9H,MAAM,WAAW,GAAG,CAAC,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IACpJ,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAA;IAC/G,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AACzC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAkB,EAAE,IAAY,EAAE,IAAU;IAC9E,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACxD,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAA;IAC5B,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAA;IACzB,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;IAChD,OAAO,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACxG,CAAC;AAED,oFAAoF;AACpF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAyB,EAAE,CAAyB,EAAE,EAAE,CACrF,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;AAEjH,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,QAAwB,EAAE,MAAkB;IAC1E,IAAI,MAAM,CAAC,EAAE,KAAK,UAAU;QAAE,OAAO,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,mBAAmB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3I,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;AACvD,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,iBAAiB,CAAC,QAAwB,EAAE,IAAY,EAAE,IAAU,EAAE,MAAkB,EAAE,IAAiC,EAAE,EAAE,GAAG,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC;IAChL,IAAI,CAAC,CAAC,OAAO,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAAE,MAAM,KAAK,CAAC,YAAY,CAAC,CAAA;IACzF,IAAI,MAAM,CAAC,EAAE,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI;QAAE,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;IAC5E,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAA;IAC5B,MAAM,KAAK,GAA4B,EAAE,CAAA;IACzC,IAAI,GAAuB,CAAA;IAC3B,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,IAAI,MAAM,CAAC,EAAE,KAAK,UAAU,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAA;QAC7G,IAAI,QAAQ;YAAE,GAAG,GAAG,KAAK,CAAC,YAAY,CAAC,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;aACxD,CAAC;YACJ,2FAA2F;YAC3F,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAAE,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;YACjG,MAAM,EAAE,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAA;YACrC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,CAAC,CAAA;YACjD,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACvB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;QACzF,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;QACtD,YAAY,GAAG,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/D,CAAC;IACD,IAAI,CAAC,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;IAC7F,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IAClD,MAAM,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,YAAY,GAAG,SAAS,CAAA;IAChF,MAAM,IAAI,GAAG,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,GAAG,YAAY,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9E,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3G,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["import { figureAxisPairsOf, figurePartsExtentOf, figureShapeProviderOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureAuthoringAction, FigureDimensionSource } from '@hatiolab/figure-model'\nimport type { FigureRevision } from './revision.js'\nimport { dimensionNow, followOf, followableAxes, measureNow, type Axis } from './links.js'\n\nexport type SizeMode = 'ratio' | 'difference' | 'custom'\nexport type SizeSource = Extract<FigureDimensionSource, { of: 'instance' | 'part' }>\n\nfunction editOf(asset: FigureAsset, part: string, axis: Axis) {\n const shape = asset.document.model.nodes.find(n => n.id === `${part}.shape`)\n const edit = shape && figureShapeProviderOf(shape)?.edit[axis]\n return shape && edit && !('refused' in edit) ? { shape, edit } : undefined\n}\nexport const measureFactor = (asset: FigureAsset, part: string, axis: Axis) => editOf(asset, part, axis)?.edit.k ?? 1\n\n/** Only expose direct own inputs (including the canonical identity wrapper). Never overwrite an arbitrary expression. */\nexport function sizeInputOf(asset: FigureAsset, part: string, axis: Axis): string | undefined {\n const e = editOf(asset, part, axis)\n if (!e) return undefined\n const model = asset.document.model\n let ref = e.shape.args[e.edit.arg]\n const seen = new Set<string>()\n while (ref && !seen.has(ref)) {\n seen.add(ref)\n if (model.inputs.some(i => i.id === ref && i.role === 'design' && i.unit === 'mm') && ref.startsWith(`${part}.`)) return ref\n const writer = model.nodes.find(n => Object.values(n.outputs).includes(ref!))\n if (writer?.op !== 'mul@1' || model.constants.find(c => c.id === writer.args[1])?.value !== 1) return undefined\n ref = writer.args[0]\n }\n}\nexport function sizeRelationOf(revision: FigureRevision, part: string, axis: Axis): { source?: SizeSource; mode: SizeMode; variable: boolean } {\n const f = followOf(revision.asset, part, axis)\n if (f.of === 'own') return { mode: 'ratio', variable: false }\n if (f.of !== 'instance' && f.of !== 'part') return { mode: 'custom', variable: true }\n const source: SizeSource = f.of === 'instance' ? { of: 'instance', axis: f.axis } : { of: 'part', part: f.part, axis: f.axis }\n const equalFactor = (source.of === 'part' ? measureFactor(revision.asset, source.part, source.axis) : 1) / measureFactor(revision.asset, part, axis)\n const mode = !(f.plus ?? 0) ? 'ratio' : Math.abs((f.times ?? 1) - equalFactor) < 1e-6 ? 'difference' : 'custom'\n return { source, mode, variable: true }\n}\n\n/**\n * What a measure follows when it is first made to follow (ADR-0159 3): what the part hangs from, along the axis its own\n * axis lies on as it stands -- a roller laid on its side follows the frame's depth with its length, not the frame's\n * height (the axes paired by figure-model, `figureAxisPairsOf`, never by their letters). On the mounting plane, or\n * fastened to nothing, the figure's size. Nothing when it lies at an angle, or the parent has no measure along that\n * axis: the author picks. Only a suggestion when a relation is made; a measure that already follows something keeps it.\n */\nexport function suggestedSizeSource(asset: FigureAsset, part: string, axis: Axis): SizeSource | undefined {\n const { parent, along } = figureAxisPairsOf(asset, part)\n const on = along[axis]?.axis\n if (!on) return undefined\n if (!parent) return { of: 'instance', axis: on }\n return followableAxes(asset, parent).includes(on) ? { of: 'part', part: parent, axis: on } : undefined\n}\n\n/** Whether two sources are one: the same part, or the figure, and the same axis. */\nexport const sameSizeSource = (a: SizeSource | undefined, b: SizeSource | undefined) =>\n !!a && !!b && a.of === b.of && a.axis === b.axis && (a.of !== 'part' || (b.of === 'part' && a.part === b.part))\n\n/** How long the source is now, in millimetres as the panel shows it (a radius read as a diameter). */\nexport function sourceMeasureOf(revision: FigureRevision, source: SizeSource): number | undefined {\n if (source.of === 'instance') return revision.asset.designInputs[`size.${source.axis}`] ?? figurePartsExtentOf(revision.asset)[source.axis]\n return measureNow(revision, source.part, source.axis)\n}\n\n/** Preserve the displayed size when choosing a source or mode. mm edits change only that row's relation. */\nexport function sizeRelationSteps(revision: FigureRevision, part: string, axis: Axis, source: SizeSource, mode: Exclude<SizeMode, 'custom'>, mm = measureNow(revision, part, axis)): FigureAuthoringAction[] {\n if (!(typeof mm === 'number' && Number.isFinite(mm) && mm > 0)) throw Error('SIZE_VALUE')\n if (source.of === 'part' && source.part === part) throw Error('SIZE_SOURCE')\n const asset = revision.asset\n const steps: FigureAuthoringAction[] = []\n let raw: number | undefined\n let sourceFactor = 1\n if (source.of === 'instance') {\n const declared = asset.document.model.inputs.some(i => i.id === `size.${source.axis}` && i.role === 'design')\n if (declared) raw = asset.designInputs[`size.${source.axis}`]\n else {\n // Do not replace an existing partial size declaration while configuring an unrelated axis.\n if (asset.document.model.inputs.some(i => /^size\\.[xyz]$/.test(i.id))) throw Error('SIZE_SOURCE')\n const at = figurePartsExtentOf(asset)\n steps.push({ kind: 'declare-instance-size', at })\n raw = at[source.axis]\n }\n } else {\n if (!followableAxes(asset, source.part).includes(source.axis)) throw Error('SIZE_SOURCE')\n raw = dimensionNow(revision, source.part, source.axis)\n sourceFactor = measureFactor(asset, source.part, source.axis)\n }\n if (!(typeof raw === 'number' && raw > 0 && Number.isFinite(raw))) throw Error('SIZE_SOURCE')\n const ownFactor = measureFactor(asset, part, axis)\n const times = mode === 'ratio' ? mm / ownFactor / raw : sourceFactor / ownFactor\n const plus = mode === 'difference' ? (mm - raw * sourceFactor) / ownFactor : 0\n steps.push({ kind: 'link-dimension', part, axis, source, times, ...(plus ? { plus } : {}), replace: true })\n return steps\n}\n"]}
1
+ {"version":3,"file":"size-response.js","sourceRoot":"","sources":["../../client/modeller/size-response.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureAxisPairsOf, figurePartsExtentOf, figureShapeProviderOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureAuthoringAction, FigureDimensionSource } from '@hatiolab/figure-model'\nimport type { FigureRevision } from './revision.js'\nimport { dimensionNow, followOf, followableAxes, measureNow, type Axis } from './links.js'\n\nexport type SizeMode = 'ratio' | 'difference' | 'custom'\nexport type SizeSource = Extract<FigureDimensionSource, { of: 'instance' | 'part' }>\n\nfunction editOf(asset: FigureAsset, part: string, axis: Axis) {\n const shape = asset.document.model.nodes.find(n => n.id === `${part}.shape`)\n const edit = shape && figureShapeProviderOf(shape)?.edit[axis]\n return shape && edit && !('refused' in edit) ? { shape, edit } : undefined\n}\nexport const measureFactor = (asset: FigureAsset, part: string, axis: Axis) => editOf(asset, part, axis)?.edit.k ?? 1\n\n/**\n * A size row's name: the axis, and 「지름」 for a measure set by its radius. A round body's one diameter across sets the\n * axis it has no row of its own for as well, and says so -- a sphere read 「X · 지름」 and 「Y · 지름」, and a person\n * looked for its Z (tutorial run 2026-11-12); now 「X·Z · 지름」 across and 「Y · 지름」 up.\n */\nexport function sizeAxisWord(asset: FigureAsset, part: string, axis: Axis, diameter: string): string {\n if (measureFactor(asset, part, axis) !== 2) return axis.toUpperCase()\n const axes = followableAxes(asset, part)\n const across = (['x', 'y', 'z'] as const).filter(a => !axes.includes(a))\n const first = axes.find(a => measureFactor(asset, part, a) === 2)\n const named = first === axis && across.length ? [axis, ...across].map(a => a.toUpperCase()).join('·') : axis.toUpperCase()\n return `${named} · ${diameter}`\n}\n\n/** Only expose direct own inputs (including the canonical identity wrapper). Never overwrite an arbitrary expression. */\nexport function sizeInputOf(asset: FigureAsset, part: string, axis: Axis): string | undefined {\n const e = editOf(asset, part, axis)\n if (!e) return undefined\n const model = asset.document.model\n let ref = e.shape.args[e.edit.arg]\n const seen = new Set<string>()\n while (ref && !seen.has(ref)) {\n seen.add(ref)\n if (model.inputs.some(i => i.id === ref && i.role === 'design' && i.unit === 'mm') && ref.startsWith(`${part}.`)) return ref\n const writer = model.nodes.find(n => Object.values(n.outputs).includes(ref!))\n if (writer?.op !== 'mul@1' || model.constants.find(c => c.id === writer.args[1])?.value !== 1) return undefined\n ref = writer.args[0]\n }\n}\nexport function sizeRelationOf(revision: FigureRevision, part: string, axis: Axis): { source?: SizeSource; mode: SizeMode; variable: boolean } {\n const f = followOf(revision.asset, part, axis)\n if (f.of === 'own') return { mode: 'ratio', variable: false }\n if (f.of !== 'instance' && f.of !== 'part') return { mode: 'custom', variable: true }\n const source: SizeSource = f.of === 'instance' ? { of: 'instance', axis: f.axis } : { of: 'part', part: f.part, axis: f.axis }\n const equalFactor = (source.of === 'part' ? measureFactor(revision.asset, source.part, source.axis) : 1) / measureFactor(revision.asset, part, axis)\n const mode = !(f.plus ?? 0) ? 'ratio' : Math.abs((f.times ?? 1) - equalFactor) < 1e-6 ? 'difference' : 'custom'\n return { source, mode, variable: true }\n}\n\n/**\n * What a measure follows when it is first made to follow (ADR-0159 3): what the part hangs from, along the axis its own\n * axis lies on as it stands -- a roller laid on its side follows the frame's depth with its length, not the frame's\n * height (the axes paired by figure-model, `figureAxisPairsOf`, never by their letters). On the mounting plane, or\n * fastened to nothing, the figure's size. Nothing when it lies at an angle, or the parent has no measure along that\n * axis: the author picks. Only a suggestion when a relation is made; a measure that already follows something keeps it.\n */\nexport function suggestedSizeSource(asset: FigureAsset, part: string, axis: Axis): SizeSource | undefined {\n const { parent, along } = figureAxisPairsOf(asset, part)\n const on = along[axis]?.axis\n if (!on) return undefined\n if (!parent) return { of: 'instance', axis: on }\n return followableAxes(asset, parent).includes(on) ? { of: 'part', part: parent, axis: on } : undefined\n}\n\n/** Whether two sources are one: the same part, or the figure, and the same axis. */\nexport const sameSizeSource = (a: SizeSource | undefined, b: SizeSource | undefined) =>\n !!a && !!b && a.of === b.of && a.axis === b.axis && (a.of !== 'part' || (b.of === 'part' && a.part === b.part))\n\n/** How long the source is now, in millimetres as the panel shows it (a radius read as a diameter). */\nexport function sourceMeasureOf(revision: FigureRevision, source: SizeSource): number | undefined {\n if (source.of === 'instance') return revision.asset.designInputs[`size.${source.axis}`] ?? figurePartsExtentOf(revision.asset)[source.axis]\n return measureNow(revision, source.part, source.axis)\n}\n\n/** Preserve the displayed size when choosing a source or mode. mm edits change only that row's relation. */\nexport function sizeRelationSteps(revision: FigureRevision, part: string, axis: Axis, source: SizeSource, mode: Exclude<SizeMode, 'custom'>, mm = measureNow(revision, part, axis)): FigureAuthoringAction[] {\n if (!(typeof mm === 'number' && Number.isFinite(mm) && mm > 0)) throw Error('SIZE_VALUE')\n if (source.of === 'part' && source.part === part) throw Error('SIZE_SOURCE')\n const asset = revision.asset\n const steps: FigureAuthoringAction[] = []\n let raw: number | undefined\n let sourceFactor = 1\n if (source.of === 'instance') {\n const declared = asset.document.model.inputs.some(i => i.id === `size.${source.axis}` && i.role === 'design')\n if (declared) raw = asset.designInputs[`size.${source.axis}`]\n else {\n // Do not replace an existing partial size declaration while configuring an unrelated axis.\n if (asset.document.model.inputs.some(i => /^size\\.[xyz]$/.test(i.id))) throw Error('SIZE_SOURCE')\n const at = figurePartsExtentOf(asset)\n steps.push({ kind: 'declare-instance-size', at })\n raw = at[source.axis]\n }\n } else {\n if (!followableAxes(asset, source.part).includes(source.axis)) throw Error('SIZE_SOURCE')\n raw = dimensionNow(revision, source.part, source.axis)\n sourceFactor = measureFactor(asset, source.part, source.axis)\n }\n if (!(typeof raw === 'number' && raw > 0 && Number.isFinite(raw))) throw Error('SIZE_SOURCE')\n const ownFactor = measureFactor(asset, part, axis)\n const times = mode === 'ratio' ? mm / ownFactor / raw : sourceFactor / ownFactor\n const plus = mode === 'difference' ? (mm - raw * sourceFactor) / ownFactor : 0\n steps.push({ kind: 'link-dimension', part, axis, source, times, ...(plus ? { plus } : {}), replace: true })\n return steps\n}\n"]}
@@ -42,11 +42,25 @@ export declare function boardFor(parts: readonly FigureRenderPart[], at?: Board
42
42
  }): Board & {
43
43
  depth: number;
44
44
  };
45
+ /** Where the figure's drawn parts reach, in mm: the box the view was last fitted round, or the one it has now. */
46
+ export interface DrawnSpan {
47
+ min: [number, number, number];
48
+ max: [number, number, number];
49
+ }
50
+ export declare function drawnSpanOf(parts: readonly FigureRenderPart[]): DrawnSpan | undefined;
51
+ /**
52
+ * Whether the figure now reaches past the box the view was fitted round, on any side, by more than a tenth of that box
53
+ * (review 2026-11-11: a thigh hung under a pelvis ran off the bottom of the view; the board's size alone, measured from
54
+ * the middle and the floor, had not changed enough to say so).
55
+ */
56
+ export declare function outgrowsFit(fitted: DrawnSpan | undefined, now: DrawnSpan | undefined): boolean;
57
+ /** How far the figure may grow past its last fit before the view follows: inside the fit's own margin. */
58
+ export declare const REFIT_GROWN = 1.1;
45
59
  /**
46
60
  * Whether the figure's reach has changed so far since the view was last fitted that the camera should fit it again:
47
- * more than twice, or less than half, on any axis (first 15 minutes, 2026-10-16: a 100 mm box typed to 3000 × 800 × 600
48
- * put the camera inside it, and the author saw grey until pressing 「화면에 맞추기」). A drag that nudges a part does not
49
- * move the camera.
61
+ * grown past `REFIT_GROWN`, or shrunk below half, on any axis (first 15 minutes, 2026-10-16: a 100 mm box typed to
62
+ * 3000 × 800 × 600 put the camera inside it, and the author saw grey until pressing 「화면에 맞추기」). A drag that nudges
63
+ * a part within the board's rounding does not move the camera.
50
64
  */
51
65
  export declare function needsRefit(fitted: (Board & {
52
66
  depth: number;