@things-factory/figure-ui 10.1.200 → 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.
- package/client/modeller/editor.ts +7 -3
- package/client/modeller/fasten-form.ts +18 -35
- package/client/modeller/fasten-geometry.ts +67 -115
- package/client/modeller/figure-fasten-dialog.ts +4 -12
- package/client/modeller/figure-links.ts +6 -7
- package/client/modeller/joint-drag.ts +5 -5
- package/client/modeller/links.ts +35 -99
- package/client/modeller/refusals.ts +2 -6
- package/dist-client/modeller/editor.js +7 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +5 -8
- package/dist-client/modeller/fasten-form.js +16 -28
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +20 -19
- package/dist-client/modeller/fasten-geometry.js +54 -113
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +4 -12
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +6 -7
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/joint-drag.js +5 -5
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/links.d.ts +19 -26
- package/dist-client/modeller/links.js +36 -94
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +1 -1
- package/dist-client/modeller/refusals.js +2 -6
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/ai-proposal.test.ts +1 -1
- package/test/browser/acceptance-page.js +11 -11
- package/test/browser/detail-page.js +1 -1
- package/test/browser/detail-tabs-page.js +1 -1
- package/test/browser/editing-page.js +1 -1
- package/test/browser/fix-alignment.check.mjs +3 -3
- package/test/browser/links.check.mjs +20 -8
- package/test/browser/modeller-page.js +3 -3
- package/test/browser/part-ids.check.mjs +2 -2
- package/test/browser/parts-page.js +2 -2
- package/test/browser/product-page.check.mjs +3 -3
- package/test/browser/product-page.js +1 -1
- package/test/browser/row-preview.check.mjs +2 -2
- package/test/browser/screen-page.js +3 -3
- package/test/browser/section-page.js +3 -3
- package/test/browser/section.check.mjs +4 -4
- package/test/clip-ai-proposal.test.ts +1 -1
- package/test/clip-consent.test.ts +1 -1
- package/test/command-preview.test.ts +8 -8
- package/test/face-mapping.test.ts +2 -2
- package/test/fasten-drawing.test.ts +14 -2
- package/test/fasten-form.test.ts +11 -3
- package/test/fasten-geometry.test.ts +22 -31
- package/test/fix-alignment.test.ts +16 -4
- package/test/gizmo.test.ts +2 -2
- package/test/joint-drag.test.ts +13 -2
- package/test/joint-fan.test.ts +1 -1
- package/test/joint-frames.test.ts +45 -23
- package/test/joints.ts +1 -1
- package/test/links.test.ts +68 -68
- package/test/live.test.ts +2 -2
- package/test/preview-size.test.ts +1 -1
- package/test/refusals.test.ts +18 -20
- package/test/resize-with-placement.test.ts +6 -6
- package/test/revision.test.ts +4 -5
- package/test/section-panel.test.ts +1 -1
- package/translations/en.json +9 -11
- package/translations/es.json +9 -11
- package/translations/ja.json +9 -11
- package/translations/ko.json +9 -11
- package/translations/ms.json +9 -11
- package/translations/zh.json +9 -11
|
@@ -1442,12 +1442,15 @@ export class FigureEditor extends LitElement {
|
|
|
1442
1442
|
return derivedOf(shown, `overhang|${part}|${i18next.language}`, () => {
|
|
1443
1443
|
const asset = shown.asset
|
|
1444
1444
|
const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)
|
|
1445
|
-
const where = (o: FigureFaceOverhang) => ({ on: partLabelOf(o.on.part), face: say(`figure.label.face-${o.on.face}`) })
|
|
1445
|
+
const where = (o: FigureFaceOverhang & { on: { part: string; face: string } }) => ({ on: partLabelOf(o.on.part), face: say(`figure.label.face-${o.on.face}`) })
|
|
1446
|
+
/* On another part's face: the plane's edges are the figure's room, said where the room is. */
|
|
1447
|
+
const onPart = (o: FigureFaceOverhang | undefined): o is FigureFaceOverhang & { on: { part: string; face: string } } => !!o && o.on !== 'plane'
|
|
1446
1448
|
const sides = (o: FigureFaceOverhang) => o.sides.map(s => say(`figure.text.overhang-side-${s.side}`, { axis: s.axis.toUpperCase(), mm: String(s.mm) })).join(', ')
|
|
1447
1449
|
const words: string[] = []
|
|
1448
1450
|
const note: string[] = []
|
|
1449
1451
|
try {
|
|
1450
|
-
const
|
|
1452
|
+
const found = figureFaceOverhangOf(asset, part, shown.inputs)
|
|
1453
|
+
const own = onPart(found) ? found : undefined
|
|
1451
1454
|
if (own?.state === 'larger') {
|
|
1452
1455
|
note.push(say('figure.text.overhang-larger', { ...where(own), sides: sides(own) }))
|
|
1453
1456
|
if (own.lopsided.length) note.push(say('figure.text.overhang-lopsided', { axes: own.lopsided.map(a => a.toUpperCase()).join(', ') }))
|
|
@@ -1455,8 +1458,9 @@ export class FigureEditor extends LitElement {
|
|
|
1455
1458
|
if (own?.state === 'out') words.push(say('figure.text.overhang-out', { ...where(own), sides: sides(own) }))
|
|
1456
1459
|
else if (own?.state === 'possible') words.push(say('figure.text.overhang-possible', where(own)))
|
|
1457
1460
|
else if (own?.state === 'unknown') words.push(say('figure.text.overhang-unknown', where(own)))
|
|
1461
|
+
if (own?.contact === 'off') words.push(say('figure.text.overhang-off', where(own)))
|
|
1458
1462
|
const held = (asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => figureFaceOverhangOf(asset, j.body1.part, shown.inputs))
|
|
1459
|
-
const past = held.filter((o): o is FigureFaceOverhang => o?.state === 'out' || o?.state === 'possible')
|
|
1463
|
+
const past = held.filter((o): o is FigureFaceOverhang => o?.state === 'out' || o?.state === 'possible' || o?.contact === 'off')
|
|
1460
1464
|
if (past.length) words.push(say('figure.text.overhang-held', { parts: past.map(o => partLabelOf(o.part)).join(', ') }))
|
|
1461
1465
|
} catch {
|
|
1462
1466
|
/* Not read: nothing said rather than a guess. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FIGURE_FACES } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
|
-
import { faceViewOf,
|
|
4
|
+
import { faceViewOf, type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js'
|
|
5
5
|
import type { FastenChange } from './figure-fasten-drawing.js'
|
|
6
6
|
import {
|
|
7
7
|
alignOf,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
fastenGapOf,
|
|
13
13
|
hangsFrom,
|
|
14
14
|
heldBy,
|
|
15
|
-
|
|
15
|
+
mountingPlaneOf,
|
|
16
16
|
placeKeptOf,
|
|
17
17
|
type Alignment,
|
|
18
18
|
type Axis
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import type { FigureRevision } from './revision.js'
|
|
21
21
|
|
|
22
22
|
/*
|
|
23
|
-
* A fastening as it is chosen, before it is done: what, which face, how, where across the face. The fastening window
|
|
23
|
+
* A fastening as it is chosen, before it is done: to what, which face, how, where across the face. The fastening window
|
|
24
24
|
* (`figure-fasten-dialog.ts`) holds one and changes it as the author chooses; the attach command is worked out from it
|
|
25
25
|
* only here, so the window, its preview and the panel's 「정렬 고치기」 send the same command for the same choice.
|
|
26
26
|
*/
|
|
@@ -28,29 +28,26 @@ import type { FigureRevision } from './revision.js'
|
|
|
28
28
|
const FACES = Object.keys(FIGURE_FACES) as FigureFace[]
|
|
29
29
|
|
|
30
30
|
export interface FastenForm {
|
|
31
|
-
face: FigureFace | 'centre'
|
|
32
31
|
toPlane: boolean
|
|
33
32
|
target: string
|
|
34
33
|
targetFace: FigureFace
|
|
35
|
-
|
|
34
|
+
/** How it meets the face: its near end on it, its far end on it (flush), or its middle (ADR-0140 3.11). */
|
|
35
|
+
facing: FastenMode
|
|
36
36
|
gap: number
|
|
37
37
|
/** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */
|
|
38
38
|
gapKept: unknown
|
|
39
|
-
twist: number
|
|
40
39
|
align: Partial<Record<Axis, Alignment>>
|
|
41
40
|
alignMm: Partial<Record<Axis, number>>
|
|
42
41
|
alignInput: Partial<Record<Axis, string>>
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
export const freshFasten = (): FastenForm => ({
|
|
46
|
-
face: 'bottom',
|
|
47
45
|
toPlane: false,
|
|
48
46
|
target: '',
|
|
49
47
|
targetFace: 'top',
|
|
50
48
|
facing: 'meet',
|
|
51
49
|
gap: 0,
|
|
52
50
|
gapKept: undefined,
|
|
53
|
-
twist: 0,
|
|
54
51
|
align: {},
|
|
55
52
|
alignMm: {},
|
|
56
53
|
alignInput: {}
|
|
@@ -97,7 +94,7 @@ export function aimFasten(
|
|
|
97
94
|
: f.target
|
|
98
95
|
? { part: f.target, face: targetFace }
|
|
99
96
|
: undefined
|
|
100
|
-
return { ...f, ...(aim ? placeKeptOf(revision, part, aim
|
|
97
|
+
return { ...f, ...(aim ? placeKeptOf(revision, part, aim) : { align: {}, alignMm: {}, alignInput: {} }) }
|
|
101
98
|
}
|
|
102
99
|
|
|
103
100
|
/** A change the drawing asks: a face, a way, a gap or a place across the face. */
|
|
@@ -109,12 +106,7 @@ export function fastenChanged(
|
|
|
109
106
|
): FastenForm {
|
|
110
107
|
const f = form
|
|
111
108
|
if ('targetFace' in change) return aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace })
|
|
112
|
-
if ('mode' in change)
|
|
113
|
-
return {
|
|
114
|
-
...f,
|
|
115
|
-
face: change.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face,
|
|
116
|
-
facing: change.mode === 'flush' ? 'flush' : 'meet'
|
|
117
|
-
}
|
|
109
|
+
if ('mode' in change) return { ...f, facing: change.mode }
|
|
118
110
|
if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined }
|
|
119
111
|
return {
|
|
120
112
|
...f,
|
|
@@ -123,15 +115,13 @@ export function fastenChanged(
|
|
|
123
115
|
}
|
|
124
116
|
}
|
|
125
117
|
|
|
126
|
-
/** What a form comes to: the face aimed at, the
|
|
118
|
+
/** What a form comes to: the face aimed at, the way, the drawing of the face and the command. */
|
|
127
119
|
export interface FastenPlan {
|
|
128
120
|
/** Nothing is aimed at until a part or the plane is chosen. */
|
|
129
121
|
aim?: FastenTarget
|
|
130
122
|
theirFaces: string[]
|
|
131
123
|
targetFace: FigureFace
|
|
132
124
|
mode: FastenMode
|
|
133
|
-
/** 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). */
|
|
134
|
-
myFace: FigureFace | 'centre'
|
|
135
125
|
view?: FaceView
|
|
136
126
|
/** The axes across the face asked in rows: where the drawing cannot set the place. */
|
|
137
127
|
alignRows: { axis: Axis; edges: boolean }[]
|
|
@@ -142,7 +132,6 @@ export interface FastenPlan {
|
|
|
142
132
|
|
|
143
133
|
export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenForm): FastenPlan {
|
|
144
134
|
const asset = revision.asset
|
|
145
|
-
const mineFaces = ownFacesOf(asset, part, f.toPlane)
|
|
146
135
|
const theirFaces = f.target ? facesOf(asset, f.target) : FACES
|
|
147
136
|
const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]) as FigureFace
|
|
148
137
|
const aim: FastenTarget | undefined = f.toPlane
|
|
@@ -150,20 +139,17 @@ export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenFo
|
|
|
150
139
|
: f.target
|
|
151
140
|
? { part: f.target, face: targetFace }
|
|
152
141
|
: undefined
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const view = faceViewOf(revision, part, aim)
|
|
142
|
+
/* 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
|
|
144
|
+
if (!aim) return { theirFaces, targetFace, mode, alignRows: [] }
|
|
145
|
+
const align = alignOf(asset, aim, f.align, f.alignMm, f.alignInput)
|
|
146
|
+
const view = faceViewOf(revision, part, aim, mode)
|
|
159
147
|
const action = {
|
|
160
148
|
kind: 'attach',
|
|
161
149
|
part,
|
|
162
|
-
face: myFace,
|
|
163
150
|
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
164
|
-
...(f.toPlane ? {} : {
|
|
151
|
+
...(f.toPlane ? (mode === 'flush' ? { facing: mode } : {}) : { facing: mode }),
|
|
165
152
|
...fastenGapOf(f),
|
|
166
|
-
twist: f.twist,
|
|
167
153
|
...(align ? { align } : {}),
|
|
168
154
|
replace: !!heldBy(asset, part)
|
|
169
155
|
} as FigureAuthoringAction
|
|
@@ -172,9 +158,8 @@ export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenFo
|
|
|
172
158
|
theirFaces,
|
|
173
159
|
targetFace,
|
|
174
160
|
mode,
|
|
175
|
-
myFace,
|
|
176
161
|
view,
|
|
177
|
-
alignRows: alignRowsOf(asset,
|
|
162
|
+
alignRows: alignRowsOf(asset, aim, view, f.alignInput),
|
|
178
163
|
action,
|
|
179
164
|
...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } })
|
|
180
165
|
}
|
|
@@ -182,16 +167,15 @@ export function fastenPlanOf(revision: FigureRevision, part: string, f: FastenFo
|
|
|
182
167
|
|
|
183
168
|
type Say = (key: string, params?: Record<string, string>) => string
|
|
184
169
|
|
|
185
|
-
/** 「
|
|
170
|
+
/** 「bin의 윗면에 올려놓기」: what the fastening is, in one line -- the part is the one the window is open on, not named again. */
|
|
186
171
|
export function fastenSentenceOf(
|
|
187
|
-
part: string,
|
|
188
172
|
form: FastenForm,
|
|
189
173
|
plan: FastenPlan,
|
|
190
174
|
planeWord: string,
|
|
191
175
|
say: Say
|
|
192
176
|
): string {
|
|
193
177
|
const to = form.toPlane ? planeWord : `${form.target}의 ${say('figure.label.face-' + plan.targetFace)}`
|
|
194
|
-
return say(`figure.text.fasten-sentence-${plan.mode}`, {
|
|
178
|
+
return say(`figure.text.fasten-sentence-${plan.mode}`, { to })
|
|
195
179
|
}
|
|
196
180
|
|
|
197
181
|
/** What the folded details hold that is not their default, said while folded; nothing for the default. */
|
|
@@ -201,7 +185,6 @@ export function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {
|
|
|
201
185
|
? [say('figure.text.fasten-summary-gap-worked-out')]
|
|
202
186
|
: form.gap
|
|
203
187
|
? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]
|
|
204
|
-
: [])
|
|
205
|
-
...(form.twist ? [say('figure.text.fasten-summary-twist', { degrees: String(form.twist) })] : [])
|
|
188
|
+
: [])
|
|
206
189
|
]
|
|
207
190
|
}
|
|
@@ -1,108 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { FigureAsset,
|
|
1
|
+
import { figureFaceInsideAlignOf, figureFaceMeasureOf, figureFaceOverhangAt, figureFastenPlaceOf } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAsset, FigureFaceMeasure, FigureFacing, FigureFastenAlignment } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { alignAxesOf, facesOf, faceAxisOf, hasInstanceSize, mountingPlaneOf, type Alignment } from './links.js'
|
|
4
|
+
import { alignAxesOf, type Alignment } from './links.js'
|
|
6
5
|
import type { FigureRevision } from './revision.js'
|
|
7
6
|
|
|
8
7
|
/*
|
|
9
8
|
* What the 붙이기 drawing reads (user 2026-10-01): the face a part is fastened to, seen from above, and where the part
|
|
10
|
-
* lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* lies on it. A fastening is chosen by the target's face and how (올려놓기 · 같은 면에 맞추기 · 가운데를 면에); the part
|
|
10
|
+
* 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,
|
|
11
|
+
* the part's extent across it, how far it comes past and whether what touches is on it -- is the kernel's
|
|
12
|
+
* (`figureFaceMeasureOf`, 3.8); this only draws it.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
type Axis = 'x' | 'y' | 'z'
|
|
16
|
-
|
|
17
|
-
export type FastenMode = 'meet' | 'flush' | 'centre'
|
|
16
|
+
export type FastenMode = FigureFacing
|
|
18
17
|
export type FastenTarget = { part: string; face: string } | 'plane'
|
|
19
18
|
|
|
20
|
-
const OPPOSITE: Record<FigureFace, FigureFace> = { left: 'right', right: 'left', bottom: 'top', top: 'bottom', back: 'front', front: 'back' }
|
|
21
|
-
const NORMAL: Record<FigureFace, [Axis, 1 | -1]> = { left: ['x', -1], right: ['x', 1], bottom: ['y', -1], top: ['y', 1], back: ['z', -1], front: ['z', 1] }
|
|
22
|
-
|
|
23
|
-
/** 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. */
|
|
24
|
-
function directionOf(revision: FigureRevision, part: string, face: string): [Axis, 1 | -1] | undefined {
|
|
25
|
-
const area = (() => {
|
|
26
|
-
try {
|
|
27
|
-
return figurePartFaceOf(revision.asset, part, face, revision.inputs)
|
|
28
|
-
} catch {
|
|
29
|
-
return undefined
|
|
30
|
-
}
|
|
31
|
-
})()
|
|
32
|
-
if (area) {
|
|
33
|
-
const k = area.normal.findIndex(v => Math.abs(v) > 0.5)
|
|
34
|
-
if (k >= 0) return [AXES[k]!, area.normal[k]! > 0 ? 1 : -1]
|
|
35
|
-
}
|
|
36
|
-
return face in NORMAL ? NORMAL[face as FigureFace] : undefined
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* The part's own face a fastening takes: the one looking against the target's face (올려놓기), with it (같은 면), or its
|
|
41
|
-
* centre. Read from where each face looks now, so a part standing turned offers the face that is really there.
|
|
42
|
-
* Undefined when the part has no such face.
|
|
43
|
-
*/
|
|
44
|
-
export function myFaceFor(revision: FigureRevision, part: string, target: FastenTarget, mode: FastenMode): FigureFace | 'centre' | undefined {
|
|
45
|
-
const asset = revision.asset
|
|
46
|
-
if (mode === 'centre') return target === 'plane' ? undefined : 'centre'
|
|
47
|
-
const want: [Axis, 1 | -1] | undefined =
|
|
48
|
-
target === 'plane'
|
|
49
|
-
? /* The plane is met from above on a floor, from in front on a wall. */ [mountingPlaneOf(asset).normal, -1]
|
|
50
|
-
: (() => {
|
|
51
|
-
const d = directionOf(revision, target.part, target.face)
|
|
52
|
-
return d && [d[0], (mode === 'meet' ? -d[1] : d[1]) as 1 | -1]
|
|
53
|
-
})()
|
|
54
|
-
if (!want) return undefined
|
|
55
|
-
const mine = facesOf(asset, part).filter((f): f is FigureFace => f in NORMAL)
|
|
56
|
-
const hit = mine.find(f => {
|
|
57
|
-
const d = directionOf(revision, part, f)
|
|
58
|
-
return d && d[0] === want[0] && d[1] === want[1]
|
|
59
|
-
})
|
|
60
|
-
return hit ?? (target !== 'plane' && mode === 'meet' && target.face in OPPOSITE ? OPPOSITE[target.face as FigureFace] : undefined)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Which way the fastening is, read back from the part's face and facing. */
|
|
64
|
-
export const modeOf = (face: string, facing: 'meet' | 'flush'): FastenMode => (face === 'centre' ? 'centre' : facing)
|
|
65
|
-
|
|
66
19
|
/**
|
|
67
|
-
* The face seen from above: its two axes across (as the form's alignment names them), the face's size on each
|
|
68
|
-
* for a plane with no declared size -- the part's
|
|
20
|
+
* The face seen from above: its two axes across (as the form's alignment names them), the face's size on each as a
|
|
21
|
+
* fastening lines up to it -- none for a plane with no declared size -- the part's real extent on each, whether its
|
|
22
|
+
* edges can be lined up with, and the kernel's measure the drawing asks again as the part is moved.
|
|
69
23
|
*/
|
|
70
24
|
export interface FaceView {
|
|
71
25
|
axes: [Axis, Axis]
|
|
72
26
|
face: Partial<Record<Axis, number>>
|
|
73
27
|
part: Record<Axis, number>
|
|
74
28
|
edges: Record<Axis, boolean>
|
|
29
|
+
measure: FigureFaceMeasure
|
|
75
30
|
}
|
|
76
31
|
|
|
77
|
-
export function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget): FaceView | undefined {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
32
|
+
export function faceViewOf(revision: FigureRevision, part: string, target: FastenTarget, facing: FastenMode): FaceView | undefined {
|
|
33
|
+
const measure = (() => {
|
|
34
|
+
try {
|
|
35
|
+
return figureFaceMeasureOf(revision.asset, part, revision.inputs, { on: target, facing })
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
40
|
+
if (!measure) return undefined
|
|
41
|
+
const across = alignAxesOf(revision.asset, target)
|
|
84
42
|
const face: Partial<Record<Axis, number>> = {}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const theirs = boxOf(revision, target.part)
|
|
89
|
-
const normal = faceAxisOf(asset, target.part, target.face)
|
|
90
|
-
for (const a of axes) if (theirs && a !== normal) face[a] = theirs.extent[a]
|
|
43
|
+
for (const a of measure.axes) {
|
|
44
|
+
const e = measure.edges[a]
|
|
45
|
+
if (e) face[a] = e.hi - e.lo
|
|
91
46
|
}
|
|
47
|
+
const extent = (a: Axis) => measure.extent[a].hi - measure.extent[a].lo
|
|
92
48
|
return {
|
|
93
|
-
axes,
|
|
49
|
+
axes: measure.axes,
|
|
94
50
|
face,
|
|
95
|
-
part: { x:
|
|
96
|
-
edges: Object.fromEntries(
|
|
51
|
+
part: { x: extent('x'), y: extent('y'), z: extent('z') },
|
|
52
|
+
edges: Object.fromEntries(measure.axes.map(a => [a, !!across.find(o => o.axis === a)?.edges])) as Record<Axis, boolean>,
|
|
53
|
+
measure
|
|
97
54
|
}
|
|
98
55
|
}
|
|
99
56
|
|
|
100
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Where the part's centre is from the face's centre along one axis, for the way it is aligned there: the kernel's
|
|
59
|
+
* `figureFastenPlaceOf` (ADR-0140 D2), the form's distances -- typed, or a share of the size read as millimetres -- as
|
|
60
|
+
* a distance, a position field's as the centre.
|
|
61
|
+
*/
|
|
101
62
|
export function offsetOf(how: Alignment | undefined, mm: number | undefined, face: number | undefined, part: number): number {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (how === 'mm' || how === 'share') return mm ?? 0
|
|
105
|
-
return 0
|
|
63
|
+
const align = how === 'mm' || how === 'share' ? { mm: mm ?? 0 } : how === 'min' || how === 'max' || how === 'centre' ? how : undefined
|
|
64
|
+
return figureFastenPlaceOf({ align: { x: align }, face: { x: face }, part: { x: part } }).x!
|
|
106
65
|
}
|
|
107
66
|
|
|
108
67
|
/**
|
|
@@ -125,56 +84,49 @@ export function alignmentAt(d: number, face: number | undefined, part: number, e
|
|
|
125
84
|
return { how: 'mm', mm: Math.round(d) }
|
|
126
85
|
}
|
|
127
86
|
|
|
87
|
+
/** The form's alignment on each axis across the face, in the kernel's words: a typed or shared distance as millimetres. */
|
|
88
|
+
function kernelAlignOf(view: FaceView, align: Partial<Record<Axis, Alignment>>, alignMm: Partial<Record<Axis, number>>): Partial<Record<Axis, FigureFastenAlignment>> {
|
|
89
|
+
const out: Partial<Record<Axis, FigureFastenAlignment>> = {}
|
|
90
|
+
for (const a of view.axes) {
|
|
91
|
+
const how = align[a] ?? 'centre'
|
|
92
|
+
out[a] = how === 'mm' || how === 'share' ? { mm: alignMm[a] ?? 0 } : how === 'min' || how === 'max' ? how : 'centre'
|
|
93
|
+
}
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
128
97
|
/**
|
|
129
|
-
* The alignment that brings the
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* is something fastened on it. `unmeasured` when the face has no size to measure against.
|
|
98
|
+
* The alignment that brings the part inside the face, for 정렬·간격 고치기: the kernel's least move
|
|
99
|
+
* (`figureFaceInsideAlignOf`), in the form's words. `inside` when nothing of it is past an edge: what is out is something
|
|
100
|
+
* fastened on it. `unmeasured` when the face has no size to measure against.
|
|
133
101
|
*/
|
|
134
102
|
export function insideAlignmentOf(
|
|
135
103
|
view: FaceView | undefined,
|
|
136
104
|
align: Partial<Record<Axis, Alignment>>,
|
|
137
105
|
alignMm: Partial<Record<Axis, number>>
|
|
138
106
|
): { align: Partial<Record<Axis, Alignment>>; alignMm: Partial<Record<Axis, number>>; moved: Axis[] } | { cannot: 'inside' | 'unmeasured' } {
|
|
139
|
-
if (!view
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!low && !high) continue
|
|
148
|
-
next.moved.push(a)
|
|
149
|
-
if (part > face) {
|
|
150
|
-
next.align[a] = 'centre'
|
|
151
|
-
continue
|
|
152
|
-
}
|
|
153
|
-
const edge = ((low ? -1 : 1) * (face - part)) / 2
|
|
154
|
-
if (view.edges[a]) next.align[a] = low ? 'min' : 'max'
|
|
155
|
-
else ((next.align[a] = 'mm'), (next.alignMm[a] = Math.round(edge)))
|
|
107
|
+
if (!view) return { cannot: 'unmeasured' }
|
|
108
|
+
const fix = figureFaceInsideAlignOf(view.measure, kernelAlignOf(view, align, alignMm), view.edges)
|
|
109
|
+
if ('cannot' in fix) return fix
|
|
110
|
+
const next = { align: { ...align }, alignMm: { ...alignMm }, moved: fix.moved as Axis[] }
|
|
111
|
+
for (const a of fix.moved as Axis[]) {
|
|
112
|
+
const how = fix.align[a]
|
|
113
|
+
if (how && typeof how === 'object') ((next.align[a] = 'mm'), (next.alignMm[a] = how.mm))
|
|
114
|
+
else if (how) next.align[a] = how
|
|
156
115
|
}
|
|
157
|
-
return next
|
|
116
|
+
return next
|
|
158
117
|
}
|
|
159
118
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const lo = at[a] - view.part[a] / 2,
|
|
168
|
-
hi = at[a] + view.part[a] / 2
|
|
169
|
-
if (lo >= face / 2 || hi <= -face / 2) touching = false
|
|
170
|
-
if (-face / 2 - lo > 0.5) sides.push({ axis: a, side: 'low', mm: Math.round(-face / 2 - lo) })
|
|
171
|
-
if (hi - face / 2 > 0.5) sides.push({ axis: a, side: 'high', mm: Math.round(hi - face / 2) })
|
|
172
|
-
}
|
|
173
|
-
return { touching, sides }
|
|
119
|
+
/**
|
|
120
|
+
* How far the part comes past the face's edges, per side, whether any of it lies over the face, and whether what
|
|
121
|
+
* touches the face is on it -- with its middle `at` from the face's middle: the kernel's (`figureFaceOverhangAt`).
|
|
122
|
+
*/
|
|
123
|
+
export function overhangOf(view: FaceView, at: Record<Axis, number>): { touching: boolean; contact: 'on' | 'off' | 'unknown'; sides: Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> } {
|
|
124
|
+
const o = figureFaceOverhangAt(view.measure, Object.fromEntries(view.axes.map(a => [a, { mm: at[a] }])))
|
|
125
|
+
return { touching: o.over, contact: o.contact, sides: o.sides as Array<{ axis: Axis; side: 'low' | 'high'; mm: number }> }
|
|
174
126
|
}
|
|
175
127
|
|
|
176
128
|
/** Whether the part turns about a joint: then 가운데를 면에 is offered -- a roller spinning in place. */
|
|
177
|
-
export const turnsInPlace = (asset: FigureAsset, part: string) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' ||
|
|
129
|
+
export const turnsInPlace = (asset: FigureAsset, part: string) => (asset.joints ?? []).some(j => j.body1.part === part && (j.type === 'revolute' || j.origin?.facing === 'centre'))
|
|
178
130
|
|
|
179
131
|
/**
|
|
180
132
|
* 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
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
fastenTargetsOf,
|
|
24
24
|
type FastenForm
|
|
25
25
|
} from './fasten-form.js'
|
|
26
|
-
import { fieldWordsOf, hasInstanceSize, heldBy, type Alignment, type Axis } from './links.js'
|
|
26
|
+
import { fieldWordsOf, hasInstanceSize, heldBy, mountingPlaneOf, type Alignment, type Axis } from './links.js'
|
|
27
27
|
import type { FigureRevision } from './revision.js'
|
|
28
28
|
|
|
29
29
|
/** The target list's entry for the plane the figure stands by. */
|
|
@@ -178,7 +178,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
178
178
|
const plan = fastenPlanOf(revision, this.partId, f)
|
|
179
179
|
const planeWord = i18next.t('figure.label.resize-place-' + (asset.placement ?? 'floor'))
|
|
180
180
|
const modes: FastenMode[] = f.toPlane
|
|
181
|
-
?
|
|
181
|
+
? mountingPlaneOf(asset).facings
|
|
182
182
|
: ['meet', 'flush', ...(turnsInPlace(asset, this.partId) ? (['centre'] as FastenMode[]) : [])]
|
|
183
183
|
const detail = fastenDetailWordsOf(f, (key, params) => i18next.t(key, params))
|
|
184
184
|
const drawing = (show: 'face' | 'how') =>
|
|
@@ -202,7 +202,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
202
202
|
${
|
|
203
203
|
plan.aim
|
|
204
204
|
? html`<p fasten-sentence role="status">
|
|
205
|
-
${fastenSentenceOf(
|
|
205
|
+
${fastenSentenceOf(f, plan, planeWord, (key, params) => i18next.t(key, params))}
|
|
206
206
|
</p>`
|
|
207
207
|
: html`<p hint fasten-sentence>${i18next.t('figure.text.fasten-target-first')}</p>`
|
|
208
208
|
}
|
|
@@ -215,7 +215,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
215
215
|
? html`<div question="face">
|
|
216
216
|
<h4>
|
|
217
217
|
${
|
|
218
|
-
/* The face asked is the target's, named: the part
|
|
218
|
+
/* 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
219
|
? i18next.t('figure.label.fasten-face-on-plane', { plane: planeWord })
|
|
220
220
|
: i18next.t('figure.label.fasten-face-on-part', { part: f.target })
|
|
221
221
|
}
|
|
@@ -246,14 +246,6 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
246
246
|
placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
|
|
247
247
|
@change=${(e: Event) => this.set({ gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined })}
|
|
248
248
|
/>
|
|
249
|
-
<span>${i18next.t('figure.label.twist')}</span>
|
|
250
|
-
<input
|
|
251
|
-
twist
|
|
252
|
-
type="number"
|
|
253
|
-
step="any"
|
|
254
|
-
.value=${String(f.twist)}
|
|
255
|
-
@change=${(e: Event) => this.set({ twist: Number((e.target as HTMLInputElement).value) || 0 })}
|
|
256
|
-
/>
|
|
257
249
|
</div>
|
|
258
250
|
</details>
|
|
259
251
|
</div>
|
|
@@ -463,7 +463,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
463
463
|
const revision = this.revision
|
|
464
464
|
if (!revision) return
|
|
465
465
|
const form = fastenFormOf(revision, this.partId)
|
|
466
|
-
/* Whether it is out
|
|
466
|
+
/* Whether it is out, and the least move that brings it in, are the kernel's measure (ADR-0140 3.8). */
|
|
467
467
|
const own = figureFaceOverhangOf(revision.asset, this.partId, revision.inputs)
|
|
468
468
|
const fixed =
|
|
469
469
|
own?.state === 'unknown'
|
|
@@ -486,21 +486,20 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
/**
|
|
489
|
-
* What holds the part, said as its state --
|
|
489
|
+
* What holds the part, said as its state -- 「바닥에 붙어 있음」 -- with the ways to change it: open the fastening,
|
|
490
490
|
* or take it off. Right after "follow size, attached" this is the result, not a refusal (designer 2026-09-27).
|
|
491
491
|
*/
|
|
492
492
|
private heldWords(asset: FigureAsset) {
|
|
493
493
|
const joint = (asset.joints ?? []).find(j => j.body1.part === this.partId)
|
|
494
|
-
const face = joint ? i18next.t(endPlaceKeyOf(joint.body1)) : ''
|
|
495
494
|
const gap = joint?.origin?.gap ?? 0
|
|
496
495
|
const place = i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`)
|
|
497
496
|
const where = !joint
|
|
498
497
|
? i18next.t('figure.text.held-by-rule')
|
|
499
498
|
: 'plane' in joint.body0
|
|
500
499
|
? typeof gap === 'number'
|
|
501
|
-
? i18next.t('figure.text.held-on-plane', {
|
|
502
|
-
: i18next.t('figure.text.held-on-plane-worked-out', {
|
|
503
|
-
: i18next.t('figure.text.held-on-part', {
|
|
500
|
+
? i18next.t('figure.text.held-on-plane', { place, gap: String(gap) })
|
|
501
|
+
: i18next.t('figure.text.held-on-plane-worked-out', { place })
|
|
502
|
+
: i18next.t('figure.text.held-on-part', { other: joint.body0.part, otherFace: i18next.t(endPlaceKeyOf(joint.body0)) })
|
|
504
503
|
return where
|
|
505
504
|
}
|
|
506
505
|
|
|
@@ -572,7 +571,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
572
571
|
return html`
|
|
573
572
|
${overhang}
|
|
574
573
|
<div fastened-summary>
|
|
575
|
-
<p fasten-sentence>${fastenSentenceOf(
|
|
574
|
+
<p fasten-sentence>${fastenSentenceOf(form, plan, planeWord, (key, params) => i18next.t(key, params))}</p>
|
|
576
575
|
<button act change-fastening ?disabled=${this.locked} @click=${() => this.openFastening()}>${i18next.t('figure.button.change-fastening')}</button>
|
|
577
576
|
</div>
|
|
578
577
|
${this.renderDetachWords()}
|
|
@@ -39,8 +39,8 @@ const round = (v: number, step: number) => Number((Math.round(v / step) * step).
|
|
|
39
39
|
const rotateMatrix = (j: FigureJoint) => eulerMatrixOrder(AXES.map(a => j.origin?.parentRotate?.[a] ?? 0), 'ZYX')
|
|
40
40
|
const eulerMatrixOrder = (deg: number[], order: THREE.EulerOrder) => new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(...(deg.map(THREE.MathUtils.degToRad) as [number, number, number]), order))
|
|
41
41
|
|
|
42
|
-
/** The part's own
|
|
43
|
-
const turnMatrix = (j: FigureJoint) =>
|
|
42
|
+
/** The part's own pose on its end, any angle, as the record says it (`origin.childRotate`, Rz·Ry·Rx, ADR-0140 3.11). */
|
|
43
|
+
const turnMatrix = (j: FigureJoint) => eulerMatrixOrder(AXES.map(a => j.origin?.childRotate?.[a] ?? 0), 'ZYX')
|
|
44
44
|
|
|
45
45
|
/** The joint frame's rotation in the figure: the part's rotation now, less its own turn and less the joint's. */
|
|
46
46
|
function jointFrameRotation(j: FigureJoint, part: FigureRenderPart, angle: number): THREE.Matrix4 {
|
|
@@ -113,9 +113,9 @@ export function jointDragOf(asset: FigureAsset, id: string, part: FigureRenderPa
|
|
|
113
113
|
}
|
|
114
114
|
return { kind: 'joint', joint: { ...j, origin: { ...origin, offset } } as FigureJoint }
|
|
115
115
|
}
|
|
116
|
-
const normal: Axis = 'plane' in j.body0 ? 'y' : FACE_AXIS[j.body0.face]!
|
|
117
|
-
/* Which way a larger gap moves the part: out of the face it meets
|
|
118
|
-
const outward =
|
|
116
|
+
const normal: Axis = 'plane' in j.body0 ? (asset.placement === 'wall' ? 'z' : 'y') : FACE_AXIS[j.body0.face]!
|
|
117
|
+
/* Which way a larger gap moves the part: out of the face it meets or sits by its middle on, into it when flush with it (ADR-0140 3.11). */
|
|
118
|
+
const outward = ((j.origin?.facing ?? 'meet') === 'flush' ? -1 : 1) * ('plane' in j.body0 ? 1 : FACE_SIGN[j.body0.face]!)
|
|
119
119
|
const along = local[normal]
|
|
120
120
|
if (Math.abs(along) >= 0.5) {
|
|
121
121
|
const gap = origin.gap ?? 0
|