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