@things-factory/figure-ui 10.1.129 → 10.1.141

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 (95) hide show
  1. package/client/modeller/align.ts +24 -18
  2. package/client/modeller/editor.ts +165 -253
  3. package/client/modeller/fasten-geometry.ts +12 -9
  4. package/client/modeller/figure-facts.ts +12 -14
  5. package/client/modeller/figure-links.ts +16 -12
  6. package/client/modeller/figure-preview.ts +13 -4
  7. package/client/modeller/figure-properties.ts +6 -5
  8. package/client/modeller/figure-verdict.ts +14 -8
  9. package/client/modeller/joint-fan.ts +15 -17
  10. package/client/modeller/links.ts +17 -23
  11. package/client/modeller/live.ts +6 -3
  12. package/client/modeller/motion-ends.ts +6 -7
  13. package/client/modeller/occupancy.ts +2 -3
  14. package/client/modeller/product-session.ts +15 -0
  15. package/client/modeller/revision.ts +66 -0
  16. package/client/pages/figure-modeller-page.ts +5 -18
  17. package/dist-client/modeller/align.d.ts +12 -7
  18. package/dist-client/modeller/align.js +21 -17
  19. package/dist-client/modeller/align.js.map +1 -1
  20. package/dist-client/modeller/editor.d.ts +8 -34
  21. package/dist-client/modeller/editor.js +175 -250
  22. package/dist-client/modeller/editor.js.map +1 -1
  23. package/dist-client/modeller/fasten-geometry.d.ts +3 -2
  24. package/dist-client/modeller/fasten-geometry.js +11 -9
  25. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  26. package/dist-client/modeller/figure-facts.d.ts +9 -1
  27. package/dist-client/modeller/figure-facts.js +11 -15
  28. package/dist-client/modeller/figure-facts.js.map +1 -1
  29. package/dist-client/modeller/figure-links.d.ts +4 -5
  30. package/dist-client/modeller/figure-links.js +14 -13
  31. package/dist-client/modeller/figure-links.js.map +1 -1
  32. package/dist-client/modeller/figure-preview.d.ts +5 -2
  33. package/dist-client/modeller/figure-preview.js +13 -4
  34. package/dist-client/modeller/figure-preview.js.map +1 -1
  35. package/dist-client/modeller/figure-properties.d.ts +3 -2
  36. package/dist-client/modeller/figure-properties.js +6 -6
  37. package/dist-client/modeller/figure-properties.js.map +1 -1
  38. package/dist-client/modeller/figure-verdict.d.ts +4 -2
  39. package/dist-client/modeller/figure-verdict.js +12 -9
  40. package/dist-client/modeller/figure-verdict.js.map +1 -1
  41. package/dist-client/modeller/joint-fan.d.ts +8 -4
  42. package/dist-client/modeller/joint-fan.js +15 -16
  43. package/dist-client/modeller/joint-fan.js.map +1 -1
  44. package/dist-client/modeller/links.d.ts +6 -5
  45. package/dist-client/modeller/links.js +17 -24
  46. package/dist-client/modeller/links.js.map +1 -1
  47. package/dist-client/modeller/live.d.ts +3 -2
  48. package/dist-client/modeller/live.js +7 -3
  49. package/dist-client/modeller/live.js.map +1 -1
  50. package/dist-client/modeller/motion-ends.d.ts +2 -2
  51. package/dist-client/modeller/motion-ends.js +5 -6
  52. package/dist-client/modeller/motion-ends.js.map +1 -1
  53. package/dist-client/modeller/occupancy.d.ts +2 -2
  54. package/dist-client/modeller/occupancy.js +2 -3
  55. package/dist-client/modeller/occupancy.js.map +1 -1
  56. package/dist-client/modeller/product-session.d.ts +44 -39
  57. package/dist-client/modeller/product-session.js +17 -0
  58. package/dist-client/modeller/product-session.js.map +1 -1
  59. package/dist-client/modeller/revision.d.ts +37 -0
  60. package/dist-client/modeller/revision.js +39 -0
  61. package/dist-client/modeller/revision.js.map +1 -0
  62. package/dist-client/pages/figure-modeller-page.d.ts +0 -6
  63. package/dist-client/pages/figure-modeller-page.js +6 -14
  64. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  65. package/dist-client/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +7 -7
  67. package/test/align.test.ts +14 -12
  68. package/test/browser/modeller-page.js +52 -4
  69. package/test/browser/modeller-revision.check.mjs +144 -0
  70. package/test/command-preview.test.ts +9 -8
  71. package/test/fasten-drawing.test.ts +1 -1
  72. package/test/fasten-geometry.test.ts +10 -9
  73. package/test/fields.test.ts +1 -1
  74. package/test/first-minute.test.ts +1 -1
  75. package/test/gizmo.test.ts +1 -1
  76. package/test/joint-fan.test.ts +30 -14
  77. package/test/joint-frames.test.ts +9 -8
  78. package/test/links.test.ts +13 -12
  79. package/test/live.test.ts +3 -3
  80. package/test/parts-wiring.test.ts +1 -1
  81. package/test/properties.test.ts +6 -5
  82. package/test/release-panel.test.ts +3 -3
  83. package/test/resize-with-placement.test.ts +8 -6
  84. package/test/revision-of.ts +22 -0
  85. package/test/revision.test.ts +172 -0
  86. package/test/section-editor.test.ts +4 -3
  87. package/test/section-panel.test.ts +5 -4
  88. package/test/verdict.test.ts +1 -1
  89. package/client/modeller/evaluated.ts +0 -47
  90. package/dist-client/modeller/evaluated.d.ts +0 -12
  91. package/dist-client/modeller/evaluated.js +0 -42
  92. package/dist-client/modeller/evaluated.js.map +0 -1
  93. package/dist-client/tsconfig.scratch.tsbuildinfo +0 -1
  94. package/test/evaluated.test.ts +0 -40
  95. package/test/mode-switch.test.ts +0 -43
@@ -5,7 +5,8 @@ import { boxShape } from './drawn-shapes.js';
5
5
  const THICKNESS = 6;
6
6
  const samePose = (a, b) => [0, 1, 2].every(i => Math.abs(a.t[i] - b.t[i]) < 1e-6 && [0, 1, 2].every(k => Math.abs(a.r[i][k] - b.r[i][k]) < 1e-9));
7
7
  /** The ends of the selected part's own motion, or nothing: a part that does not move, follows another, or turns without end. */
8
- export function motionEndsOf(asset, part, values = {}) {
8
+ export function motionEndsOf(revision, part) {
9
+ const asset = revision.asset;
9
10
  const joint = asset.joints?.find(j => j.body1.part === part);
10
11
  /* A turn without end is a revolute joint whose control has no range: the range check below leaves it out. */
11
12
  if (!joint || joint.type === 'fixed' || joint.mimic)
@@ -14,13 +15,11 @@ export function motionEndsOf(asset, part, values = {}) {
14
15
  const input = asset.document.model.inputs.find(i => i.id === control && i.role === 'state');
15
16
  if (!input || typeof input.min !== 'number' || typeof input.max !== 'number' || input.min === input.max)
16
17
  return null;
17
- /* The values in hand, each where it belongs: design values into the figure, controls into the evaluation. */
18
- const pick = (of) => Object.fromEntries(Object.entries(values).filter(([id]) => id in of));
19
- const shown = { ...asset, designInputs: { ...asset.designInputs, ...pick(asset.designInputs) } };
20
- const state = pick(asset.stateDefaults);
18
+ /* The version's design values are its asset's; its pose is every control as set now, this one at each end. */
19
+ const state = Object.fromEntries(Object.entries(revision.inputs).filter(([id]) => id in asset.stateDefaults));
21
20
  const at = (value) => {
22
21
  try {
23
- return compileFigureAsset(shown).evaluate({ ...state, [control]: value }).geometry;
22
+ return compileFigureAsset(asset).evaluate({ ...state, [control]: value }).geometry;
24
23
  }
25
24
  catch {
26
25
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"motion-ends.js","sourceRoot":"","sources":["../../client/modeller/motion-ends.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset } from '@hatiolab/figure-model'\nimport type { FigureAsset } from '@hatiolab/figure-model'\n\nimport { PREVIEW, TARGET_FACE } from './command-preview.js'\nimport type { FigureRenderPart } from './renderer.js'\nimport { boxShape } from './drawn-shapes.js'\n\n/*\n * Where a part's motion takes it (ADR-0097 §5.5, designer 2026-09-27): the bodies its control moves, drawn translucent at\n * the control's lowest and highest value -- evaluated there by figure-model, not guessed -- and, for a slide, the line it\n * travels. Only the two ends are shown; nothing here says what lies between them is clear.\n */\n\ntype Pose = { r: number[][]; t: number[] }\ntype Body = FigureRenderPart & { pose: Pose }\n\n/** The line a slide travels along, thin enough not to hide the part. */\nconst THICKNESS = 6\n\nexport interface MotionEnds {\n control: string\n min: number\n max: number\n unit: string\n /** The moving bodies at the lowest value, then at the highest. */\n ghost: FigureRenderPart[]\n}\n\nconst samePose = (a: Pose, b: Pose) => [0, 1, 2].every(i => Math.abs(a.t[i]! - b.t[i]!) < 1e-6 && [0, 1, 2].every(k => Math.abs(a.r[i]![k]! - b.r[i]![k]!) < 1e-9))\n\n/** The ends of the selected part's own motion, or nothing: a part that does not move, follows another, or turns without end. */\nexport function motionEndsOf(asset: FigureAsset, part: string, values: Record<string, number> = {}): MotionEnds | null {\n const joint = asset.joints?.find(j => j.body1.part === part)\n /* A turn without end is a revolute joint whose control has no range: the range check below leaves it out. */\n if (!joint || joint.type === 'fixed' || joint.mimic) return null\n const control = joint.state?.id ?? joint.id\n const input = asset.document.model.inputs.find(i => i.id === control && i.role === 'state')\n if (!input || typeof input.min !== 'number' || typeof input.max !== 'number' || input.min === input.max) return null\n /* The values in hand, each where it belongs: design values into the figure, controls into the evaluation. */\n const pick = (of: Record<string, number>) => Object.fromEntries(Object.entries(values).filter(([id]) => id in of))\n const shown = { ...asset, designInputs: { ...asset.designInputs, ...pick(asset.designInputs) } }\n const state = pick(asset.stateDefaults)\n const at = (value: number) => {\n try {\n return compileFigureAsset(shown).evaluate({ ...state, [control]: value }).geometry as unknown as Body[]\n } catch {\n return null\n }\n }\n const low = at(input.min)\n const high = at(input.max)\n if (!low || !high) return null\n const byId = new Map(high.filter(b => b.pose).map(b => [b.id, b]))\n const moving = low.filter(b => b.pose && 'shape' in b && byId.has(b.id) && !samePose(b.pose, byId.get(b.id)!.pose))\n if (!moving.length) return null\n const ghost: FigureRenderPart[] = [\n ...moving.map(b => ({ ...b, id: `${PREVIEW}:min:${b.id}`, material: { token: PREVIEW, transparent: true } })),\n ...moving.map(b => ({ ...byId.get(b.id)!, id: `${PREVIEW}:max:${b.id}`, material: { token: PREVIEW, transparent: true } }))\n ]\n /* A slide: the line its own body travels, from the lowest value to the highest. */\n const own = moving.find(b => b.id === part)\n if (joint.type === 'prismatic' && own) {\n const from = own.pose.t\n const to = byId.get(part)!.pose.t\n const v = [0, 1, 2].map(i => to[i]! - from[i]!)\n const length = Math.hypot(...v)\n if (length > 1e-6) {\n const x = v.map(c => c / length)\n /* Any two directions square to it and to each other complete its frame. */\n const helper = Math.abs(x[1]!) < 0.9 ? [0, 1, 0] : [1, 0, 0]\n const zRaw = [x[1]! * helper[2]! - x[2]! * helper[1]!, x[2]! * helper[0]! - x[0]! * helper[2]!, x[0]! * helper[1]! - x[1]! * helper[0]!]\n const zl = Math.hypot(...zRaw)\n const z = zRaw.map(c => c / zl)\n const y = [z[1]! * x[2]! - z[2]! * x[1]!, z[2]! * x[0]! - z[0]! * x[2]!, z[0]! * x[1]! - z[1]! * x[0]!]\n ghost.push({\n id: `${TARGET_FACE}:slide`,\n shape: boxShape(length, THICKNESS, THICKNESS),\n pose: { r: [0, 1, 2].map(i => [x[i]!, y[i]!, z[i]!]), t: [0, 1, 2].map(i => (from[i]! + to[i]!) / 2) },\n material: { token: TARGET_FACE }\n })\n }\n }\n return { control, min: input.min, max: input.max, unit: input.unit, ghost }\n}\n"]}
1
+ {"version":3,"file":"motion-ends.js","sourceRoot":"","sources":["../../client/modeller/motion-ends.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset } from '@hatiolab/figure-model'\n\nimport { PREVIEW, TARGET_FACE } from './command-preview.js'\nimport type { FigureRenderPart } from './renderer.js'\nimport { boxShape } from './drawn-shapes.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * Where a part's motion takes it (ADR-0097 §5.5, designer 2026-09-27): the bodies its control moves, drawn translucent at\n * the control's lowest and highest value -- evaluated there by figure-model, not guessed -- and, for a slide, the line it\n * travels. Only the two ends are shown; nothing here says what lies between them is clear.\n */\n\ntype Pose = { r: number[][]; t: number[] }\ntype Body = FigureRenderPart & { pose: Pose }\n\n/** The line a slide travels along, thin enough not to hide the part. */\nconst THICKNESS = 6\n\nexport interface MotionEnds {\n control: string\n min: number\n max: number\n unit: string\n /** The moving bodies at the lowest value, then at the highest. */\n ghost: FigureRenderPart[]\n}\n\nconst samePose = (a: Pose, b: Pose) => [0, 1, 2].every(i => Math.abs(a.t[i]! - b.t[i]!) < 1e-6 && [0, 1, 2].every(k => Math.abs(a.r[i]![k]! - b.r[i]![k]!) < 1e-9))\n\n/** The ends of the selected part's own motion, or nothing: a part that does not move, follows another, or turns without end. */\nexport function motionEndsOf(revision: FigureRevision, part: string): MotionEnds | null {\n const asset = revision.asset\n const joint = asset.joints?.find(j => j.body1.part === part)\n /* A turn without end is a revolute joint whose control has no range: the range check below leaves it out. */\n if (!joint || joint.type === 'fixed' || joint.mimic) return null\n const control = joint.state?.id ?? joint.id\n const input = asset.document.model.inputs.find(i => i.id === control && i.role === 'state')\n if (!input || typeof input.min !== 'number' || typeof input.max !== 'number' || input.min === input.max) return null\n /* The version's design values are its asset's; its pose is every control as set now, this one at each end. */\n const state = Object.fromEntries(Object.entries(revision.inputs).filter(([id]) => id in asset.stateDefaults))\n const at = (value: number) => {\n try {\n return compileFigureAsset(asset).evaluate({ ...state, [control]: value }).geometry as unknown as Body[]\n } catch {\n return null\n }\n }\n const low = at(input.min)\n const high = at(input.max)\n if (!low || !high) return null\n const byId = new Map(high.filter(b => b.pose).map(b => [b.id, b]))\n const moving = low.filter(b => b.pose && 'shape' in b && byId.has(b.id) && !samePose(b.pose, byId.get(b.id)!.pose))\n if (!moving.length) return null\n const ghost: FigureRenderPart[] = [\n ...moving.map(b => ({ ...b, id: `${PREVIEW}:min:${b.id}`, material: { token: PREVIEW, transparent: true } })),\n ...moving.map(b => ({ ...byId.get(b.id)!, id: `${PREVIEW}:max:${b.id}`, material: { token: PREVIEW, transparent: true } }))\n ]\n /* A slide: the line its own body travels, from the lowest value to the highest. */\n const own = moving.find(b => b.id === part)\n if (joint.type === 'prismatic' && own) {\n const from = own.pose.t\n const to = byId.get(part)!.pose.t\n const v = [0, 1, 2].map(i => to[i]! - from[i]!)\n const length = Math.hypot(...v)\n if (length > 1e-6) {\n const x = v.map(c => c / length)\n /* Any two directions square to it and to each other complete its frame. */\n const helper = Math.abs(x[1]!) < 0.9 ? [0, 1, 0] : [1, 0, 0]\n const zRaw = [x[1]! * helper[2]! - x[2]! * helper[1]!, x[2]! * helper[0]! - x[0]! * helper[2]!, x[0]! * helper[1]! - x[1]! * helper[0]!]\n const zl = Math.hypot(...zRaw)\n const z = zRaw.map(c => c / zl)\n const y = [z[1]! * x[2]! - z[2]! * x[1]!, z[2]! * x[0]! - z[0]! * x[2]!, z[0]! * x[1]! - z[1]! * x[0]!]\n ghost.push({\n id: `${TARGET_FACE}:slide`,\n shape: boxShape(length, THICKNESS, THICKNESS),\n pose: { r: [0, 1, 2].map(i => [x[i]!, y[i]!, z[i]!]), t: [0, 1, 2].map(i => (from[i]! + to[i]!) / 2) },\n material: { token: TARGET_FACE }\n })\n }\n }\n return { control, min: input.min, max: input.max, unit: input.unit, ghost }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type FigureAuthoringAction, type FigureRoomRefit } from '@hatiolab/figure-model';
1
+ import { type FigureAsset, type FigureAuthoringAction, type FigureRoomRefit } from '@hatiolab/figure-model';
2
2
  /**
3
3
  * 「부품에 맞추기」의 짝 — 공간을 지금 부품에서 다시 선언하는 한 걸음.
4
4
  *
@@ -11,7 +11,7 @@ import { type FigureAuthoringAction, type FigureRoomRefit } from '@hatiolab/figu
11
11
  * 막는다. 배치 기준은 이미 선언된 것을 지키고, 처음 선언하는 도형은 바닥에 선다고 둔다 — 새로 만든 도형의
12
12
  * 모델러 기본이 바닥이다.
13
13
  */
14
- export declare function occupancyFromParts(source: string,
14
+ export declare function occupancyFromParts(asset: FigureAsset,
15
15
  /** A different face to place it by — the properties tab's choice. Absent keeps the declared one. */
16
16
  placement?: 'floor' | 'ceiling' | 'center' | 'wall'): Extract<FigureAuthoringAction, {
17
17
  kind: 'declare-occupancy';
@@ -1,4 +1,4 @@
1
- import { parseFigureAsset, proposeFigureOccupancy } from '@hatiolab/figure-model';
1
+ import { proposeFigureOccupancy } from '@hatiolab/figure-model';
2
2
  /**
3
3
  * 「부품에 맞추기」의 짝 — 공간을 지금 부품에서 다시 선언하는 한 걸음.
4
4
  *
@@ -11,10 +11,9 @@ import { parseFigureAsset, proposeFigureOccupancy } from '@hatiolab/figure-model
11
11
  * 막는다. 배치 기준은 이미 선언된 것을 지키고, 처음 선언하는 도형은 바닥에 선다고 둔다 — 새로 만든 도형의
12
12
  * 모델러 기본이 바닥이다.
13
13
  */
14
- export function occupancyFromParts(source,
14
+ export function occupancyFromParts(asset,
15
15
  /** A different face to place it by — the properties tab's choice. Absent keeps the declared one. */
16
16
  placement) {
17
- const asset = parseFigureAsset(source);
18
17
  return {
19
18
  kind: 'declare-occupancy',
20
19
  proposal: proposeFigureOccupancy(asset, { over: 'range' }),
@@ -1 +1 @@
1
- {"version":3,"file":"occupancy.js","sourceRoot":"","sources":["../../client/modeller/occupancy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAoD,MAAM,wBAAwB,CAAA;AAEnI;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAc;AACd,oGAAoG;AACpG,SAAmD;IAEnD,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACtC,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,sBAAsB,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1D,qGAAqG;QACrG,GAAG,CAAC,SAAS,IAAI,SAAS,KAAK,CAAC,KAAK,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClF,CAAA;AACH,CAAC;AAcD,MAAM,UAAU,eAAe,CAAC,KAAsB;IACpD,MAAM,IAAI,GAAG,CAAC,CAA+B,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/F,MAAM,IAAI,GAAmB,EAAE,CAAA;IAC/B,KAAK,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,CAAU,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EACjC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ;YAAE,SAAQ;QACpC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU;YACzC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;IAChJ,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { parseFigureAsset, proposeFigureOccupancy, type FigureAuthoringAction, type FigureRoomRefit } from '@hatiolab/figure-model'\n\n/**\n * 「부품에 맞추기」의 짝 — 공간을 지금 부품에서 다시 선언하는 한 걸음.\n *\n * figure-model 은 이것을 둘로 나눠 두었다: 제안(`proposeFigureOccupancy`)은 무엇이 선언될지를 보여 주고, 확인\n * (`declare-occupancy`)은 그 제안이 읽은 모양 그대로일 때만 받는다. 저작자가 누른 단추가 그 확인이다. 제안과 확인\n * 사이에 모델이 바뀌면 figure-model 이 `OCCUPANCY_STALE` 로 거절하므로, 여기서는 누른 순간의 원본으로 둘을 한\n * 번에 만든다.\n *\n * 범위는 움직임 전체다(`over: 'range'`). 쉬는 자세만 담으면 움직이는 부품이 선언한 공간을 넘어 발행 판정이 다시\n * 막는다. 배치 기준은 이미 선언된 것을 지키고, 처음 선언하는 도형은 바닥에 선다고 둔다 — 새로 만든 도형의\n * 모델러 기본이 바닥이다.\n */\nexport function occupancyFromParts(\n source: string,\n /** A different face to place it by — the properties tab's choice. Absent keeps the declared one. */\n placement?: 'floor' | 'ceiling' | 'center' | 'wall'\n): Extract<FigureAuthoringAction, { kind: 'declare-occupancy' }> {\n const asset = parseFigureAsset(source)\n return {\n kind: 'declare-occupancy',\n proposal: proposeFigureOccupancy(asset, { over: 'range' }),\n /* The placement is the figure's own now (ADR-0097): named only when a different one is asked for. */\n ...(placement && placement !== (asset.placement ?? 'floor') ? { placement } : {})\n }\n}\n\n/**\n * 공간 다시 계산의 확인 표 — 선언된 공간과 제안된 공간을 그린 크기와 가장 큰 크기에서, 축마다 한 줄로. 같은 줄은\n * 같다고 적는다. 밖으로 나간 부품은 figure-model 이 잰 것만 적는다 — 재지 않은 것을 「넘친다」고 단정하지 않는다\n * (설계자 2026-09-27).\n */\nexport interface RoomRefitRow {\n at: 'authored' | 'largest'\n axis: 'x' | 'y' | 'z'\n declared: string\n proposed: string\n same: boolean\n}\nexport function roomRefitRowsOf(refit: FigureRoomRefit): RoomRefitRow[] {\n const span = (b: { min: number; max: number }) => `${Math.round(b.min)} ~ ${Math.round(b.max)}`\n const rows: RoomRefitRow[] = []\n for (const at of ['authored', 'largest'] as const) {\n const declared = refit.declared[at],\n proposed = refit.proposed[at]\n if (!declared || !proposed) continue\n for (const axis of ['x', 'y', 'z'] as const)\n rows.push({ at, axis, declared: span(declared[axis]), proposed: span(proposed[axis]), same: span(declared[axis]) === span(proposed[axis]) })\n }\n return rows\n}\n"]}
1
+ {"version":3,"file":"occupancy.js","sourceRoot":"","sources":["../../client/modeller/occupancy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAsE,MAAM,wBAAwB,CAAA;AAEnI;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAkB;AAClB,oGAAoG;AACpG,SAAmD;IAEnD,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,sBAAsB,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1D,qGAAqG;QACrG,GAAG,CAAC,SAAS,IAAI,SAAS,KAAK,CAAC,KAAK,CAAC,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClF,CAAA;AACH,CAAC;AAcD,MAAM,UAAU,eAAe,CAAC,KAAsB;IACpD,MAAM,IAAI,GAAG,CAAC,CAA+B,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/F,MAAM,IAAI,GAAmB,EAAE,CAAA;IAC/B,KAAK,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,CAAU,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,EACjC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ;YAAE,SAAQ;QACpC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU;YACzC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;IAChJ,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { proposeFigureOccupancy, type FigureAsset, type FigureAuthoringAction, type FigureRoomRefit } from '@hatiolab/figure-model'\n\n/**\n * 「부품에 맞추기」의 짝 — 공간을 지금 부품에서 다시 선언하는 한 걸음.\n *\n * figure-model 은 이것을 둘로 나눠 두었다: 제안(`proposeFigureOccupancy`)은 무엇이 선언될지를 보여 주고, 확인\n * (`declare-occupancy`)은 그 제안이 읽은 모양 그대로일 때만 받는다. 저작자가 누른 단추가 그 확인이다. 제안과 확인\n * 사이에 모델이 바뀌면 figure-model 이 `OCCUPANCY_STALE` 로 거절하므로, 여기서는 누른 순간의 원본으로 둘을 한\n * 번에 만든다.\n *\n * 범위는 움직임 전체다(`over: 'range'`). 쉬는 자세만 담으면 움직이는 부품이 선언한 공간을 넘어 발행 판정이 다시\n * 막는다. 배치 기준은 이미 선언된 것을 지키고, 처음 선언하는 도형은 바닥에 선다고 둔다 — 새로 만든 도형의\n * 모델러 기본이 바닥이다.\n */\nexport function occupancyFromParts(\n asset: FigureAsset,\n /** A different face to place it by — the properties tab's choice. Absent keeps the declared one. */\n placement?: 'floor' | 'ceiling' | 'center' | 'wall'\n): Extract<FigureAuthoringAction, { kind: 'declare-occupancy' }> {\n return {\n kind: 'declare-occupancy',\n proposal: proposeFigureOccupancy(asset, { over: 'range' }),\n /* The placement is the figure's own now (ADR-0097): named only when a different one is asked for. */\n ...(placement && placement !== (asset.placement ?? 'floor') ? { placement } : {})\n }\n}\n\n/**\n * 공간 다시 계산의 확인 표 — 선언된 공간과 제안된 공간을 그린 크기와 가장 큰 크기에서, 축마다 한 줄로. 같은 줄은\n * 같다고 적는다. 밖으로 나간 부품은 figure-model 이 잰 것만 적는다 — 재지 않은 것을 「넘친다」고 단정하지 않는다\n * (설계자 2026-09-27).\n */\nexport interface RoomRefitRow {\n at: 'authored' | 'largest'\n axis: 'x' | 'y' | 'z'\n declared: string\n proposed: string\n same: boolean\n}\nexport function roomRefitRowsOf(refit: FigureRoomRefit): RoomRefitRow[] {\n const span = (b: { min: number; max: number }) => `${Math.round(b.min)} ~ ${Math.round(b.max)}`\n const rows: RoomRefitRow[] = []\n for (const at of ['authored', 'largest'] as const) {\n const declared = refit.declared[at],\n proposed = refit.proposed[at]\n if (!declared || !proposed) continue\n for (const axis of ['x', 'y', 'z'] as const)\n rows.push({ at, axis, declared: span(declared[axis]), proposed: span(proposed[axis]), same: span(declared[axis]) === span(proposed[axis]) })\n }\n return rows\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type FigureRevision } from './revision.js';
1
2
  import { type FigureInputChange, type FigurePrimitiveCreation, type FigurePartEdit, type FigureAuthoringAction, type FigureProposalStep, type FigureClipConsent } from '@hatiolab/figure-model';
2
3
  /** Consent to let clips go, and the figure and version it was given on. */
3
4
  export interface FigureClipConsentFor extends FigureClipConsent {
@@ -46,6 +47,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
46
47
  canUndo?: undefined;
47
48
  canRedo?: undefined;
48
49
  palette?: undefined;
50
+ asset?: undefined;
49
51
  } | {
50
52
  id: string | undefined;
51
53
  loading: boolean;
@@ -57,15 +59,16 @@ export declare function createFigureProductSession(store: FigureProductStore): {
57
59
  dirty: boolean;
58
60
  canUndo: boolean;
59
61
  canRedo: boolean;
60
- fields: Array<
61
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
62
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
62
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
63
63
  value: number;
64
64
  }>;
65
+ asset: import("@hatiolab/figure-model").FigureAsset;
65
66
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
66
67
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
67
68
  palette: Record<string, string>;
68
69
  };
70
+ /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */
71
+ revisionNow: () => FigureRevision | undefined;
69
72
  /**
70
73
  * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`
71
74
  * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.
@@ -88,6 +91,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
88
91
  canUndo?: undefined;
89
92
  canRedo?: undefined;
90
93
  palette?: undefined;
94
+ asset?: undefined;
91
95
  } | {
92
96
  id: string | undefined;
93
97
  loading: boolean;
@@ -99,11 +103,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
99
103
  dirty: boolean;
100
104
  canUndo: boolean;
101
105
  canRedo: boolean;
102
- fields: Array<
103
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
104
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
106
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
105
107
  value: number;
106
108
  }>;
109
+ asset: import("@hatiolab/figure-model").FigureAsset;
107
110
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
108
111
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
109
112
  palette: Record<string, string>;
@@ -125,6 +128,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
125
128
  canUndo?: undefined;
126
129
  canRedo?: undefined;
127
130
  palette?: undefined;
131
+ asset?: undefined;
128
132
  } | {
129
133
  id: string | undefined;
130
134
  loading: boolean;
@@ -136,11 +140,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
136
140
  dirty: boolean;
137
141
  canUndo: boolean;
138
142
  canRedo: boolean;
139
- fields: Array<
140
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
141
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
143
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
142
144
  value: number;
143
145
  }>;
146
+ asset: import("@hatiolab/figure-model").FigureAsset;
144
147
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
145
148
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
146
149
  palette: Record<string, string>;
@@ -162,6 +165,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
162
165
  canUndo?: undefined;
163
166
  canRedo?: undefined;
164
167
  palette?: undefined;
168
+ asset?: undefined;
165
169
  } | {
166
170
  id: string | undefined;
167
171
  loading: boolean;
@@ -173,11 +177,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
173
177
  dirty: boolean;
174
178
  canUndo: boolean;
175
179
  canRedo: boolean;
176
- fields: Array<
177
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
178
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
180
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
179
181
  value: number;
180
182
  }>;
183
+ asset: import("@hatiolab/figure-model").FigureAsset;
181
184
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
182
185
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
183
186
  palette: Record<string, string>;
@@ -193,6 +196,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
193
196
  canUndo?: undefined;
194
197
  canRedo?: undefined;
195
198
  palette?: undefined;
199
+ asset?: undefined;
196
200
  } | {
197
201
  context: string;
198
202
  name: string;
@@ -200,11 +204,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
200
204
  dirty: boolean;
201
205
  canUndo: boolean;
202
206
  canRedo: boolean;
203
- fields: Array<
204
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
205
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
207
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
206
208
  value: number;
207
209
  }>;
210
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
208
211
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
209
212
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
210
213
  palette: Record<string, string>;
@@ -224,6 +227,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
224
227
  canUndo?: undefined;
225
228
  canRedo?: undefined;
226
229
  palette?: undefined;
230
+ asset?: undefined;
227
231
  } | {
228
232
  context: string;
229
233
  name: string;
@@ -231,11 +235,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
231
235
  dirty: boolean;
232
236
  canUndo: boolean;
233
237
  canRedo: boolean;
234
- fields: Array<
235
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
236
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
238
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
237
239
  value: number;
238
240
  }>;
241
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
239
242
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
240
243
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
241
244
  palette: Record<string, string>;
@@ -263,6 +266,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
263
266
  canUndo?: undefined;
264
267
  canRedo?: undefined;
265
268
  palette?: undefined;
269
+ asset?: undefined;
266
270
  } | {
267
271
  context: string;
268
272
  name: string;
@@ -270,11 +274,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
270
274
  dirty: boolean;
271
275
  canUndo: boolean;
272
276
  canRedo: boolean;
273
- fields: Array<
274
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
275
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
277
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
276
278
  value: number;
277
279
  }>;
280
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
278
281
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
279
282
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
280
283
  palette: Record<string, string>;
@@ -290,6 +293,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
290
293
  canUndo?: undefined;
291
294
  canRedo?: undefined;
292
295
  palette?: undefined;
296
+ asset?: undefined;
293
297
  } | {
294
298
  context: string;
295
299
  name: string;
@@ -297,11 +301,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
297
301
  dirty: boolean;
298
302
  canUndo: boolean;
299
303
  canRedo: boolean;
300
- fields: Array<
301
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
302
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
304
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
303
305
  value: number;
304
306
  }>;
307
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
305
308
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
306
309
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
307
310
  palette: Record<string, string>;
@@ -317,6 +320,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
317
320
  canUndo?: undefined;
318
321
  canRedo?: undefined;
319
322
  palette?: undefined;
323
+ asset?: undefined;
320
324
  } | {
321
325
  context: string;
322
326
  name: string;
@@ -324,11 +328,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
324
328
  dirty: boolean;
325
329
  canUndo: boolean;
326
330
  canRedo: boolean;
327
- fields: Array<
328
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
329
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
331
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
330
332
  value: number;
331
333
  }>;
334
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
332
335
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
333
336
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
334
337
  palette: Record<string, string>;
@@ -344,6 +347,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
344
347
  canUndo?: undefined;
345
348
  canRedo?: undefined;
346
349
  palette?: undefined;
350
+ asset?: undefined;
347
351
  } | {
348
352
  removal: import("@hatiolab/figure-model").RemovalNotice;
349
353
  context: string;
@@ -355,6 +359,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
355
359
  fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
356
360
  value: number;
357
361
  }>;
362
+ asset: import("@hatiolab/figure-model").FigureAsset;
358
363
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
359
364
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
360
365
  palette: Record<string, string>;
@@ -381,6 +386,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
381
386
  canUndo?: undefined;
382
387
  canRedo?: undefined;
383
388
  palette?: undefined;
389
+ asset?: undefined;
384
390
  } | {
385
391
  context: string;
386
392
  name: string;
@@ -388,11 +394,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
388
394
  dirty: boolean;
389
395
  canUndo: boolean;
390
396
  canRedo: boolean;
391
- fields: Array<
392
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
393
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
397
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
394
398
  value: number;
395
399
  }>;
400
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
396
401
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
397
402
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
398
403
  palette: Record<string, string>;
@@ -414,6 +419,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
414
419
  canUndo?: undefined;
415
420
  canRedo?: undefined;
416
421
  palette?: undefined;
422
+ asset?: undefined;
417
423
  } | {
418
424
  context: string;
419
425
  name: string;
@@ -421,11 +427,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
421
427
  dirty: boolean;
422
428
  canUndo: boolean;
423
429
  canRedo: boolean;
424
- fields: Array<
425
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
426
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
430
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
427
431
  value: number;
428
432
  }>;
433
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
429
434
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
430
435
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
431
436
  palette: Record<string, string>;
@@ -441,6 +446,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
441
446
  canUndo?: undefined;
442
447
  canRedo?: undefined;
443
448
  palette?: undefined;
449
+ asset?: undefined;
444
450
  } | {
445
451
  context: string;
446
452
  name: string;
@@ -448,11 +454,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
448
454
  dirty: boolean;
449
455
  canUndo: boolean;
450
456
  canRedo: boolean;
451
- fields: Array<
452
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
453
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
457
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
454
458
  value: number;
455
459
  }>;
460
+ readonly asset: import("@hatiolab/figure-model").FigureAsset;
456
461
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
457
462
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
458
463
  palette: Record<string, string>;
@@ -483,6 +488,7 @@ export declare function createFigureProductSession(store: FigureProductStore): {
483
488
  canUndo?: undefined;
484
489
  canRedo?: undefined;
485
490
  palette?: undefined;
491
+ asset?: undefined;
486
492
  } | {
487
493
  id: string | undefined;
488
494
  loading: boolean;
@@ -494,11 +500,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
494
500
  dirty: boolean;
495
501
  canUndo: boolean;
496
502
  canRedo: boolean;
497
- fields: Array<
498
- /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
499
- import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
503
+ fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
500
504
  value: number;
501
505
  }>;
506
+ asset: import("@hatiolab/figure-model").FigureAsset;
502
507
  geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
503
508
  bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
504
509
  palette: Record<string, string>;
@@ -1,4 +1,5 @@
1
1
  import { thumbnailHashOf } from './thumbnail-hash.js';
2
+ import { revisionFromView } from './revision.js';
2
3
  import { createFigureAsset, createFigureEditorWorkspace, serializeFigureAsset, parseFigureAsset } from '@hatiolab/figure-model';
3
4
  /** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
4
5
  export function createFigureProductSession(store) {
@@ -31,6 +32,20 @@ export function createFigureProductSession(store) {
31
32
  function read() {
32
33
  return { ...workspace.read(), id, loading, saving, error };
33
34
  }
35
+ /*
36
+ The version on screen, one object for as long as it is the version: the workspace hands out one asset per version
37
+ (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.
38
+ */
39
+ let shown;
40
+ function revisionNow() {
41
+ const view = workspace.read();
42
+ if (!view.context || view.revision === null)
43
+ return (shown = undefined);
44
+ const context = view.context;
45
+ if (shown?.asset !== view.asset)
46
+ shown = revisionFromView(view, () => workspace.export({ context }).text);
47
+ return shown;
48
+ }
34
49
  /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */
35
50
  const unconfirmed = new Map();
36
51
  /*
@@ -78,6 +93,8 @@ export function createFigureProductSession(store) {
78
93
  }
79
94
  return {
80
95
  read,
96
+ /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */
97
+ revisionNow,
81
98
  /**
82
99
  * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`
83
100
  * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.
@@ -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 {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\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. */\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/** 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 /** 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\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 function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string): T {\n const before = workspace.read().revision\n const result = run()\n if (workspace.read().revision === before) return result\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 function read() {\n return { ...workspace.read(), id, loading, saving, error }\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 function writable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\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 done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\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 startNew({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset('새 Figure')))\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 Error('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 made from an AI proposal is created under the proposal's key (ADR-0097 §5.6 item 5): the same\n * proposal taken twice meets the first figure on the server, never a second one. Only on a new figure not yet\n * touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw Error('손대지 않은 새 모델에만 쓸 수 있습니다.')\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 Error('제안 식별자가 올바르지 않습니다.')\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 Error('먼저 모델을 정상적으로 열어 주세요.')\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() }))\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 } = writable()\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 }: { thumbnail?: string; type?: 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 saving = true\n try {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }) })\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 }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\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 /*\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 } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\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 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. */\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/** 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 /** 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\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 function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string): T {\n const before = workspace.read().revision\n const result = run()\n if (workspace.read().revision === before) return result\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 function read() {\n return { ...workspace.read(), id, 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 function writable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\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 done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !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 startNew({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset('새 Figure')))\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 Error('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 made from an AI proposal is created under the proposal's key (ADR-0097 §5.6 item 5): the same\n * proposal taken twice meets the first figure on the server, never a second one. Only on a new figure not yet\n * touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw Error('손대지 않은 새 모델에만 쓸 수 있습니다.')\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 Error('제안 식별자가 올바르지 않습니다.')\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 Error('먼저 모델을 정상적으로 열어 주세요.')\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() }))\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 } = writable()\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 }: { thumbnail?: string; type?: 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 saving = true\n try {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }) })\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 }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\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 /*\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 } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}