@things-factory/figure-ui 10.1.203 → 10.1.205

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 (86) hide show
  1. package/client/modeller/canvas.ts +91 -9
  2. package/client/modeller/editor-styles.ts +7 -22
  3. package/client/modeller/editor.ts +176 -145
  4. package/client/modeller/fasten-form.ts +91 -9
  5. package/client/modeller/figure-fasten-dialog.ts +42 -9
  6. package/client/modeller/figure-fasten-drawing.ts +233 -12
  7. package/client/modeller/figure-section-editor.ts +6 -6
  8. package/client/modeller/figure-slot-layout-panel.ts +1 -1
  9. package/client/modeller/gizmo.ts +12 -1
  10. package/client/modeller/links.ts +48 -6
  11. package/client/modeller/section-drawing.ts +7 -7
  12. package/client/modeller/size-drag.ts +112 -0
  13. package/client/modeller/slot-layout-host.ts +3 -3
  14. package/client/modeller/stage-model.ts +6 -9
  15. package/client/pages/figure-modeller-page.ts +1 -4
  16. package/dist-client/modeller/canvas.d.ts +24 -3
  17. package/dist-client/modeller/canvas.js +96 -11
  18. package/dist-client/modeller/canvas.js.map +1 -1
  19. package/dist-client/modeller/editor-styles.js +7 -22
  20. package/dist-client/modeller/editor-styles.js.map +1 -1
  21. package/dist-client/modeller/editor.d.ts +28 -32
  22. package/dist-client/modeller/editor.js +176 -153
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/fasten-form.d.ts +36 -3
  25. package/dist-client/modeller/fasten-form.js +86 -8
  26. package/dist-client/modeller/fasten-form.js.map +1 -1
  27. package/dist-client/modeller/figure-fasten-dialog.d.ts +17 -3
  28. package/dist-client/modeller/figure-fasten-dialog.js +47 -10
  29. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  30. package/dist-client/modeller/figure-fasten-drawing.d.ts +33 -1
  31. package/dist-client/modeller/figure-fasten-drawing.js +235 -11
  32. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  33. package/dist-client/modeller/figure-section-editor.d.ts +3 -3
  34. package/dist-client/modeller/figure-section-editor.js +4 -4
  35. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  36. package/dist-client/modeller/figure-slot-layout-panel.js +1 -1
  37. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
  38. package/dist-client/modeller/gizmo.d.ts +12 -1
  39. package/dist-client/modeller/gizmo.js +8 -0
  40. package/dist-client/modeller/gizmo.js.map +1 -1
  41. package/dist-client/modeller/links.d.ts +14 -2
  42. package/dist-client/modeller/links.js +42 -4
  43. package/dist-client/modeller/links.js.map +1 -1
  44. package/dist-client/modeller/section-drawing.d.ts +3 -4
  45. package/dist-client/modeller/section-drawing.js +10 -9
  46. package/dist-client/modeller/section-drawing.js.map +1 -1
  47. package/dist-client/modeller/size-drag.d.ts +38 -0
  48. package/dist-client/modeller/size-drag.js +85 -0
  49. package/dist-client/modeller/size-drag.js.map +1 -0
  50. package/dist-client/modeller/slot-layout-host.d.ts +1 -3
  51. package/dist-client/modeller/slot-layout-host.js +3 -3
  52. package/dist-client/modeller/slot-layout-host.js.map +1 -1
  53. package/dist-client/modeller/stage-model.d.ts +5 -7
  54. package/dist-client/modeller/stage-model.js +6 -6
  55. package/dist-client/modeller/stage-model.js.map +1 -1
  56. package/dist-client/pages/figure-modeller-page.d.ts +0 -2
  57. package/dist-client/pages/figure-modeller-page.js +1 -6
  58. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  59. package/dist-client/tsconfig.tsbuildinfo +1 -1
  60. package/dist-server/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +7 -7
  62. package/test/align.test.ts +7 -11
  63. package/test/browser/acceptance-page.js +10 -0
  64. package/test/browser/editing.check.mjs +17 -13
  65. package/test/browser/links-page.js +2 -2
  66. package/test/browser/links.check.mjs +94 -56
  67. package/test/browser/modeller-page.js +30 -18
  68. package/test/browser/modeller-revision.check.mjs +39 -0
  69. package/test/browser/modeller.check.mjs +32 -10
  70. package/test/browser/screen-page.js +0 -2
  71. package/test/browser/screen.check.mjs +11 -13
  72. package/test/browser/section.check.mjs +2 -2
  73. package/test/fasten-form.test.ts +82 -2
  74. package/test/gizmo.test.ts +13 -15
  75. package/test/links.test.ts +8 -3
  76. package/test/resize-with-placement.test.ts +14 -13
  77. package/test/revision.test.ts +3 -2
  78. package/test/section-editor.test.ts +9 -10
  79. package/test/size-drag.test.ts +114 -0
  80. package/test/slot-layout-host.test.ts +13 -8
  81. package/translations/en.json +25 -22
  82. package/translations/es.json +25 -22
  83. package/translations/ja.json +25 -22
  84. package/translations/ko.json +25 -22
  85. package/translations/ms.json +25 -22
  86. package/translations/zh.json +25 -22
@@ -1,5 +1,5 @@
1
- import { FIGURE_FACES } from '@hatiolab/figure-model'
2
- import type { FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model'
1
+ import { FIGURE_FACES, figureFastenTargetAt, figurePlaneFacingOf, figureSurfaceKeptPlaceOf } from '@hatiolab/figure-model'
2
+ import type { FigureAuthoringAction, FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
3
3
 
4
4
  import { faceViewOf, type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js'
5
5
  import type { FastenChange } from './figure-fasten-drawing.js'
@@ -14,10 +14,12 @@ import {
14
14
  heldBy,
15
15
  mountingPlaneOf,
16
16
  placeKeptOf,
17
+ surfaceOf,
17
18
  type Alignment,
18
19
  type Axis
19
20
  } from './links.js'
20
21
  import type { FigureRevision } from './revision.js'
22
+ import { ownerOf } from './fields.js'
21
23
 
22
24
  /*
23
25
  * A fastening as it is chosen, before it is done: to what, which face, how, where across the face. The fastening window
@@ -39,6 +41,8 @@ export interface FastenForm {
39
41
  align: Partial<Record<Axis, Alignment>>
40
42
  alignMm: Partial<Record<Axis, number>>
41
43
  alignInput: Partial<Record<Axis, string>>
44
+ /** On a point of the target's curved surface instead of a face (ADR-0140 3.9): where, as the record says it. */
45
+ surface?: FigureSurfacePlace
42
46
  }
43
47
 
44
48
  export const freshFasten = (): FastenForm => ({
@@ -72,18 +76,42 @@ export function fastenTargetsOf(revision: FigureRevision, part: string): Array<{
72
76
  }
73
77
 
74
78
  /**
75
- * A new aim: the plane, another part, or another face of the same part. Where on the face is filled in as where the
76
- * part stands now, so fastening it keeps it there (`placeKeptOf`); the author changes it from there.
79
+ * The point of a target's curved surface on the side the part stands on now (ADR-0140 3.9): the kernel's
80
+ * (`figureSurfaceKeptPlaceOf`), or, the part standing on its axis, the surface's first point.
81
+ */
82
+ export function surfaceKeptOf(revision: FigureRevision, part: string, target: string): FigureSurfacePlace | undefined {
83
+ const surface = surfaceOf(revision.asset, target)
84
+ if (!surface) return undefined
85
+ return (
86
+ figureSurfaceKeptPlaceOf(revision.asset, part, target, { ...revision.inputs }) ??
87
+ (surface.form === 'around' ? { around: 0, along: 'centre' } : { toward: { x: 0, y: 1, z: 0 } })
88
+ )
89
+ }
90
+
91
+ /**
92
+ * A new aim: the plane, another part, another face of the same part, or a point of its curved surface. Where on the
93
+ * face -- or the surface -- is filled in as where the part stands now, so fastening it keeps it there (`placeKeptOf`,
94
+ * `surfaceKeptOf`); the author changes it from there. A part aimed at a sphere, or beside a cylinder's side within its
95
+ * length, is aimed at the surface: it is fastened as it stands (ADR-0140 3.6).
77
96
  */
78
97
  export function aimFasten(
79
98
  revision: FigureRevision,
80
99
  part: string,
81
100
  form: FastenForm,
82
- to: { plane: true } | { part: string } | { targetFace: FigureFace }
101
+ to: { plane: true } | { part: string } | { targetFace: FigureFace } | { surface: true }
83
102
  ): FastenForm {
103
+ if ('surface' in to) return { ...form, surface: surfaceKeptOf(revision, part, form.target), align: {}, alignMm: {}, alignInput: {} }
104
+ if ('part' in to) {
105
+ /* Beside its curved surface -- a sphere's anywhere, a cylinder's side within its length -- or with no face at all. */
106
+ const beside = figureSurfaceKeptPlaceOf(revision.asset, part, to.part, { ...revision.inputs })
107
+ const kept = beside ?? (!facesOf(revision.asset, to.part).length ? surfaceKeptOf(revision, part, to.part) : undefined)
108
+ if (kept) return { ...form, toPlane: false, target: to.part, surface: kept, align: {}, alignMm: {}, alignInput: {} }
109
+ }
110
+ form = { ...form, surface: undefined }
111
+ /* Aimed at the plane, it meets it toward the side the placement uses -- below a ceiling -- until the author says (ADR-0140 3.11). */
84
112
  const f: FastenForm =
85
113
  'plane' in to
86
- ? { ...form, toPlane: true, target: '' }
114
+ ? { ...form, toPlane: true, target: '', facing: form.toPlane ? form.facing : (figurePlaneFacingOf(revision.asset) ?? form.facing) }
87
115
  : 'part' in to
88
116
  ? { ...form, toPlane: false, target: to.part }
89
117
  : { ...form, targetFace: to.targetFace }
@@ -106,6 +134,8 @@ export function fastenChanged(
106
134
  ): FastenForm {
107
135
  const f = form
108
136
  if ('targetFace' in change) return aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace })
137
+ if ('onSurface' in change) return aimFasten(revision, part, f, { surface: true })
138
+ if ('surface' in change) return { ...f, surface: change.surface }
109
139
  if ('mode' in change) return { ...f, facing: change.mode }
110
140
  if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined }
111
141
  return {
@@ -128,6 +158,8 @@ export interface FastenPlan {
128
158
  action?: FigureAuthoringAction
129
159
  /** The face the command would meet, lit on the canvas while it is tried. */
130
160
  face?: { part: string; face: string }
161
+ /** On a point of the target's curved surface: which surface, about which axis, and where (ADR-0140 3.9). */
162
+ surface?: { form: 'around' | 'toward'; axis: Axis; place: FigureSurfacePlace }
131
163
  }
132
164
 
133
165
  export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenForm): FastenPlan {
@@ -140,7 +172,21 @@ export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenFo
140
172
  ? { part: f.target, face: targetFace }
141
173
  : undefined
142
174
  /* The plane takes a part standing on it or hanging below it, a wall only one standing out of it (ADR-0140 3.11). */
143
- const mode: FastenMode = f.toPlane ? (mountingPlaneOf(asset).facings.includes(f.facing) ? f.facing : 'meet') : f.facing
175
+ const planeWay = figurePlaneFacingOf(asset)
176
+ const mode: FastenMode = f.toPlane ? (mountingPlaneOf(asset).facings.includes(f.facing) ? f.facing : (planeWay ?? 'meet')) : f.facing
177
+ const surface = !f.toPlane && f.target && f.surface ? surfaceOf(asset, f.target) : undefined
178
+ if (surface) {
179
+ /* On the surface: where is the surface's own words, so no alignment goes with it (ADR-0140 3.9). */
180
+ const action = {
181
+ kind: 'attach',
182
+ part,
183
+ to: { part: f.target, surface: f.surface },
184
+ facing: mode,
185
+ ...fastenGapOf(f),
186
+ replace: !!heldBy(asset, part)
187
+ } as FigureAuthoringAction
188
+ return { theirFaces, targetFace, mode, alignRows: [], action, surface: { ...surface, place: f.surface! } }
189
+ }
144
190
  if (!aim) return { theirFaces, targetFace, mode, alignRows: [] }
145
191
  const align = alignOf(asset, aim, f.align, f.alignMm, f.alignInput)
146
192
  const view = faceViewOf(revision, part, aim, mode)
@@ -148,7 +194,8 @@ export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenFo
148
194
  kind: 'attach',
149
195
  part,
150
196
  to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
151
- ...(f.toPlane ? (mode === 'flush' ? { facing: mode } : {}) : { facing: mode }),
197
+ /* On the plane the kernel takes the placement's way when none is said: only another way is said. */
198
+ ...(f.toPlane ? (mode === planeWay ? {} : { facing: mode }) : { facing: mode }),
152
199
  ...fastenGapOf(f),
153
200
  ...(align ? { align } : {}),
154
201
  replace: !!heldBy(asset, part)
@@ -174,7 +221,8 @@ export function fastenSentenceOf(
174
221
  planeWord: string,
175
222
  say: Say
176
223
  ): string {
177
- const to = form.toPlane ? planeWord : `${form.target}의 ${say('figure.label.face-' + plan.targetFace)}`
224
+ const where = plan.surface ? say('figure.label.surface-' + plan.surface.form) : say('figure.label.face-' + plan.targetFace)
225
+ const to = form.toPlane ? planeWord : `${form.target}의 ${where}`
178
226
  return say(`figure.text.fasten-sentence-${plan.mode}`, { to })
179
227
  }
180
228
 
@@ -188,3 +236,37 @@ export function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {
188
236
  : [])
189
237
  ]
190
238
  }
239
+
240
+ /**
241
+ * A part pressed on the canvas while the window is open (ADR-0140 stage 4): aimed at, on the face or the point pressed,
242
+ * its middle where it was pressed -- the kernel's reading of the press (`figureFastenTargetAt`), the point and normal brought into that part's own
243
+ * frame by the pose its body is drawn at (`at.id`, a row's copy too). The part itself, one hanging from it, or one with
244
+ * nothing to fasten to is not aimed at; a press on a curve there is no fastening to aims at the part alone. Each says
245
+ * why, by its words' key.
246
+ */
247
+ export function aimFastenAt(
248
+ revision: FigureRevision,
249
+ part: string,
250
+ form: FastenForm,
251
+ at: { id: string; point?: readonly number[]; normal?: readonly number[] }
252
+ ): { form?: FastenForm; note?: 'aim-self' | 'aim-cannot' | 'target-would-loop' | 'aim-not-here'; part: string } {
253
+ const target = ownerOf(at.id)
254
+ const offered = fastenTargetsOf(revision, part).find(t => t.part === target)
255
+ if (target === part) return { note: 'aim-self', part: target }
256
+ if (!offered) return { note: 'aim-cannot', part: target }
257
+ if (offered.loops) return { note: 'target-would-loop', part: target }
258
+ const aimed = aimFasten(revision, part, form, { part: target })
259
+ const pose = (revision.geometry as Array<{ id: string; pose?: { r: number[][]; t: number[] } }>).find(g => g.id === at.id)?.pose
260
+ if (!pose || !at.point || !at.normal) return { form: aimed, part: target }
261
+ /* Into the part's own frame: rᵀ (p − t), and the normal by rᵀ. */
262
+ const own = (v: readonly number[], from: readonly number[] = [0, 0, 0]) => [0, 1, 2].map(c => [0, 1, 2].reduce((sum, i) => sum + pose.r[i]![c]! * (v[i]! - from[i]!), 0))
263
+ const lands = figureFastenTargetAt(revision.asset, target, own(at.point, pose.t), own(at.normal), { ...revision.inputs })
264
+ if ('face' in lands) {
265
+ /* On the face where it was pressed, as a point of a curved surface is: its middle there. */
266
+ const onFace = aimFasten(revision, part, { ...aimed, surface: undefined }, { targetFace: lands.face as FigureFace })
267
+ const at = lands.at as Partial<Record<Axis, number>>
268
+ return { form: { ...onFace, align: Object.fromEntries(Object.keys(at).map(a => [a, 'mm'])) as FastenForm['align'], alignMm: { ...at }, alignInput: {} }, part: target }
269
+ }
270
+ if ('surface' in lands) return { form: { ...aimed, toPlane: false, target, surface: lands.surface, align: {}, alignMm: {}, alignInput: {} }, part: target }
271
+ return { form: aimed, note: 'aim-not-here', part: target }
272
+ }
@@ -15,6 +15,7 @@ import type { FastenChange } from './figure-fasten-drawing.js'
15
15
  import { turnsInPlace, type FastenMode } from './fasten-geometry.js'
16
16
  import {
17
17
  aimFasten,
18
+ aimFastenAt,
18
19
  fastenChanged,
19
20
  fastenDetailWordsOf,
20
21
  fastenFormOf,
@@ -23,16 +24,17 @@ import {
23
24
  fastenTargetsOf,
24
25
  type FastenForm
25
26
  } from './fasten-form.js'
26
- import { fieldWordsOf, hasInstanceSize, heldBy, mountingPlaneOf, type Alignment, type Axis } from './links.js'
27
+ import { fieldWordsOf, hasInstanceSize, heldBy, mountingPlaneOf, surfaceOf, type Alignment, type Axis } from './links.js'
27
28
  import type { FigureRevision } from './revision.js'
28
29
 
29
30
  /** The target list's entry for the plane the figure stands by. */
30
31
  const PLANE = '__plane'
31
32
 
32
33
  /**
33
- * 붙이기 창: what the selected part is fastened to, on which face, how (user 2026-10-17: 「속성에는 심플하고 직관적인
34
- * 결과 위주로, 편집은 팝업에서 넓은 공간을 써서」). The editor stands it over the side panel with its title and close,
35
- * where the canvas stays in sight (2026-10-19: the standard popup, always modal, hid the 3D it was choosing for).
34
+ * 붙이기 창: what the selected part is fastened to, on which face, how -- chosen with room to see it, in the standard
35
+ * popup the editor opens (`openPopup`: its title, its close, moved by its head; no backdrop, so the canvas can be
36
+ * pressed while it is open) (user 2026-10-17: 「속성에는 심플하고 직관적인 결과 위주로, 편집은 팝업에서 넓은 공간을
37
+ * 써서」; 2026-10-18: the standard popup, not a window of its own; put back 2026-10-20).
36
38
  *
37
39
  * Everything chosen here is tried, not done: the command it comes to is told to the editor as it changes
38
40
  * (`preview-command`), and the canvas draws it. 「적용」 sends it once (`fasten-apply`), one step of the history;
@@ -46,11 +48,16 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
46
48
  SidePanelStyles,
47
49
  CommonPopupStyles,
48
50
  css`
51
+ /*
52
+ The popup holds it at its own height and cuts what is past the screen: the window scrolls itself, within the
53
+ screen less the popup's head and margins, 취소 · 적용 kept at its foot (below).
54
+ */
49
55
  :host {
50
56
  display: grid;
51
57
  gap: 16px;
52
58
  padding: var(--popup-padding, 16px);
53
59
  overflow: auto;
60
+ max-height: calc(100dvh - 32px - 56px);
54
61
  }
55
62
  /* The result in one line, at the top: it changes with every choice below it. */
56
63
  p[fasten-sentence] {
@@ -104,7 +111,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
104
111
  margin: 0;
105
112
  color: var(--md-sys-color-error);
106
113
  }
107
- /* 취소 · 적용 stay in sight at the window's foot when what is above them is taller than the panel. */
114
+ /* 취소 · 적용 stay in sight at the window's foot when what is above them is taller than the screen. */
108
115
  ox-popup-actions {
109
116
  position: sticky;
110
117
  bottom: calc(-1 * var(--popup-padding, 16px));
@@ -126,6 +133,8 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
126
133
  @property() error = ''
127
134
 
128
135
  @state() private form?: FastenForm
136
+ /** Why a press on the canvas aimed at nothing, or only at the part (ADR-0140 stage 4); cleared by the next choice. */
137
+ @state() private aimNote = ''
129
138
  /** The command the part's own fastening comes to, as opened: tried unchanged, nothing new is said of it. */
130
139
  private opened?: string
131
140
  private told = ''
@@ -195,12 +204,17 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
195
204
  .alignMm=${f.alignMm}
196
205
  .gap=${f.gap}
197
206
  .partName=${this.partId}
207
+ .surfaceForm=${f.toPlane || !f.target ? '' : (surfaceOf(asset, f.target)?.form ?? '')}
208
+ .surface=${plan.surface}
198
209
  ?locked=${this.locked}
199
- @fasten-change=${(e: CustomEvent<FastenChange>) => (this.form = fastenChanged(revision, this.partId, this.form!, e.detail))}
210
+ @fasten-change=${(e: CustomEvent<FastenChange>) => {
211
+ this.aimNote = ''
212
+ this.form = fastenChanged(revision, this.partId, this.form!, e.detail)
213
+ }}
200
214
  ></figure-fasten-drawing>`
201
215
  return html`
202
216
  ${
203
- plan.aim
217
+ plan.aim || plan.surface
204
218
  ? html`<p fasten-sentence role="status">
205
219
  ${fastenSentenceOf(f, plan, planeWord, (key, params) => i18next.t(key, params))}
206
220
  </p>`
@@ -211,13 +225,15 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
211
225
  ${this.renderTargets(revision, f, planeWord)}
212
226
  </div>
213
227
  ${
214
- plan.aim
228
+ plan.aim || plan.surface
215
229
  ? html`<div question="face">
216
230
  <h4>
217
231
  ${
218
232
  /* The face asked is the target's, named: the part meets it by its real shape, by no face of its own. */ f.toPlane
219
233
  ? i18next.t('figure.label.fasten-face-on-plane', { plane: planeWord })
220
- : i18next.t('figure.label.fasten-face-on-part', { part: f.target })
234
+ : /* A sphere has no face to choose: where on its surface. */ plan.theirFaces.length
235
+ ? i18next.t('figure.label.fasten-face-on-part', { part: f.target })
236
+ : i18next.t('figure.label.fasten-face-on-surface', { part: f.target })
221
237
  }
222
238
  </h4>
223
239
  ${drawing('face')}
@@ -252,6 +268,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
252
268
  ${/* The one it has, tried unchanged: that it stays where it is goes without saying. */ JSON.stringify(plan.action ?? null) === this.opened ? nothing : this.renderPreviewWords()}`
253
269
  : nothing
254
270
  }
271
+ ${this.aimNote ? html`<p hint aim-note role="status">${this.aimNote}</p>` : nothing}
255
272
  ${this.error ? html`<p error role="alert">${this.error}</p>` : nothing}
256
273
  <ox-popup-actions>
257
274
  <md-text-button slot="cancel" cancel @click=${this.cancel}>${i18next.t('figure.button.cancel')}</md-text-button>
@@ -310,11 +327,25 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
310
327
 
311
328
  private aim(part: string) {
312
329
  if (this.locked || !this.revision || !this.form) return
330
+ this.aimNote = ''
313
331
  const chosen = this.form.toPlane ? PLANE : this.form.target
314
332
  if (part === chosen) return
315
333
  this.form = aimFasten(this.revision, this.partId, this.form, part === PLANE ? { plane: true } : { part })
316
334
  }
317
335
 
336
+ /**
337
+ * A part pressed on the canvas while the window is open (ADR-0140 stage 4): aimed at, on the face or the point pressed
338
+ * -- the kernel's reading of the press (`figureFastenTargetAt`), the point and normal brought into that part's own
339
+ * frame by the pose it is drawn at. The part itself, or one hanging from it, is not aimed at; and a press on a curve
340
+ * there is no fastening to aims at the part alone. Each is said.
341
+ */
342
+ aimAt(at: { id: string; point?: number[]; normal?: number[] }) {
343
+ if (this.locked || !this.revision || !this.form) return
344
+ const aimed = aimFastenAt(this.revision, this.partId, this.form, at)
345
+ this.aimNote = aimed.note ? i18next.t(`figure.text.${aimed.note}`, { part: aimed.part, mine: this.partId }) : ''
346
+ if (aimed.form) this.form = aimed.form
347
+ }
348
+
318
349
  /** The part under the pointer or the focus in the list, for the canvas to light; the plane lights nothing. */
319
350
  private hover(part: string | null) {
320
351
  const lit = part && part !== PLANE ? part : null
@@ -410,6 +441,8 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
410
441
  this.setAttribute('role', 'dialog')
411
442
  this.setAttribute('aria-label', i18next.t('figure.label.fastening'))
412
443
  this.addEventListener('keydown', this.onKey)
444
+ /* The popup's fitting width is the overlay's own setting; the window asks it for room for the face and the way side by side. */
445
+ this.closest<HTMLElement>('ox-floating-overlay')?.style.setProperty('--overlay-center-fit-width', '640px')
413
446
  }
414
447
 
415
448
  private onKey = async (e: KeyboardEvent) => {