@things-factory/figure-ui 10.1.147 → 10.1.149
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/bootstrap.ts +10 -2
- package/client/graphql/index.ts +20 -0
- package/client/modeller/canvas.ts +6 -2
- package/client/modeller/carrier-declaration.ts +63 -0
- package/client/modeller/editor.ts +62 -27
- package/client/modeller/figure-carrier.ts +117 -0
- package/client/modeller/figure-cavity-drawing.ts +3 -9
- package/client/modeller/figure-first-save.ts +4 -7
- package/client/modeller/figure-history-panel.ts +2 -3
- package/client/modeller/figure-links.ts +46 -60
- package/client/modeller/figure-material-panel.ts +4 -14
- package/client/modeller/figure-mesh-choice.ts +68 -0
- package/client/modeller/figure-parts.ts +23 -1
- package/client/modeller/figure-preview.ts +2 -2
- package/client/modeller/figure-properties.ts +5 -3
- package/client/modeller/figure-section-editor.ts +12 -33
- package/client/modeller/figure-section-panel.ts +6 -7
- package/client/modeller/figure-slot-layout-panel.ts +208 -0
- package/client/modeller/figure-verdict.ts +21 -31
- package/client/modeller/first-save.ts +23 -0
- package/client/modeller/links.ts +52 -1
- package/client/modeller/material-edit.ts +38 -0
- package/client/modeller/maturity.ts +28 -0
- package/client/modeller/mesh-import.ts +77 -0
- package/client/modeller/modeller-route.ts +9 -0
- package/client/modeller/mounting-part.ts +15 -0
- package/client/modeller/newest-answer.ts +21 -0
- package/client/modeller/preview-model.ts +5 -0
- package/client/modeller/refusals.ts +168 -82
- package/client/modeller/section-controls.ts +25 -1
- package/client/modeller/section-drawing.ts +43 -1
- package/client/modeller/slot-layout-host.ts +140 -0
- package/client/pages/figure-modeller-page.ts +6 -5
- package/client/viewparts/detail-route.ts +7 -0
- package/client/viewparts/figure-detail-dock.ts +1 -3
- package/dist-client/bootstrap.d.ts +1 -1
- package/dist-client/bootstrap.js +9 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/graphql/index.d.ts +5 -0
- package/dist-client/graphql/index.js +19 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/canvas.d.ts +2 -0
- package/dist-client/modeller/canvas.js +10 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/carrier-declaration.d.ts +31 -0
- package/dist-client/modeller/carrier-declaration.js +53 -0
- package/dist-client/modeller/carrier-declaration.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +13 -0
- package/dist-client/modeller/editor.js +62 -27
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-carrier.d.ts +26 -0
- package/dist-client/modeller/figure-carrier.js +123 -0
- package/dist-client/modeller/figure-carrier.js.map +1 -0
- package/dist-client/modeller/figure-cavity-drawing.js +3 -11
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +4 -0
- package/dist-client/modeller/figure-first-save.js +5 -8
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -4
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +4 -1
- package/dist-client/modeller/figure-links.js +47 -57
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-material-panel.d.ts +0 -1
- package/dist-client/modeller/figure-material-panel.js +4 -14
- package/dist-client/modeller/figure-material-panel.js.map +1 -1
- package/dist-client/modeller/figure-mesh-choice.d.ts +16 -0
- package/dist-client/modeller/figure-mesh-choice.js +75 -0
- package/dist-client/modeller/figure-mesh-choice.js.map +1 -0
- package/dist-client/modeller/figure-parts.d.ts +1 -1
- package/dist-client/modeller/figure-parts.js +22 -0
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +2 -2
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +2 -1
- package/dist-client/modeller/figure-properties.js +5 -3
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/figure-section-editor.d.ts +2 -2
- package/dist-client/modeller/figure-section-editor.js +12 -33
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/figure-section-panel.js +6 -7
- package/dist-client/modeller/figure-section-panel.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.d.ts +36 -0
- package/dist-client/modeller/figure-slot-layout-panel.js +198 -0
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -0
- package/dist-client/modeller/figure-verdict.d.ts +1 -1
- package/dist-client/modeller/figure-verdict.js +19 -37
- package/dist-client/modeller/figure-verdict.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +10 -0
- package/dist-client/modeller/first-save.js +16 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/links.d.ts +26 -1
- package/dist-client/modeller/links.js +43 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/material-edit.d.ts +11 -0
- package/dist-client/modeller/material-edit.js +28 -0
- package/dist-client/modeller/material-edit.js.map +1 -0
- package/dist-client/modeller/maturity.d.ts +15 -0
- package/dist-client/modeller/maturity.js +23 -0
- package/dist-client/modeller/maturity.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +47 -0
- package/dist-client/modeller/mesh-import.js +47 -0
- package/dist-client/modeller/mesh-import.js.map +1 -0
- package/dist-client/modeller/modeller-route.d.ts +5 -0
- package/dist-client/modeller/modeller-route.js +9 -0
- package/dist-client/modeller/modeller-route.js.map +1 -1
- package/dist-client/modeller/mounting-part.d.ts +14 -0
- package/dist-client/modeller/mounting-part.js +10 -0
- package/dist-client/modeller/mounting-part.js.map +1 -0
- package/dist-client/modeller/newest-answer.d.ts +12 -0
- package/dist-client/modeller/newest-answer.js +21 -0
- package/dist-client/modeller/newest-answer.js.map +1 -0
- package/dist-client/modeller/preview-model.d.ts +2 -0
- package/dist-client/modeller/preview-model.js +4 -0
- package/dist-client/modeller/preview-model.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +26 -5
- package/dist-client/modeller/refusals.js +184 -86
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/section-controls.d.ts +17 -1
- package/dist-client/modeller/section-controls.js +18 -0
- package/dist-client/modeller/section-controls.js.map +1 -1
- package/dist-client/modeller/section-drawing.d.ts +24 -1
- package/dist-client/modeller/section-drawing.js +42 -0
- package/dist-client/modeller/section-drawing.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +81 -0
- package/dist-client/modeller/slot-layout-host.js +105 -0
- package/dist-client/modeller/slot-layout-host.js.map +1 -0
- package/dist-client/pages/figure-modeller-page.js +5 -4
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/viewparts/detail-route.d.ts +5 -0
- package/dist-client/viewparts/detail-route.js +8 -0
- package/dist-client/viewparts/detail-route.js.map +1 -0
- package/dist-client/viewparts/figure-detail-dock.d.ts +0 -1
- package/dist-client/viewparts/figure-detail-dock.js +1 -3
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/package.json +9 -8
- package/test/align.test.ts +0 -9
- package/test/browser/assistant-stand-in.js +32 -0
- package/test/browser/build.mjs +6 -1
- package/test/browser/editing-page.js +224 -0
- package/test/browser/editing.check.mjs +550 -0
- package/test/browser/links-page.js +113 -0
- package/test/browser/links.check.mjs +568 -0
- package/test/browser/modeller.check.mjs +21 -0
- package/test/browser/parts-page.js +120 -0
- package/test/browser/parts.check.mjs +328 -0
- package/test/browser/places-mesh-page.js +36 -0
- package/test/browser/places-mesh.check.mjs +239 -0
- package/test/browser/product-page.check.mjs +682 -0
- package/test/browser/product-page.js +276 -0
- package/test/browser/screen-page.js +112 -0
- package/test/browser/screen-product-page.js +62 -0
- package/test/browser/screen.check.mjs +1003 -0
- package/test/browser/section-page.js +256 -0
- package/test/browser/section.check.mjs +719 -0
- package/test/browser/serve.mjs +20 -12
- package/test/carrier-declaration.test.ts +69 -0
- package/test/cavity-drawing.test.ts +2 -18
- package/test/clip-ai-proposal.test.ts +1 -5
- package/test/clip-consent.test.ts +0 -5
- package/test/clips.test.ts +0 -7
- package/test/command-preview.test.ts +0 -68
- package/test/controls.test.ts +0 -9
- package/test/detail-dock.test.ts +6 -25
- package/test/face-mapping.test.ts +1 -20
- package/test/fasten-drawing.test.ts +0 -23
- package/test/fields.test.ts +6 -33
- package/test/figure-assistant-surface.test.ts +32 -38
- package/test/first-minute.test.ts +3 -55
- package/test/first-save-suggest.test.ts +11 -8
- package/test/first-save.test.ts +4 -26
- package/test/gizmo.test.ts +0 -25
- package/test/joint-fan.test.ts +0 -10
- package/test/keys.test.ts +0 -7
- package/test/layout.test.ts +4 -73
- package/test/links.test.ts +48 -67
- package/test/live.test.ts +0 -34
- package/test/material.test.ts +17 -26
- package/test/maturity.test.ts +13 -16
- package/test/mesh-import.test.ts +76 -0
- package/test/modeller-route.test.ts +3 -19
- package/test/package-download.test.ts +6 -7
- package/test/parts-wiring.test.ts +9 -51
- package/test/preview.test.ts +5 -46
- package/test/properties.test.ts +11 -26
- package/test/proposal-record.test.ts +2 -19
- package/test/recipes.test.ts +0 -15
- package/test/refusals.test.ts +257 -151
- package/test/release-entry.test.ts +1 -9
- package/test/release-panel.test.ts +1 -22
- package/test/resize-with-placement.test.ts +0 -32
- package/test/revert-choice.test.ts +1 -22
- package/test/revert-keeping.test.ts +1 -12
- package/test/save-landed.test.ts +16 -14
- package/test/scene-canvas.test.ts +2 -13
- package/test/section-editor.test.ts +30 -83
- package/test/section-panel.test.ts +22 -17
- package/test/slot-layout-host.test.ts +117 -0
- package/test/theme.test.ts +5 -9
- package/test/verdict.test.ts +44 -41
- package/test/view.test.ts +5 -12
- package/translations/en.json +61 -1
- package/translations/es.json +61 -1
- package/translations/ja.json +61 -1
- package/translations/ko.json +61 -1
- package/translations/ms.json +61 -1
- package/translations/zh.json +61 -1
- package/test/thumbnail.test.ts +0 -29
|
@@ -1,31 +1,40 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* A command refused
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* A command figure-model refused, in the author's words with the numbers and where to put it right -- not the kernel's
|
|
3
|
+
* English (user, 2026-09-24: 「what is this warning message means?」). Kept apart from the element so it is tested
|
|
4
|
+
* without a browser.
|
|
5
|
+
*
|
|
6
|
+
* Every sentence here is made from the refusal's `code` and `details` (ADR-0127), never from its message: the kernel's
|
|
7
|
+
* English is for logs and readers of the kernel, and a screen that read it went quiet whenever a sentence changed or a
|
|
8
|
+
* new one came under the same code (ADR-0054). A refusal with no details the screen knows is said with its code.
|
|
5
9
|
*/
|
|
10
|
+
import { FIGURE_INSTANCE_SIZE_INPUTS, type FigureRefusalDetails } from '@hatiolab/figure-model'
|
|
6
11
|
|
|
7
12
|
type Say = (key: string, params: Record<string, string>) => string
|
|
13
|
+
type Reason = FigureRefusalDetails['reason']
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
const codeOf = (error: unknown) => (error as { code?: string } | null | undefined)?.code
|
|
16
|
+
/** The refusal's details when they are of this reason, or nothing. */
|
|
17
|
+
function detailsOf<R extends Reason>(error: unknown, reason: R): Extract<FigureRefusalDetails, { reason: R }> | undefined {
|
|
18
|
+
const details = (error as { details?: FigureRefusalDetails } | null | undefined)?.details
|
|
19
|
+
return details?.reason === reason ? (details as Extract<FigureRefusalDetails, { reason: R }>) : undefined
|
|
20
|
+
}
|
|
21
|
+
const mmOf = (mm: number) => mm.toLocaleString(undefined, { maximumFractionDigits: 1 })
|
|
22
|
+
const numberOf = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
23
|
+
/** Bytes in KiB, rounded up: a figure a few bytes over its limit is not said to be at it. */
|
|
24
|
+
const kibOf = (bytes: number) => Math.ceil(bytes / 1024).toLocaleString()
|
|
25
|
+
|
|
26
|
+
/** A size range the figure cannot be drawn over, or one that leaves out its own size, in words; nothing for any other refusal. */
|
|
10
27
|
export function sizeRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
11
|
-
if ((error
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (outside) {
|
|
22
|
-
const [, axis, at, min, max] = outside
|
|
23
|
-
return say('figure.text.size-range-leaves-out-size', { axis: axis!.toUpperCase(), at: mmOf(at!), min: mmOf(min!), max: mmOf(max!) })
|
|
24
|
-
}
|
|
25
|
-
const zero = /size\.([xyz]):\s*the smallest size must be above zero/.exec(message)
|
|
26
|
-
if (zero) return say('figure.text.size-range-from-zero', { axis: zero[1]!.toUpperCase() })
|
|
27
|
-
const gone = /size\.([xyz]):\s*the figure is authored at ([\d.]+) mm, where a length is already gone/.exec(message)
|
|
28
|
-
if (gone) return say('figure.text.size-range-part-gone', { axis: gone[1]!.toUpperCase(), at: mmOf(gone[2]!) })
|
|
28
|
+
if (codeOf(error) !== 'INSTANCE_SIZE_RANGE') return null
|
|
29
|
+
const reach = detailsOf(error, 'size-cannot-reach')
|
|
30
|
+
if (reach)
|
|
31
|
+
return say(reach.bound === 'min' ? 'figure.text.size-range-too-small' : 'figure.text.size-range-too-large', { axis: reach.axis.toUpperCase(), mm: mmOf(reach.need), part: reach.part ?? '' })
|
|
32
|
+
const outside = detailsOf(error, 'size-outside-range')
|
|
33
|
+
if (outside) return say('figure.text.size-range-leaves-out-size', { axis: outside.axis.toUpperCase(), at: mmOf(outside.at), min: mmOf(outside.min), max: mmOf(outside.max) })
|
|
34
|
+
const zero = detailsOf(error, 'size-min-not-positive')
|
|
35
|
+
if (zero) return say('figure.text.size-range-from-zero', { axis: zero.axis.toUpperCase() })
|
|
36
|
+
const gone = detailsOf(error, 'size-already-gone')
|
|
37
|
+
if (gone) return say('figure.text.size-range-part-gone', { axis: gone.axis.toUpperCase(), at: mmOf(gone.at) })
|
|
29
38
|
return null
|
|
30
39
|
}
|
|
31
40
|
|
|
@@ -34,16 +43,14 @@ export function sizeRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
|
34
43
|
* to measure), so the author is offered the same link that narrows the range to what the figure then allows.
|
|
35
44
|
*/
|
|
36
45
|
export function narrowingRetryOf<A extends { kind: string }>(action: A, error: unknown, say: Say): { message: string; label: string; retry: A & { narrowRange: true } } | null {
|
|
37
|
-
if (action.kind !== 'link-dimension' || (error
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const mm = Number(raw).toLocaleString(undefined, { maximumFractionDigits: 1 })
|
|
43
|
-
const words = { axis: axis!.toUpperCase(), mm, part: part ?? '' }
|
|
46
|
+
if (action.kind !== 'link-dimension' || codeOf(error) !== 'INSTANCE_SIZE_RANGE') return null
|
|
47
|
+
const reach = detailsOf(error, 'size-cannot-reach')
|
|
48
|
+
if (!reach) return null
|
|
49
|
+
const words = { axis: reach.axis.toUpperCase(), mm: mmOf(reach.need), part: reach.part ?? '' }
|
|
50
|
+
const below = reach.bound === 'min'
|
|
44
51
|
return {
|
|
45
|
-
message: say(
|
|
46
|
-
label: say(
|
|
52
|
+
message: say(below ? 'figure.text.link-needs-smaller-range' : 'figure.text.link-needs-larger-range', words),
|
|
53
|
+
label: say(below ? 'figure.button.narrow-range-from' : 'figure.button.narrow-range-to', words),
|
|
47
54
|
retry: { ...action, narrowRange: true }
|
|
48
55
|
}
|
|
49
56
|
}
|
|
@@ -53,16 +60,19 @@ export function narrowingRetryOf<A extends { kind: string }>(action: A, error: u
|
|
|
53
60
|
* on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
|
|
54
61
|
*/
|
|
55
62
|
export function placementRefusalOf(error: unknown, say: Say): string | null {
|
|
56
|
-
if ((error
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
if (codeOf(error) !== 'PLACEMENT_PLANE') return null
|
|
64
|
+
const misfit = detailsOf(error, 'placement-face')
|
|
65
|
+
if (!misfit) return null
|
|
66
|
+
return say('figure.text.placement-plane-refused', {
|
|
67
|
+
parts: misfit.parts.map(p => `${p.part} (${p.face})`).join(', '),
|
|
68
|
+
placement: say('figure.label.placement-' + misfit.placement, {}),
|
|
69
|
+
faces: misfit.takes.join(', ')
|
|
70
|
+
})
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
/** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
|
|
64
74
|
export function inDeclaredRoom(error: unknown): boolean {
|
|
65
|
-
return (error
|
|
75
|
+
return codeOf(error) === 'PART_REFERENCED' && !!detailsOf(error, 'in-declared-space')
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
/**
|
|
@@ -71,18 +81,36 @@ export function inDeclaredRoom(error: unknown): boolean {
|
|
|
71
81
|
* left is the declared space (with a way on: fit it again) and a row -- one laid along the part, or hanging in its row.
|
|
72
82
|
*/
|
|
73
83
|
export function partRefusalOf(error: unknown, part: string, say: Say): string | null {
|
|
74
|
-
const code = (error
|
|
84
|
+
const code = codeOf(error)
|
|
75
85
|
if (code !== 'PART_REFERENCED' && code !== 'PART_ROW') return null
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
const carried = /^(.+?) hangs? in the row /.exec(message)
|
|
82
|
-
if (carried) return say('figure.text.part-row-carries', { part, others: carried[1]! })
|
|
86
|
+
if (detailsOf(error, 'in-declared-space')) return say('figure.text.part-in-declared-space', { part })
|
|
87
|
+
const along = detailsOf(error, 'row-along')
|
|
88
|
+
if (along) return say('figure.text.part-row-along', { part, rows: along.rows.join(', ') })
|
|
89
|
+
const carried = detailsOf(error, 'hangs-in-row')
|
|
90
|
+
if (carried) return say('figure.text.part-row-carries', { part, others: carried.parts.join(', ') })
|
|
83
91
|
return say('figure.text.part-is-referenced', { part })
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
/** A part the gizmo cannot move because a joint or a fastening places it, named as the author knows it (`part`). */
|
|
95
|
+
export function placedRefusalOf(error: unknown, part: string, say: Say): string | null {
|
|
96
|
+
if (codeOf(error) !== 'EDIT_TARGET') return null
|
|
97
|
+
if (detailsOf(error, 'placed-by-joint')) return say('figure.text.gizmo-joint-places', { part })
|
|
98
|
+
if (detailsOf(error, 'placed-by-fastening')) return say('figure.text.gizmo-fastening-places', { part })
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A part name refused: not a name, already used, or another part is named under the part. The rule is the kernel's. */
|
|
103
|
+
export function partNameRefusalOf(error: unknown, say: Say): string | null {
|
|
104
|
+
if (codeOf(error) !== 'PART_ID') return null
|
|
105
|
+
const invalid = detailsOf(error, 'name-invalid')
|
|
106
|
+
if (invalid) return say('figure.text.part-name-invalid', { name: invalid.name })
|
|
107
|
+
const taken = detailsOf(error, 'name-taken')
|
|
108
|
+
if (taken) return say('figure.text.part-name-taken', { name: taken.name })
|
|
109
|
+
const nested = detailsOf(error, 'name-has-nested')
|
|
110
|
+
if (nested) return say('figure.text.part-name-has-nested', { nested: nested.nested })
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
/** What removing a part let go of (figure-model's `RemovalNotice`), said in one line, or nothing when it let go of nothing. */
|
|
87
115
|
export function removalNoticeOf(
|
|
88
116
|
notice:
|
|
@@ -97,12 +125,11 @@ export function removalNoticeOf(
|
|
|
97
125
|
say: Say
|
|
98
126
|
): string | null {
|
|
99
127
|
if (!notice) return null
|
|
100
|
-
const number = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
101
128
|
const lines: string[] = []
|
|
102
129
|
const still = notice.released.filter(r => !r.control).map(r => r.part)
|
|
103
130
|
if (still.length) lines.push(say('figure.text.removal-released', { parts: still.join(', ') }))
|
|
104
131
|
for (const r of notice.released.filter(r => r.control)) lines.push(say('figure.text.removal-released-motion', { part: r.part, control: r.control! }))
|
|
105
|
-
for (const c of notice.controls) lines.push(say('figure.text.removal-own-control', { part: c.part, control: c.control, min:
|
|
132
|
+
for (const c of notice.controls) lines.push(say('figure.text.removal-own-control', { part: c.part, control: c.control, min: numberOf(c.min), max: numberOf(c.max) }))
|
|
106
133
|
const fixed = [...new Set(notice.fixed.map(f => f.part))]
|
|
107
134
|
if (fixed.length) lines.push(say('figure.text.removal-fixed', { parts: fixed.join(', ') }))
|
|
108
135
|
if (notice.bindings?.length) lines.push(say('figure.text.removal-bindings', { places: notice.bindings.map(b => b.label ?? b.id).join(', ') }))
|
|
@@ -112,64 +139,123 @@ export function removalNoticeOf(
|
|
|
112
139
|
|
|
113
140
|
/**
|
|
114
141
|
* A recipe refused at one of its steps: which step, and why -- the particular sentence when there is one (a size
|
|
115
|
-
* range, a value's range, a placement), otherwise the kernel's code. The
|
|
142
|
+
* range, a value's range, a placement), otherwise the kernel's code. The step is the refusal's path, `steps[2]`: the
|
|
143
|
+
* contract's path grammar, not its English (ADR-0127 decision 6).
|
|
116
144
|
*/
|
|
117
145
|
export function recipeRefusalOf(error: unknown, say: Say): string | null {
|
|
118
146
|
const step = /^steps\[(\d+)\]$/.exec((error as { path?: string })?.path ?? '')
|
|
119
147
|
if (!step) return null
|
|
120
148
|
const why = sizeRangeRefusalOf(error, say) ?? inputRangeRefusalOf(error, say) ?? placementRefusalOf(error, say)
|
|
121
|
-
return say('figure.text.recipe-step-refused', { step: String(Number(step[1]) + 1), why: why ?? '', code: (error
|
|
149
|
+
return say('figure.text.recipe-step-refused', { step: String(Number(step[1]) + 1), why: why ?? '', code: codeOf(error) ?? '' })
|
|
122
150
|
}
|
|
123
151
|
|
|
124
152
|
/**
|
|
125
|
-
* A
|
|
126
|
-
*
|
|
127
|
-
* is said with its number.
|
|
153
|
+
* A corner or an inside the part cannot take (ADR-0100 stage 1): what was too large or too thick, or what holds on
|
|
154
|
+
* to what the edit would take away.
|
|
128
155
|
*/
|
|
156
|
+
function sectionRefusalOf(error: unknown, say: Say): string | null {
|
|
157
|
+
const mapping = detailsOf(error, 'faces-taken-away')
|
|
158
|
+
if (mapping) return say('figure.text.section-face-in-use', { faces: mapping.lost.map(l => say('figure.label.face-' + l.face, {})).join(', ') })
|
|
159
|
+
if (codeOf(error) === 'DIMENSION_LINKED') return say('figure.text.section-dimension-linked', {})
|
|
160
|
+
if (detailsOf(error, 'section-invalid')?.issues.some(i => i.code === 'CORNER_TOO_LARGE')) return say('figure.text.section-corner-too-large', {})
|
|
161
|
+
const inside = detailsOf(error, 'inside-invalid')?.refusal
|
|
162
|
+
if (inside?.code === 'CAVITY' && inside.refused === 'NO_ROOM_INSIDE') return say('figure.text.section-wall-too-thick', {})
|
|
163
|
+
if (inside?.code === 'CAVITY_NO_LENGTH') return say('figure.text.section-ends-too-thick', {})
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
129
167
|
/**
|
|
130
|
-
* A
|
|
131
|
-
*
|
|
168
|
+
* A slot layout refused (ADR-0118): the anchor, slot or value it was refused for. The layout's own findings are named
|
|
169
|
+
* by where they are; the editor checks them before Apply, so they rarely reach here.
|
|
132
170
|
*/
|
|
133
|
-
function
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (
|
|
171
|
+
export function slotLayoutRefusalOf(error: unknown, say: Say): string | null {
|
|
172
|
+
if (detailsOf(error, 'slot-layout-label-missing')) return say('figure.text.slot-layout-label-missing', {})
|
|
173
|
+
if (detailsOf(error, 'slot-anchors-missing')) return say('figure.text.slot-anchors-missing', {})
|
|
174
|
+
const twice = detailsOf(error, 'slot-anchor-duplicate')
|
|
175
|
+
if (twice) return say('figure.text.slot-anchor-duplicate', { anchor: twice.anchor })
|
|
176
|
+
const flat = detailsOf(error, 'slot-anchor-not-flat')
|
|
177
|
+
if (flat) return say('figure.text.slot-anchor-not-flat', { anchor: flat.anchor, part: flat.part })
|
|
178
|
+
const gone = detailsOf(error, 'slot-anchor-no-part')
|
|
179
|
+
if (gone) return say('figure.text.slot-anchor-no-part', { anchor: gone.anchor, part: gone.part })
|
|
180
|
+
const issues = detailsOf(error, 'slot-layout-issues')
|
|
181
|
+
if (issues) return say('figure.text.slot-layout-issues', { path: issues.issues[0]?.path ?? '' })
|
|
182
|
+
const empty = detailsOf(error, 'slot-accepts-empty')
|
|
183
|
+
if (empty) return say('figure.text.slot-accepts-empty', { slot: empty.slot })
|
|
184
|
+
const taken = detailsOf(error, 'slot-id-taken')
|
|
185
|
+
if (taken) return say('figure.text.slot-id-taken', { slot: taken.slot })
|
|
144
186
|
return null
|
|
145
187
|
}
|
|
146
188
|
|
|
189
|
+
/** The figure's limits: its nodes, its size saved, how deep it nests. */
|
|
190
|
+
function documentLimitRefusalOf(error: unknown, say: Say): string | null {
|
|
191
|
+
if (codeOf(error) !== 'DOCUMENT_LIMIT') return null
|
|
192
|
+
const nodes = detailsOf(error, 'node-limit')
|
|
193
|
+
if (nodes) return say('figure.text.edit-refused-node-limit', { max: String(nodes.max) })
|
|
194
|
+
const large = detailsOf(error, 'document-too-large')
|
|
195
|
+
if (large)
|
|
196
|
+
return large.at === undefined
|
|
197
|
+
? say('figure.text.document-too-large-unmeasured', { max: kibOf(large.max) })
|
|
198
|
+
: say('figure.text.document-too-large', { max: kibOf(large.max), at: kibOf(large.at) })
|
|
199
|
+
const deep = detailsOf(error, 'nesting-too-deep')
|
|
200
|
+
if (deep) return say('figure.text.document-nesting-too-deep', { max: String(deep.max), at: String(deep.at) })
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A refusal in the author's words when the screen has words for it, otherwise said as a refusal with its code -- never
|
|
206
|
+
* the kernel's English on the screen (the duplicate of a row's part showed 「EDIT_TARGET: … not a repeated template」,
|
|
207
|
+
* 2026-09-27).
|
|
208
|
+
*/
|
|
147
209
|
export function editRefusalOf(error: unknown, say: Say): string {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
210
|
+
return (
|
|
211
|
+
sizeRangeRefusalOf(error, say) ??
|
|
212
|
+
inputRangeRefusalOf(error, say) ??
|
|
213
|
+
repeatPitchRefusalOf(error, say) ??
|
|
214
|
+
rowLimitRefusalOf(error, say) ??
|
|
215
|
+
placementRefusalOf(error, say) ??
|
|
216
|
+
partNameRefusalOf(error, say) ??
|
|
217
|
+
documentLimitRefusalOf(error, say) ??
|
|
218
|
+
sectionRefusalOf(error, say) ??
|
|
219
|
+
slotLayoutRefusalOf(error, say) ??
|
|
220
|
+
say('figure.text.edit-refused', { code: codeOf(error) ?? '' })
|
|
221
|
+
)
|
|
154
222
|
}
|
|
155
223
|
|
|
156
224
|
/**
|
|
157
|
-
* A value outside the range its input declares, with the range and the value given
|
|
158
|
-
*
|
|
159
|
-
* The repeat pitch's refusal shares the code and has its own words (`figure.text.repeat-pitch-too-small`).
|
|
225
|
+
* A value outside the range its input declares, with the range and the value given. The figure's own size is named by
|
|
226
|
+
* its axis; any other input by its range only.
|
|
160
227
|
*/
|
|
161
228
|
export function inputRangeRefusalOf(error: unknown, say: Say): string | null {
|
|
162
|
-
if ((error
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const number = (text: string) => Number(text).toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
168
|
-
const axis = /^size\.([xyz])$/.exec(input!)?.[1]
|
|
169
|
-
const params = { value: number(value!), min: number(min!), max: number(max!) }
|
|
229
|
+
if (codeOf(error) !== 'INPUT_RANGE') return null
|
|
230
|
+
const range = detailsOf(error, 'value-outside-range')
|
|
231
|
+
if (!range) return null
|
|
232
|
+
const axis = (Object.entries(FIGURE_INSTANCE_SIZE_INPUTS) as Array<[string, string]>).find(([, id]) => id === range.input)?.[0]
|
|
233
|
+
const params = { value: numberOf(range.value), min: numberOf(range.min), max: numberOf(range.max) }
|
|
170
234
|
return axis ? say('figure.text.input-range-size', { ...params, axis: axis.toUpperCase() }) : say('figure.text.input-range', params)
|
|
171
235
|
}
|
|
172
236
|
|
|
237
|
+
/**
|
|
238
|
+
* A row past its limits: a pitch so fine it would hold more copies than a row takes, a row that would expand to more
|
|
239
|
+
* bodies than a figure draws, or a number of copies outside what a row may have.
|
|
240
|
+
*/
|
|
241
|
+
export function rowLimitRefusalOf(error: unknown, say: Say): string | null {
|
|
242
|
+
const copies = detailsOf(error, 'copies-over-limit')
|
|
243
|
+
if (copies) return say('figure.text.row-copies-over-limit', { count: copies.count.toLocaleString(), limit: copies.limit.toLocaleString() })
|
|
244
|
+
const bodies = detailsOf(error, 'geometry-over-limit')
|
|
245
|
+
if (bodies) return say('figure.text.row-geometry-over-limit', { count: bodies.count.toLocaleString(), limit: bodies.limit.toLocaleString() })
|
|
246
|
+
const count = detailsOf(error, 'row-copies-out-of-range')
|
|
247
|
+
if (count) return say('figure.text.row-copies-out-of-range', { count: count.count.toLocaleString(), min: String(count.min), max: String(count.max) })
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A row's pitch under which the figure, at the length it is authored, would hold more copies than a row takes. */
|
|
252
|
+
export function repeatPitchRefusalOf(error: unknown, say: Say): string | null {
|
|
253
|
+
if (codeOf(error) !== 'INPUT_RANGE') return null
|
|
254
|
+
const pitch = detailsOf(error, 'pitch-too-many-copies')
|
|
255
|
+
if (!pitch) return null
|
|
256
|
+
return say('figure.text.repeat-pitch-too-small', { length: pitch.length.toLocaleString(), min: String(pitch.least), limit: String(pitch.limit) })
|
|
257
|
+
}
|
|
258
|
+
|
|
173
259
|
/**
|
|
174
260
|
* Why "follow the size and keep where the figure is placed" was not applied, in the author's words (ADR-0097): the plan
|
|
175
261
|
* says what it would have had to overwrite, and for a part through the plane how far. A code the screen has no words
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FIGURE_SECTION_PLANE, checkFigureSection, figureExtrusionFeaturesOf } from '@hatiolab/figure-model'
|
|
2
|
-
import type { FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* What the 단면 section of the properties tab reads and asks (ADR-0100 §3.3): a pushed part's corners and its inside.
|
|
@@ -62,6 +62,9 @@ export function cornerLimitOf(shape: FigureEvaluatedShape): number | undefined {
|
|
|
62
62
|
return maxes.length ? Math.min(...maxes) : undefined
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** A corner radius as the one command: 0 takes the rounding away. */
|
|
66
|
+
export const cornerActionOf = (part: string, radius: number): FigureAuthoringAction => ({ kind: 'set-section-corner', part, corner: radius > 0 ? radius : null })
|
|
67
|
+
|
|
65
68
|
/** Whether a corner radius can be taken; if not, the most it can be. */
|
|
66
69
|
export function cornerRefusalOf(shape: FigureEvaluatedShape, radius: number): { max: number } | null {
|
|
67
70
|
const e = shape.extrusion
|
|
@@ -100,6 +103,27 @@ export function cavityRefusalOf(shape: FigureEvaluatedShape, request: FigureCavi
|
|
|
100
103
|
return { code: 'not-positive' }
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* What the kernel would refuse of an inside, as words: an i18n key and its values; null when it would take it. For one
|
|
108
|
+
* side's wall with the walls one per side, the most is that side's with the opposite kept, not the kernel's all-round one.
|
|
109
|
+
*/
|
|
110
|
+
export function cavityRefusalWordsOf(
|
|
111
|
+
shape: FigureEvaluatedShape,
|
|
112
|
+
request: { wall: Record<string, number>; start: FigureCavityRequest['start']; end: FigureCavityRequest['end'] },
|
|
113
|
+
perSide: boolean,
|
|
114
|
+
side?: string
|
|
115
|
+
): { key: string; params: Record<string, string> } | null {
|
|
116
|
+
const ask = cavityRefusalOf(shape, { wall: { ...request.wall }, start: request.start, end: request.end })
|
|
117
|
+
if (!ask) return null
|
|
118
|
+
if (ask.code === 'wall-too-thick')
|
|
119
|
+
return { key: 'figure.text.section-wall-over', params: { max: String(perSide && side ? mm(sideWallLimitOf(shape, request.wall, side)) : ask.max) } }
|
|
120
|
+
if (ask.code === 'ends-too-thick') return { key: 'figure.text.section-ends-over', params: { max: String(ask.max) } }
|
|
121
|
+
return { key: 'figure.text.section-not-positive', params: {} }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether a part can have an inside: a pushed rectangle or ellipse (a drawn section's is a later stage). */
|
|
125
|
+
export const hollowableOf = (shape: FigureEvaluatedShape) => shape.extrusion?.section.kind === 'rectangle' || shape.extrusion?.section.kind === 'ellipse'
|
|
126
|
+
|
|
103
127
|
/** What the inside makes of the part (§3.3): open at both ends a pipe, at one a container, at neither a closed space. */
|
|
104
128
|
export function cavityResultOf(cavity: NonNullable<FigureEvaluatedShape['cavity']>): 'pipe' | 'container' | 'sealed' {
|
|
105
129
|
const open = [cavity.start, cavity.end].filter(e => e === 'open').length
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FIGURE_SECTION_PLANE, FIGURE_SECTION_POINT_LIMIT, checkFigureSection, figureExtrusionFeaturesOf, figureSectionSidesOf } from '@hatiolab/figure-model'
|
|
2
|
-
import type { FigureCavityRequest, FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureAuthoringAction, FigureCavityRequest, FigureEvaluatedShape, FigureSection, FigureSectionIssue, FigureSectionMeasure, FigureSectionPoint } from '@hatiolab/figure-model'
|
|
3
|
+
import { END_FACE } from './section-controls.js'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
* What the section editor does to a drawn section's points, and how it says what is wrong with them (ADR-0100 §3.2).
|
|
@@ -191,3 +192,44 @@ export function workingIssuesOf(
|
|
|
191
192
|
}
|
|
192
193
|
return { issues, words }
|
|
193
194
|
}
|
|
195
|
+
|
|
196
|
+
/** The inside said so two can be compared: its walls by side, its ends. */
|
|
197
|
+
export const cavityKeyOf = (w: SectionWorking) => (w.cavity ? [Object.entries(w.cavity.wall).sort(), w.cavity.start, w.cavity.end] : null)
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The one command for everything changed since the editor opened: a drawn section's points always, and of the rest only
|
|
201
|
+
* what changed -- a measure left alone may follow another value and must not be written; an apex taken away is null
|
|
202
|
+
* (back to a push), an inside taken away is null.
|
|
203
|
+
*/
|
|
204
|
+
export function sectionActionOf(part: string, kind: FigureSection['kind'] | undefined, opened: SectionWorking, w: SectionWorking): FigureAuthoringAction {
|
|
205
|
+
const changed = (a: number | undefined, b: number | undefined) => Math.abs((a ?? 0) - (b ?? 0)) > 1e-9
|
|
206
|
+
const across = Object.fromEntries(Object.entries(w.across).filter(([k, v]) => changed(v, opened.across[k as Axis])))
|
|
207
|
+
const apexMoved = !w.apex !== !opened.apex || (!!w.apex && Object.entries(w.apex).some(([k, v]) => changed(v, opened.apex![k as Axis])))
|
|
208
|
+
const cavityMoved = JSON.stringify(cavityKeyOf(w)) !== JSON.stringify(cavityKeyOf(opened))
|
|
209
|
+
return {
|
|
210
|
+
kind: 'set-section',
|
|
211
|
+
part,
|
|
212
|
+
...(kind === 'polygon' ? { points: w.points.map(p => ({ ...p })) } : {}),
|
|
213
|
+
...(Object.keys(across).length ? { across } : {}),
|
|
214
|
+
...(changed(w.length, opened.length) ? { length: w.length } : {}),
|
|
215
|
+
...(apexMoved ? { apex: w.apex ? { ...w.apex } : null } : {}),
|
|
216
|
+
...(cavityMoved ? { cavity: w.cavity ? { wall: { ...w.cavity.wall }, start: w.cavity.start, end: w.cavity.end } : null } : {})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* What a measure's field says under it: that it follows another value, or -- given where along the push a length
|
|
222
|
+
* change keeps the part (1 its start, -1 its end, 0 its centre) -- the face it keeps; null when there is nothing to say.
|
|
223
|
+
*/
|
|
224
|
+
export function measureNoteOf(measure: FigureSectionMeasure | undefined, kept: -1 | 0 | 1 | undefined, axis: Axis): { key: string; face?: string } | null {
|
|
225
|
+
if (measure === 'linked') return { key: 'figure.text.section-editor-linked' }
|
|
226
|
+
if (kept === undefined) return null
|
|
227
|
+
if (kept === 0) return { key: 'figure.text.section-editor-keeps-centre' }
|
|
228
|
+
return { key: 'figure.text.section-editor-keeps', face: END_FACE(axis, kept === 1 ? 'start' : 'end') }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The words beside a drawn section's edit button: how many points it has, and the apex when its end is one point. */
|
|
232
|
+
export function sectionPointsWordsOf(drawn: ReturnType<typeof drawnOf>): { key: string; params: Record<string, string> } | null {
|
|
233
|
+
if (drawn?.section.kind !== 'polygon') return null
|
|
234
|
+
return { key: drawn.apex ? 'figure.text.section-points-apex' : 'figure.text.section-points', params: { points: String(drawn.section.points.length) } }
|
|
235
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What the Figure modeller gives the shared slot layout editor (`@operato/slot-layout-editor`, the one fmsim hosts too),
|
|
3
|
+
* and what it makes of the editor's result (ADR-0118). The editor edits tiers and slots; the modeller owns the anchors
|
|
4
|
+
* -- a part's top or bottom face, named -- and sends the whole layout as one `set-slot-layout` command.
|
|
5
|
+
*
|
|
6
|
+
* A figure's released slot layout is the only one there is (ADR-0118 decision 8, user): no scene or run overrides it, so
|
|
7
|
+
* this host offers no "use default" and no per-instance value.
|
|
8
|
+
*
|
|
9
|
+
* Kept free of Lit, i18n and the editor's element so node tests read it directly.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
figureShapeBoundsOf,
|
|
13
|
+
figureSlotLayoutOf,
|
|
14
|
+
figureSlotLayoutIssues,
|
|
15
|
+
slotLayoutPreset,
|
|
16
|
+
SLOT_LAYOUT_PRESET_KEYS,
|
|
17
|
+
SLOT_LAYOUT_PRESET_PARAMS,
|
|
18
|
+
type FigureAsset,
|
|
19
|
+
type SlotLayout,
|
|
20
|
+
type SlotPresetSize
|
|
21
|
+
} from '@hatiolab/figure-model'
|
|
22
|
+
import type { FigureRenderPart } from './renderer.js'
|
|
23
|
+
|
|
24
|
+
export type SlotAnchorFace = 'top' | 'bottom'
|
|
25
|
+
export interface SlotAnchor {
|
|
26
|
+
id: string
|
|
27
|
+
part: string
|
|
28
|
+
face: SlotAnchorFace
|
|
29
|
+
}
|
|
30
|
+
/** What the panel edits besides the tiers: the layout's name, what its slots are, and the anchors they stand on. */
|
|
31
|
+
export interface SlotLayoutForm {
|
|
32
|
+
label: string
|
|
33
|
+
role: 'seat' | 'grip'
|
|
34
|
+
anchors: SlotAnchor[]
|
|
35
|
+
}
|
|
36
|
+
/** A layout problem as the editor shows it: a code, where, and its values. */
|
|
37
|
+
export interface SlotEditorIssue {
|
|
38
|
+
code: string
|
|
39
|
+
path: string
|
|
40
|
+
params?: Record<string, string | number>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Body = { id: string; shape?: Parameters<typeof figureShapeBoundsOf>[0] }
|
|
44
|
+
|
|
45
|
+
/** The figure's layout as the form and the layout, or a form to start one from on the selected part's top. */
|
|
46
|
+
export function slotLayoutFormOf(asset: FigureAsset, part: string): { form: SlotLayoutForm; layout?: SlotLayout } {
|
|
47
|
+
const had = figureSlotLayoutOf(asset as never)
|
|
48
|
+
if (had) return { form: { label: had.label, role: had.role, anchors: had.anchors.map(a => ({ ...a, face: a.face as SlotAnchorFace })) }, layout: had.layout }
|
|
49
|
+
return { form: { label: '', role: 'seat', anchors: part ? [{ id: part, part, face: 'top' }] : [] } }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** How big each anchor's face is, along the part's own X (width) and Z (depth): what the editor checks slots against. */
|
|
53
|
+
export function anchorFacesOf(anchors: readonly SlotAnchor[], geometry: readonly Body[]): Record<string, { width: number; depth: number }> {
|
|
54
|
+
const out: Record<string, { width: number; depth: number }> = {}
|
|
55
|
+
for (const a of anchors) {
|
|
56
|
+
const body = geometry.find(g => g.id === a.part)
|
|
57
|
+
if (!body?.shape) continue
|
|
58
|
+
const b = figureShapeBoundsOf(body.shape)
|
|
59
|
+
out[a.id] = { width: b.x.max - b.x.min, depth: b.z.max - b.z.min }
|
|
60
|
+
}
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Where an author starts: the equipment presets (figure-model, ADR-0118 decision 8), laid out on this figure's anchors --
|
|
66
|
+
* a one-anchor preset on the first, a twin fork on the first two. A single slot stands on a top and hangs from a bottom.
|
|
67
|
+
* The editor's own controls then make it what the equipment is.
|
|
68
|
+
*/
|
|
69
|
+
export function slotPresetsOf(anchors: readonly SlotAnchor[], say: (key: string) => string) {
|
|
70
|
+
const first = anchors[0]
|
|
71
|
+
const on = { ...(first ? { anchor: first.id } : {}), ...(anchors.length >= 2 ? { forks: [anchors[0]!.id, anchors[1]!.id] as [string, string] } : {}) }
|
|
72
|
+
const mount = first?.face === 'bottom' ? ('HANG' as const) : ('ON' as const)
|
|
73
|
+
return SLOT_LAYOUT_PRESET_KEYS.map(key => ({
|
|
74
|
+
key,
|
|
75
|
+
label: say(`figure.label.slot-preset-${key}`),
|
|
76
|
+
params: SLOT_LAYOUT_PRESET_PARAMS[key].map(p => ({ key: p.key, label: say(`figure.label.slot-preset-param-${p.key}`), integer: p.integer })),
|
|
77
|
+
build: (size: SlotPresetSize, params: Record<string, number>) => slotLayoutPreset(key, size, params, { ...on, mount })
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* What figure-model asks beyond the editor's own checks: every slot names the types it takes (ADR-0118 decision 5) -- the
|
|
83
|
+
* editor's "any type" is a board's, not a figure's. The kernel's own check (`figureSlotLayoutIssues`, the one a
|
|
84
|
+
* placement's override is held to as well), said before Apply rather than refused after it; the editor already shows the
|
|
85
|
+
* common findings, so only the figure's own are added.
|
|
86
|
+
*/
|
|
87
|
+
export function figureSlotIssues(layout: SlotLayout, anchors: readonly string[] = layout.tiers.map(t => t.anchorId)): SlotEditorIssue[] {
|
|
88
|
+
return figureSlotLayoutIssues(layout, anchors).filter(issue => issue.code === 'ACCEPTS_REQUIRED')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The command the panel sends: the whole layout, its anchors and its name, as one step of the history. */
|
|
92
|
+
export function slotLayoutCommandOf(form: SlotLayoutForm, layout: SlotLayout, fallbackLabel: string) {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'set-slot-layout' as const,
|
|
95
|
+
label: form.label.trim() || fallbackLabel,
|
|
96
|
+
role: form.role,
|
|
97
|
+
anchors: form.anchors.map(a => ({ id: a.id.trim(), part: a.part, face: a.face })),
|
|
98
|
+
layout: structuredClone(layout)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A new anchor's id: the part's, or the part's with a number when that is taken. */
|
|
103
|
+
export function anchorIdFor(part: string, taken: readonly string[]): string {
|
|
104
|
+
if (!taken.includes(part)) return part
|
|
105
|
+
let n = 2
|
|
106
|
+
while (taken.includes(`${part}_${n}`)) n++
|
|
107
|
+
return `${part}_${n}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* The token the canvas draws slots in: translucent, never picked, never saved. */
|
|
111
|
+
export const SLOT_MARK = '__slot'
|
|
112
|
+
|
|
113
|
+
type Evaluated = { id: string; pose: { r: number[][]; t: number[] }; region: { dimensions: number[] } | null }
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The unfolded slots as boxes the canvas draws (ADR-0118 decision 4): each as big as what it allows, standing on its
|
|
117
|
+
* place -- or hanging from it, for a tier that hangs things -- so an author sees where every slot of the rule falls.
|
|
118
|
+
*/
|
|
119
|
+
export function slotMarksOf(places: readonly Evaluated[], layout: SlotLayout | undefined) {
|
|
120
|
+
if (!layout) return []
|
|
121
|
+
const mountOf = new Map(layout.tiers.map(t => [t.tierNo, t.mount]))
|
|
122
|
+
const marks: FigureRenderPart[] = []
|
|
123
|
+
for (const p of places) {
|
|
124
|
+
const slot = /^t(\d+)_s\d+$/.exec(p.id)
|
|
125
|
+
if (!slot || !p.region) continue
|
|
126
|
+
const [width, height, depth] = p.region.dimensions as [number, number, number]
|
|
127
|
+
const down = mountOf.get(Number(slot[1])) === 'HANG'
|
|
128
|
+
marks.push({
|
|
129
|
+
id: `${SLOT_MARK}:${p.id}`,
|
|
130
|
+
shape: {
|
|
131
|
+
provider: 'extrude',
|
|
132
|
+
dimensions: { width, height, depth },
|
|
133
|
+
extrusion: { axis: 'y', section: { kind: 'rectangle', size: { x: width, z: depth } }, start: down ? -height : 0, end: down ? 0 : height }
|
|
134
|
+
},
|
|
135
|
+
pose: p.pose,
|
|
136
|
+
material: { token: SLOT_MARK, transparent: true }
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
return marks
|
|
140
|
+
}
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from '@things-factory/ai-assistant/dist-client/index.js'
|
|
30
30
|
import type { FirstSave } from '../modeller/first-save.js'
|
|
31
31
|
import { typeOf } from '../modeller/first-save.js'
|
|
32
|
-
import {
|
|
32
|
+
import { editRefusalOf } from '../modeller/refusals.js'
|
|
33
33
|
import { proposalOutcomeFrom, takeFigureOffer, takePendingNewFigure } from '../modeller/ai-proposal.js'
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -291,10 +291,11 @@ export class FigureModellerPage extends PageView {
|
|
|
291
291
|
this.commandError =
|
|
292
292
|
(error as { code?: string })?.code === 'OCCUPANCY_NO_PARTS'
|
|
293
293
|
? i18next.t('figure.text.fit-needs-parts')
|
|
294
|
-
:
|
|
295
|
-
(error
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
: (error as { code?: string })?.code
|
|
295
|
+
? editRefusalOf(error, (key, params) => i18next.t(key, params))
|
|
296
|
+
: error instanceof Error
|
|
297
|
+
? error.message
|
|
298
|
+
: String(error)
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogue preview belongs to the page it was opened on: it closes once the shell leaves that page, by any route
|
|
3
|
+
* (`opened` is that page, `current` the page the shell is on now). Opened on no known page, it is never closed by this.
|
|
4
|
+
*/
|
|
5
|
+
export function leftOpeningPage(opened: string | undefined, current: string | undefined) {
|
|
6
|
+
return opened !== undefined && current !== opened
|
|
7
|
+
}
|
|
@@ -9,6 +9,7 @@ import { figureCostOf, parseFigureAsset, type FigureAsset } from '@hatiolab/figu
|
|
|
9
9
|
import { capacityFrom } from '../modeller/scene-budget.js'
|
|
10
10
|
import { fetchFigure, fetchReleasedFigure } from '../graphql/index.js'
|
|
11
11
|
import { modellerPathOf } from '../modeller/modeller-route.js'
|
|
12
|
+
import { leftOpeningPage } from './detail-route.js'
|
|
12
13
|
import type { Figure } from '../types.js'
|
|
13
14
|
import './figure-asset-viewer.js'
|
|
14
15
|
|
|
@@ -177,9 +178,6 @@ function visibleBar(root: Document | ShadowRoot): Element | undefined {
|
|
|
177
178
|
let dock: FigureDetailDock | undefined
|
|
178
179
|
/** The page the preview was opened on; the preview belongs to that page and closes when the shell leaves it. */
|
|
179
180
|
let openedOn: string | undefined
|
|
180
|
-
export function leftOpeningPage(opened: string | undefined, current: string | undefined) {
|
|
181
|
-
return opened !== undefined && current !== opened
|
|
182
|
-
}
|
|
183
181
|
function hideDetail() {
|
|
184
182
|
store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } })
|
|
185
183
|
}
|