@things-factory/figure-ui 10.1.180 → 10.1.183
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-styles.ts +28 -9
- package/client/modeller/editor.ts +105 -12
- package/client/modeller/fasten-form.ts +207 -0
- package/client/modeller/fasten-geometry.ts +5 -1
- package/client/modeller/figure-fasten-dialog.ts +442 -0
- package/client/modeller/figure-fasten-drawing.ts +32 -11
- package/client/modeller/figure-links.ts +56 -300
- package/client/modeller/modeller-keys.ts +4 -4
- package/client/pages/figure-modeller-page.ts +17 -11
- package/dist-client/modeller/editor-styles.js +28 -9
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +23 -2
- package/dist-client/modeller/editor.js +121 -12
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +69 -0
- package/dist-client/modeller/fasten-form.js +131 -0
- package/dist-client/modeller/fasten-form.js.map +1 -0
- package/dist-client/modeller/fasten-geometry.d.ts +3 -1
- package/dist-client/modeller/fasten-geometry.js +6 -1
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +64 -0
- package/dist-client/modeller/figure-fasten-dialog.js +418 -0
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -0
- package/dist-client/modeller/figure-fasten-drawing.d.ts +6 -3
- package/dist-client/modeller/figure-fasten-drawing.js +36 -10
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +15 -39
- package/dist-client/modeller/figure-links.js +58 -301
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/modeller-keys.js +4 -4
- package/dist-client/modeller/modeller-keys.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +5 -1
- package/dist-client/pages/figure-modeller-page.js +14 -12
- 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/package.json +14 -14
- package/test/browser/links-page.js +4 -4
- package/test/browser/links.check.mjs +184 -46
- package/test/browser/modeller-page.js +23 -0
- package/test/browser/modeller.check.mjs +19 -20
- package/test/browser/namespace-page.js +2 -29
- package/test/browser/popup-stand-in.js +48 -0
- package/test/browser/product-page.check.mjs +4 -3
- package/test/browser/product-page.js +19 -32
- package/test/browser/props.check.mjs +28 -7
- package/test/browser/screen.check.mjs +28 -23
- package/test/browser/section-page.js +13 -41
- package/test/browser/section.check.mjs +39 -30
- package/test/fasten-form.test.ts +77 -0
- package/test/fasten-geometry.test.ts +2 -0
- package/test/first-minute.test.ts +7 -4
- package/translations/en.json +4 -3
- package/translations/es.json +4 -3
- package/translations/ja.json +4 -3
- package/translations/ko.json +4 -3
- package/translations/ms.json +4 -3
- package/translations/zh.json +4 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { FigureAuthoringAction, FigureFace } from '@hatiolab/figure-model';
|
|
2
|
+
import { type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js';
|
|
3
|
+
import type { FastenChange } from './figure-fasten-drawing.js';
|
|
4
|
+
import { type Alignment, type Axis } from './links.js';
|
|
5
|
+
import type { FigureRevision } from './revision.js';
|
|
6
|
+
export interface FastenForm {
|
|
7
|
+
face: FigureFace | 'centre';
|
|
8
|
+
toPlane: boolean;
|
|
9
|
+
target: string;
|
|
10
|
+
targetFace: FigureFace;
|
|
11
|
+
facing: 'meet' | 'flush';
|
|
12
|
+
gap: number;
|
|
13
|
+
/** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */
|
|
14
|
+
gapKept: unknown;
|
|
15
|
+
twist: number;
|
|
16
|
+
align: Partial<Record<Axis, Alignment>>;
|
|
17
|
+
alignMm: Partial<Record<Axis, number>>;
|
|
18
|
+
alignInput: Partial<Record<Axis, string>>;
|
|
19
|
+
}
|
|
20
|
+
export declare const freshFasten: () => FastenForm;
|
|
21
|
+
/** 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). */
|
|
22
|
+
export declare function fastenFormOf(revision: FigureRevision, part: string): FastenForm;
|
|
23
|
+
/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */
|
|
24
|
+
export declare function fastenTargetsOf(revision: FigureRevision, part: string): Array<{
|
|
25
|
+
part: string;
|
|
26
|
+
loops: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* A new aim: the plane, another part, or another face of the same part. Where on the face is filled in as where the
|
|
30
|
+
* part stands now, so fastening it keeps it there (`placeKeptOf`); the author changes it from there.
|
|
31
|
+
*/
|
|
32
|
+
export declare function aimFasten(revision: FigureRevision, part: string, form: FastenForm, to: {
|
|
33
|
+
plane: true;
|
|
34
|
+
} | {
|
|
35
|
+
part: string;
|
|
36
|
+
} | {
|
|
37
|
+
targetFace: FigureFace;
|
|
38
|
+
}): FastenForm;
|
|
39
|
+
/** A change the drawing asks: a face, a way, a gap or a place across the face. */
|
|
40
|
+
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. */
|
|
42
|
+
export interface FastenPlan {
|
|
43
|
+
/** Nothing is aimed at until a part or the plane is chosen. */
|
|
44
|
+
aim?: FastenTarget;
|
|
45
|
+
theirFaces: string[];
|
|
46
|
+
targetFace: FigureFace;
|
|
47
|
+
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
|
+
view?: FaceView;
|
|
51
|
+
/** The axes across the face asked in rows: where the drawing cannot set the place. */
|
|
52
|
+
alignRows: {
|
|
53
|
+
axis: Axis;
|
|
54
|
+
edges: boolean;
|
|
55
|
+
}[];
|
|
56
|
+
action?: FigureAuthoringAction;
|
|
57
|
+
/** The face the command would meet, lit on the canvas while it is tried. */
|
|
58
|
+
face?: {
|
|
59
|
+
part: string;
|
|
60
|
+
face: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export declare function fastenPlanOf(revision: FigureRevision, part: string, f: FastenForm): FastenPlan;
|
|
64
|
+
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;
|
|
67
|
+
/** What the folded details hold that is not their default, said while folded; nothing for the default. */
|
|
68
|
+
export declare function fastenDetailWordsOf(form: FastenForm, say: Say): string[];
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
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';
|
|
4
|
+
/*
|
|
5
|
+
* A fastening as it is chosen, before it is done: what, which face, how, where across the face. The fastening window
|
|
6
|
+
* (`figure-fasten-dialog.ts`) holds one and changes it as the author chooses; the attach command is worked out from it
|
|
7
|
+
* only here, so the window, its preview and the panel's 「정렬 고치기」 send the same command for the same choice.
|
|
8
|
+
*/
|
|
9
|
+
const FACES = Object.keys(FIGURE_FACES);
|
|
10
|
+
export const freshFasten = () => ({
|
|
11
|
+
face: 'bottom',
|
|
12
|
+
toPlane: false,
|
|
13
|
+
target: '',
|
|
14
|
+
targetFace: 'top',
|
|
15
|
+
facing: 'meet',
|
|
16
|
+
gap: 0,
|
|
17
|
+
gapKept: undefined,
|
|
18
|
+
twist: 0,
|
|
19
|
+
align: {},
|
|
20
|
+
alignMm: {},
|
|
21
|
+
alignInput: {}
|
|
22
|
+
});
|
|
23
|
+
/** 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). */
|
|
24
|
+
export function fastenFormOf(revision, part) {
|
|
25
|
+
const own = fasteningOf(revision, part);
|
|
26
|
+
return own && !own.atOrigin ? { ...freshFasten(), ...own } : freshFasten();
|
|
27
|
+
}
|
|
28
|
+
/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */
|
|
29
|
+
export function fastenTargetsOf(revision, part) {
|
|
30
|
+
const asset = revision.asset;
|
|
31
|
+
return (asset.appearance
|
|
32
|
+
.map(look => look.target)
|
|
33
|
+
.filter(one => one !== part && canFasten(asset, one))
|
|
34
|
+
/* Not a part that hangs from this one: fastening a frame to its own roller would close a loop. */
|
|
35
|
+
.map(one => ({ part: one, loops: hangsFrom(asset, one, part) })));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A new aim: the plane, another part, or another face of the same part. Where on the face is filled in as where the
|
|
39
|
+
* part stands now, so fastening it keeps it there (`placeKeptOf`); the author changes it from there.
|
|
40
|
+
*/
|
|
41
|
+
export function aimFasten(revision, part, form, to) {
|
|
42
|
+
const f = 'plane' in to
|
|
43
|
+
? { ...form, toPlane: true, target: '' }
|
|
44
|
+
: 'part' in to
|
|
45
|
+
? { ...form, toPlane: false, target: to.part }
|
|
46
|
+
: { ...form, targetFace: to.targetFace };
|
|
47
|
+
const theirFaces = f.target ? facesOf(revision.asset, f.target) : FACES;
|
|
48
|
+
const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]);
|
|
49
|
+
const aim = f.toPlane
|
|
50
|
+
? 'plane'
|
|
51
|
+
: f.target
|
|
52
|
+
? { part: f.target, face: targetFace }
|
|
53
|
+
: undefined;
|
|
54
|
+
return { ...f, ...(aim ? placeKeptOf(revision, part, aim, !!f.twist) : { align: {}, alignMm: {}, alignInput: {} }) };
|
|
55
|
+
}
|
|
56
|
+
/** A change the drawing asks: a face, a way, a gap or a place across the face. */
|
|
57
|
+
export function fastenChanged(revision, part, form, change) {
|
|
58
|
+
const f = form;
|
|
59
|
+
if ('targetFace' in change)
|
|
60
|
+
return aimFasten(revision, part, f, { targetFace: change.targetFace });
|
|
61
|
+
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
|
+
};
|
|
67
|
+
if ('gap' in change)
|
|
68
|
+
return { ...f, gap: change.gap, gapKept: undefined };
|
|
69
|
+
return {
|
|
70
|
+
...f,
|
|
71
|
+
align: { ...f.align, [change.axis]: change.how },
|
|
72
|
+
alignMm: change.how === 'mm' ? { ...f.alignMm, [change.axis]: change.mm ?? 0 } : f.alignMm
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function fastenPlanOf(revision, part, f) {
|
|
76
|
+
const asset = revision.asset;
|
|
77
|
+
const mineFaces = ownFacesOf(asset, part, f.toPlane);
|
|
78
|
+
const theirFaces = f.target ? facesOf(asset, f.target) : FACES;
|
|
79
|
+
const targetFace = (theirFaces.includes(f.targetFace) ? f.targetFace : theirFaces[0]);
|
|
80
|
+
const aim = f.toPlane
|
|
81
|
+
? 'plane'
|
|
82
|
+
: f.target
|
|
83
|
+
? { part: f.target, face: targetFace }
|
|
84
|
+
: 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]);
|
|
88
|
+
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);
|
|
92
|
+
const action = {
|
|
93
|
+
kind: 'attach',
|
|
94
|
+
part,
|
|
95
|
+
face: myFace,
|
|
96
|
+
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
97
|
+
...(f.toPlane ? {} : { facing: f.facing }),
|
|
98
|
+
...fastenGapOf(f),
|
|
99
|
+
twist: f.twist,
|
|
100
|
+
...(align ? { align } : {}),
|
|
101
|
+
replace: !!heldBy(asset, part)
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
aim,
|
|
105
|
+
theirFaces,
|
|
106
|
+
targetFace,
|
|
107
|
+
mode,
|
|
108
|
+
myFace,
|
|
109
|
+
view,
|
|
110
|
+
alignRows: alignRowsOf(asset, part, aim, view, !!f.twist, f.alignInput),
|
|
111
|
+
action,
|
|
112
|
+
...(f.toPlane ? {} : { face: { part: f.target, face: targetFace } })
|
|
113
|
+
};
|
|
114
|
+
}
|
|
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) {
|
|
117
|
+
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 });
|
|
119
|
+
}
|
|
120
|
+
/** What the folded details hold that is not their default, said while folded; nothing for the default. */
|
|
121
|
+
export function fastenDetailWordsOf(form, say) {
|
|
122
|
+
return [
|
|
123
|
+
...(form.gapKept !== undefined
|
|
124
|
+
? [say('figure.text.fasten-summary-gap-worked-out')]
|
|
125
|
+
: form.gap
|
|
126
|
+
? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]
|
|
127
|
+
: []),
|
|
128
|
+
...(form.twist ? [say('figure.text.fasten-summary-twist', { degrees: String(form.twist) })] : [])
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=fasten-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fasten-form.js","sourceRoot":"","sources":["../../client/modeller/fasten-form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAqD,MAAM,sBAAsB,CAAA;AAEvH,OAAO,EACL,OAAO,EACP,WAAW,EACX,SAAS,EACT,OAAO,EACP,WAAW,EACX,WAAW,EACX,SAAS,EACT,MAAM,EACN,UAAU,EACV,WAAW,EAGZ,MAAM,YAAY,CAAA;AAGnB;;;;GAIG;AAEH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAiB,CAAA;AAiBvD,MAAM,CAAC,MAAM,WAAW,GAAG,GAAe,EAAE,CAAC,CAAC;IAC5C,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,EAAE;IACV,UAAU,EAAE,KAAK;IACjB,MAAM,EAAE,MAAM;IACd,GAAG,EAAE,CAAC;IACN,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,CAAC;IACR,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,EAAE;IACX,UAAU,EAAE,EAAE;CACf,CAAC,CAAA;AAEF,oHAAoH;AACpH,MAAM,UAAU,YAAY,CAAC,QAAwB,EAAE,IAAY;IACjE,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACvC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAE,EAAE,GAAG,WAAW,EAAE,EAAE,GAAG,GAAG,EAAiB,CAAC,CAAC,CAAC,WAAW,EAAE,CAAA;AAC5F,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,eAAe,CAAC,QAAwB,EAAE,IAAY;IACpE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAA;IAC5B,OAAO,CACL,KAAK,CAAC,UAAU;SACb,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;SACxB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACrD,kGAAkG;SACjG,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CACnE,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CACvB,QAAwB,EACxB,IAAY,EACZ,IAAgB,EAChB,EAAmE;IAEnE,MAAM,CAAC,GACL,OAAO,IAAI,EAAE;QACX,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;QACxC,CAAC,CAAC,MAAM,IAAI,EAAE;YACZ,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,EAAE;YAC9C,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC,UAAU,EAAE,CAAA;IAC9C,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IACvE,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAe,CAAA;IACnG,MAAM,GAAG,GAA6B,CAAC,CAAC,OAAO;QAC7C,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE;YACtC,CAAC,CAAC,SAAS,CAAA;IACf,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,EAAE,CAAA;AACtH,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAC3B,QAAwB,EACxB,IAAY,EACZ,IAAgB,EAChB,MAAoB;IAEpB,MAAM,CAAC,GAAG,IAAI,CAAA;IACd,IAAI,YAAY,IAAI,MAAM;QAAE,OAAO,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAwB,EAAE,CAAC,CAAA;IAChH,IAAI,MAAM,IAAI,MAAM;QAClB,OAAO;YACL,GAAG,CAAC;YACJ,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;YACnF,MAAM,EAAE,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;SACnD,CAAA;IACH,IAAI,KAAK,IAAI,MAAM;QAAE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;IACzE,OAAO;QACL,GAAG,CAAC;QACJ,KAAK,EAAE,EAAE,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,GAAG,EAAE;QAChD,OAAO,EAAE,MAAM,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;KAC3F,CAAA;AACH,CAAC;AAmBD,MAAM,UAAU,YAAY,CAAC,QAAwB,EAAE,IAAY,EAAE,CAAa;IAChF,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAA;IAC5B,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IACpD,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IAC9D,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAe,CAAA;IACnG,MAAM,GAAG,GAA6B,CAAC,CAAC,OAAO;QAC7C,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE;YACtC,CAAC,CAAC,SAAS,CAAA;IACf,MAAM,IAAI,GAAe,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;IACtE,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACtE,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,CAAC,CAAA;IAC/E,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,CAAA;IACxE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,CAAA;IACpF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,CAAA;IAC5C,MAAM,MAAM,GAAG;QACb,IAAI,EAAE,QAAQ;QACd,IAAI;QACJ,IAAI,EAAE,MAAM;QACZ,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE;QAClF,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1C,GAAG,WAAW,CAAC,CAAC,CAAC;QACjB,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC;KACN,CAAA;IAC1B,OAAO;QACL,GAAG;QACH,UAAU;QACV,UAAU;QACV,IAAI;QACJ,MAAM;QACN,IAAI;QACJ,SAAS,EAAE,WAAW,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC;QACvE,MAAM;QACN,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC;KACrE,CAAA;AACH,CAAC;AAID,4GAA4G;AAC5G,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,IAAgB,EAChB,IAAgB,EAChB,SAAiB,EACjB,GAAQ;IAER,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC,oBAAoB,GAAG,IAAI,CAAC,UAAU,CAAC,EAAE,CAAA;IACtG,OAAO,GAAG,CAAC,+BAA+B,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,oBAAoB,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;AACrH,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,mBAAmB,CAAC,IAAgB,EAAE,GAAQ;IAC5D,OAAO;QACL,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS;YAC5B,CAAC,CAAC,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;YACpD,CAAC,CAAC,IAAI,CAAC,GAAG;gBACR,CAAC,CAAC,CAAC,GAAG,CAAC,gCAAgC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACnE,CAAC,CAAC,EAAE,CAAC;QACT,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kCAAkC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAClG,CAAA;AACH,CAAC","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"]}
|
|
@@ -75,7 +75,9 @@ export declare function screenOf(axes: readonly [Axis, Axis]): {
|
|
|
75
75
|
* Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the
|
|
76
76
|
* roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge
|
|
77
77
|
* 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,
|
|
78
|
-
* else just under it -- above it when the drawing ends first.
|
|
78
|
+
* else just under it -- above it when the drawing ends first. Under a part as tall as the face, just under it is the
|
|
79
|
+
* 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.
|
|
80
|
+
* Widths are estimated: 7 px a letter at 11-12 px; a line of it stands 11 px over its baseline and 3 under.
|
|
79
81
|
*/
|
|
80
82
|
export declare function fastenLabelsOf(face: {
|
|
81
83
|
x: number;
|
|
@@ -170,7 +170,9 @@ export function screenOf(axes) {
|
|
|
170
170
|
* Where the fastening drawing writes its two names so neither lies over the other (user 2026-10-16: 「roller」 over the
|
|
171
171
|
* roller's thin bar, and 「frame 윗면 2000 × 600」 cut by it). The face's name stands above the face, inside its top edge
|
|
172
172
|
* 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,
|
|
173
|
-
* else just under it -- above it when the drawing ends first.
|
|
173
|
+
* else just under it -- above it when the drawing ends first. Under a part as tall as the face, just under it is the
|
|
174
|
+
* 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.
|
|
175
|
+
* Widths are estimated: 7 px a letter at 11-12 px; a line of it stands 11 px over its baseline and 3 under.
|
|
174
176
|
*/
|
|
175
177
|
export function fastenLabelsOf(face, part, partName, height) {
|
|
176
178
|
const facePlace = face.y >= 16 ? { x: face.x, y: face.y - 5 } : { x: face.x + 4, y: face.y + 14 };
|
|
@@ -178,6 +180,9 @@ export function fastenLabelsOf(face, part, partName, height) {
|
|
|
178
180
|
if (fits)
|
|
179
181
|
return { face: facePlace, part: { x: part.x + part.w / 2, y: part.y + part.h / 2 + 4, inside: true } };
|
|
180
182
|
const below = part.y + part.h + 14;
|
|
183
|
+
const edge = face.y + face.h;
|
|
184
|
+
if (below - 11 < edge + 2 && below + 3 > edge - 2 && edge + 14 <= height - 4)
|
|
185
|
+
return { face: facePlace, part: { x: part.x + part.w / 2, y: edge + 14, inside: false } };
|
|
181
186
|
return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } };
|
|
182
187
|
}
|
|
183
188
|
//# sourceMappingURL=fasten-geometry.js.map
|
|
@@ -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. Widths are estimated: 7 px a letter at 11-12 px.\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 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 { 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"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import '@material/web/button/filled-button.js';
|
|
2
|
+
import '@material/web/button/text-button.js';
|
|
3
|
+
import { LitElement, nothing } from 'lit';
|
|
4
|
+
import '@operato/input/ox-input-select-buttons.js';
|
|
5
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
6
|
+
import './figure-fasten-drawing.js';
|
|
7
|
+
import type { FigureRevision } from './revision.js';
|
|
8
|
+
declare const FigureFastenDialog_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
9
|
+
/**
|
|
10
|
+
* 붙이기 창: what the selected part is fastened to, on which face, how -- chosen with room to see it, in the standard
|
|
11
|
+
* popup the editor opens (`openPopup`: its title, its close, moved by its head) (user 2026-10-17: 「속성에는 심플하고
|
|
12
|
+
* 직관적인 결과 위주로, 편집은 팝업에서 넓은 공간을 써서」; 2026-10-18: the standard popup, not a window of its own).
|
|
13
|
+
*
|
|
14
|
+
* Everything chosen here is tried, not done: the command it comes to is told to the editor as it changes
|
|
15
|
+
* (`preview-command`), and the canvas draws it. 「적용」 sends it once (`fasten-apply`), one step of the history;
|
|
16
|
+
* 「취소」 and Esc close it with nothing written (`fasten-cancel`). A row of the target list under the pointer or the
|
|
17
|
+
* arrows asks the canvas to light that part (`fasten-hover`). The window is the only one telling the preview while it
|
|
18
|
+
* is open; the editor keeps the panel quiet.
|
|
19
|
+
*/
|
|
20
|
+
export declare class FigureFastenDialog extends FigureFastenDialog_base {
|
|
21
|
+
static styles: import("lit").CSSResult[];
|
|
22
|
+
/** The figure at the version on screen. */
|
|
23
|
+
revision?: FigureRevision;
|
|
24
|
+
/** The part being fastened. */
|
|
25
|
+
partId: string;
|
|
26
|
+
locked: boolean;
|
|
27
|
+
/** What the command would do, in words (the editor works it out: `command-preview.ts`). */
|
|
28
|
+
previewWords: string[];
|
|
29
|
+
/** Why the last 「적용」 was refused, in the editor's words. */
|
|
30
|
+
error: string;
|
|
31
|
+
private form?;
|
|
32
|
+
/** The command the part's own fastening comes to, as opened: tried unchanged, nothing new is said of it. */
|
|
33
|
+
private opened?;
|
|
34
|
+
private told;
|
|
35
|
+
private loadedFor?;
|
|
36
|
+
private hovered;
|
|
37
|
+
willUpdate(): void;
|
|
38
|
+
firstUpdated(): Promise<void>;
|
|
39
|
+
updated(): void;
|
|
40
|
+
disconnectedCallback(): void;
|
|
41
|
+
render(): typeof nothing | import("lit-html").TemplateResult<1>;
|
|
42
|
+
/**
|
|
43
|
+
* What to fasten to: the plane and the other parts, as the standard choice chips. The arrows move among them and
|
|
44
|
+
* the canvas lights the part under the focus or the pointer; a part hanging from this one is not offered, and why is
|
|
45
|
+
* said under them.
|
|
46
|
+
*/
|
|
47
|
+
private renderTargets;
|
|
48
|
+
private aim;
|
|
49
|
+
/** The part under the pointer or the focus in the list, for the canvas to light; the plane lights nothing. */
|
|
50
|
+
private hover;
|
|
51
|
+
private set;
|
|
52
|
+
/**
|
|
53
|
+
* Where on the target face the part sits, on the axes the drawing cannot set: centred, level with the target's edge on
|
|
54
|
+
* either side, a distance off centre, a share of the figure's size, or the position field it follows.
|
|
55
|
+
*/
|
|
56
|
+
private renderAlignment;
|
|
57
|
+
private renderPreviewWords;
|
|
58
|
+
/** Enter applies what is chosen, Esc closes with nothing written; a key the window does not take goes on. */
|
|
59
|
+
connectedCallback(): void;
|
|
60
|
+
private onKey;
|
|
61
|
+
private apply;
|
|
62
|
+
private cancel;
|
|
63
|
+
}
|
|
64
|
+
export {};
|