@things-factory/figure-ui 10.1.163 → 10.1.164

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.
@@ -77,7 +77,13 @@ let FigureDetailDock = class FigureDetailDock extends localize(i18next)(LitEleme
77
77
  button[icon]:hover:not(:disabled) { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }
78
78
  button[icon]:disabled { opacity: 0.38; cursor: default; }
79
79
  button[icon] md-icon { --md-icon-size: 18px; }
80
- [body] { flex: 1; overflow: auto; min-height: 0; }
80
+ /*
81
+ The figure stays in view under what it is (which model, how many fit) and its tabs; only the tab's body scrolls
82
+ (user 2026-10-15: a clip played at the bottom of a scrolled dock moved a figure no longer seen).
83
+ */
84
+ [body] { flex: 1; display: flex; flex-direction: column; min-height: 0; }
85
+ [body] > * { flex: none; }
86
+ [body] > figure-asset-viewer { flex: 1; }
81
87
  [meta] { display: grid; gap: 8px; padding: 0 16px 12px; }
82
88
  /* Which model is shown: words that change colour, not boxes. */
83
89
  [source] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; color: var(--md-sys-color-on-surface-variant); }
@@ -91,7 +97,7 @@ let FigureDetailDock = class FigureDetailDock extends localize(i18next)(LitEleme
91
97
  dl[stats] > div { display: flex; align-items: baseline; gap: 4px; }
92
98
  dl[stats] dd { font-weight: 600; font-variant-numeric: tabular-nums; }
93
99
  p { margin: 0; line-height: 1.5; }
94
- [description] { color: var(--md-sys-color-on-surface-variant); }
100
+ [description] { padding: 10px 16px 0; color: var(--md-sys-color-on-surface-variant); }
95
101
  [status] { display: grid; gap: 8px; justify-items: start; padding: 12px 16px; }
96
102
  @media (max-width: 1439.98px) {
97
103
  :host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }
@@ -199,10 +205,10 @@ let FigureDetailDock = class FigureDetailDock extends localize(i18next)(LitEleme
199
205
  <div><dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd></div>
200
206
  <div><dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd></div>
201
207
  </dl>` : nothing}
202
- ${saved.description ? html `<p description>${saved.description}</p>` : nothing}
203
208
  </section>` : nothing}
204
209
  ${this.asset ? html `<figure-asset-viewer .asset=${this.asset}
205
210
  .keepShot=${saved?.id ? (dataUrl) => this.keepThumbnail(saved.id, dataUrl) : undefined}>
211
+ ${saved?.description ? html `<p slot="facts" description>${saved.description}</p>` : nothing}
206
212
  ${saved ? html `<figure-facts-list slot="facts" .asset=${this.asset} .row=${saved}></figure-facts-list>` : nothing}
207
213
  </figure-asset-viewer>` : nothing}
208
214
  </div>`;
@@ -1 +1 @@
1
- {"version":3,"file":"figure-detail-dock.js","sourceRoot":"","sources":["../../client/viewparts/figure-detail-dock.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { appendViewpart, UPDATE_VIEWPART, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout'\nimport { navigate, store } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { ASSISTANT_DOCK_VIEWPART } from '@things-factory/ai-assistant/dist-client/components/assistant-dock.js'\nimport { figureCostOf, parseFigureAsset, type FigureAsset } from '@hatiolab/figure-model'\nimport { capacityFrom } from '../modeller/scene-budget.js'\nimport { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'\nimport { fetchFigure, fetchReleasedFigure, updateFigure } from '../graphql/index.js'\nimport { modellerPathOf } from '../modeller/modeller-route.js'\nimport { leftOpeningPage } from './detail-route.js'\nimport type { Figure } from '../types.js'\nimport '@things-factory/figure-viewer/dist-client/figure-asset-viewer.js'\nimport '@things-factory/figure-viewer/dist-client/figure-facts-list.js'\n\nconst VIEWPART = 'figure-detail'\n/** Said on window when a figure's card picture was replaced from the preview: `{ detail: { id } }`. */\nexport const FIGURE_THUMBNAIL_CHANGED = 'figure-thumbnail-changed'\nexport interface FigureDetailRecord {\n saved: Figure\n released?: { version: number; source: string }\n}\n\n/** Catalogue details stay while the catalogue is shown; choosing a size or a pose has no write path. */\n@customElement('figure-detail-dock')\nexport class FigureDetailDock extends localize(i18next)(LitElement) {\n /*\n The modeller's side panel look (SidePanelStyles). What the preview is opened for sits above the model -- name, type,\n which model is shown, how many fit -- as plain text; the controls are borderless icons and quiet text, so the model\n and its numbers are what the eye meets.\n */\n static styles = [ScrollbarStyles, SidePanelStyles, css`\n :host { display: flex; flex-direction: column; width: 400px; max-width: 100vw; height: 100%; min-height: 0;\n background: var(--md-sys-color-surface); border-left: 1px solid var(--md-sys-color-outline-variant); }\n header { display: flex; align-items: flex-start; gap: 2px; padding: 12px 8px 4px 16px; }\n [title-block] { flex: 1; min-width: 0; display: grid; gap: 2px; }\n h2 { margin: 0; font-size: var(--ui-t-title, 16px); font-weight: 600; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n code { font-family: var(--mono-font, monospace); color: var(--md-sys-color-on-surface-variant); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n button { font: inherit; color: inherit; cursor: pointer; }\n button:focus-visible { outline: 2px solid var(--md-sys-color-primary); outline-offset: 1px; }\n button[icon] { display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: none; border-radius: 6px;\n background: transparent; color: var(--md-sys-color-on-surface-variant); }\n button[icon]:hover:not(:disabled) { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }\n button[icon]:disabled { opacity: 0.38; cursor: default; }\n button[icon] md-icon { --md-icon-size: 18px; }\n [body] { flex: 1; overflow: auto; min-height: 0; }\n [meta] { display: grid; gap: 8px; padding: 0 16px 12px; }\n /* Which model is shown: words that change colour, not boxes. */\n [source] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; color: var(--md-sys-color-on-surface-variant); }\n [source] button { padding: 0; border: none; background: none; color: var(--md-sys-color-on-surface-variant); }\n [source] button:hover { color: var(--md-sys-color-on-surface); }\n [source] button[aria-pressed='true'] { color: var(--md-sys-color-primary); font-weight: 600; }\n dl { margin: 0; }\n dt { color: var(--md-sys-color-on-surface-variant); }\n dd { margin: 0; color: var(--md-sys-color-on-surface); }\n dl[stats] { display: flex; flex-wrap: wrap; gap: 4px 16px; }\n dl[stats] > div { display: flex; align-items: baseline; gap: 4px; }\n dl[stats] dd { font-weight: 600; font-variant-numeric: tabular-nums; }\n p { margin: 0; line-height: 1.5; }\n [description] { color: var(--md-sys-color-on-surface-variant); }\n [status] { display: grid; gap: 8px; justify-items: start; padding: 12px 16px; }\n @media (max-width: 1439.98px) {\n :host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }\n }\n `]\n @property({ attribute: false }) load: (id: string) => Promise<FigureDetailRecord> = async id => {\n const [saved, released] = await Promise.all([fetchFigure(id), fetchReleasedFigure(id)])\n if (!saved?.source) throw Error(i18next.t('figure.text.detail-no-source'))\n return { saved, released }\n }\n /**\n * Keeps the picture taken in the preview as the figure's card picture. A patch of the picture alone does not make a\n * released figure a draft (`updateFigure`). The catalogue is told, and reads its cards again.\n */\n @property({ attribute: false }) keepThumbnail: (id: string, dataUrl: string) => Promise<unknown> = async (id, dataUrl) => {\n await updateFigure(id, { thumbnail: dataUrl })\n window.dispatchEvent(new CustomEvent(FIGURE_THUMBNAIL_CHANGED, { detail: { id } }))\n }\n @state() private figureId = ''\n @state() private record?: FigureDetailRecord\n @state() private asset?: FigureAsset\n @state() private sourceKind: 'saved' | 'released' = 'saved'\n @state() private loading = false\n @state() private error = ''\n @state() private cost?: ReturnType<typeof figureCostOf>\n private ticket = 0\n private unsubscribe?: () => void\n private boundary?: Element\n private boundaryObserver?: ResizeObserver\n\n async show(id: string) {\n const ticket = ++this.ticket\n this.figureId = id\n this.record = undefined\n this.asset = undefined\n this.cost = undefined\n this.loading = true\n this.error = ''\n try {\n const record = await this.load(id)\n if (ticket !== this.ticket) return\n this.record = record\n this.choose(record.released ? 'released' : 'saved')\n } catch {\n if (ticket === this.ticket) this.error = i18next.t('figure.text.detail-load-failed')\n } finally {\n if (ticket === this.ticket) this.loading = false\n }\n }\n private choose(kind: 'saved' | 'released') {\n this.sourceKind = kind\n this.asset = undefined\n this.cost = undefined\n this.error = ''\n try {\n const source = kind === 'released' ? this.record?.released?.source : this.record?.saved.source\n if (!source) throw Error('missing source')\n const asset = parseFigureAsset(source)\n this.cost = figureCostOf(asset)\n this.asset = asset\n } catch {\n this.error = i18next.t('figure.text.detail-invalid-source')\n }\n }\n connectedCallback() {\n super.connectedCallback()\n window.addEventListener('resize', this.place)\n window.addEventListener('popstate', this.place)\n this.unsubscribe = store.subscribe(() => {\n const state = store.getState() as { layout?: { viewparts?: Record<string, { show?: boolean }> }; route?: { page?: string } }\n const parts = state.layout?.viewparts\n if (parts?.[VIEWPART]?.show && (parts?.[ASSISTANT_DOCK_VIEWPART]?.show || leftOpeningPage(openedOn, state.route?.page))) hideDetail()\n else requestAnimationFrame(this.place)\n })\n this.place()\n window.addEventListener('keydown', this.escape)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n window.removeEventListener('resize', this.place)\n window.removeEventListener('popstate', this.place)\n window.removeEventListener('keydown', this.escape)\n this.unsubscribe?.()\n this.boundaryObserver?.disconnect()\n this.boundary = undefined\n }\n private place = () => {\n const bar = visibleBar(document)\n if (bar !== this.boundary) {\n this.boundaryObserver?.disconnect()\n this.boundary = bar\n if (bar) {\n this.boundaryObserver = new ResizeObserver(this.place)\n this.boundaryObserver.observe(bar)\n }\n }\n this.style.setProperty('--figure-detail-top', `${bar?.getBoundingClientRect().bottom ?? 44}px`)\n }\n private escape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') hideDetail()\n }\n render() {\n const saved = this.record?.saved\n const released = this.record?.released\n const cost = this.cost\n const capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined\n const savedLabel = saved\n ? `${i18next.t('figure.label.detail-saved')} · ${i18next.t(saved.state === 'released' ? 'figure.label.figure-released' : 'figure.label.draft')}`\n : ''\n return html`<header>\n <div title-block><h2 title=${saved?.name || ''}>${saved?.name || i18next.t('figure.title.detail')}</h2>\n ${saved ? html`<code title=${fullName(saved)}>${fullName(saved)}</code>` : nothing}</div>\n <button icon edit aria-label=${i18next.t('figure.button.detail-edit')} title=${i18next.t('figure.button.detail-edit')} ?disabled=${!saved}\n @click=${() => {\n // The modeller shows the figure itself; a preview of it beside the editor only covers the work.\n hideDetail()\n navigate(modellerPathOf(saved))\n }}><md-icon>edit</md-icon></button>\n <button icon aria-label=${i18next.t('figure.button.close')} title=${i18next.t('figure.button.close')} @click=${() => hideDetail()}><md-icon>close</md-icon></button>\n </header>\n <div body>\n ${this.loading ? html`<p status role=\"status\">${i18next.t('figure.text.detail-loading')}</p>` : nothing}\n ${this.error ? html`<div status role=\"alert\"><p>${this.error}</p><button act @click=${() => this.show(this.figureId)}><md-icon>refresh</md-icon>${i18next.t('figure.button.detail-retry')}</button></div>` : nothing}\n ${saved ? html`<section meta>\n ${released\n ? html`<div source role=\"group\" aria-label=${i18next.t('figure.label.detail-source')}>\n <button aria-pressed=${String(this.sourceKind === 'released')} @click=${() => this.choose('released')}>${i18next.t('figure.label.detail-released', { version: released.version })}</button>\n <button aria-pressed=${String(this.sourceKind === 'saved')} @click=${() => this.choose('saved')}>${savedLabel}</button>\n </div>`\n : html`<div source><span title=${i18next.t('figure.text.detail-unreleased')}>${savedLabel}</span></div>`}\n ${this.asset ? html`<dl stats>\n <div><dt>${i18next.t('figure.label.placement-limit')}</dt><dd>${capacity\n ? i18next.t('figure.text.fits-n-instances', { n: capacity.instances.toLocaleString() })\n : i18next.t('figure.text.not-measured-yet')}</dd></div>\n <div><dt>${i18next.t('figure.label.triangles')}</dt><dd>${cost?.triangles.toLocaleString() ?? '—'}</dd></div>\n <div><dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd></div>\n <div><dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd></div>\n </dl>` : nothing}\n ${saved.description ? html`<p description>${saved.description}</p>` : nothing}\n </section>` : nothing}\n ${this.asset ? html`<figure-asset-viewer .asset=${this.asset}\n .keepShot=${saved?.id ? (dataUrl: string) => this.keepThumbnail(saved.id, dataUrl) : undefined}>\n ${saved ? html`<figure-facts-list slot=\"facts\" .asset=${this.asset} .row=${saved}></figure-facts-list>` : nothing}\n </figure-asset-viewer>` : nothing}\n </div>`\n }\n}\n\n/** The name a board stores: `{namespace}/{type}` (ADR-0114). */\nfunction fullName(figure: Figure): string {\n return figure.namespace ? `${figure.namespace}/${figure.type}` : figure.type\n}\n\nfunction visibleBar(root: Document | ShadowRoot): Element | undefined {\n for (const element of root.querySelectorAll('*')) {\n if (element.tagName === 'OX-CANVAS-BAR' && element.getBoundingClientRect().height > 0) return element\n if (element.shadowRoot) {\n const found = visibleBar(element.shadowRoot)\n if (found) return found\n }\n }\n}\n\nlet dock: FigureDetailDock | undefined\n/** The page the preview was opened on; the preview belongs to that page and closes when the shell leaves it. */\nlet openedOn: string | undefined\nfunction hideDetail() {\n store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } })\n}\nexport function openFigureDetail(figure: Figure) {\n if (!dock) {\n dock = document.createElement('figure-detail-dock') as FigureDetailDock\n }\n openedOn = (store.getState() as { route?: { page?: string } }).route?.page\n store.dispatch({ type: UPDATE_VIEWPART, name: ASSISTANT_DOCK_VIEWPART, override: { show: false } })\n appendViewpart({ name: VIEWPART, position: VIEWPART_POSITION.ASIDEBAR,\n viewpart: { show: true, persistent: true, level: VIEWPART_LEVEL.TOPMOST, template: html`${dock}` } })\n void dock.show(figure.id)\n}\n"]}
1
+ {"version":3,"file":"figure-detail-dock.js","sourceRoot":"","sources":["../../client/viewparts/figure-detail-dock.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\nimport { appendViewpart, UPDATE_VIEWPART, VIEWPART_POSITION, VIEWPART_LEVEL } from '@operato/layout'\nimport { navigate, store } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { ASSISTANT_DOCK_VIEWPART } from '@things-factory/ai-assistant/dist-client/components/assistant-dock.js'\nimport { figureCostOf, parseFigureAsset, type FigureAsset } from '@hatiolab/figure-model'\nimport { capacityFrom } from '../modeller/scene-budget.js'\nimport { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'\nimport { fetchFigure, fetchReleasedFigure, updateFigure } from '../graphql/index.js'\nimport { modellerPathOf } from '../modeller/modeller-route.js'\nimport { leftOpeningPage } from './detail-route.js'\nimport type { Figure } from '../types.js'\nimport '@things-factory/figure-viewer/dist-client/figure-asset-viewer.js'\nimport '@things-factory/figure-viewer/dist-client/figure-facts-list.js'\n\nconst VIEWPART = 'figure-detail'\n/** Said on window when a figure's card picture was replaced from the preview: `{ detail: { id } }`. */\nexport const FIGURE_THUMBNAIL_CHANGED = 'figure-thumbnail-changed'\nexport interface FigureDetailRecord {\n saved: Figure\n released?: { version: number; source: string }\n}\n\n/** Catalogue details stay while the catalogue is shown; choosing a size or a pose has no write path. */\n@customElement('figure-detail-dock')\nexport class FigureDetailDock extends localize(i18next)(LitElement) {\n /*\n The modeller's side panel look (SidePanelStyles). What the preview is opened for sits above the model -- name, type,\n which model is shown, how many fit -- as plain text; the controls are borderless icons and quiet text, so the model\n and its numbers are what the eye meets.\n */\n static styles = [ScrollbarStyles, SidePanelStyles, css`\n :host { display: flex; flex-direction: column; width: 400px; max-width: 100vw; height: 100%; min-height: 0;\n background: var(--md-sys-color-surface); border-left: 1px solid var(--md-sys-color-outline-variant); }\n header { display: flex; align-items: flex-start; gap: 2px; padding: 12px 8px 4px 16px; }\n [title-block] { flex: 1; min-width: 0; display: grid; gap: 2px; }\n h2 { margin: 0; font-size: var(--ui-t-title, 16px); font-weight: 600; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n code { font-family: var(--mono-font, monospace); color: var(--md-sys-color-on-surface-variant); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n button { font: inherit; color: inherit; cursor: pointer; }\n button:focus-visible { outline: 2px solid var(--md-sys-color-primary); outline-offset: 1px; }\n button[icon] { display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: none; border-radius: 6px;\n background: transparent; color: var(--md-sys-color-on-surface-variant); }\n button[icon]:hover:not(:disabled) { background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); }\n button[icon]:disabled { opacity: 0.38; cursor: default; }\n button[icon] md-icon { --md-icon-size: 18px; }\n /*\n The figure stays in view under what it is (which model, how many fit) and its tabs; only the tab's body scrolls\n (user 2026-10-15: a clip played at the bottom of a scrolled dock moved a figure no longer seen).\n */\n [body] { flex: 1; display: flex; flex-direction: column; min-height: 0; }\n [body] > * { flex: none; }\n [body] > figure-asset-viewer { flex: 1; }\n [meta] { display: grid; gap: 8px; padding: 0 16px 12px; }\n /* Which model is shown: words that change colour, not boxes. */\n [source] { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; color: var(--md-sys-color-on-surface-variant); }\n [source] button { padding: 0; border: none; background: none; color: var(--md-sys-color-on-surface-variant); }\n [source] button:hover { color: var(--md-sys-color-on-surface); }\n [source] button[aria-pressed='true'] { color: var(--md-sys-color-primary); font-weight: 600; }\n dl { margin: 0; }\n dt { color: var(--md-sys-color-on-surface-variant); }\n dd { margin: 0; color: var(--md-sys-color-on-surface); }\n dl[stats] { display: flex; flex-wrap: wrap; gap: 4px 16px; }\n dl[stats] > div { display: flex; align-items: baseline; gap: 4px; }\n dl[stats] dd { font-weight: 600; font-variant-numeric: tabular-nums; }\n p { margin: 0; line-height: 1.5; }\n [description] { padding: 10px 16px 0; color: var(--md-sys-color-on-surface-variant); }\n [status] { display: grid; gap: 8px; justify-items: start; padding: 12px 16px; }\n @media (max-width: 1439.98px) {\n :host { position: fixed; inset: var(--figure-detail-top, 44px) 0 0 auto; height: calc(100dvh - var(--figure-detail-top, 44px)); z-index: 11; box-shadow: -4px 0 16px rgb(0 0 0 / 20%); }\n }\n `]\n @property({ attribute: false }) load: (id: string) => Promise<FigureDetailRecord> = async id => {\n const [saved, released] = await Promise.all([fetchFigure(id), fetchReleasedFigure(id)])\n if (!saved?.source) throw Error(i18next.t('figure.text.detail-no-source'))\n return { saved, released }\n }\n /**\n * Keeps the picture taken in the preview as the figure's card picture. A patch of the picture alone does not make a\n * released figure a draft (`updateFigure`). The catalogue is told, and reads its cards again.\n */\n @property({ attribute: false }) keepThumbnail: (id: string, dataUrl: string) => Promise<unknown> = async (id, dataUrl) => {\n await updateFigure(id, { thumbnail: dataUrl })\n window.dispatchEvent(new CustomEvent(FIGURE_THUMBNAIL_CHANGED, { detail: { id } }))\n }\n @state() private figureId = ''\n @state() private record?: FigureDetailRecord\n @state() private asset?: FigureAsset\n @state() private sourceKind: 'saved' | 'released' = 'saved'\n @state() private loading = false\n @state() private error = ''\n @state() private cost?: ReturnType<typeof figureCostOf>\n private ticket = 0\n private unsubscribe?: () => void\n private boundary?: Element\n private boundaryObserver?: ResizeObserver\n\n async show(id: string) {\n const ticket = ++this.ticket\n this.figureId = id\n this.record = undefined\n this.asset = undefined\n this.cost = undefined\n this.loading = true\n this.error = ''\n try {\n const record = await this.load(id)\n if (ticket !== this.ticket) return\n this.record = record\n this.choose(record.released ? 'released' : 'saved')\n } catch {\n if (ticket === this.ticket) this.error = i18next.t('figure.text.detail-load-failed')\n } finally {\n if (ticket === this.ticket) this.loading = false\n }\n }\n private choose(kind: 'saved' | 'released') {\n this.sourceKind = kind\n this.asset = undefined\n this.cost = undefined\n this.error = ''\n try {\n const source = kind === 'released' ? this.record?.released?.source : this.record?.saved.source\n if (!source) throw Error('missing source')\n const asset = parseFigureAsset(source)\n this.cost = figureCostOf(asset)\n this.asset = asset\n } catch {\n this.error = i18next.t('figure.text.detail-invalid-source')\n }\n }\n connectedCallback() {\n super.connectedCallback()\n window.addEventListener('resize', this.place)\n window.addEventListener('popstate', this.place)\n this.unsubscribe = store.subscribe(() => {\n const state = store.getState() as { layout?: { viewparts?: Record<string, { show?: boolean }> }; route?: { page?: string } }\n const parts = state.layout?.viewparts\n if (parts?.[VIEWPART]?.show && (parts?.[ASSISTANT_DOCK_VIEWPART]?.show || leftOpeningPage(openedOn, state.route?.page))) hideDetail()\n else requestAnimationFrame(this.place)\n })\n this.place()\n window.addEventListener('keydown', this.escape)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n window.removeEventListener('resize', this.place)\n window.removeEventListener('popstate', this.place)\n window.removeEventListener('keydown', this.escape)\n this.unsubscribe?.()\n this.boundaryObserver?.disconnect()\n this.boundary = undefined\n }\n private place = () => {\n const bar = visibleBar(document)\n if (bar !== this.boundary) {\n this.boundaryObserver?.disconnect()\n this.boundary = bar\n if (bar) {\n this.boundaryObserver = new ResizeObserver(this.place)\n this.boundaryObserver.observe(bar)\n }\n }\n this.style.setProperty('--figure-detail-top', `${bar?.getBoundingClientRect().bottom ?? 44}px`)\n }\n private escape = (e: KeyboardEvent) => {\n if (e.key === 'Escape') hideDetail()\n }\n render() {\n const saved = this.record?.saved\n const released = this.record?.released\n const cost = this.cost\n const capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined\n const savedLabel = saved\n ? `${i18next.t('figure.label.detail-saved')} · ${i18next.t(saved.state === 'released' ? 'figure.label.figure-released' : 'figure.label.draft')}`\n : ''\n return html`<header>\n <div title-block><h2 title=${saved?.name || ''}>${saved?.name || i18next.t('figure.title.detail')}</h2>\n ${saved ? html`<code title=${fullName(saved)}>${fullName(saved)}</code>` : nothing}</div>\n <button icon edit aria-label=${i18next.t('figure.button.detail-edit')} title=${i18next.t('figure.button.detail-edit')} ?disabled=${!saved}\n @click=${() => {\n // The modeller shows the figure itself; a preview of it beside the editor only covers the work.\n hideDetail()\n navigate(modellerPathOf(saved))\n }}><md-icon>edit</md-icon></button>\n <button icon aria-label=${i18next.t('figure.button.close')} title=${i18next.t('figure.button.close')} @click=${() => hideDetail()}><md-icon>close</md-icon></button>\n </header>\n <div body>\n ${this.loading ? html`<p status role=\"status\">${i18next.t('figure.text.detail-loading')}</p>` : nothing}\n ${this.error ? html`<div status role=\"alert\"><p>${this.error}</p><button act @click=${() => this.show(this.figureId)}><md-icon>refresh</md-icon>${i18next.t('figure.button.detail-retry')}</button></div>` : nothing}\n ${saved ? html`<section meta>\n ${released\n ? html`<div source role=\"group\" aria-label=${i18next.t('figure.label.detail-source')}>\n <button aria-pressed=${String(this.sourceKind === 'released')} @click=${() => this.choose('released')}>${i18next.t('figure.label.detail-released', { version: released.version })}</button>\n <button aria-pressed=${String(this.sourceKind === 'saved')} @click=${() => this.choose('saved')}>${savedLabel}</button>\n </div>`\n : html`<div source><span title=${i18next.t('figure.text.detail-unreleased')}>${savedLabel}</span></div>`}\n ${this.asset ? html`<dl stats>\n <div><dt>${i18next.t('figure.label.placement-limit')}</dt><dd>${capacity\n ? i18next.t('figure.text.fits-n-instances', { n: capacity.instances.toLocaleString() })\n : i18next.t('figure.text.not-measured-yet')}</dd></div>\n <div><dt>${i18next.t('figure.label.triangles')}</dt><dd>${cost?.triangles.toLocaleString() ?? '—'}</dd></div>\n <div><dt>${i18next.t('figure.label.budget-groups')}</dt><dd>${cost?.groups ?? '—'}</dd></div>\n <div><dt>${i18next.t('figure.label.budget-parts')}</dt><dd>${this.asset.appearance.length}</dd></div>\n </dl>` : nothing}\n </section>` : nothing}\n ${this.asset ? html`<figure-asset-viewer .asset=${this.asset}\n .keepShot=${saved?.id ? (dataUrl: string) => this.keepThumbnail(saved.id, dataUrl) : undefined}>\n ${saved?.description ? html`<p slot=\"facts\" description>${saved.description}</p>` : nothing}\n ${saved ? html`<figure-facts-list slot=\"facts\" .asset=${this.asset} .row=${saved}></figure-facts-list>` : nothing}\n </figure-asset-viewer>` : nothing}\n </div>`\n }\n}\n\n/** The name a board stores: `{namespace}/{type}` (ADR-0114). */\nfunction fullName(figure: Figure): string {\n return figure.namespace ? `${figure.namespace}/${figure.type}` : figure.type\n}\n\nfunction visibleBar(root: Document | ShadowRoot): Element | undefined {\n for (const element of root.querySelectorAll('*')) {\n if (element.tagName === 'OX-CANVAS-BAR' && element.getBoundingClientRect().height > 0) return element\n if (element.shadowRoot) {\n const found = visibleBar(element.shadowRoot)\n if (found) return found\n }\n }\n}\n\nlet dock: FigureDetailDock | undefined\n/** The page the preview was opened on; the preview belongs to that page and closes when the shell leaves it. */\nlet openedOn: string | undefined\nfunction hideDetail() {\n store.dispatch({ type: UPDATE_VIEWPART, name: VIEWPART, override: { show: false } })\n}\nexport function openFigureDetail(figure: Figure) {\n if (!dock) {\n dock = document.createElement('figure-detail-dock') as FigureDetailDock\n }\n openedOn = (store.getState() as { route?: { page?: string } }).route?.page\n store.dispatch({ type: UPDATE_VIEWPART, name: ASSISTANT_DOCK_VIEWPART, override: { show: false } })\n appendViewpart({ name: VIEWPART, position: VIEWPART_POSITION.ASIDEBAR,\n viewpart: { show: true, persistent: true, level: VIEWPART_LEVEL.TOPMOST, template: html`${dock}` } })\n void dock.show(figure.id)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.163",
3
+ "version": "10.1.164",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "npm run build:server && npm run build:client",
20
20
  "test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts",
21
- "test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs",
21
+ "test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs",
22
22
  "copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
23
23
  "build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
24
24
  "build:server": "npx tsc --p ./server/tsconfig.json",
@@ -43,9 +43,9 @@
43
43
  "@operato/utils": "^10.0.0",
44
44
  "@things-factory/ai-assistant": "^10.1.161",
45
45
  "@things-factory/figure-service": "^10.1.163",
46
- "@things-factory/figure-viewer": "^10.1.163",
46
+ "@things-factory/figure-viewer": "^10.1.164",
47
47
  "@things-factory/setting-base": "^10.1.159",
48
48
  "three": "^0.185.1"
49
49
  },
50
- "gitHead": "fe2a9c39294d64cd50e52b283105390bb0fa6cee"
50
+ "gitHead": "812391f1ef292805b9f1d3049bcb1bfc0b371d38"
51
51
  }
@@ -49,11 +49,19 @@ export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {
49
49
  const out = mkdtempSync(join(realpathSync(tmpdir()), 'figure-ui-browser-' + page.replace(/\W+/g, '-') + '-'))
50
50
  made.push(out)
51
51
  const emitted = join(out, 'client')
52
+ /*
53
+ figure-viewer from this checkout too: the installed one is the main tree's dist-client, built whenever someone last
54
+ built it, and a check of a change to the viewer passed or failed on that build instead.
55
+ */
56
+ const viewer = join(out, 'figure-viewer')
52
57
 
53
58
  /* Types are the unit suite's and tsc's business; here only what runs. */
54
59
  execFileSync(join(ROOT, 'node_modules/.bin/tsc'), ['-p', join(PACKAGE, 'client/tsconfig.json'), '--outDir', emitted, '--noCheck', '--declaration', 'false', '--incremental', 'false'], {
55
60
  stdio: 'inherit'
56
61
  })
62
+ execFileSync(join(ROOT, 'node_modules/.bin/tsc'), ['-p', join(PACKAGE, '../figure-viewer/client/tsconfig.json'), '--outDir', viewer, '--noCheck', '--declaration', 'false', '--incremental', 'false'], {
63
+ stdio: 'inherit'
64
+ })
57
65
 
58
66
  const figureModel = process.env.FIGURE_MODEL ? resolve(process.env.FIGURE_MODEL, 'dist/index.js') : undefined
59
67
  if (figureModel && !existsSync(figureModel)) throw Error(`FIGURE_MODEL has no build: ${figureModel}`)
@@ -71,6 +79,7 @@ export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {
71
79
  name: 'figure-ui-client',
72
80
  resolveId(id) {
73
81
  if (id.startsWith('figure-ui-client/')) return join(emitted, id.slice('figure-ui-client/'.length))
82
+ if (id.startsWith('@things-factory/figure-viewer/dist-client/')) return join(viewer, id.slice('@things-factory/figure-viewer/dist-client/'.length))
74
83
  if (standIns[id]) return join(HERE, standIns[id])
75
84
  if (figureModel && id === '@hatiolab/figure-model') return figureModel
76
85
  if (thingsScene && id === '@hatiolab/things-scene') return thingsScene
@@ -0,0 +1,36 @@
1
+ /*
2
+ * The catalogue's detail dock with a figure whose motion is longer than the dock: twelve parts, each sliding on its own
3
+ * joint with its own clip -- and the robot dog, whose walk and run are clips of a motion group its data chooses. Fixtures
4
+ * are in memory; nothing is saved.
5
+ */
6
+ import 'figure-ui-client/viewparts/figure-detail-dock.js'
7
+ import { addFigureBox, applyFigureAuthoring, createFigureAsset, editFigurePart, proposeFigureOccupancy, robotDogFigure, serializeFigureAsset } from '@hatiolab/figure-model'
8
+
9
+ const PARTS = 12
10
+
11
+ let asset = createFigureAsset('Twelve slides')
12
+ for (let i = 0; i < PARTS; i++) {
13
+ const id = `slide-${i}`
14
+ asset = addFigureBox(asset, { id, size: { x: 40, y: 40, z: 40 }, position: { x: i * 60, y: 20, z: 0 } })
15
+ asset = applyFigureAuthoring(asset, { kind: 'set-joint', joint: {
16
+ id, type: 'prismatic', body0: { plane: 'mounting-plane' }, body1: { part: id, face: 'bottom' }, axis: 'Z', lowerLimit: 0, upperLimit: 100, state: { id: `${id}-z`, start: 0 }
17
+ } })
18
+ asset = editFigurePart(asset, { kind: 'set-driver', state: `${id}-z`, clip: `cycle-${i}`, duration: 2, keys: [{ seconds: 0, value: 0 }, { seconds: 1, value: 100 }, { seconds: 2, value: 0 }] })
19
+ }
20
+ asset = applyFigureAuthoring(asset, { kind: 'declare-occupancy', proposal: proposeFigureOccupancy(asset, { over: 'range' }) })
21
+ const source = serializeFigureAsset(asset)
22
+
23
+ const dock = document.createElement('figure-detail-dock')
24
+ const dog = serializeFigureAsset(robotDogFigure().asset)
25
+ dock.load = async id =>
26
+ id === 'dog'
27
+ ? { saved: { id, name: 'Robot dog', type: 'DOG_TEST', state: 'draft', category: 'test', description: 'Walk and run in one motion group.', source: dog } }
28
+ : { saved: { id, name: 'Twelve slides', type: 'TABS_TEST', state: 'draft', category: 'test', description: 'Twelve parts, each on its own slide.', source } }
29
+ dock.keepThumbnail = async () => {}
30
+ document.body.append(dock)
31
+
32
+ window.harness = {
33
+ dock,
34
+ show: id => dock.show(id),
35
+ clips: PARTS
36
+ }
@@ -0,0 +1,123 @@
1
+ /*
2
+ * The detail dock's figure stays in view: the canvas stands at the top, the facts, the size and the motion are tabs
3
+ * under it, and only the tab's body scrolls -- so a clip pressed at the bottom of a long motion list plays a figure that
4
+ * is still seen (user 2026-10-15: the motion sat so far down that the figure scrolled out of view).
5
+ *
6
+ * Run: node --test test/browser/detail-tabs.check.mjs
7
+ */
8
+ import { test, describe, before, after } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+
11
+ import { buildPage } from './build.mjs'
12
+ import { openPage } from './serve.mjs'
13
+
14
+ const sleep = ms => new Promise(done => setTimeout(done, ms))
15
+
16
+ let opened
17
+ before(async () => {
18
+ /* A short window, so the twelve clips are longer than the dock. */
19
+ opened = await openPage(await buildPage('detail-tabs-page.js'), { width: 1200, height: 700 })
20
+ await opened.page.evaluate(() => harness.show('tabs'))
21
+ await until(() => !!document.querySelector('figure-detail-dock')?.shadowRoot?.querySelector('figure-asset-viewer')?.shadowRoot?.querySelector('[stage] canvas'), 'the figure drawn')
22
+ })
23
+ after(async () => {
24
+ await opened?.close()
25
+ })
26
+
27
+ const page = () => opened.page
28
+ /* The viewer inside the dock, and what it shows. */
29
+ const inViewer = (fn, ...args) =>
30
+ page().evaluate(
31
+ (source, a) => (0, eval)(`(${source})`)(document.querySelector('figure-detail-dock').shadowRoot.querySelector('figure-asset-viewer'), ...a),
32
+ String(fn),
33
+ args
34
+ )
35
+ async function until(fn, what, ms = 15000) {
36
+ const deadline = Date.now() + ms
37
+ for (;;) {
38
+ if (await page().evaluate(`(${fn})()`).catch(() => false)) return
39
+ if (Date.now() > deadline) assert.fail(`waited for ${what}`)
40
+ await sleep(50)
41
+ }
42
+ }
43
+ const shownTab = () => inViewer(v => [...v.shadowRoot.querySelectorAll('button[role="tab"]')].filter(b => b.getAttribute('aria-selected') === 'true').map(b => b.dataset.tab))
44
+ async function pressTab(tab) {
45
+ const at = await inViewer((v, t) => {
46
+ const r = v.shadowRoot.querySelector(`button[data-tab="${t}"]`).getBoundingClientRect()
47
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 }
48
+ }, tab)
49
+ await page().mouse.click(at.x, at.y)
50
+ await until(`() => document.querySelector('figure-detail-dock').shadowRoot.querySelector('figure-asset-viewer').tab === '${tab}'`, `the ${tab} tab`)
51
+ await inViewer(v => v.updateComplete)
52
+ }
53
+ /* The canvas's box against the window: wholly inside it, or not. */
54
+ const canvasInView = () =>
55
+ inViewer(v => {
56
+ const r = v.shadowRoot.querySelector('[viewport]').getBoundingClientRect()
57
+ return r.height > 0 && r.top >= 0 && r.bottom <= innerHeight
58
+ })
59
+
60
+ describe('the detail dock: the figure at the top, the rest in tabs under it', () => {
61
+ test('it opens on the facts: the three tabs, the facts and the description shown, no slider', async () => {
62
+ assert.deepEqual(await inViewer(v => [...v.shadowRoot.querySelectorAll('button[role="tab"]')].map(b => b.dataset.tab)), ['info', 'size', 'motion'])
63
+ assert.deepEqual(await shownTab(), ['info'])
64
+ assert.equal(await inViewer(v => v.shadowRoot.querySelector('slot[name="facts"]')?.assignedElements().map(e => e.localName).join(' ')), 'p figure-facts-list')
65
+ assert.equal(await inViewer(v => v.shadowRoot.querySelectorAll('input[type="range"]').length), 0)
66
+ })
67
+
68
+ test('the size tab: the three sizes and nothing of the motion', async () => {
69
+ await pressTab('size')
70
+ assert.deepEqual(await inViewer(v => [...v.shadowRoot.querySelectorAll('input[type="range"]')].map(i => i.getAttribute('aria-label'))), ['X mm', 'Y mm', 'Z mm'])
71
+ assert.equal(await inViewer(v => v.shadowRoot.querySelectorAll('button[play]').length), 0)
72
+ assert.equal(await inViewer(v => !!v.shadowRoot.querySelector('slot[name="facts"]')), false)
73
+ })
74
+
75
+ test('the last clip of a long motion list, pressed with the mouse, plays with the figure still in view', async () => {
76
+ await pressTab('motion')
77
+ assert.equal(await inViewer(v => v.shadowRoot.querySelectorAll('button[play]').length), 12)
78
+ /* Longer than its room: the tab's body scrolls, nothing else does. */
79
+ assert.ok(await inViewer(v => { const b = v.shadowRoot.querySelector('[tab-body]'); return b.scrollHeight > b.clientHeight }), 'the motion is longer than the tab body')
80
+ await inViewer(v => [...v.shadowRoot.querySelectorAll('button[play]')].at(-1).scrollIntoView({ block: 'nearest' }))
81
+ const at = await inViewer(v => { const r = [...v.shadowRoot.querySelectorAll('button[play]')].at(-1).getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 } })
82
+ await page().mouse.click(at.x, at.y)
83
+ await until(() => document.querySelector('figure-detail-dock').shadowRoot.querySelector('figure-asset-viewer').shadowRoot.querySelectorAll('button[play]')[11]?.getAttribute('aria-pressed') === 'true', 'the clip playing')
84
+ assert.ok(await inViewer(v => v.shadowRoot.querySelector('[tab-body]').scrollTop > 0), 'the tab body scrolled to the clip')
85
+ assert.equal(await page().evaluate(() => document.querySelector('figure-detail-dock').shadowRoot.querySelector('[body]').scrollTop), 0)
86
+ assert.ok(await canvasInView(), 'the canvas wholly in the window')
87
+ })
88
+
89
+ test('a tab left and come back to keeps what was tried: the clip still playing', async () => {
90
+ await pressTab('info')
91
+ await pressTab('motion')
92
+ assert.equal(await inViewer(v => v.shadowRoot.querySelectorAll('button[play]')[11].getAttribute('aria-pressed')), 'true')
93
+ assert.ok(await canvasInView(), 'the canvas wholly in the window')
94
+ })
95
+
96
+ /*
97
+ * A clip of a motion group the figure's data chooses (2026-10-15): the robot dog's run, pressed, stood still on idle --
98
+ * the dock told only clip speeds, which move a clip in no group, and the data's default chose idle. It plays now.
99
+ */
100
+ test("a group clip the data chooses -- the robot dog's run -- moves the legs when pressed", async () => {
101
+ await page().evaluate(() => harness.show('dog'))
102
+ await until(() => document.querySelector('figure-detail-dock')?.shadowRoot?.querySelector('figure-asset-viewer')?.asset?.motionGroups?.length > 0 && !!document.querySelector('figure-detail-dock').shadowRoot.querySelector('figure-asset-viewer').shadowRoot.querySelector('[stage] canvas'), 'the dog drawn')
103
+ await pressTab('motion')
104
+ const thigh = () => inViewer(v => v.scene.root.components[0]._realObject.rig.stateOverrides()['fl-thigh'] ?? 0)
105
+ const still = await thigh()
106
+ await inViewer(v => v.shadowRoot.querySelector('button[play][aria-label="run"]').scrollIntoView({ block: 'nearest' }))
107
+ const at = await inViewer(v => { const r = v.shadowRoot.querySelector('button[play][aria-label="run"]').getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 } })
108
+ await page().mouse.click(at.x, at.y)
109
+ await until(() => document.querySelector('figure-detail-dock').shadowRoot.querySelector('figure-asset-viewer').shadowRoot.querySelector('button[play][aria-label="run"]').getAttribute('aria-pressed') === 'true', 'run pressed')
110
+ /* The leg swings: three readings a fifth of a second apart are not one value. */
111
+ const seen = new Set()
112
+ for (let i = 0; i < 3; i++) {
113
+ await sleep(200)
114
+ seen.add(Math.round((await thigh()) * 10))
115
+ }
116
+ assert.ok(seen.size > 1, `the front left thigh moved (was ${still}, read ${[...seen].map(v => v / 10).join(', ')})`)
117
+ assert.equal(await inViewer(v => v.scene.root.components[0].state.figureValues?.mode), 'run')
118
+ })
119
+
120
+ test('nothing went wrong on the page', () => {
121
+ assert.deepEqual(opened.errors, [])
122
+ })
123
+ })
@@ -9,7 +9,7 @@ import assert from 'node:assert/strict'
9
9
  import { readFileSync } from 'node:fs'
10
10
  import { fileURLToPath } from 'node:url'
11
11
 
12
- import { editFigurePart, quadrupedRobotFigure, robotDogFigure } from '@hatiolab/figure-model'
12
+ import { applyFigureProposal, editFigurePart, quadrupedRobotFigure, robotArmFigure, robotDogFigure, compileFigureMotion, sampleFigureMotion } from '@hatiolab/figure-model'
13
13
  import type { FigureAsset, FigureDataField } from '@hatiolab/figure-model'
14
14
 
15
15
  import { clipGroupsOf, freeGroupIdOf, groupEdit, undrivenOf } from '../client/modeller/clips.ts'
@@ -17,12 +17,14 @@ import {
17
17
  accessorOfRow,
18
18
  behaviorsOf,
19
19
  bindingDraftOf,
20
+ endlessJointsOf,
20
21
  fieldAt,
21
22
  fieldOfKind,
22
23
  fitsField,
23
24
  freeIdOf,
24
25
  motionWrittenByData,
25
26
  portDraftOf,
27
+ rotationStepsOf,
26
28
  sampleOf,
27
29
  schemaWithField,
28
30
  schemaWithoutField,
@@ -156,6 +158,44 @@ test('상태 표시 초안: 첫 색 토큰에 -- 커널이 받는다', () => {
156
158
  assert.equal(portDraftOf(DOG, 'state').id, 'state-2')
157
159
  })
158
160
 
161
+ test('「+ 회전」: 끝없이 도는 관절에 쌓이는 클립·그룹·연결을 한 번에 -- 최대 rpm 이 배속 3', () => {
162
+ const arm = editFigurePart(robotArmFigure(), {
163
+ kind: 'set-data-schema',
164
+ schema: [
165
+ { name: 'running', kind: 'boolean' },
166
+ { name: 'rpm', kind: 'number' },
167
+ { name: 'dir', kind: 'text', values: ['FWD', 'REV', 'HOLD'] }
168
+ ]
169
+ })
170
+ assert.deepEqual(endlessJointsOf(arm), [{ input: 'j6-tool-roll', label: 'j6-tool-roll', unit: 'deg' }])
171
+ const steps = rotationStepsOf(arm, { input: 'j6-tool-roll', label: '툴 회전', on: 'running', speed: { path: 'rpm', max: 120 }, direction: { path: 'dir', forward: 'FWD', reverse: 'REV' } })
172
+ const spun = applyFigureProposal(arm, steps)
173
+ /* 한 바퀴 1.5 초 = 3 × 60 / 120, 값이 쌓이는 wrap. */
174
+ const drivers = spun.drivers!.filter(d => d.state === 'j6-tool-roll')
175
+ assert.deepEqual(drivers.map(d => [d.clip, d.time.duration, d.keys.at(-1)!.value, d.loop, d.accumulates]), [
176
+ ['j6-tool-roll-spin', 1.5, 360, 'wrap', true],
177
+ ['j6-tool-roll-spin-reverse', 1.5, -360, 'wrap', true]
178
+ ])
179
+ assert.deepEqual(spun.motionGroups, [{ id: 'j6-tool-roll-spin', label: '툴 회전', clips: ['j6-tool-roll-spin', 'j6-tool-roll-spin-reverse'], defaultClip: 'j6-tool-roll-spin', transitionSeconds: 0 }])
180
+ /* 범위가 없던 속도 필드는 0..최대 rpm 으로. */
181
+ assert.deepEqual(spun.dataInterface!.schema.find(f => f.name === 'rpm'), { name: 'rpm', kind: 'number', min: 0, max: 120 })
182
+ /* 120 rpm 이 배속 3, 60 rpm 이 1.5; REV 는 역회전, 꺼짐은 멈춤, 목록 밖 HOLD 는 정회전. */
183
+ const at = (data: unknown) => JSON.parse(JSON.stringify(sampleOf(spun, data).result.motion))
184
+ assert.deepEqual(at({ running: true, rpm: 120, dir: 'FWD' }), { 'j6-tool-roll-spin': { clip: 'j6-tool-roll-spin', speed: 3, paused: false } })
185
+ assert.deepEqual(at({ running: false, rpm: 60, dir: 'REV' }), { 'j6-tool-roll-spin': { clip: 'j6-tool-roll-spin-reverse', speed: 1.5, paused: true } })
186
+ assert.equal(at({ dir: 'HOLD' })['j6-tool-roll-spin'].clip, 'j6-tool-roll-spin')
187
+ /* 한 바퀴를 넘어도 이어서 돈다: 1.5 초 뒤 다시 0, 1.875 초는 90°. */
188
+ const plan = compileFigureMotion(spun)
189
+ const angle = (t: number, clip: string) => sampleFigureMotion(plan, { 'j6-tool-roll-spin': clip }, { [clip]: t })['j6-tool-roll']!
190
+ assert.ok(Math.abs(angle(1.875, 'j6-tool-roll-spin') - 90) < 1e-6)
191
+ assert.ok(Math.abs(angle(1.875, 'j6-tool-roll-spin-reverse') + 90) < 1e-6)
192
+ /* 필드를 고르지 않으면 클립과 그룹만, 1 초에 한 바퀴. */
193
+ const bare = applyFigureProposal(arm, rotationStepsOf(arm, { input: 'j6-tool-roll', label: '회전' }))
194
+ assert.deepEqual([bare.drivers!.find(d => d.state === 'j6-tool-roll')!.time.duration, bare.dataInterface!.bindings.length], [1, 0])
195
+ /* 끝이 있는 관절은 카드가 받지 않는다. */
196
+ assert.throws(() => rotationStepsOf(arm, { input: 'j2-shoulder', label: '회전' }))
197
+ })
198
+
159
199
  test('데이터 · 동작 거절은 커널 문장이 아니라 reason 과 값으로 말한다', () => {
160
200
  assert.match(
161
201
  refusalOf(() =>
@@ -194,6 +234,8 @@ test('데이터 탭 · 클립 패널 · 미리보기의 배선과 문구', () =>
194
234
  assert.match(editor, /<figure-data-panel/)
195
235
  assert.match(editor, /\.sample=\$\{this\.dataSample\}/)
196
236
  assert.match(editor, /this\.dataSample = e\.detail\.data/)
237
+ /* 「+ 회전」은 여러 명령을 되돌리기 한 단계로: 하나가 거절되면 아무것도 바뀌지 않는다. */
238
+ assert.match(editor, /@part-edits=\$\{\(e: CustomEvent\) => this\.run\(\(\) => this\.session!\.applyRecipe\(e\.detail\.edits\)\)\}/)
197
239
  const preview = read('../client/modeller/figure-preview.ts')
198
240
  /* 샘플은 보드의 데이터 소스처럼 Figure 의 data 로 간다 -- 저장 모델에는 쓰지 않는다. */
199
241
  assert.match(preview, /component\.set\?\.\('data', /)
@@ -1116,6 +1116,19 @@
1116
1116
  "figure.label.data-item-slot": "default place",
1117
1117
  "figure.text.data-no-behavior": "Nothing of this figure can be moved by this value.",
1118
1118
  "figure.button.add-data-binding": "add a binding",
1119
+ "figure.label.data-rotation": "+ rotation",
1120
+ "figure.text.data-rotation-explained": "Turns a joint that turns without end (a roller, a fan, a turntable) by the data. It makes a one-turn clip, a motion group and the bindings in one step; each can be edited afterwards. The most rpm plays at speed 3.",
1121
+ "figure.label.data-rotation-of": "{name} rotation",
1122
+ "figure.label.data-rotation-joint": "joint",
1123
+ "figure.label.data-rotation-on": "on",
1124
+ "figure.label.data-rotation-speed": "speed (rpm)",
1125
+ "figure.label.data-rotation-max": "max rpm",
1126
+ "figure.label.data-rotation-direction": "direction",
1127
+ "figure.label.data-rotation-forward": "forward value",
1128
+ "figure.label.data-rotation-reverse": "reverse value",
1129
+ "figure.text.data-rotation-needs-max": "Write the most rpm the speed field gives.",
1130
+ "figure.button.data-add-rotation": "make the rotation",
1131
+ "figure.label.data-none": "none",
1119
1132
  "figure.button.remove-data-binding": "remove the binding",
1120
1133
  "figure.label.data-ports": "status display",
1121
1134
  "figure.text.data-ports-explained": "Where a status code shows. The board's status template picks the colour; here you choose the colour tokens that take it and the parts that blink or glow.",
@@ -1116,6 +1116,19 @@
1116
1116
  "figure.label.data-item-slot": "lugar predeterminado",
1117
1117
  "figure.text.data-no-behavior": "Ningún comportamiento de esta figura puede moverse con este valor.",
1118
1118
  "figure.button.add-data-binding": "añadir vínculo",
1119
+ "figure.label.data-rotation": "+ rotación",
1120
+ "figure.text.data-rotation-explained": "Hace girar con los datos una articulación que gira sin fin (rodillo, ventilador, mesa giratoria). Crea de una vez un clip de una vuelta, un grupo de movimiento y los vínculos; cada uno se puede editar después. Las rpm máximas van a velocidad 3.",
1121
+ "figure.label.data-rotation-of": "rotación de {name}",
1122
+ "figure.label.data-rotation-joint": "articulación",
1123
+ "figure.label.data-rotation-on": "encendido",
1124
+ "figure.label.data-rotation-speed": "velocidad (rpm)",
1125
+ "figure.label.data-rotation-max": "rpm máx.",
1126
+ "figure.label.data-rotation-direction": "dirección",
1127
+ "figure.label.data-rotation-forward": "valor de avance",
1128
+ "figure.label.data-rotation-reverse": "valor de retroceso",
1129
+ "figure.text.data-rotation-needs-max": "Escriba las rpm máximas del campo de velocidad.",
1130
+ "figure.button.data-add-rotation": "crear la rotación",
1131
+ "figure.label.data-none": "ninguno",
1119
1132
  "figure.button.remove-data-binding": "quitar el vínculo",
1120
1133
  "figure.label.data-ports": "indicación de estado",
1121
1134
  "figure.text.data-ports-explained": "Dónde se muestra un código de estado. La plantilla de estado del tablero elige el color; aquí elige los tokens de color que lo reciben y las piezas que parpadean o brillan.",
@@ -1116,6 +1116,19 @@
1116
1116
  "figure.label.data-item-slot": "既定の位置",
1117
1117
  "figure.text.data-no-behavior": "この値で動かせる動作がありません。",
1118
1118
  "figure.button.add-data-binding": "連結を追加",
1119
+ "figure.label.data-rotation": "+ 回転",
1120
+ "figure.text.data-rotation-explained": "終わりなく回る関節(ローラー・ファン・ターンテーブル)をデータで回します。1回転のクリップと動作グループ、連結を一度に作り、作ったものは個別に直せます。最大rpmで倍速3になります。",
1121
+ "figure.label.data-rotation-of": "{name} 回転",
1122
+ "figure.label.data-rotation-joint": "関節",
1123
+ "figure.label.data-rotation-on": "オン",
1124
+ "figure.label.data-rotation-speed": "速度 (rpm)",
1125
+ "figure.label.data-rotation-max": "最大rpm",
1126
+ "figure.label.data-rotation-direction": "方向",
1127
+ "figure.label.data-rotation-forward": "正転の値",
1128
+ "figure.label.data-rotation-reverse": "逆転の値",
1129
+ "figure.text.data-rotation-needs-max": "速度フィールドの最大rpmを書きます。",
1130
+ "figure.button.data-add-rotation": "回転を作る",
1131
+ "figure.label.data-none": "なし",
1119
1132
  "figure.button.remove-data-binding": "連結を削除",
1120
1133
  "figure.label.data-ports": "状態表示",
1121
1134
  "figure.text.data-ports-explained": "状態コードを表示する場所です。色はボードの状態テンプレートが決め、ここではその色を受ける色トークンと点滅・発光する部品を選びます。",
@@ -1116,6 +1116,19 @@
1116
1116
  "figure.label.data-item-slot": "기본 자리",
1117
1117
  "figure.text.data-no-behavior": "이 값으로 움직일 수 있는 작동이 없습니다.",
1118
1118
  "figure.button.add-data-binding": "연결 추가",
1119
+ "figure.label.data-rotation": "+ 회전",
1120
+ "figure.text.data-rotation-explained": "끝없이 도는 관절(롤러·팬·턴테이블)을 데이터로 돌립니다. 한 바퀴 클립과 동작 그룹, 연결을 한 번에 만들고, 만든 것은 하나씩 고칠 수 있습니다. 최대 rpm에서 배속이 3이 됩니다.",
1121
+ "figure.label.data-rotation-of": "{name} 회전",
1122
+ "figure.label.data-rotation-joint": "관절",
1123
+ "figure.label.data-rotation-on": "켜짐",
1124
+ "figure.label.data-rotation-speed": "속도 (rpm)",
1125
+ "figure.label.data-rotation-max": "최대 rpm",
1126
+ "figure.label.data-rotation-direction": "방향",
1127
+ "figure.label.data-rotation-forward": "정회전 값",
1128
+ "figure.label.data-rotation-reverse": "역회전 값",
1129
+ "figure.text.data-rotation-needs-max": "속도 필드의 최대 rpm을 적습니다.",
1130
+ "figure.button.data-add-rotation": "회전 만들기",
1131
+ "figure.label.data-none": "없음",
1119
1132
  "figure.button.remove-data-binding": "연결 삭제",
1120
1133
  "figure.label.data-ports": "상태 표시",
1121
1134
  "figure.text.data-ports-explained": "상태 코드를 보여 줄 자리입니다. 색은 보드의 상태 템플릿이 정하고, 여기서는 그 색을 받을 색 토큰과 점멸·발광을 받을 부품을 고릅니다.",
@@ -1116,6 +1116,19 @@
1116
1116
  "figure.label.data-item-slot": "tempat lalai",
1117
1117
  "figure.text.data-no-behavior": "Tiada perkara figure ini boleh digerakkan oleh nilai ini.",
1118
1118
  "figure.button.add-data-binding": "tambah ikatan",
1119
+ "figure.label.data-rotation": "+ putaran",
1120
+ "figure.text.data-rotation-explained": "Memutar sendi yang berputar tanpa had (penggelek, kipas, meja putar) mengikut data. Ia membuat klip satu putaran, kumpulan gerakan dan ikatan sekali gus; setiap satu boleh diubah kemudian. Rpm maksimum dimainkan pada kelajuan 3.",
1121
+ "figure.label.data-rotation-of": "putaran {name}",
1122
+ "figure.label.data-rotation-joint": "sendi",
1123
+ "figure.label.data-rotation-on": "hidup",
1124
+ "figure.label.data-rotation-speed": "kelajuan (rpm)",
1125
+ "figure.label.data-rotation-max": "rpm maks",
1126
+ "figure.label.data-rotation-direction": "arah",
1127
+ "figure.label.data-rotation-forward": "nilai ke depan",
1128
+ "figure.label.data-rotation-reverse": "nilai songsang",
1129
+ "figure.text.data-rotation-needs-max": "Tulis rpm maksimum medan kelajuan.",
1130
+ "figure.button.data-add-rotation": "buat putaran",
1131
+ "figure.label.data-none": "tiada",
1119
1132
  "figure.button.remove-data-binding": "buang ikatan",
1120
1133
  "figure.label.data-ports": "paparan status",
1121
1134
  "figure.text.data-ports-explained": "Tempat kod status dipaparkan. Templat status papan memilih warna; di sini pilih token warna yang menerimanya dan bahagian yang berkelip atau bercahaya.",
@@ -1116,6 +1116,19 @@
1116
1116
  "figure.label.data-item-slot": "默认位置",
1117
1117
  "figure.text.data-no-behavior": "此值无法驱动任何动作。",
1118
1118
  "figure.button.add-data-binding": "添加绑定",
1119
+ "figure.label.data-rotation": "+ 旋转",
1120
+ "figure.text.data-rotation-explained": "用数据驱动无限旋转的关节(滚筒、风扇、转台)。一次生成一圈片段、动作组和绑定,之后可逐一修改。最大 rpm 对应倍速 3。",
1121
+ "figure.label.data-rotation-of": "{name} 旋转",
1122
+ "figure.label.data-rotation-joint": "关节",
1123
+ "figure.label.data-rotation-on": "开启",
1124
+ "figure.label.data-rotation-speed": "速度 (rpm)",
1125
+ "figure.label.data-rotation-max": "最大 rpm",
1126
+ "figure.label.data-rotation-direction": "方向",
1127
+ "figure.label.data-rotation-forward": "正转值",
1128
+ "figure.label.data-rotation-reverse": "反转值",
1129
+ "figure.text.data-rotation-needs-max": "请填写速度字段的最大 rpm。",
1130
+ "figure.button.data-add-rotation": "生成旋转",
1131
+ "figure.label.data-none": "无",
1119
1132
  "figure.button.remove-data-binding": "删除绑定",
1120
1133
  "figure.label.data-ports": "状态显示",
1121
1134
  "figure.text.data-ports-explained": "显示状态代码的位置。颜色由看板的状态模板决定,这里选择接收颜色的颜色令牌以及闪烁或发光的部件。",