@things-factory/figure-ui 10.1.198 → 10.1.201

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 (74) hide show
  1. package/client/modeller/editor.ts +7 -3
  2. package/client/modeller/fasten-form.ts +18 -35
  3. package/client/modeller/fasten-geometry.ts +67 -115
  4. package/client/modeller/figure-fasten-dialog.ts +4 -12
  5. package/client/modeller/figure-links.ts +6 -7
  6. package/client/modeller/joint-drag.ts +9 -11
  7. package/client/modeller/links.ts +35 -99
  8. package/client/modeller/refusals.ts +2 -6
  9. package/dist-client/modeller/editor.js +7 -2
  10. package/dist-client/modeller/editor.js.map +1 -1
  11. package/dist-client/modeller/fasten-form.d.ts +5 -8
  12. package/dist-client/modeller/fasten-form.js +16 -28
  13. package/dist-client/modeller/fasten-form.js.map +1 -1
  14. package/dist-client/modeller/fasten-geometry.d.ts +20 -19
  15. package/dist-client/modeller/fasten-geometry.js +54 -113
  16. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  17. package/dist-client/modeller/figure-fasten-dialog.js +4 -12
  18. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  19. package/dist-client/modeller/figure-links.d.ts +1 -1
  20. package/dist-client/modeller/figure-links.js +6 -7
  21. package/dist-client/modeller/figure-links.js.map +1 -1
  22. package/dist-client/modeller/joint-drag.js +9 -11
  23. package/dist-client/modeller/joint-drag.js.map +1 -1
  24. package/dist-client/modeller/links.d.ts +19 -26
  25. package/dist-client/modeller/links.js +36 -94
  26. package/dist-client/modeller/links.js.map +1 -1
  27. package/dist-client/modeller/refusals.d.ts +1 -1
  28. package/dist-client/modeller/refusals.js +2 -6
  29. package/dist-client/modeller/refusals.js.map +1 -1
  30. package/dist-client/tsconfig.tsbuildinfo +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +6 -6
  33. package/test/ai-proposal.test.ts +1 -1
  34. package/test/browser/acceptance-page.js +11 -11
  35. package/test/browser/detail-page.js +1 -1
  36. package/test/browser/detail-tabs-page.js +1 -1
  37. package/test/browser/editing-page.js +1 -1
  38. package/test/browser/fix-alignment.check.mjs +3 -3
  39. package/test/browser/links.check.mjs +20 -8
  40. package/test/browser/modeller-page.js +3 -3
  41. package/test/browser/part-ids.check.mjs +2 -2
  42. package/test/browser/parts-page.js +2 -2
  43. package/test/browser/product-page.check.mjs +3 -3
  44. package/test/browser/product-page.js +1 -1
  45. package/test/browser/row-preview.check.mjs +2 -2
  46. package/test/browser/screen-page.js +3 -3
  47. package/test/browser/section-page.js +3 -3
  48. package/test/browser/section.check.mjs +4 -4
  49. package/test/clip-ai-proposal.test.ts +1 -1
  50. package/test/clip-consent.test.ts +1 -1
  51. package/test/command-preview.test.ts +8 -8
  52. package/test/face-mapping.test.ts +2 -2
  53. package/test/fasten-drawing.test.ts +14 -2
  54. package/test/fasten-form.test.ts +11 -3
  55. package/test/fasten-geometry.test.ts +22 -31
  56. package/test/fix-alignment.test.ts +16 -4
  57. package/test/gizmo.test.ts +2 -2
  58. package/test/joint-drag.test.ts +13 -2
  59. package/test/joint-fan.test.ts +1 -1
  60. package/test/joint-frames.test.ts +74 -45
  61. package/test/joints.ts +1 -1
  62. package/test/links.test.ts +68 -68
  63. package/test/live.test.ts +2 -2
  64. package/test/preview-size.test.ts +1 -1
  65. package/test/refusals.test.ts +18 -20
  66. package/test/resize-with-placement.test.ts +6 -6
  67. package/test/revision.test.ts +4 -5
  68. package/test/section-panel.test.ts +1 -1
  69. package/translations/en.json +9 -11
  70. package/translations/es.json +9 -11
  71. package/translations/ja.json +9 -11
  72. package/translations/ko.json +9 -11
  73. package/translations/ms.json +9 -11
  74. package/translations/zh.json +9 -11
@@ -4,15 +4,14 @@ import type { FastenChange } from './figure-fasten-drawing.js';
4
4
  import { type Alignment, type Axis } from './links.js';
5
5
  import type { FigureRevision } from './revision.js';
6
6
  export interface FastenForm {
7
- face: FigureFace | 'centre';
8
7
  toPlane: boolean;
9
8
  target: string;
10
9
  targetFace: FigureFace;
11
- facing: 'meet' | 'flush';
10
+ /** How it meets the face: its near end on it, its far end on it (flush), or its middle (ADR-0140 3.11). */
11
+ facing: FastenMode;
12
12
  gap: number;
13
13
  /** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */
14
14
  gapKept: unknown;
15
- twist: number;
16
15
  align: Partial<Record<Axis, Alignment>>;
17
16
  alignMm: Partial<Record<Axis, number>>;
18
17
  alignInput: Partial<Record<Axis, string>>;
@@ -38,15 +37,13 @@ export declare function aimFasten(revision: FigureRevision, part: string, form:
38
37
  }): FastenForm;
39
38
  /** A change the drawing asks: a face, a way, a gap or a place across the face. */
40
39
  export declare function fastenChanged(revision: FigureRevision, part: string, form: FastenForm, change: FastenChange): FastenForm;
41
- /** What a form comes to: the face aimed at, the part's own face, the way, the drawing of the face and the command. */
40
+ /** What a form comes to: the face aimed at, the way, the drawing of the face and the command. */
42
41
  export interface FastenPlan {
43
42
  /** Nothing is aimed at until a part or the plane is chosen. */
44
43
  aim?: FastenTarget;
45
44
  theirFaces: string[];
46
45
  targetFace: FigureFace;
47
46
  mode: FastenMode;
48
- /** The part's own face: not chosen, the target's face and the way give it (user 2026-10-01; the kernel takes no other). */
49
- myFace: FigureFace | 'centre';
50
47
  view?: FaceView;
51
48
  /** The axes across the face asked in rows: where the drawing cannot set the place. */
52
49
  alignRows: {
@@ -62,8 +59,8 @@ export interface FastenPlan {
62
59
  }
63
60
  export declare function fastenPlanOf(revision: FigureRevision, part: string, f: FastenForm): FastenPlan;
64
61
  type Say = (key: string, params?: Record<string, string>) => string;
65
- /** 「lid 의 아랫면을 bin 의 윗면에 맞닿게」: what the fastening is, in one line, the part's own face said as a result. */
66
- export declare function fastenSentenceOf(part: string, form: FastenForm, plan: FastenPlan, planeWord: string, say: Say): string;
62
+ /** 「bin의 윗면에 올려놓기」: what the fastening is, in one line -- the part is the one the window is open on, not named again. */
63
+ export declare function fastenSentenceOf(form: FastenForm, plan: FastenPlan, planeWord: string, say: Say): string;
67
64
  /** What the folded details hold that is not their default, said while folded; nothing for the default. */
68
65
  export declare function fastenDetailWordsOf(form: FastenForm, say: Say): string[];
69
66
  export {};
@@ -1,21 +1,19 @@
1
1
  import { FIGURE_FACES } from '@hatiolab/figure-model';
2
- import { faceViewOf, modeOf, myFaceFor } from './fasten-geometry.js';
3
- import { alignOf, alignRowsOf, canFasten, facesOf, fasteningOf, fastenGapOf, hangsFrom, heldBy, ownFacesOf, placeKeptOf } from './links.js';
2
+ import { faceViewOf } from './fasten-geometry.js';
3
+ import { alignOf, alignRowsOf, canFasten, facesOf, fasteningOf, fastenGapOf, hangsFrom, heldBy, mountingPlaneOf, placeKeptOf } from './links.js';
4
4
  /*
5
- * A fastening as it is chosen, before it is done: what, which face, how, where across the face. The fastening window
5
+ * A fastening as it is chosen, before it is done: to what, which face, how, where across the face. The fastening window
6
6
  * (`figure-fasten-dialog.ts`) holds one and changes it as the author chooses; the attach command is worked out from it
7
7
  * only here, so the window, its preview and the panel's 「정렬 고치기」 send the same command for the same choice.
8
8
  */
9
9
  const FACES = Object.keys(FIGURE_FACES);
10
10
  export const freshFasten = () => ({
11
- face: 'bottom',
12
11
  toPlane: false,
13
12
  target: '',
14
13
  targetFace: 'top',
15
14
  facing: 'meet',
16
15
  gap: 0,
17
16
  gapKept: undefined,
18
- twist: 0,
19
17
  align: {},
20
18
  alignMm: {},
21
19
  alignInput: {}
@@ -51,7 +49,7 @@ export function aimFasten(revision, part, form, to) {
51
49
  : f.target
52
50
  ? { part: f.target, face: targetFace }
53
51
  : undefined;
54
- return { ...f, ...(aim ? placeKeptOf(revision, part, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) };
52
+ return { ...f, ...(aim ? placeKeptOf(revision, part, aim) : { align: {}, alignMm: {}, alignInput: {} }) };
55
53
  }
56
54
  /** A change the drawing asks: a face, a way, a gap or a place across the face. */
57
55
  export function fastenChanged(revision, part, form, change) {
@@ -59,11 +57,7 @@ export function fastenChanged(revision, part, form, change) {
59
57
  if ('targetFace' in change)
60
58
  return aimFasten(revision, part, f, { targetFace: change.targetFace });
61
59
  if ('mode' in change)
62
- return {
63
- ...f,
64
- face: change.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face,
65
- facing: change.mode === 'flush' ? 'flush' : 'meet'
66
- };
60
+ return { ...f, facing: change.mode };
67
61
  if ('gap' in change)
68
62
  return { ...f, gap: change.gap, gapKept: undefined };
69
63
  return {
@@ -74,7 +68,6 @@ export function fastenChanged(revision, part, form, change) {
74
68
  }
75
69
  export function fastenPlanOf(revision, part, f) {
76
70
  const asset = revision.asset;
77
- const mineFaces = ownFacesOf(asset, part, f.toPlane);
78
71
  const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
79
72
  const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]);
80
73
  const aim = f.toPlane
@@ -82,21 +75,18 @@ export function fastenPlanOf(revision, part, f) {
82
75
  : f.target
83
76
  ? { part: f.target, face: targetFace }
84
77
  : undefined;
85
- const mode = f.toPlane ? 'meet' : modeOf(f.face, f.facing);
86
- const derived = aim ? myFaceFor(revision, part, aim, mode) : undefined;
87
- const myFace = derived ?? (mineFaces.includes(f.face) ? f.face : mineFaces[0]);
78
+ /* The plane takes a part standing on it or hanging below it, a wall only one standing out of it (ADR-0140 3.11). */
79
+ const mode = f.toPlane ? (mountingPlaneOf(asset).facings.includes(f.facing) ? f.facing : 'meet') : f.facing;
88
80
  if (!aim)
89
- return { theirFaces, targetFace, mode, myFace, alignRows: [] };
90
- const align = alignOf(asset, part, aim, f.align, f.alignMm, !!f.twist, f.alignInput);
91
- const view = faceViewOf(revision, part, aim);
81
+ return { theirFaces, targetFace, mode, alignRows: [] };
82
+ const align = alignOf(asset, aim, f.align, f.alignMm, f.alignInput);
83
+ const view = faceViewOf(revision, part, aim, mode);
92
84
  const action = {
93
85
  kind: 'attach',
94
86
  part,
95
- face: myFace,
96
87
  to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
97
- ...(f.toPlane ? {} : { facing: f.facing }),
88
+ ...(f.toPlane ? (mode === 'flush' ? { facing: mode } : {}) : { facing: mode }),
98
89
  ...fastenGapOf(f),
99
- twist: f.twist,
100
90
  ...(align ? { align } : {}),
101
91
  replace: !!heldBy(asset, part)
102
92
  };
@@ -105,17 +95,16 @@ export function fastenPlanOf(revision, part, f) {
105
95
  theirFaces,
106
96
  targetFace,
107
97
  mode,
108
- myFace,
109
98
  view,
110
- alignRows: alignRowsOf(asset, part, aim, view, !!f.twist, f.alignInput),
99
+ alignRows: alignRowsOf(asset, aim, view, f.alignInput),
111
100
  action,
112
101
  ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } })
113
102
  };
114
103
  }
115
- /** 「lid 의 아랫면을 bin 의 윗면에 맞닿게」: what the fastening is, in one line, the part's own face said as a result. */
116
- export function fastenSentenceOf(part, form, plan, planeWord, say) {
104
+ /** 「bin의 윗면에 올려놓기」: what the fastening is, in one line -- the part is the one the window is open on, not named again. */
105
+ export function fastenSentenceOf(form, plan, planeWord, say) {
117
106
  const to = form.toPlane ? planeWord : `${form.target}의 ${say('figure.label.face-' + plan.targetFace)}`;
118
- return say(`figure.text.fasten-sentence-${plan.mode}`, { part, face: say('figure.label.face-' + plan.myFace), to });
107
+ return say(`figure.text.fasten-sentence-${plan.mode}`, { to });
119
108
  }
120
109
  /** What the folded details hold that is not their default, said while folded; nothing for the default. */
121
110
  export function fastenDetailWordsOf(form, say) {
@@ -124,8 +113,7 @@ export function fastenDetailWordsOf(form, say) {
124
113
  ? [say('figure.text.fasten-summary-gap-worked-out')]
125
114
  : form.gap
126
115
  ? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]
127
- : []),
128
- ...(form.twist ? [say('figure.text.fasten-summary-twist', { degrees: String(form.twist) })] : [])
116
+ : [])
129
117
  ];
130
118
  }
131
119
  //# sourceMappingURL=fasten-form.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fasten-form.js","sourceRoot":"","sources":["../../client/modeller/fasten-form.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_FACES } from '@hatiolab/figure-model'\nimport type { FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model'\n\nimport { faceViewOf, modeOf, myFaceFor, type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js'\nimport type { FastenChange } from './figure-fasten-drawing.js'\nimport {\n alignOf,\n alignRowsOf,\n canFasten,\n facesOf,\n fasteningOf,\n fastenGapOf,\n hangsFrom,\n heldBy,\n ownFacesOf,\n placeKeptOf,\n type Alignment,\n type Axis\n} from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * A fastening as it is chosen, before it is done: what, which face, how, where across the face. The fastening window\n * (`figure-fasten-dialog.ts`) holds one and changes it as the author chooses; the attach command is worked out from it\n * only here, so the window, its preview and the panel's 「정렬 고치기」 send the same command for the same choice.\n */\n\nconst FACES = Object.keys(FIGURE_FACES) as FigureFace[]\n\nexport interface FastenForm {\n face: FigureFace | 'centre'\n toPlane: boolean\n target: string\n targetFace: FigureFace\n facing: 'meet' | 'flush'\n gap: number\n /** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */\n gapKept: unknown\n twist: number\n align: Partial<Record<Axis, Alignment>>\n alignMm: Partial<Record<Axis, number>>\n alignInput: Partial<Record<Axis, string>>\n}\n\nexport const freshFasten = (): FastenForm => ({\n face: 'bottom',\n toPlane: false,\n target: '',\n targetFace: 'top',\n facing: 'meet',\n gap: 0,\n gapKept: undefined,\n twist: 0,\n align: {},\n alignMm: {},\n alignInput: {}\n})\n\n/** The part's own fastening as a form, or a fresh one: a window opens on what the part has (walk 1, 2026-09-26). */\nexport function fastenFormOf(revision: FigureRevision, part: string): FastenForm {\n const own = fasteningOf(revision, part)\n return own && !own.atOrigin ? ({ ...freshFasten(), ...own } as FastenForm) : freshFasten()\n}\n\n/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */\nexport function fastenTargetsOf(revision: FigureRevision, part: string): Array<{ part: string; loops: boolean }> {\n const asset = revision.asset\n return (\n asset.appearance\n .map(look => look.target)\n .filter(one => one !== part && canFasten(asset, one))\n /* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */\n .map(one => ({ part: one, loops: hangsFrom(asset, one, part) }))\n )\n}\n\n/**\n * A new aim: the plane, another part, or another face of the same part. Where on the face is filled in as where the\n * part stands now, so fastening it keeps it there (`placeKeptOf`); the author changes it from there.\n */\nexport function aimFasten(\n revision: FigureRevision,\n part: string,\n form: FastenForm,\n to: { plane: true } | { part: string } | { targetFace: FigureFace }\n): FastenForm {\n const f: FastenForm =\n 'plane' in to\n ? { ...form, toPlane: true, target: '' }\n : 'part' in to\n ? { ...form, toPlane: false, target: to.part }\n : { ...form, targetFace: to.targetFace }\n const theirFaces = f.target ? facesOf(revision.asset, f.target) : FACES\n const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]) as FigureFace\n const aim: FastenTarget | undefined = f.toPlane\n ? 'plane'\n : f.target\n ? { part: f.target, face: targetFace }\n : undefined\n return { ...f, ...(aim ? placeKeptOf(revision, part, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) }\n}\n\n/** A change the drawing asks: a face, a way, a gap or a place across the face. */\nexport function fastenChanged(\n revision: FigureRevision,\n part: string,\n form: FastenForm,\n change: FastenChange\n): FastenForm {\n const f = form\n if ('targetFace' in change) return aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace })\n if ('mode' in change)\n return {\n ...f,\n face: change.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face,\n facing: change.mode === 'flush' ? 'flush' : 'meet'\n }\n if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined }\n return {\n ...f,\n align: { ...f.align, [change.axis]: change.how },\n alignMm: change.how === 'mm' ? { ...f.alignMm, [change.axis]: change.mm ?? 0 } : f.alignMm\n }\n}\n\n/** What a form comes to: the face aimed at, the part's own face, the way, the drawing of the face and the command. */\nexport interface FastenPlan {\n /** Nothing is aimed at until a part or the plane is chosen. */\n aim?: FastenTarget\n theirFaces: string[]\n targetFace: FigureFace\n mode: FastenMode\n /** The part's own face: not chosen, the target's face and the way give it (user 2026-10-01; the kernel takes no other). */\n myFace: FigureFace | 'centre'\n view?: FaceView\n /** The axes across the face asked in rows: where the drawing cannot set the place. */\n alignRows: { axis: Axis; edges: boolean }[]\n action?: FigureAuthoringAction\n /** The face the command would meet, lit on the canvas while it is tried. */\n face?: { part: string; face: string }\n}\n\nexport function fastenPlanOf(revision: FigureRevision, part: string, f: FastenForm): FastenPlan {\n const asset = revision.asset\n const mineFaces = ownFacesOf(asset, part, f.toPlane)\n const theirFaces = f.target ? facesOf(asset, f.target) : FACES\n const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]) as FigureFace\n const aim: FastenTarget | undefined = f.toPlane\n ? 'plane'\n : f.target\n ? { part: f.target, face: targetFace }\n : undefined\n const mode: FastenMode = f.toPlane ? 'meet' : modeOf(f.face, f.facing)\n const derived = aim ? myFaceFor(revision, part, aim, mode) : undefined\n const myFace = derived ?? (mineFaces.includes(f.face) ? f.face : mineFaces[0]!)\n if (!aim) return { theirFaces, targetFace, mode, myFace, alignRows: [] }\n const align = alignOf(asset, part, aim, f.align, f.alignMm, !!f.twist, f.alignInput)\n const view = faceViewOf(revision, part, aim)\n const action = {\n kind: 'attach',\n part,\n face: myFace,\n to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },\n ...(f.toPlane ? {} : { facing: f.facing }),\n ...fastenGapOf(f),\n twist: f.twist,\n ...(align ? { align } : {}),\n replace: !!heldBy(asset, part)\n } as FigureAuthoringAction\n return {\n aim,\n theirFaces,\n targetFace,\n mode,\n myFace,\n view,\n alignRows: alignRowsOf(asset, part, aim, view, !!f.twist, f.alignInput),\n action,\n ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } })\n }\n}\n\ntype Say = (key: string, params?: Record<string, string>) => string\n\n/** 「lid 의 아랫면을 bin 의 윗면에 맞닿게」: what the fastening is, in one line, the part's own face said as a result. */\nexport function fastenSentenceOf(\n part: string,\n form: FastenForm,\n plan: FastenPlan,\n planeWord: string,\n say: Say\n): string {\n const to = form.toPlane ? planeWord : `${form.target}의 ${say('figure.label.face-' + plan.targetFace)}`\n return say(`figure.text.fasten-sentence-${plan.mode}`, { part, face: say('figure.label.face-' + plan.myFace), to })\n}\n\n/** What the folded details hold that is not their default, said while folded; nothing for the default. */\nexport function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {\n return [\n ...(form.gapKept !== undefined\n ? [say('figure.text.fasten-summary-gap-worked-out')]\n : form.gap\n ? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]\n : []),\n ...(form.twist ? [say('figure.text.fasten-summary-twist', { degrees: String(form.twist) })] : [])\n ]\n}\n"]}
1
+ {"version":3,"file":"fasten-form.js","sourceRoot":"","sources":["../../client/modeller/fasten-form.ts"],"names":[],"mappings":"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","sourcesContent":["import { FIGURE_FACES } from '@hatiolab/figure-model'\nimport type { FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model'\n\nimport { faceViewOf, type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js'\nimport type { FastenChange } from './figure-fasten-drawing.js'\nimport {\n alignOf,\n alignRowsOf,\n canFasten,\n facesOf,\n fasteningOf,\n fastenGapOf,\n hangsFrom,\n heldBy,\n mountingPlaneOf,\n placeKeptOf,\n type Alignment,\n type Axis\n} from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * A fastening as it is chosen, before it is done: to what, which face, how, where across the face. The fastening window\n * (`figure-fasten-dialog.ts`) holds one and changes it as the author chooses; the attach command is worked out from it\n * only here, so the window, its preview and the panel's 「정렬 고치기」 send the same command for the same choice.\n */\n\nconst FACES = Object.keys(FIGURE_FACES) as FigureFace[]\n\nexport interface FastenForm {\n toPlane: boolean\n target: string\n targetFace: FigureFace\n /** How it meets the face: its near end on it, its far end on it (flush), or its middle (ADR-0140 3.11). */\n facing: FastenMode\n gap: number\n /** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */\n gapKept: unknown\n align: Partial<Record<Axis, Alignment>>\n alignMm: Partial<Record<Axis, number>>\n alignInput: Partial<Record<Axis, string>>\n}\n\nexport const freshFasten = (): FastenForm => ({\n toPlane: false,\n target: '',\n targetFace: 'top',\n facing: 'meet',\n gap: 0,\n gapKept: undefined,\n align: {},\n alignMm: {},\n alignInput: {}\n})\n\n/** The part's own fastening as a form, or a fresh one: a window opens on what the part has (walk 1, 2026-09-26). */\nexport function fastenFormOf(revision: FigureRevision, part: string): FastenForm {\n const own = fasteningOf(revision, part)\n return own && !own.atOrigin ? ({ ...freshFasten(), ...own } as FastenForm) : freshFasten()\n}\n\n/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */\nexport function fastenTargetsOf(revision: FigureRevision, part: string): Array<{ part: string; loops: boolean }> {\n const asset = revision.asset\n return (\n asset.appearance\n .map(look => look.target)\n .filter(one => one !== part && canFasten(asset, one))\n /* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */\n .map(one => ({ part: one, loops: hangsFrom(asset, one, part) }))\n )\n}\n\n/**\n * A new aim: the plane, another part, or another face of the same part. Where on the face is filled in as where the\n * part stands now, so fastening it keeps it there (`placeKeptOf`); the author changes it from there.\n */\nexport function aimFasten(\n revision: FigureRevision,\n part: string,\n form: FastenForm,\n to: { plane: true } | { part: string } | { targetFace: FigureFace }\n): FastenForm {\n const f: FastenForm =\n 'plane' in to\n ? { ...form, toPlane: true, target: '' }\n : 'part' in to\n ? { ...form, toPlane: false, target: to.part }\n : { ...form, targetFace: to.targetFace }\n const theirFaces = f.target ? facesOf(revision.asset, f.target) : FACES\n const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]) as FigureFace\n const aim: FastenTarget | undefined = f.toPlane\n ? 'plane'\n : f.target\n ? { part: f.target, face: targetFace }\n : undefined\n return { ...f, ...(aim ? placeKeptOf(revision, part, aim) : { align: {}, alignMm: {}, alignInput: {} }) }\n}\n\n/** A change the drawing asks: a face, a way, a gap or a place across the face. */\nexport function fastenChanged(\n revision: FigureRevision,\n part: string,\n form: FastenForm,\n change: FastenChange\n): FastenForm {\n const f = form\n if ('targetFace' in change) return aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace })\n if ('mode' in change) return { ...f, facing: change.mode }\n if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined }\n return {\n ...f,\n align: { ...f.align, [change.axis]: change.how },\n alignMm: change.how === 'mm' ? { ...f.alignMm, [change.axis]: change.mm ?? 0 } : f.alignMm\n }\n}\n\n/** What a form comes to: the face aimed at, the way, the drawing of the face and the command. */\nexport interface FastenPlan {\n /** Nothing is aimed at until a part or the plane is chosen. */\n aim?: FastenTarget\n theirFaces: string[]\n targetFace: FigureFace\n mode: FastenMode\n view?: FaceView\n /** The axes across the face asked in rows: where the drawing cannot set the place. */\n alignRows: { axis: Axis; edges: boolean }[]\n action?: FigureAuthoringAction\n /** The face the command would meet, lit on the canvas while it is tried. */\n face?: { part: string; face: string }\n}\n\nexport function fastenPlanOf(revision: FigureRevision, part: string, f: FastenForm): FastenPlan {\n const asset = revision.asset\n const theirFaces = f.target ? facesOf(asset, f.target) : FACES\n const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]) as FigureFace\n const aim: FastenTarget | undefined = f.toPlane\n ? 'plane'\n : f.target\n ? { part: f.target, face: targetFace }\n : undefined\n /* The plane takes a part standing on it or hanging below it, a wall only one standing out of it (ADR-0140 3.11). */\n const mode: FastenMode = f.toPlane ? (mountingPlaneOf(asset).facings.includes(f.facing) ? f.facing : 'meet') : f.facing\n if (!aim) return { theirFaces, targetFace, mode, alignRows: [] }\n const align = alignOf(asset, aim, f.align, f.alignMm, f.alignInput)\n const view = faceViewOf(revision, part, aim, mode)\n const action = {\n kind: 'attach',\n part,\n to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },\n ...(f.toPlane ? (mode === 'flush' ? { facing: mode } : {}) : { facing: mode }),\n ...fastenGapOf(f),\n ...(align ? { align } : {}),\n replace: !!heldBy(asset, part)\n } as FigureAuthoringAction\n return {\n aim,\n theirFaces,\n targetFace,\n mode,\n view,\n alignRows: alignRowsOf(asset, aim, view, f.alignInput),\n action,\n ...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } })\n }\n}\n\ntype Say = (key: string, params?: Record<string, string>) => string\n\n/** 「bin의 윗면에 올려놓기」: what the fastening is, in one line -- the part is the one the window is open on, not named again. */\nexport function fastenSentenceOf(\n form: FastenForm,\n plan: FastenPlan,\n planeWord: string,\n say: Say\n): string {\n const to = form.toPlane ? planeWord : `${form.target}의 ${say('figure.label.face-' + plan.targetFace)}`\n return say(`figure.text.fasten-sentence-${plan.mode}`, { to })\n}\n\n/** What the folded details hold that is not their default, said while folded; nothing for the default. */\nexport function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {\n return [\n ...(form.gapKept !== undefined\n ? [say('figure.text.fasten-summary-gap-worked-out')]\n : form.gap\n ? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]\n : [])\n ]\n}\n"]}
@@ -1,32 +1,30 @@
1
- import type { FigureAsset, FigureFace } from '@hatiolab/figure-model';
1
+ import type { FigureAsset, FigureFaceMeasure, FigureFacing } from '@hatiolab/figure-model';
2
2
  import { type Alignment } from './links.js';
3
3
  import type { FigureRevision } from './revision.js';
4
4
  type Axis = 'x' | 'y' | 'z';
5
- export type FastenMode = 'meet' | 'flush' | 'centre';
5
+ export type FastenMode = FigureFacing;
6
6
  export type FastenTarget = {
7
7
  part: string;
8
8
  face: string;
9
9
  } | 'plane';
10
10
  /**
11
- * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its
12
- * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.
13
- * Undefined when the part has no such face.
14
- */
15
- export declare function myFaceFor(revision: FigureRevision, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined;
16
- /** Which way the fastening is, read back from the part's face and facing. */
17
- export declare const modeOf: (face: string, facing: "meet" | "flush") => FastenMode;
18
- /**
19
- * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each -- none
20
- * for a plane with no declared size -- the part's footprint on each, and whether its edges can be lined up with.
11
+ * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each as a
12
+ * fastening lines up to it -- none for a plane with no declared size -- the part's real extent on each, whether its
13
+ * edges can be lined up with, and the kernel's measure the drawing asks again as the part is moved.
21
14
  */
22
15
  export interface FaceView {
23
16
  axes: [Axis, Axis];
24
17
  face: Partial<Record<Axis, number>>;
25
18
  part: Record<Axis, number>;
26
19
  edges: Record<Axis, boolean>;
20
+ measure: FigureFaceMeasure;
27
21
  }
28
- export declare function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget): FaceView | undefined;
29
- /** Where the part's centre is from the face's centre along one axis, for the way it is aligned there. */
22
+ export declare function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget, facing: FastenMode): FaceView | undefined;
23
+ /**
24
+ * Where the part's centre is from the face's centre along one axis, for the way it is aligned there: the kernel's
25
+ * `figureFastenPlaceOf` (ADR-0140 D2), the form's distances -- typed, or a share of the size read as millimetres -- as
26
+ * a distance, a position field's as the centre.
27
+ */
30
28
  export declare function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number;
31
29
  /**
32
30
  * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:
@@ -39,10 +37,9 @@ export declare function alignmentAt(d: number, face: number | undefined, part: n
39
37
  caught?: 'centre' | 'inside' | 'outside';
40
38
  };
41
39
  /**
42
- * The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the
43
- * least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre
44
- * when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out
45
- * is something fastened on it. `unmeasured` when the face has no size to measure against.
40
+ * The alignment that brings the part inside the face, for 정렬·간격 고치기: the kernel's least move
41
+ * (`figureFaceInsideAlignOf`), in the form's words. `inside` when nothing of it is past an edge: what is out is something
42
+ * fastened on it. `unmeasured` when the face has no size to measure against.
46
43
  */
47
44
  export declare function insideAlignmentOf(view: FaceView | undefined, align: Partial<Record<Axis, Alignment>>, alignMm: Partial<Record<Axis, number>>): {
48
45
  align: Partial<Record<Axis, Alignment>>;
@@ -51,9 +48,13 @@ export declare function insideAlignmentOf(view: FaceView | undefined, align: Par
51
48
  } | {
52
49
  cannot: 'inside' | 'unmeasured';
53
50
  };
54
- /** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
51
+ /**
52
+ * How far the part comes past the face's edges, per side, whether any of it lies over the face, and whether what
53
+ * touches the face is on it -- with its middle `at` from the face's middle: the kernel's (`figureFaceOverhangAt`).
54
+ */
55
55
  export declare function overhangOf(view: FaceView, at: Record<Axis, number>): {
56
56
  touching: boolean;
57
+ contact: 'on' | 'off' | 'unknown';
57
58
  sides: Array<{
58
59
  axis: Axis;
59
60
  side: 'low' | 'high';
@@ -1,90 +1,40 @@
1
- import { figurePartFaceOf } from '@hatiolab/figure-model';
2
- import { boxOf } from './align.js';
3
- import { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf } from './links.js';
4
- const AXES = ['x', 'y', 'z'];
5
- const OPPOSITE = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' };
6
- const NORMAL = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] };
7
- /** The world direction a part's face looks along now, as drawn, turns and all; by its name when the face's outline is not read. */
8
- function directionOf(revision, part, face) {
9
- const area = (() => {
1
+ import { figureFaceInsideAlignOf, figureFaceMeasureOf, figureFaceOverhangAt, figureFastenPlaceOf } from '@hatiolab/figure-model';
2
+ import { alignAxesOf } from './links.js';
3
+ export function faceViewOf(revision, part, target, facing) {
4
+ const measure = (() => {
10
5
  try {
11
- return figurePartFaceOf(revision.asset, part, face, revision.inputs);
6
+ return figureFaceMeasureOf(revision.asset, part, revision.inputs, { on: target, facing });
12
7
  }
13
8
  catch {
14
9
  return undefined;
15
10
  }
16
11
  })();
17
- if (area) {
18
- const k = area.normal.findIndex(v => Math.abs(v) > 0.5);
19
- if (k >= 0)
20
- return [AXES[k], area.normal[k] > 0 ? 1 : -1];
21
- }
22
- return face in NORMAL ? NORMAL[face] : undefined;
23
- }
24
- /**
25
- * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its
26
- * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.
27
- * Undefined when the part has no such face.
28
- */
29
- export function myFaceFor(revision, part, target, mode) {
30
- const asset = revision.asset;
31
- if (mode === 'centre')
32
- return target === 'plane' ? undefined : 'centre';
33
- const want = target === 'plane'
34
- ? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]
35
- : (() => {
36
- const d = directionOf(revision, target.part, target.face);
37
- return d && [d[0], (mode === 'meet' ? -d[1] : d[1])];
38
- })();
39
- if (!want)
40
- return undefined;
41
- const mine = facesOf(asset, part).filter((f) => f in NORMAL);
42
- const hit = mine.find(f => {
43
- const d = directionOf(revision, part, f);
44
- return d && d[0] === want[0] && d[1] === want[1];
45
- });
46
- return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face] : undefined);
47
- }
48
- /** Which way the fastening is, read back from the part's face and facing. */
49
- export const modeOf = (face, facing) => (face === 'centre' ? 'centre' : facing);
50
- export function faceViewOf(revision, part, target) {
51
- const asset = revision.asset;
52
- const across = alignAxesOf(asset, part, target);
53
- if (across.length !== 2)
54
- return undefined;
55
- const box = boxOf(revision, part);
56
- if (!box)
12
+ if (!measure)
57
13
  return undefined;
58
- const axes = [across[0].axis, across[1].axis];
14
+ const across = alignAxesOf(revision.asset, target);
59
15
  const face = {};
60
- if (target === 'plane') {
61
- for (const a of axes)
62
- if (hasInstanceSize(asset, a))
63
- face[a] = asset.designInputs[`size.${a}`];
64
- }
65
- else {
66
- const theirs = boxOf(revision, target.part);
67
- const normal = faceAxisOf(asset, target.part, target.face);
68
- for (const a of axes)
69
- if (theirs && a !== normal)
70
- face[a] = theirs.extent[a];
16
+ for (const a of measure.axes) {
17
+ const e = measure.edges[a];
18
+ if (e)
19
+ face[a] = e.hi - e.lo;
71
20
  }
21
+ const extent = (a) => measure.extent[a].hi - measure.extent[a].lo;
72
22
  return {
73
- axes,
23
+ axes: measure.axes,
74
24
  face,
75
- part: { x: box.extent.x, y: box.extent.y, z: box.extent.z },
76
- edges: Object.fromEntries(across.map(o => [o.axis, o.edges]))
25
+ part: { x: extent('x'), y: extent('y'), z: extent('z') },
26
+ edges: Object.fromEntries(measure.axes.map(a => [a, !!across.find(o => o.axis === a)?.edges])),
27
+ measure
77
28
  };
78
29
  }
79
- /** Where the part's centre is from the face's centre along one axis, for the way it is aligned there. */
30
+ /**
31
+ * Where the part's centre is from the face's centre along one axis, for the way it is aligned there: the kernel's
32
+ * `figureFastenPlaceOf` (ADR-0140 D2), the form's distances -- typed, or a share of the size read as millimetres -- as
33
+ * a distance, a position field's as the centre.
34
+ */
80
35
  export function offsetOf(how, mm, face, part) {
81
- if (how === 'min' && face !== undefined)
82
- return -(face - part) / 2;
83
- if (how === 'max' && face !== undefined)
84
- return (face - part) / 2;
85
- if (how === 'mm' || how === 'share')
86
- return mm ?? 0;
87
- return 0;
36
+ const align = how === 'mm' || how === 'share' ? { mm: mm ?? 0 } : how === 'min' || how === 'max' || how === 'centre' ? how : undefined;
37
+ return figureFastenPlaceOf({ align: { x: align }, face: { x: face }, part: { x: part } }).x;
88
38
  }
89
39
  /**
90
40
  * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:
@@ -108,55 +58,46 @@ export function alignmentAt(d, face, part, edges, catchMm) {
108
58
  }
109
59
  return { how: 'mm', mm: Math.round(d) };
110
60
  }
61
+ /** The form's alignment on each axis across the face, in the kernel's words: a typed or shared distance as millimetres. */
62
+ function kernelAlignOf(view, align, alignMm) {
63
+ const out = {};
64
+ for (const a of view.axes) {
65
+ const how = align[a] ?? 'centre';
66
+ out[a] = how === 'mm' || how === 'share' ? { mm: alignMm[a] ?? 0 } : how === 'min' || how === 'max' ? how : 'centre';
67
+ }
68
+ return out;
69
+ }
111
70
  /**
112
- * The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the
113
- * least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre
114
- * when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out
115
- * is something fastened on it. `unmeasured` when the face has no size to measure against.
71
+ * The alignment that brings the part inside the face, for 정렬·간격 고치기: the kernel's least move
72
+ * (`figureFaceInsideAlignOf`), in the form's words. `inside` when nothing of it is past an edge: what is out is something
73
+ * fastened on it. `unmeasured` when the face has no size to measure against.
116
74
  */
117
75
  export function insideAlignmentOf(view, align, alignMm) {
118
- if (!view || view.axes.some(a => view.face[a] === undefined))
76
+ if (!view)
119
77
  return { cannot: 'unmeasured' };
120
- const next = { align: { ...align }, alignMm: { ...alignMm }, moved: [] };
121
- for (const a of view.axes) {
122
- const face = view.face[a], part = view.part[a];
123
- const at = offsetOf(align[a], alignMm[a], face, part);
124
- const low = -face / 2 - (at - part / 2) > 0.5, high = at + part / 2 - face / 2 > 0.5;
125
- if (!low && !high)
126
- continue;
127
- next.moved.push(a);
128
- if (part > face) {
129
- next.align[a] = 'centre';
130
- continue;
131
- }
132
- const edge = ((low ? -1 : 1) * (face - part)) / 2;
133
- if (view.edges[a])
134
- next.align[a] = low ? 'min' : 'max';
135
- else
136
- ((next.align[a] = 'mm'), (next.alignMm[a] = Math.round(edge)));
78
+ const fix = figureFaceInsideAlignOf(view.measure, kernelAlignOf(view, align, alignMm), view.edges);
79
+ if ('cannot' in fix)
80
+ return fix;
81
+ const next = { align: { ...align }, alignMm: { ...alignMm }, moved: fix.moved };
82
+ for (const a of fix.moved) {
83
+ const how = fix.align[a];
84
+ if (how && typeof how === 'object')
85
+ ((next.align[a] = 'mm'), (next.alignMm[a] = how.mm));
86
+ else if (how)
87
+ next.align[a] = how;
137
88
  }
138
- return next.moved.length ? next : { cannot: 'inside' };
89
+ return next;
139
90
  }
140
- /** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */
91
+ /**
92
+ * How far the part comes past the face's edges, per side, whether any of it lies over the face, and whether what
93
+ * touches the face is on it -- with its middle `at` from the face's middle: the kernel's (`figureFaceOverhangAt`).
94
+ */
141
95
  export function overhangOf(view, at) {
142
- const sides = [];
143
- let touching = true;
144
- for (const a of view.axes) {
145
- const face = view.face[a];
146
- if (face === undefined)
147
- continue;
148
- const lo = at[a] - view.part[a] / 2, hi = at[a] + view.part[a] / 2;
149
- if (lo >= face / 2 || hi <= -face / 2)
150
- touching = false;
151
- if (-face / 2 - lo > 0.5)
152
- sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) });
153
- if (hi - face / 2 > 0.5)
154
- sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) });
155
- }
156
- return { touching, sides };
96
+ const o = figureFaceOverhangAt(view.measure, Object.fromEntries(view.axes.map(a => [a, { mm: at[a] }])));
97
+ return { touching: o.over, contact: o.contact, sides: o.sides };
157
98
  }
158
99
  /** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */
159
- export const turnsInPlace = (asset, part) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || ('face' in j.body1 && j.body1.face === 'centre')));
100
+ export const turnsInPlace = (asset, part) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.origin?.facing === 'centre'));
160
101
  /**
161
102
  * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up
162
103
  * for Y, which rises, so a front face is seen as it stands and not upside down.
@@ -1 +1 @@
1
- {"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figurePartFaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFace } from '@hatiolab/figure-model'\n\nimport { boxOf } from './align.js'\nimport { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf, type Alignment } from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part\n * lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part's\n * own face follows from those -- the kernel takes no other pairing (figure-model `attach`: meet is the facing face,\n * flush the same one, a face along another axis would need a turn).\n */\n\ntype Axis = 'x' | 'y' | 'z'\nconst AXES: Axis[] = ['x', 'y', 'z']\nexport type FastenMode = 'meet' | 'flush' | 'centre'\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\nconst OPPOSITE: Record<FigureFace, FigureFace> = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' }\nconst NORMAL: Record<FigureFace, [Axis, 1 | -1]> = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] }\n\n/** The world direction a part's face looks along now, as drawn, turns and all; by its name when the face's outline is not read. */\nfunction directionOf(revision: FigureRevision, part: string, face: string): [Axis, 1 | -1] | undefined {\n const area = (() => {\n try {\n return figurePartFaceOf(revision.asset, part, face, revision.inputs)\n } catch {\n return undefined\n }\n })()\n if (area) {\n const k = area.normal.findIndex(v => Math.abs(v) > 0.5)\n if (k >= 0) return [AXES[k]!, area.normal[k]! > 0 ? 1 : -1]\n }\n return face in NORMAL ? NORMAL[face as FigureFace] : undefined\n}\n\n/**\n * The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its\n * centre. Read from where each face looks now, so a part standing turned offers the face that is really there.\n * Undefined when the part has no such face.\n */\nexport function myFaceFor(revision: FigureRevision, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined {\n const asset = revision.asset\n if (mode === 'centre') return target === 'plane' ? undefined : 'centre'\n const want: [Axis, 1 | -1] | undefined =\n target === 'plane'\n ? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]\n : (() => {\n const d = directionOf(revision, target.part, target.face)\n return d && [d[0], (mode === 'meet' ? -d[1] : d[1]) as 1 | -1]\n })()\n if (!want) return undefined\n const mine = facesOf(asset, part).filter((f): f is FigureFace => f in NORMAL)\n const hit = mine.find(f => {\n const d = directionOf(revision, part, f)\n return d && d[0] === want[0] && d[1] === want[1]\n })\n return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face as FigureFace] : undefined)\n}\n\n/** Which way the fastening is, read back from the part's face and facing. */\nexport const modeOf = (face: string, facing: 'meet' | 'flush'): FastenMode => (face === 'centre' ? 'centre' : facing)\n\n/**\n * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each -- none\n * for a plane with no declared size -- the part's footprint on each, and whether its edges can be lined up with.\n */\nexport interface FaceView {\n axes: [Axis, Axis]\n face: Partial<Record<Axis, number>>\n part: Record<Axis, number>\n edges: Record<Axis, boolean>\n}\n\nexport function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget): FaceView | undefined {\n const asset = revision.asset\n const across = alignAxesOf(asset, part, target)\n if (across.length !== 2) return undefined\n const box = boxOf(revision, part)\n if (!box) return undefined\n const axes = [across[0]!.axis, across[1]!.axis] as [Axis, Axis]\n const face: Partial<Record<Axis, number>> = {}\n if (target === 'plane') {\n for (const a of axes) if (hasInstanceSize(asset, a)) face[a] = asset.designInputs[`size.${a}`]!\n } else {\n const theirs = boxOf(revision, target.part)\n const normal = faceAxisOf(asset, target.part, target.face)\n for (const a of axes) if (theirs && a !== normal) face[a] = theirs.extent[a]\n }\n return {\n axes,\n face,\n part: { x: box.extent.x, y: box.extent.y, z: box.extent.z },\n edges: Object.fromEntries(across.map(o => [o.axis, o.edges])) as Record<Axis, boolean>\n }\n}\n\n/** Where the part's centre is from the face's centre along one axis, for the way it is aligned there. */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n if (how === 'min' && face !== undefined) return -(face - part) / 2\n if (how === 'max' && face !== undefined) return (face - part) / 2\n if (how === 'mm' || how === 'share') return mm ?? 0\n return 0\n}\n\n/**\n * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:\n * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when\n * within `catchMm`; anywhere else a distance off centre, whole millimetres.\n */\nexport function alignmentAt(d: number, face: number | undefined, part: number, edges: boolean, catchMm: number): { how: Alignment; mm?: number; caught?: 'centre' | 'inside' | 'outside' } {\n if (Math.abs(d) <= catchMm) return { how: 'centre', caught: 'centre' }\n if (face !== undefined) {\n const inside = (face - part) / 2,\n outside = (face + part) / 2\n for (const [at, how] of [\n [-inside, 'min'],\n [inside, 'max']\n ] as const)\n if (Math.abs(d - at) <= catchMm && inside > 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }\n for (const at of [-outside, outside]) if (Math.abs(d - at) <= catchMm) return { how: 'mm', mm: Math.round(at), caught: 'outside' }\n }\n return { how: 'mm', mm: Math.round(d) }\n}\n\n/**\n * The alignment that brings the footprint inside the face, for 정렬·간격 고치기: on each axis it comes past an edge, the\n * least move -- to that edge from inside (`min` / `max`, or that distance where edges are not offered) -- or the centre\n * when the part is wider than the face and cannot be inside. `inside` when nothing of it is past an edge: what is out\n * is something fastened on it. `unmeasured` when the face has no size to measure against.\n */\nexport function insideAlignmentOf(\n view: FaceView | undefined,\n align: Partial<Record<Axis, Alignment>>,\n alignMm: Partial<Record<Axis, number>>\n): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; moved: Axis[] } | { cannot: 'inside' | 'unmeasured' } {\n if (!view || view.axes.some(a => view.face[a] === undefined)) return { cannot: 'unmeasured' }\n const next = { align: { ...align }, alignMm: { ...alignMm }, moved: [] as Axis[] }\n for (const a of view.axes) {\n const face = view.face[a]!,\n part = view.part[a]\n const at = offsetOf(align[a], alignMm[a], face, part)\n const low = -face / 2 - (at - part / 2) > 0.5,\n high = at + part / 2 - face / 2 > 0.5\n if (!low && !high) continue\n next.moved.push(a)\n if (part > face) {\n next.align[a] = 'centre'\n continue\n }\n const edge = ((low ? -1 : 1) * (face - part)) / 2\n if (view.edges[a]) next.align[a] = low ? 'min' : 'max'\n else ((next.align[a] = 'mm'), (next.alignMm[a] = Math.round(edge)))\n }\n return next.moved.length ? next : { cannot: 'inside' }\n}\n\n/** How far the footprint comes past the face's edges, per side, and whether it touches the face at all. */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> = []\n let touching = true\n for (const a of view.axes) {\n const face = view.face[a]\n if (face === undefined) continue\n const lo = at[a] - view.part[a] / 2,\n hi = at[a] + view.part[a] / 2\n if (lo >= face / 2 || hi <= -face / 2) touching = false\n if (-face / 2 - lo > 0.5) sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) })\n if (hi - face / 2 > 0.5) sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) })\n }\n return { touching, sides }\n}\n\n/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */\nexport const turnsInPlace = (asset: FigureAsset, part: string) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || ('face' in j.body1 && j.body1.face === 'centre')))\n\n/**\n * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up\n * for Y, which rises, so a front face is seen as it stands and not upside down.\n */\nexport function screenOf(axes: readonly [Axis, Axis]): { h: Axis; v: Axis; sign: Record<Axis, 1 | -1> } {\n const h = axes.includes('x') ? 'x' : 'z'\n const v = axes.find(a => a !== h)!\n return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } as Record<Axis, 1 | -1> }\n}\n\n/**\n * Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the\n * roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge\n * only when the face fills the drawing. The part's name stands in the part when the part is wide and tall enough for it,\n * else just under it -- above it when the drawing ends first. Under a part as tall as the face, just under it is the\n * face's own edge, and the name lay on that line (2026-10-18, the roller in the fastening window): it goes under the edge.\n * Widths are estimated: 7 px a letter at 11-12 px; a line of it stands 11 px over its baseline and 3 under.\n */\nexport function fastenLabelsOf(\n face: { x: number; y: number; w: number; h: number },\n part: { x: number; y: number; w: number; h: number },\n partName: string,\n height: number\n): { face: { x: number; y: number }; part: { x: number; y: number; inside: boolean } } {\n const facePlace = face.y >= 16 ? { x: face.x, y: face.y - 5 } : { x: face.x + 4, y: face.y + 14 }\n const fits = part.w >= partName.length * 7 + 8 && part.h >= 16\n if (fits) return { face: facePlace, part: { x: part.x + part.w / 2, y: part.y + part.h / 2 + 4, inside: true } }\n const below = part.y + part.h + 14\n const edge = face.y + face.h\n if (below - 11 < edge + 2 && below + 3 > edge - 2 && edge + 14 <= height - 4) return { face: facePlace, part: { x: part.x + part.w / 2, y: edge + 14, inside: false } }\n return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } }\n}\n"]}
1
+ {"version":3,"file":"fasten-geometry.js","sourceRoot":"","sources":["../../client/modeller/fasten-geometry.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureFaceInsideAlignOf, figureFaceMeasureOf, figureFaceOverhangAt, figureFastenPlaceOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureFaceMeasure, FigureFacing, FigureFastenAlignment } from '@hatiolab/figure-model'\n\nimport { alignAxesOf, type Alignment } from './links.js'\nimport type { FigureRevision } from './revision.js'\n\n/*\n * What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part\n * lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part\n * meets it by its real shape in the pose it has, by no face of its own (ADR-0140 3.2). What is measured -- the face,\n * the part's extent across it, how far it comes past and whether what touches is on it -- is the kernel's\n * (`figureFaceMeasureOf`, 3.8); this only draws it.\n */\n\ntype Axis = 'x' | 'y' | 'z'\nexport type FastenMode = FigureFacing\nexport type FastenTarget = { part: string; face: string } | 'plane'\n\n/**\n * The face seen from above: its two axes across (as the form's alignment names them), the face's size on each as a\n * fastening lines up to it -- none for a plane with no declared size -- the part's real extent on each, whether its\n * edges can be lined up with, and the kernel's measure the drawing asks again as the part is moved.\n */\nexport interface FaceView {\n axes: [Axis, Axis]\n face: Partial<Record<Axis, number>>\n part: Record<Axis, number>\n edges: Record<Axis, boolean>\n measure: FigureFaceMeasure\n}\n\nexport function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget, facing: FastenMode): FaceView | undefined {\n const measure = (() => {\n try {\n return figureFaceMeasureOf(revision.asset, part, revision.inputs, { on: target, facing })\n } catch {\n return undefined\n }\n })()\n if (!measure) return undefined\n const across = alignAxesOf(revision.asset, target)\n const face: Partial<Record<Axis, number>> = {}\n for (const a of measure.axes) {\n const e = measure.edges[a]\n if (e) face[a] = e.hi - e.lo\n }\n const extent = (a: Axis) => measure.extent[a].hi - measure.extent[a].lo\n return {\n axes: measure.axes,\n face,\n part: { x: extent('x'), y: extent('y'), z: extent('z') },\n edges: Object.fromEntries(measure.axes.map(a => [a, !!across.find(o => o.axis === a)?.edges])) as Record<Axis, boolean>,\n measure\n }\n}\n\n/**\n * Where the part's centre is from the face's centre along one axis, for the way it is aligned there: the kernel's\n * `figureFastenPlaceOf` (ADR-0140 D2), the form's distances -- typed, or a share of the size read as millimetres -- as\n * a distance, a position field's as the centre.\n */\nexport function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {\n const align = how === 'mm' || how === 'share' ? { mm: mm ?? 0 } : how === 'min' || how === 'max' || how === 'centre' ? how : undefined\n return figureFastenPlaceOf({ align: { x: align }, face: { x: face }, part: { x: part } }).x!\n}\n\n/**\n * A place dropped on the face, as the form's alignment: caught at the centre, at an edge from inside (level with it:\n * `min` / `max`, where the edges can be lined up with), or at an edge from outside (touching it, as a distance), when\n * within `catchMm`; anywhere else a distance off centre, whole millimetres.\n */\nexport function alignmentAt(d: number, face: number | undefined, part: number, edges: boolean, catchMm: number): { how: Alignment; mm?: number; caught?: 'centre' | 'inside' | 'outside' } {\n if (Math.abs(d) <= catchMm) return { how: 'centre', caught: 'centre' }\n if (face !== undefined) {\n const inside = (face - part) / 2,\n outside = (face + part) / 2\n for (const [at, how] of [\n [-inside, 'min'],\n [inside, 'max']\n ] as const)\n if (Math.abs(d - at) <= catchMm && inside > 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }\n for (const at of [-outside, outside]) if (Math.abs(d - at) <= catchMm) return { how: 'mm', mm: Math.round(at), caught: 'outside' }\n }\n return { how: 'mm', mm: Math.round(d) }\n}\n\n/** The form's alignment on each axis across the face, in the kernel's words: a typed or shared distance as millimetres. */\nfunction kernelAlignOf(view: FaceView, align: Partial<Record<Axis, Alignment>>, alignMm: Partial<Record<Axis, number>>): Partial<Record<Axis, FigureFastenAlignment>> {\n const out: Partial<Record<Axis, FigureFastenAlignment>> = {}\n for (const a of view.axes) {\n const how = align[a] ?? 'centre'\n out[a] = how === 'mm' || how === 'share' ? { mm: alignMm[a] ?? 0 } : how === 'min' || how === 'max' ? how : 'centre'\n }\n return out\n}\n\n/**\n * The alignment that brings the part inside the face, for 정렬·간격 고치기: the kernel's least move\n * (`figureFaceInsideAlignOf`), in the form's words. `inside` when nothing of it is past an edge: what is out is something\n * fastened on it. `unmeasured` when the face has no size to measure against.\n */\nexport function insideAlignmentOf(\n view: FaceView | undefined,\n align: Partial<Record<Axis, Alignment>>,\n alignMm: Partial<Record<Axis, number>>\n): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; moved: Axis[] } | { cannot: 'inside' | 'unmeasured' } {\n if (!view) return { cannot: 'unmeasured' }\n const fix = figureFaceInsideAlignOf(view.measure, kernelAlignOf(view, align, alignMm), view.edges)\n if ('cannot' in fix) return fix\n const next = { align: { ...align }, alignMm: { ...alignMm }, moved: fix.moved as Axis[] }\n for (const a of fix.moved as Axis[]) {\n const how = fix.align[a]\n if (how && typeof how === 'object') ((next.align[a] = 'mm'), (next.alignMm[a] = how.mm))\n else if (how) next.align[a] = how\n }\n return next\n}\n\n/**\n * How far the part comes past the face's edges, per side, whether any of it lies over the face, and whether what\n * touches the face is on it -- with its middle `at` from the face's middle: the kernel's (`figureFaceOverhangAt`).\n */\nexport function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; contact: 'on' | 'off' | 'unknown'; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {\n const o = figureFaceOverhangAt(view.measure, Object.fromEntries(view.axes.map(a => [a, { mm: at[a] }])))\n return { touching: o.over, contact: o.contact, sides: o.sides as Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> }\n}\n\n/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */\nexport const turnsInPlace = (asset: FigureAsset, part: string) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.origin?.facing === 'centre'))\n\n/**\n * How the face lies on the screen: X across when it is one of the face's axes, else Z; the other down the screen -- up\n * for Y, which rises, so a front face is seen as it stands and not upside down.\n */\nexport function screenOf(axes: readonly [Axis, Axis]): { h: Axis; v: Axis; sign: Record<Axis, 1 | -1> } {\n const h = axes.includes('x') ? 'x' : 'z'\n const v = axes.find(a => a !== h)!\n return { h, v, sign: { x: 1, y: 1, z: 1, [v]: v === 'y' ? -1 : 1 } as Record<Axis, 1 | -1> }\n}\n\n/**\n * Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the\n * roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge\n * only when the face fills the drawing. The part's name stands in the part when the part is wide and tall enough for it,\n * else just under it -- above it when the drawing ends first. Under a part as tall as the face, just under it is the\n * face's own edge, and the name lay on that line (2026-10-18, the roller in the fastening window): it goes under the edge.\n * Widths are estimated: 7 px a letter at 11-12 px; a line of it stands 11 px over its baseline and 3 under.\n */\nexport function fastenLabelsOf(\n face: { x: number; y: number; w: number; h: number },\n part: { x: number; y: number; w: number; h: number },\n partName: string,\n height: number\n): { face: { x: number; y: number }; part: { x: number; y: number; inside: boolean } } {\n const facePlace = face.y >= 16 ? { x: face.x, y: face.y - 5 } : { x: face.x + 4, y: face.y + 14 }\n const fits = part.w >= partName.length * 7 + 8 && part.h >= 16\n if (fits) return { face: facePlace, part: { x: part.x + part.w / 2, y: part.y + part.h / 2 + 4, inside: true } }\n const below = part.y + part.h + 14\n const edge = face.y + face.h\n if (below - 11 < edge + 2 && below + 3 > edge - 2 && edge + 14 <= height - 4) return { face: facePlace, part: { x: part.x + part.w / 2, y: edge + 14, inside: false } }\n return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } }\n}\n"]}