@things-factory/figure-ui 10.1.205 → 10.1.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/editor-styles.ts +16 -0
- package/client/modeller/editor.ts +43 -41
- package/client/modeller/fasten-form.ts +50 -11
- package/client/modeller/figure-fasten-dialog.ts +3 -2
- package/client/modeller/figure-links.ts +2 -2
- package/client/modeller/figure-preview.ts +6 -6
- package/client/modeller/gizmo.ts +10 -0
- package/client/modeller/keys.ts +10 -1
- package/client/modeller/links.ts +32 -41
- package/client/modeller/palette.ts +1 -1
- package/client/modeller/product-session.ts +16 -4
- package/client/modeller/refusal-teller.ts +3 -25
- package/client/modeller/refusals.ts +19 -26
- package/dist-client/modeller/editor-styles.js +16 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +7 -3
- package/dist-client/modeller/editor.js +47 -42
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +16 -7
- package/dist-client/modeller/fasten-form.js +45 -11
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.d.ts +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +3 -3
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +1 -1
- package/dist-client/modeller/figure-links.js +2 -2
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +4 -4
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/gizmo.d.ts +6 -0
- package/dist-client/modeller/gizmo.js +11 -0
- package/dist-client/modeller/gizmo.js.map +1 -1
- package/dist-client/modeller/keys.d.ts +1 -1
- package/dist-client/modeller/keys.js +6 -0
- package/dist-client/modeller/keys.js.map +1 -1
- package/dist-client/modeller/links.d.ts +16 -22
- package/dist-client/modeller/links.js +26 -39
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/palette.js +1 -1
- package/dist-client/modeller/palette.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +15 -13
- package/dist-client/modeller/product-session.js +15 -5
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusal-teller.d.ts +1 -12
- package/dist-client/modeller/refusal-teller.js +3 -22
- package/dist-client/modeller/refusal-teller.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +6 -14
- package/dist-client/modeller/refusals.js +16 -27
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/test/browser/acceptance-page.js +1 -1
- package/test/browser/links.check.mjs +30 -13
- package/test/browser/modeller.check.mjs +45 -16
- package/test/browser/parts.check.mjs +49 -0
- package/test/browser/section.check.mjs +27 -0
- package/test/fasten-form.test.ts +42 -2
- package/test/gizmo.test.ts +12 -1
- package/test/keys.test.ts +11 -0
- package/test/links.test.ts +38 -33
- package/test/refusals.test.ts +17 -21
- package/translations/en.json +8 -13
- package/translations/es.json +8 -13
- package/translations/ja.json +8 -13
- package/translations/ko.json +8 -13
- package/translations/ms.json +8 -13
- package/translations/zh.json +8 -13
|
@@ -131,6 +131,22 @@ export const EditorStyles = css`
|
|
|
131
131
|
min-width: 0;
|
|
132
132
|
min-height: 0;
|
|
133
133
|
}
|
|
134
|
+
/* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */
|
|
135
|
+
p[same-measure] {
|
|
136
|
+
position: absolute;
|
|
137
|
+
bottom: 12px;
|
|
138
|
+
left: 50%;
|
|
139
|
+
z-index: 5;
|
|
140
|
+
transform: translateX(-50%);
|
|
141
|
+
margin: 0;
|
|
142
|
+
padding: 4px 10px;
|
|
143
|
+
border-radius: 999em;
|
|
144
|
+
background: var(--md-sys-color-surface-container-high);
|
|
145
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
146
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
}
|
|
134
150
|
/* A new figure's first question, over the empty canvas. */
|
|
135
151
|
div[start] {
|
|
136
152
|
position: absolute;
|
|
@@ -30,13 +30,13 @@ import './figure-preview.js'
|
|
|
30
30
|
import './figure-settings.js'
|
|
31
31
|
import { DEFAULT_VIEW, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
32
32
|
import './figure-capability-panel.js'
|
|
33
|
-
import { gizmoChanges, gizmoReachOf, sizeKeepOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
33
|
+
import { gizmoChanges, gizmoReachOf, sameMeasureAxesOf, sizeKeepOf, type GizmoReach, type GizmoResult } from './gizmo.js'
|
|
34
34
|
import { keptBySizeOf, partsAtFrame, SIZE_KEEPS_WORDS, startSizeDrag, throughPlaneOf, type SizeDrag, type SizeDragFrame } from './size-drag.js'
|
|
35
35
|
import type { KeyAction } from './keys.js'
|
|
36
36
|
import { ModellerKeys } from './modeller-keys.js'
|
|
37
37
|
import { PaneFold } from './pane-fold.js'
|
|
38
38
|
import { PanelMemory } from './panel-memory.js'
|
|
39
|
-
import { RefusalTeller
|
|
39
|
+
import { RefusalTeller } from './refusal-teller.js'
|
|
40
40
|
import { groupFields, ownerOf, partLabelOf, repeatOf, type FieldLike, type FieldGroup } from './fields.js'
|
|
41
41
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
42
42
|
import { EditorStyles } from './editor-styles.js'
|
|
@@ -60,11 +60,11 @@ type HeldValues = { position?: number[]; rotation?: number[]; size?: (number | u
|
|
|
60
60
|
import { occupancyFromParts, roomRefitRowsOf } from './occupancy.js'
|
|
61
61
|
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
62
62
|
import type { FigureGateFinding } from '../types.js'
|
|
63
|
-
import { editRefusalOf, inDeclaredRoom,
|
|
63
|
+
import { editRefusalOf, inDeclaredRoom, partNameRefusalOf, sizeRangeFitsOf, partRefusalOf, placedRefusalOf, recipeRefusalOf, removalNoticeOf } from './refusals.js'
|
|
64
64
|
import type { FigureRenderPart } from './renderer.js'
|
|
65
65
|
import type { FigureBudgetLine } from './maturity.js'
|
|
66
66
|
import { HelpToggles } from './help.js'
|
|
67
|
-
import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf,
|
|
67
|
+
import { endPlaceKeyOf, fieldWordsOf, followOf, sizeFollowersOf, measureNow, partSummaryOf, instanceRangeOf, instanceSizeToDeclare, proportionToFollow, rangeWith, turnsAboutAJoint, valueNow } from './links.js'
|
|
68
68
|
import { jointFanOf } from './joint-fan.js'
|
|
69
69
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
70
70
|
import { jointDragOf } from './joint-drag.js'
|
|
@@ -135,8 +135,6 @@ export class FigureEditor extends LitElement {
|
|
|
135
135
|
* edit was made (`from`).
|
|
136
136
|
*/
|
|
137
137
|
@state() private faceMapping?: { action: FigureAuthoringAction; need: FigureFaceMappingNeed; from: 'section-editor' | 'panel' }
|
|
138
|
-
/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
|
|
139
|
-
@state() private retry?: Retry
|
|
140
138
|
@state() private tab: SideTab = 'properties'
|
|
141
139
|
/** The view settings or the release history, open over the work from the bar. */
|
|
142
140
|
@state() private overlay?: BarOverlay
|
|
@@ -268,6 +266,14 @@ export class FigureEditor extends LitElement {
|
|
|
268
266
|
selected: () => this.selected,
|
|
269
267
|
act: action => this.keyAction(action)
|
|
270
268
|
})
|
|
269
|
+
/** The part Ctrl+C took: pasted as the kernel's duplicate. Not kept past the editor, nor written anywhere. */
|
|
270
|
+
private copied = ''
|
|
271
|
+
/** A copied part still in the figure is duplicated -- a part of its own, named by figure-model; one gone, nothing. */
|
|
272
|
+
private pasteCopied() {
|
|
273
|
+
const asset = this.session?.revisionNow()?.asset
|
|
274
|
+
if (!this.copied || !asset?.document.model.nodes.some(n => n.op === 'place@1' && n.id === this.copied)) return
|
|
275
|
+
this.duplicatePart(this.copied)
|
|
276
|
+
}
|
|
271
277
|
private keyAction(action: KeyAction) {
|
|
272
278
|
const view = this.session!.read()
|
|
273
279
|
const canvas = this.renderRoot.querySelector<FigureCanvas>('figure-canvas')
|
|
@@ -275,6 +281,9 @@ export class FigureEditor extends LitElement {
|
|
|
275
281
|
else if (action === 'undo') view.canUndo && this.run(() => this.session!.undo())
|
|
276
282
|
else if (action === 'redo') view.canRedo && this.run(() => this.session!.redo())
|
|
277
283
|
else if (action === 'duplicate') this.duplicatePart(this.selected)
|
|
284
|
+
else if (action === 'copy') this.copied = this.selected
|
|
285
|
+
else if (action === 'paste') this.pasteCopied()
|
|
286
|
+
else if (action === 'hold') return
|
|
278
287
|
else if (action === 'remove') this.removePart(this.selected)
|
|
279
288
|
else this.selected = ''
|
|
280
289
|
}
|
|
@@ -283,9 +292,7 @@ export class FigureEditor extends LitElement {
|
|
|
283
292
|
session: () => this.session,
|
|
284
293
|
error: () => this.error,
|
|
285
294
|
notice: () => this.notice,
|
|
286
|
-
|
|
287
|
-
refuse: message => (this.error = message),
|
|
288
|
-
command: action => this.linkCommand(action)
|
|
295
|
+
refuse: message => (this.error = message)
|
|
289
296
|
})
|
|
290
297
|
private panelMemory = new PanelMemory(() => this.renderRoot)
|
|
291
298
|
/** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
|
|
@@ -331,6 +338,14 @@ export class FigureEditor extends LitElement {
|
|
|
331
338
|
if (closed && this.selected === editing) this.selected = next
|
|
332
339
|
})
|
|
333
340
|
}
|
|
341
|
+
/** While the size tool is on a round part: which of its handles change the one diameter (ADR-0140 4.1). */
|
|
342
|
+
private renderSameMeasure(unavailable: boolean) {
|
|
343
|
+
if (unavailable || this.gizmoMode !== 'scale' || !this.selected) return nothing
|
|
344
|
+
const axes = sameMeasureAxesOf(this.reachOf(this.selected))
|
|
345
|
+
return axes
|
|
346
|
+
? html`<p same-measure role="status">${i18next.t('figure.text.gizmo-same-measure', { axes: axes.map(a => a.toUpperCase()).join(' · ') })}</p>`
|
|
347
|
+
: nothing
|
|
348
|
+
}
|
|
334
349
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
335
350
|
private useGizmo(mode: 'translate' | 'rotate' | 'scale') {
|
|
336
351
|
/*
|
|
@@ -412,6 +427,9 @@ export class FigureEditor extends LitElement {
|
|
|
412
427
|
}
|
|
413
428
|
|
|
414
429
|
private changed() {
|
|
430
|
+
/* What the step did to the size ends the author typed, said with whatever else it said. */
|
|
431
|
+
const fits = this.session?.takeSizeRangeFits() ?? []
|
|
432
|
+
if (fits.length) this.notice = [this.notice, sizeRangeFitsOf(fits, (key, params) => i18next.t(key, params))].filter(Boolean).join(' ')
|
|
415
433
|
this.requestUpdate()
|
|
416
434
|
this.dispatchEvent(new CustomEvent('changed', { bubbles: true, composed: true }))
|
|
417
435
|
}
|
|
@@ -507,7 +525,6 @@ export class FigureEditor extends LitElement {
|
|
|
507
525
|
this.run(() => this.session!.edit({ kind: 'move-part', target: id, to }))
|
|
508
526
|
}
|
|
509
527
|
private removePart(id: string, refitRoom = false) {
|
|
510
|
-
this.retry = undefined
|
|
511
528
|
try {
|
|
512
529
|
const view = this.session!.edit({ kind: 'remove-part', target: id, ...(refitRoom ? { refitRoom: true } : {}) }) as { removal?: Parameters<typeof removalNoticeOf>[0] }
|
|
513
530
|
this.error = ''
|
|
@@ -965,7 +982,6 @@ export class FigureEditor extends LitElement {
|
|
|
965
982
|
private linkCommand(action: FigureAuthoringAction, from: 'section-editor' | 'panel' = 'panel'): boolean {
|
|
966
983
|
/* Committed or refused, a command in hand is let go of: the figure as it stands is drawn again. */
|
|
967
984
|
this.holdAction(null)
|
|
968
|
-
this.retry = undefined
|
|
969
985
|
this.notice = ''
|
|
970
986
|
const hadSize = this.hasSize()
|
|
971
987
|
const hadSpace = !!this.shown()?.asset.occupancy
|
|
@@ -996,12 +1012,6 @@ export class FigureEditor extends LitElement {
|
|
|
996
1012
|
this.error = ''
|
|
997
1013
|
return false
|
|
998
1014
|
}
|
|
999
|
-
const narrowing = narrowingRetryOf(action, error, (key, params) => i18next.t(key, params))
|
|
1000
|
-
if (narrowing) {
|
|
1001
|
-
this.error = narrowing.message
|
|
1002
|
-
this.retry = { label: narrowing.label, action: narrowing.retry as FigureAuthoringAction }
|
|
1003
|
-
return false
|
|
1004
|
-
}
|
|
1005
1015
|
this.error =
|
|
1006
1016
|
code === 'DIMENSION_CYCLE'
|
|
1007
1017
|
? i18next.t('figure.text.follow-would-loop')
|
|
@@ -1086,21 +1096,21 @@ export class FigureEditor extends LitElement {
|
|
|
1086
1096
|
label.addEventListener('pointerup', up)
|
|
1087
1097
|
label.addEventListener('pointercancel', up)
|
|
1088
1098
|
}
|
|
1089
|
-
/** The sizes a board may give the figure, and the
|
|
1099
|
+
/** The sizes a board may give the figure: the ends the author typed, and the rest as far as its parts allow. */
|
|
1090
1100
|
private renderInstanceRange(shown: FigureRevision | undefined, unavailable: boolean) {
|
|
1091
1101
|
if (!shown) return nothing
|
|
1092
1102
|
const asset = shown.asset
|
|
1093
|
-
/*
|
|
1103
|
+
/* Computed once per version: figure-model evaluates the figure at its edges. */
|
|
1094
1104
|
const measured = derivedOf(shown, 'instance-range', () => {
|
|
1095
1105
|
let of: ReturnType<typeof instanceRangeOf>
|
|
1096
|
-
let toDeclare: ReturnType<typeof
|
|
1106
|
+
let toDeclare: ReturnType<typeof instanceSizeToDeclare>
|
|
1097
1107
|
try {
|
|
1098
1108
|
of = instanceRangeOf(asset)
|
|
1099
1109
|
} catch {
|
|
1100
1110
|
of = undefined
|
|
1101
1111
|
}
|
|
1102
1112
|
try {
|
|
1103
|
-
toDeclare = of ? undefined :
|
|
1113
|
+
toDeclare = of ? undefined : instanceSizeToDeclare(asset)
|
|
1104
1114
|
} catch {
|
|
1105
1115
|
toDeclare = undefined
|
|
1106
1116
|
}
|
|
@@ -1121,13 +1131,17 @@ export class FigureEditor extends LitElement {
|
|
|
1121
1131
|
return html`<section board-size>
|
|
1122
1132
|
${title('figure.text.board-size-fixed-explained')}
|
|
1123
1133
|
<p fixed-size>${i18next.t('figure.text.board-size-fixed', { x: mm(toDeclare.at.x), y: mm(toDeclare.at.y), z: mm(toDeclare.at.z) })}</p>
|
|
1124
|
-
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size',
|
|
1134
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', at: toDeclare.at })}>
|
|
1125
1135
|
<md-icon>open_in_full</md-icon>${i18next.t('figure.button.declare-instance-size')}
|
|
1126
1136
|
</button>
|
|
1127
1137
|
</section>`
|
|
1128
1138
|
if (!range) return nothing
|
|
1129
|
-
const differs = rangeDiffers(range.declared, range.measured)
|
|
1130
1139
|
const followers = sizeFollowersOf(asset)
|
|
1140
|
+
/*
|
|
1141
|
+
An end the author typed shows its number; an open end is empty, with what the parts allow written faintly in it
|
|
1142
|
+
(ADR-0087 decision 6). Typing narrows that end, emptying the field opens it again.
|
|
1143
|
+
*/
|
|
1144
|
+
const shownEnd = (r: (typeof range)['x'], bound: 'min' | 'max') => (bound === 'max' && !Number.isFinite(r.max) ? '∞' : mm(r[bound]))
|
|
1131
1145
|
return html`<section board-size>
|
|
1132
1146
|
${title('figure.text.board-size-range-explained')}
|
|
1133
1147
|
${(['x', 'y', 'z'] as const).map(
|
|
@@ -1139,11 +1153,12 @@ export class FigureEditor extends LitElement {
|
|
|
1139
1153
|
aria-label=${`size.${axis}.${bound}`}
|
|
1140
1154
|
type="number"
|
|
1141
1155
|
step="any"
|
|
1142
|
-
.value=${live(String(Math.round(range
|
|
1156
|
+
.value=${live(range[axis].narrowed[bound] ? String(Math.round(range[axis][bound])) : '')}
|
|
1157
|
+
placeholder=${shownEnd(range[axis], bound)}
|
|
1143
1158
|
?disabled=${unavailable}
|
|
1144
1159
|
@change=${(e: Event) => {
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1160
|
+
const typed = (e.target as HTMLInputElement).valueAsNumber
|
|
1161
|
+
this.linkCommand({ kind: 'declare-instance-size', range: rangeWith(range, axis, bound, typed), replace: true })
|
|
1147
1162
|
}}
|
|
1148
1163
|
/>`
|
|
1149
1164
|
)}
|
|
@@ -1157,19 +1172,6 @@ export class FigureEditor extends LitElement {
|
|
|
1157
1172
|
})
|
|
1158
1173
|
: i18next.t('figure.text.board-size-no-followers')}
|
|
1159
1174
|
</p>
|
|
1160
|
-
${differs
|
|
1161
|
-
? html`<p hint>
|
|
1162
|
-
${i18next.t('figure.text.instance-range-can-widen', {
|
|
1163
|
-
ranges: (['x', 'y', 'z'] as const)
|
|
1164
|
-
.filter(axis => rangeDiffers(range.declared, { ...range.declared, [axis]: range.measured[axis] }))
|
|
1165
|
-
.map(axis => `${axis.toUpperCase()} ${mm(range.measured[axis].min)} – ${mm(range.measured[axis].max)}`)
|
|
1166
|
-
.join(' · ')
|
|
1167
|
-
})}
|
|
1168
|
-
</p>
|
|
1169
|
-
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })}>
|
|
1170
|
-
<md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
|
|
1171
|
-
</button>`
|
|
1172
|
-
: nothing}
|
|
1173
1175
|
${this.renderSizingRule(asset, unavailable)}
|
|
1174
1176
|
</section>`
|
|
1175
1177
|
}
|
|
@@ -1325,7 +1327,7 @@ export class FigureEditor extends LitElement {
|
|
|
1325
1327
|
this.publishing?.part === part
|
|
1326
1328
|
? this.publishing
|
|
1327
1329
|
: listed && input
|
|
1328
|
-
? { part, label: listed.label, min: input.min
|
|
1330
|
+
? /* A published input has both ends (figure-model checkPublicInput). */ { part, label: listed.label, min: input.min!, max: input.max! }
|
|
1329
1331
|
: undefined
|
|
1330
1332
|
const open = !!form
|
|
1331
1333
|
const edit = (key: 'label' | 'min' | 'max') => (e: Event) => {
|
|
@@ -1921,7 +1923,7 @@ export class FigureEditor extends LitElement {
|
|
|
1921
1923
|
@render-error=${(e: CustomEvent<Error>) => {
|
|
1922
1924
|
this.error = e.detail.message
|
|
1923
1925
|
}}
|
|
1924
|
-
></figure-canvas
|
|
1926
|
+
></figure-canvas>${this.renderSameMeasure(unavailable)}</div>
|
|
1925
1927
|
<aside slot="end">
|
|
1926
1928
|
<div tabs>
|
|
1927
1929
|
${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FIGURE_FACES, figureFastenTargetAt, figurePlaneFacingOf, figureSurfaceKeptPlaceOf } from '@hatiolab/figure-model'
|
|
1
|
+
import { FIGURE_FACES, figureFastenKeptGapOf, figureFastenTargetAt, figurePlaneFacingOf, figureSurfaceKeptPlaceOf } from '@hatiolab/figure-model'
|
|
2
2
|
import type { FigureAuthoringAction, FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
3
3
|
|
|
4
4
|
import { faceViewOf, type FaceView, type FastenMode, type FastenTarget } from './fasten-geometry.js'
|
|
@@ -38,6 +38,12 @@ export interface FastenForm {
|
|
|
38
38
|
gap: number
|
|
39
39
|
/** The record's gap when it is worked out (a length that follows something): kept until a number is typed. */
|
|
40
40
|
gapKept: unknown
|
|
41
|
+
/**
|
|
42
|
+
* Where the gap came from (ADR-0140 stage 4, 「현재 상태로 붙이기」): `kept` -- the distance the part stands at, worked
|
|
43
|
+
* out again whenever where or how it is fastened changes, so fastening does not move it; `typed` -- the author's
|
|
44
|
+
* number, left as it is; `pressed` -- 0, the part put where the canvas was pressed.
|
|
45
|
+
*/
|
|
46
|
+
gapFrom: 'kept' | 'typed' | 'pressed'
|
|
41
47
|
align: Partial<Record<Axis, Alignment>>
|
|
42
48
|
alignMm: Partial<Record<Axis, number>>
|
|
43
49
|
alignInput: Partial<Record<Axis, string>>
|
|
@@ -52,6 +58,7 @@ export const freshFasten = (): FastenForm => ({
|
|
|
52
58
|
facing: 'meet',
|
|
53
59
|
gap: 0,
|
|
54
60
|
gapKept: undefined,
|
|
61
|
+
gapFrom: 'kept',
|
|
55
62
|
align: {},
|
|
56
63
|
alignMm: {},
|
|
57
64
|
alignInput: {}
|
|
@@ -60,7 +67,9 @@ export const freshFasten = (): FastenForm => ({
|
|
|
60
67
|
/** The part's own fastening as a form, or a fresh one: a window opens on what the part has (walk 1, 2026-09-26). */
|
|
61
68
|
export function fastenFormOf(revision: FigureRevision, part: string): FastenForm {
|
|
62
69
|
const own = fasteningOf(revision, part)
|
|
63
|
-
|
|
70
|
+
if (!own || own.atOrigin) return freshFasten()
|
|
71
|
+
/* A gap the record works out is the author's; a number is where the part stands, so aiming again keeps it there. */
|
|
72
|
+
return { ...freshFasten(), ...own, gapFrom: own.gapKept !== undefined ? 'typed' : 'kept' } as FastenForm
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
/** What a part may be fastened to: every other part that can be, each said whether it would close a loop. */
|
|
@@ -133,11 +142,11 @@ export function fastenChanged(
|
|
|
133
142
|
change: FastenChange
|
|
134
143
|
): FastenForm {
|
|
135
144
|
const f = form
|
|
136
|
-
if ('targetFace' in change) return aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace })
|
|
137
|
-
if ('onSurface' in change) return aimFasten(revision, part, f, { surface: true })
|
|
138
|
-
if ('surface' in change) return { ...f, surface: change.surface }
|
|
139
|
-
if ('mode' in change) return { ...f, facing: change.mode }
|
|
140
|
-
if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined }
|
|
145
|
+
if ('targetFace' in change) return keptGapOf(revision, part, aimFasten(revision, part, f, { targetFace: change.targetFace as FigureFace }))
|
|
146
|
+
if ('onSurface' in change) return keptGapOf(revision, part, aimFasten(revision, part, f, { surface: true }))
|
|
147
|
+
if ('surface' in change) return keptGapOf(revision, part, { ...f, surface: change.surface })
|
|
148
|
+
if ('mode' in change) return keptGapOf(revision, part, { ...f, facing: change.mode })
|
|
149
|
+
if ('gap' in change) return { ...f, gap: change.gap, gapKept: undefined, gapFrom: 'typed' }
|
|
141
150
|
return {
|
|
142
151
|
...f,
|
|
143
152
|
align: { ...f.align, [change.axis]: change.how },
|
|
@@ -145,6 +154,29 @@ export function fastenChanged(
|
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The form with the gap that keeps the part where it stands (ADR-0140 stage 4, user 2026-10-20 「현재 상태로 붙이기」):
|
|
159
|
+
* figure-model's `figureFastenKeptGapOf` for what it is aimed at and how it meets it. A gap the author typed, or the 0 a
|
|
160
|
+
* canvas press puts, is left as it is. Nothing aimed at yet, the form is as it was.
|
|
161
|
+
*/
|
|
162
|
+
export function keptGapOf(revision: FigureRevision, part: string, form: FastenForm): FastenForm {
|
|
163
|
+
if (form.gapFrom !== 'kept') return form
|
|
164
|
+
const asset = revision.asset
|
|
165
|
+
const theirFaces = form.target ? facesOf(asset, form.target) : FACES
|
|
166
|
+
const targetFace = (theirFaces.includes(form.targetFace) ? form.targetFace : theirFaces[0]) as FigureFace
|
|
167
|
+
const planeWay = figurePlaneFacingOf(asset)
|
|
168
|
+
const mode: FastenMode = form.toPlane ? (mountingPlaneOf(asset).facings.includes(form.facing) ? form.facing : (planeWay ?? 'meet')) : form.facing
|
|
169
|
+
const to = form.toPlane
|
|
170
|
+
? ('plane' as const)
|
|
171
|
+
: form.target && form.surface && surfaceOf(asset, form.target)
|
|
172
|
+
? { part: form.target, surface: form.surface }
|
|
173
|
+
: form.target
|
|
174
|
+
? { part: form.target, face: targetFace }
|
|
175
|
+
: undefined
|
|
176
|
+
if (!to) return form
|
|
177
|
+
return { ...form, gap: figureFastenKeptGapOf(asset, part, to, mode, { ...revision.inputs }), gapKept: undefined }
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
/** What a form comes to: the face aimed at, the way, the drawing of the face and the command. */
|
|
149
181
|
export interface FastenPlan {
|
|
150
182
|
/** Nothing is aimed at until a part or the plane is chosen. */
|
|
@@ -226,12 +258,16 @@ export function fastenSentenceOf(
|
|
|
226
258
|
return say(`figure.text.fasten-sentence-${plan.mode}`, { to })
|
|
227
259
|
}
|
|
228
260
|
|
|
229
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* What the folded details hold that is not their default, said while folded; nothing for the default. The gap's default
|
|
263
|
+
* is the one it starts at -- the distance the part stands at, or 0 where the canvas was pressed -- so only a number typed
|
|
264
|
+
* is said.
|
|
265
|
+
*/
|
|
230
266
|
export function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {
|
|
231
267
|
return [
|
|
232
268
|
...(form.gapKept !== undefined
|
|
233
269
|
? [say('figure.text.fasten-summary-gap-worked-out')]
|
|
234
|
-
: form.gap
|
|
270
|
+
: form.gapFrom === 'typed' && form.gap
|
|
235
271
|
? [say('figure.text.fasten-summary-gap', { mm: String(form.gap) })]
|
|
236
272
|
: [])
|
|
237
273
|
]
|
|
@@ -244,6 +280,9 @@ export function fastenDetailWordsOf(form: FastenForm, say: Say): string[] {
|
|
|
244
280
|
* nothing to fasten to is not aimed at; a press on a curve there is no fastening to aims at the part alone. Each says
|
|
245
281
|
* why, by its words' key.
|
|
246
282
|
*/
|
|
283
|
+
/* Put where the canvas was pressed: on the face or the surface there, with no gap (ADR-0140 stage 4). */
|
|
284
|
+
const PRESSED = { gap: 0, gapKept: undefined, gapFrom: 'pressed' as const }
|
|
285
|
+
|
|
247
286
|
export function aimFastenAt(
|
|
248
287
|
revision: FigureRevision,
|
|
249
288
|
part: string,
|
|
@@ -265,8 +304,8 @@ export function aimFastenAt(
|
|
|
265
304
|
/* On the face where it was pressed, as a point of a curved surface is: its middle there. */
|
|
266
305
|
const onFace = aimFasten(revision, part, { ...aimed, surface: undefined }, { targetFace: lands.face as FigureFace })
|
|
267
306
|
const at = lands.at as Partial<Record<Axis, number>>
|
|
268
|
-
return { form: { ...onFace, align: Object.fromEntries(Object.keys(at).map(a => [a, 'mm'])) as FastenForm['align'], alignMm: { ...at }, alignInput: {} }, part: target }
|
|
307
|
+
return { form: { ...onFace, ...PRESSED, align: Object.fromEntries(Object.keys(at).map(a => [a, 'mm'])) as FastenForm['align'], alignMm: { ...at }, alignInput: {} }, part: target }
|
|
269
308
|
}
|
|
270
|
-
if ('surface' in lands) return { form: { ...aimed, toPlane: false, target, surface: lands.surface, align: {}, alignMm: {}, alignInput: {} }, part: target }
|
|
309
|
+
if ('surface' in lands) return { form: { ...aimed, ...PRESSED, toPlane: false, target, surface: lands.surface, align: {}, alignMm: {}, alignInput: {} }, part: target }
|
|
271
310
|
return { form: aimed, note: 'aim-not-here', part: target }
|
|
272
311
|
}
|
|
@@ -16,6 +16,7 @@ import { turnsInPlace, type FastenMode } from './fasten-geometry.js'
|
|
|
16
16
|
import {
|
|
17
17
|
aimFasten,
|
|
18
18
|
aimFastenAt,
|
|
19
|
+
keptGapOf,
|
|
19
20
|
fastenChanged,
|
|
20
21
|
fastenDetailWordsOf,
|
|
21
22
|
fastenFormOf,
|
|
@@ -260,7 +261,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
260
261
|
step="any"
|
|
261
262
|
.value=${f.gapKept !== undefined ? '' : String(f.gap)}
|
|
262
263
|
placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
|
|
263
|
-
@change=${(e: Event) => this.set({ gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined })}
|
|
264
|
+
@change=${(e: Event) => this.set({ gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined, gapFrom: 'typed' })}
|
|
264
265
|
/>
|
|
265
266
|
</div>
|
|
266
267
|
</details>
|
|
@@ -330,7 +331,7 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
|
|
|
330
331
|
this.aimNote = ''
|
|
331
332
|
const chosen = this.form.toPlane ? PLANE : this.form.target
|
|
332
333
|
if (part === chosen) return
|
|
333
|
-
this.form = aimFasten(this.revision, this.partId, this.form, part === PLANE ? { plane: true } : { part })
|
|
334
|
+
this.form = keptGapOf(this.revision, this.partId, aimFasten(this.revision, this.partId, this.form, part === PLANE ? { plane: true } : { part }))
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
/**
|
|
@@ -278,7 +278,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
278
278
|
<section size-response>
|
|
279
279
|
<h3>${i18next.t('figure.label.group-size')}</h3>
|
|
280
280
|
<p group-question>${i18next.t('figure.label.resize-intent')}</p>
|
|
281
|
-
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.
|
|
281
|
+
${canFollow(asset, this.partId) ? this.renderDimensions(asset) : html`<p hint>${i18next.t('figure.text.part-cannot-follow')}</p>`}
|
|
282
282
|
</section>
|
|
283
283
|
${positions.length
|
|
284
284
|
? html`<section position-response>
|
|
@@ -302,7 +302,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
302
302
|
${groupMenu([{ label: i18next.t('figure.button.detach'), icon: 'link_off', disabled: this.locked || !held, run: () => this.command({ kind: 'detach', part: this.partId }) }])}
|
|
303
303
|
</h3>${this.help.hint('figure.text.fastening-explained')}
|
|
304
304
|
<slot name="place"></slot>
|
|
305
|
-
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.
|
|
305
|
+
${canFasten(asset, this.partId) ? this.renderFastening(asset) : html`<p hint>${i18next.t('figure.text.part-cannot-fasten')}</p>`}
|
|
306
306
|
</section>
|
|
307
307
|
`
|
|
308
308
|
this.candidate = this.fastenCandidate
|
|
@@ -3,14 +3,14 @@ import { customElement, property, query, state } from 'lit/decorators.js'
|
|
|
3
3
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { compileFigureAsset } from '@hatiolab/figure-model'
|
|
6
|
-
import type { FigureAsset, FigureDataRow, FigureDataValue } from '@hatiolab/figure-model'
|
|
6
|
+
import type { FigureAsset, FigureDataRow, FigureDataValue, FigureGraphStateInput } from '@hatiolab/figure-model'
|
|
7
7
|
import { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene'
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
10
10
|
import { dialShape } from './angle-dial.js'
|
|
11
11
|
import { domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
|
|
12
12
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
13
|
-
import { authoredSizeOf, occupancyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf,
|
|
13
|
+
import { authoredSizeOf, occupancyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
14
14
|
import { previewSizeWordsOf } from './preview-size.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -335,7 +335,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
335
335
|
if (!asset.occupancy) return html`<div sizes><p said>${i18next.t('figure.text.preview-needs-space')}</p></div>`
|
|
336
336
|
return html`<div sizes>
|
|
337
337
|
${(['x', 'y', 'z'] as const).map(axis => {
|
|
338
|
-
const range =
|
|
338
|
+
const range = sizeSpanOf(asset, axis)
|
|
339
339
|
if (!range) return nothing
|
|
340
340
|
return html`<label>
|
|
341
341
|
<span>${i18next.t(`figure.label.measure-${axis}`)}</span>
|
|
@@ -377,7 +377,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
377
377
|
this.scene?.root?.fitCameraToModel?.()
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
private renderDrivers(asset: FigureAsset, controls:
|
|
380
|
+
private renderDrivers(asset: FigureAsset, controls: FigureGraphStateInput[], clips: string[]) {
|
|
381
381
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
|
|
382
382
|
return html`<div clips>
|
|
383
383
|
${changed.length
|
|
@@ -609,8 +609,8 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
609
609
|
type: 'model-layer',
|
|
610
610
|
width: board.width,
|
|
611
611
|
height: board.height,
|
|
612
|
-
/* As tall as the figure may grow: twice for the eight, the
|
|
613
|
-
depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, view.ceilingHeight, this.way === 'one' ? Math.max(1, (
|
|
612
|
+
/* As tall as the figure may grow: twice for the eight, the top of the height slider for the one. */
|
|
613
|
+
depth: previewBoardDepth({ placement: asset.placement, base: { y: size.y } }, view.ceilingHeight, this.way === 'one' ? Math.max(1, (sizeSpanOf(asset, 'y')?.max ?? size.y) / size.y) : PREVIEW_FACTOR),
|
|
614
614
|
threed: true,
|
|
615
615
|
sky: 'color',
|
|
616
616
|
...sceneLook(view),
|
package/client/modeller/gizmo.ts
CHANGED
|
@@ -111,6 +111,16 @@ export function gizmoReachOf(asset: AssetLike, part: string): GizmoReach {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** Where a drag left the part, in the asset frame: millimetres, degrees and per-axis scale. */
|
|
114
|
+
/**
|
|
115
|
+
* The size handles that change one measure (ADR-0140 4.1): a round part's radius across is one value, so its X and Z
|
|
116
|
+
* handles are the same diameter, and a sphere's three are one. Said on the canvas while the size tool is on, to see how
|
|
117
|
+
* it handles before deciding to show fewer. Nothing for a part whose handles each change their own measure.
|
|
118
|
+
*/
|
|
119
|
+
export function sameMeasureAxesOf(reach: GizmoReach): Array<'x' | 'y' | 'z'> | undefined {
|
|
120
|
+
if (!reach.scale || !reach.round) return undefined
|
|
121
|
+
return reach.round.along.length ? ['x', 'z'] : ['x', 'y', 'z']
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
export interface GizmoResult {
|
|
115
125
|
position?: [number, number, number]
|
|
116
126
|
rotation?: [number, number, number]
|
package/client/modeller/keys.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
* Typing is never taken: a key pressed in a field belongs to the field -- Backspace in a name box deletes a letter,
|
|
6
6
|
* not the part.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
/*
|
|
9
|
+
* `copy` · `paste` are the modeller's own: a part copied and pasted is the kernel's duplicate, with a name of its own.
|
|
10
|
+
* Left to the scene, Ctrl+C / Ctrl+V copied the canvas object with the part's name -- two objects of one name, the copy
|
|
11
|
+
* moving the original (user 2026-10-21). `hold` is a key kept from the scene that the modeller has no use for: the
|
|
12
|
+
* scene's cut and bulk paste act on canvas objects alone and leave the figure out of step with them.
|
|
13
|
+
*/
|
|
14
|
+
export type KeyAction = 'undo' | 'redo' | 'duplicate' | 'remove' | 'deselect' | 'copy' | 'paste' | 'hold' | GizmoKey
|
|
9
15
|
|
|
10
16
|
/** The gizmo's modes on W, E and R, as the scene has them. */
|
|
11
17
|
export type GizmoKey = 'translate' | 'rotate' | 'scale'
|
|
@@ -36,6 +42,9 @@ export function keyActionOf(e: KeyLike, selected: string): KeyAction | null {
|
|
|
36
42
|
const key = e.key.toLowerCase()
|
|
37
43
|
if (mod && key === 'z') return e.shiftKey ? 'redo' : 'undo'
|
|
38
44
|
if (mod && key === 'y') return 'redo'
|
|
45
|
+
if (mod && key === 'c') return selected ? 'copy' : 'hold'
|
|
46
|
+
if (mod && key === 'v') return e.shiftKey ? 'hold' : 'paste'
|
|
47
|
+
if (mod && key === 'x') return 'hold'
|
|
39
48
|
if (!selected) return null
|
|
40
49
|
if (mod && key === 'd') return 'duplicate'
|
|
41
50
|
if (!mod && (key === 'delete' || key === 'backspace')) return 'remove'
|
package/client/modeller/links.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion,
|
|
2
|
-
import type { FigureFollow,
|
|
1
|
+
import { FIGURE_FACES, figureShapeFaces, figurePartsExtentOf, compileFigureAsset, figureFollowOf, proposeFigureProportion, figureAttachmentsOf, figureBindingsOf, figureInstanceSizeRangeOf, figureFollowableAxesOf, figureShapeProviderOf, figureMimicTermsOf, figureFastenKeptPlaceOf, figureFastenAxesOf, figureJointAsFixed, figureJointWithMotion, figureSurfaceOf } from '@hatiolab/figure-model'
|
|
2
|
+
import type { FigureFollow, FigureInstanceSizeRange } from '@hatiolab/figure-model'
|
|
3
3
|
import type { FigureAsset, FigureAuthoringAction, FigureBindingPlace, FigureDimensionSource, FigureFace, FigureFacing, FigureJoint, FigureSurfacePlace } from '@hatiolab/figure-model'
|
|
4
4
|
import { startingShapeKindOf } from './starting-shape.js'
|
|
5
5
|
import { boxOf } from './align.js'
|
|
@@ -733,53 +733,48 @@ export const holdsThings = (asset: FigureAsset) => (asset.document.capabilities.
|
|
|
733
733
|
export const acceptsOf = (typed: string) => [...new Set(typed.split(',').map(one => one.trim()).filter(Boolean))]
|
|
734
734
|
|
|
735
735
|
/**
|
|
736
|
-
* The sizes a board may give this figure: the
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const declared = {} as FigureSizeRange
|
|
744
|
-
const authored = {} as Record<Axis, number>
|
|
745
|
-
for (const axis of AXES) {
|
|
746
|
-
const input = inputs.find(i => i.id === `size.${axis}` && i.role === 'design')
|
|
747
|
-
const at = asset.designInputs[`size.${axis}`]
|
|
748
|
-
if (!input || typeof at !== 'number') return undefined
|
|
749
|
-
declared[axis] = { min: input.min ?? 0, max: input.max ?? Infinity }
|
|
750
|
-
authored[axis] = at
|
|
751
|
-
}
|
|
752
|
-
const start = defaultInstanceSizeRange(authored)
|
|
753
|
-
const measured = foldInstanceSizeRange(start, figureInstanceSizeLimitsOf(asset, start)).range
|
|
754
|
-
return { declared, measured }
|
|
736
|
+
* The sizes a board may give this figure, per axis: the ends its author narrowed, and the rest as far as its parts
|
|
737
|
+
* allow, computed by figure-model (`figureInstanceSizeRangeOf`, ADR-0087 decision 6) -- the screen does not measure
|
|
738
|
+
* on its own. Nothing when no size is declared.
|
|
739
|
+
*/
|
|
740
|
+
export function instanceRangeOf(asset: FigureAsset): Record<Axis, FigureInstanceSizeRange> | undefined {
|
|
741
|
+
const range = figureInstanceSizeRangeOf(asset)
|
|
742
|
+
return AXES.every(axis => range[axis]) ? (range as Record<Axis, FigureInstanceSizeRange>) : undefined
|
|
755
743
|
}
|
|
756
744
|
|
|
757
745
|
/**
|
|
758
|
-
* The size
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
|
|
746
|
+
* The size to declare for a figure that has none yet: its parts' extent now, the size it is authored at. Undefined when
|
|
747
|
+
* the figure already has a size or has nothing to measure. Without a declared size a board cannot resize the figure,
|
|
748
|
+
* and no part is offered to follow its size -- every figure made in the modeller was left there (user, 2026-09-25).
|
|
762
749
|
*/
|
|
763
|
-
export function
|
|
750
|
+
export function instanceSizeToDeclare(asset: FigureAsset): { at: Record<Axis, number> } | undefined {
|
|
764
751
|
if (instanceRangeOf(asset) || !asset.appearance.length) return undefined
|
|
765
752
|
/* The parts as they rest -- not the room their joints swing them through, which declared a door 8208 mm every way. */
|
|
766
753
|
const at = figurePartsExtentOf(asset) as Record<Axis, number>
|
|
767
754
|
if (AXES.some(axis => !(at[axis] > 0))) return undefined
|
|
768
|
-
|
|
769
|
-
return { range: foldInstanceSizeRange(start, figureInstanceSizeLimitsOf(asset, start)).range, at }
|
|
755
|
+
return { at }
|
|
770
756
|
}
|
|
771
757
|
|
|
772
|
-
/** Whether measuring again would change the declared range (to a micrometre). */
|
|
773
758
|
/**
|
|
774
|
-
* The
|
|
775
|
-
*
|
|
776
|
-
* value is not a number; whether the figure holds there is the command's to say.
|
|
759
|
+
* The ends the author narrowed, with one end typed or cleared: a number narrows that end, an empty field opens it
|
|
760
|
+
* again to what the parts allow. Whether the figure holds there is the command's to say.
|
|
777
761
|
*/
|
|
778
|
-
export function rangeWith(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
762
|
+
export function rangeWith(
|
|
763
|
+
range: Record<Axis, FigureInstanceSizeRange>,
|
|
764
|
+
axis: Axis,
|
|
765
|
+
bound: 'min' | 'max',
|
|
766
|
+
value: number
|
|
767
|
+
): Partial<Record<Axis, { min?: number; max?: number }>> {
|
|
768
|
+
const out: Partial<Record<Axis, { min?: number; max?: number }>> = {}
|
|
769
|
+
for (const a of AXES) {
|
|
770
|
+
const ends: { min?: number; max?: number } = {}
|
|
771
|
+
for (const b of ['min', 'max'] as const) {
|
|
772
|
+
const typed = a === axis && b === bound
|
|
773
|
+
if (typed ? Number.isFinite(value) : range[a].narrowed[b]) ends[b] = typed ? value : range[a][b]
|
|
774
|
+
}
|
|
775
|
+
if (Object.keys(ends).length) out[a] = ends
|
|
776
|
+
}
|
|
777
|
+
return out
|
|
783
778
|
}
|
|
784
779
|
|
|
785
780
|
/**
|
|
@@ -807,10 +802,6 @@ export function valueNow(revision: FigureRevision, ref: string | undefined): num
|
|
|
807
802
|
}
|
|
808
803
|
}
|
|
809
804
|
|
|
810
|
-
export function rangeDiffers(a: FigureSizeRange, b: FigureSizeRange): boolean {
|
|
811
|
-
return AXES.some(axis => Math.abs(a[axis].min - b[axis].min) > 1e-3 || Math.abs(a[axis].max - b[axis].max) > 1e-3)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
805
|
/**
|
|
815
806
|
* Whether the figure is a mechanism -- a part turns about a joint. Such a figure should grow by one ratio (chief
|
|
816
807
|
* architect's ruling 2026-09-24); the modeller proposes it, the author declares it.
|
|
@@ -16,7 +16,7 @@ export function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimit
|
|
|
16
16
|
case 'rounded-box':
|
|
17
17
|
return { ...standing(100), kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }
|
|
18
18
|
case 'sphere':
|
|
19
|
-
return { ...standing(100), kind, dimensions: {
|
|
19
|
+
return { ...standing(100), kind, dimensions: { radius: 50, radiusY: 50 } }
|
|
20
20
|
case 'frustum':
|
|
21
21
|
return { ...standing(100), kind, dimensions: { radiusTop: 0, radiusBottom: 50, height: 100 } }
|
|
22
22
|
case 'polygon':
|