@things-factory/figure-ui 10.1.151 → 10.1.153
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/ai-proposal.ts +14 -13
- package/client/modeller/command-preview-controller.ts +210 -0
- package/client/modeller/editor-styles.ts +307 -0
- package/client/modeller/editor.ts +65 -634
- package/client/modeller/fasten-geometry.ts +32 -0
- package/client/modeller/figure-first-save.ts +9 -1
- package/client/modeller/figure-links.ts +32 -14
- package/client/modeller/mesh-import.ts +0 -10
- package/client/modeller/modeller-keys.ts +60 -0
- package/client/modeller/pane-fold.ts +61 -0
- package/client/modeller/panel-memory.ts +53 -0
- package/client/modeller/refusal-teller.ts +93 -0
- package/client/modeller/refusals.ts +32 -2
- package/dist-client/modeller/ai-proposal.d.ts +6 -5
- package/dist-client/modeller/ai-proposal.js +9 -9
- package/dist-client/modeller/ai-proposal.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +1 -1
- package/dist-client/modeller/command-preview-controller.d.ts +75 -0
- package/dist-client/modeller/command-preview-controller.js +178 -0
- package/dist-client/modeller/command-preview-controller.js.map +1 -0
- package/dist-client/modeller/editor-styles.d.ts +2 -0
- package/dist-client/modeller/editor-styles.js +307 -0
- package/dist-client/modeller/editor-styles.js.map +1 -0
- package/dist-client/modeller/editor.d.ts +5 -56
- package/dist-client/modeller/editor.js +76 -656
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +13 -0
- package/dist-client/modeller/fasten-geometry.js +29 -0
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +1 -0
- package/dist-client/modeller/figure-first-save.js +8 -1
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -2
- package/dist-client/modeller/figure-links.js +35 -14
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/mesh-import.d.ts +0 -2
- package/dist-client/modeller/mesh-import.js +0 -11
- package/dist-client/modeller/mesh-import.js.map +1 -1
- package/dist-client/modeller/modeller-keys.d.ts +22 -0
- package/dist-client/modeller/modeller-keys.js +43 -0
- package/dist-client/modeller/modeller-keys.js.map +1 -0
- package/dist-client/modeller/pane-fold.d.ts +15 -0
- package/dist-client/modeller/pane-fold.js +60 -0
- package/dist-client/modeller/pane-fold.js.map +1 -0
- package/dist-client/modeller/panel-memory.d.ts +15 -0
- package/dist-client/modeller/panel-memory.js +54 -0
- package/dist-client/modeller/panel-memory.js.map +1 -0
- package/dist-client/modeller/product-session.d.ts +4 -28
- package/dist-client/modeller/refusal-teller.d.ts +36 -0
- package/dist-client/modeller/refusal-teller.js +81 -0
- package/dist-client/modeller/refusal-teller.js.map +1 -0
- package/dist-client/modeller/refusals.d.ts +12 -1
- package/dist-client/modeller/refusals.js +26 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/build.mjs +7 -2
- package/test/browser/editing.check.mjs +14 -0
- package/test/browser/fix-alignment.check.mjs +112 -0
- package/test/browser/links.check.mjs +69 -13
- package/test/browser/mesh-select.check.mjs +91 -0
- package/test/browser/modeller-page.js +36 -0
- package/test/browser/modeller-revision.check.mjs +18 -6
- package/test/browser/modeller.check.mjs +25 -0
- package/test/browser/part-ids.check.mjs +24 -6
- package/test/browser/parts-page.js +9 -1
- package/test/browser/parts.check.mjs +61 -2
- package/test/browser/places-mesh.check.mjs +25 -9
- package/test/browser/product-page.check.mjs +42 -2
- package/test/browser/row-preview.check.mjs +81 -0
- package/test/browser/screen.check.mjs +35 -1
- package/test/clip-ai-proposal.test.ts +4 -1
- package/test/clip-consent.test.ts +9 -2
- package/test/fields.test.ts +5 -9
- package/test/fix-alignment.test.ts +32 -0
- package/test/layout.test.ts +3 -1
- package/test/mesh-import.test.ts +1 -8
- package/test/parts-wiring.test.ts +1 -5
- package/test/refusals.test.ts +46 -10
- package/test/theme.test.ts +5 -15
- package/translations/en.json +8 -0
- package/translations/es.json +8 -0
- package/translations/ja.json +8 -0
- package/translations/ko.json +8 -0
- package/translations/ms.json +8 -0
- package/translations/zh.json +8 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/** The `figure-apply` event's detail. The page that takes it sets `applied`, and `error` when refused. */\nexport interface FigureApplyDetail {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n code?: string\n /** The clips a refused proposal would have stopped (CLIPS_DROPPED), by name. */\n clips?: string[]\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(code: string | undefined, error: string | undefined, say: Say, clips?: string[]): FigureProposalOutcome {\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : code === 'CLIPS_DROPPED'\n ? say('figure.text.proposal-would-drop-clips', { clips: (clips ?? []).join(', ') })\n : say('figure.text.proposal-refused', { reason: error ?? '' })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /* Created under the proposal's key: taken twice, it meets the first figure, never a second (ADR-0097 §5.6 item 5). */\n if (detail.key) session.createUnder(detail.key)\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n detail.code = (error as { code?: string })?.code\n detail.error = error instanceof Error ? error.message : String(error)\n const clips = (error as { clips?: Array<{ clip: string }> })?.clips\n if (clips) detail.clips = clips.map(c => c.clip)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }\n const create = card.create === true\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail.code, detail.error, say, detail.clips)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened.code, opened.error, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n }\n }\n const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)\n pending = mine\n })\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure():\n | { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void }\n | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/**\n * The `figure-apply` event's detail. The page that takes it sets `applied`, and when refused `error` and the refusal in\n * figure-model's shape (`code`, `details`, the `clips` it would stop).\n */\nexport interface FigureApplyDetail extends FigureRefusalRecord {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(refusal: FigureRefusalRecord & { error?: string }, say: Say): FigureProposalOutcome {\n const { code, error } = refusal\n const clips = clipLossOf(refusal)\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : clips\n ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })\n : say('figure.text.proposal-refused', { reason: error ?? '' })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /* Created under the proposal's key: taken twice, it meets the first figure, never a second (ADR-0097 §5.6 item 5). */\n if (detail.key) session.createUnder(detail.key)\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n Object.assign(detail, refusalRecordOf(error))\n detail.error = error instanceof Error ? error.message : String(error)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }\n const create = card.create === true\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail, say)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n }\n }\n const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)\n pending = mine\n })\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure():\n | { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void }\n | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type FigureAuthoringAction } from '@hatiolab/figure-model';
|
|
3
|
+
import { type HeldPreview } from './command-preview.js';
|
|
4
|
+
import type { FigureProductSession } from './product-session.js';
|
|
5
|
+
import type { FigureRenderPart } from './renderer.js';
|
|
6
|
+
import { type FigureRevision } from './revision.js';
|
|
7
|
+
/** The words a panel is given when there are none: one array, so it does not read as a change. */
|
|
8
|
+
export declare const NO_WORDS: string[];
|
|
9
|
+
/** A command a form is trying, with the face it names when it names one. */
|
|
10
|
+
export type PreviewCandidate = {
|
|
11
|
+
action: FigureAuthoringAction;
|
|
12
|
+
face?: {
|
|
13
|
+
part: string;
|
|
14
|
+
face: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** What the preview reads of the editor. */
|
|
18
|
+
export interface CommandPreviewHost {
|
|
19
|
+
session(): FigureProductSession | undefined;
|
|
20
|
+
selected(): string;
|
|
21
|
+
/** The figure on screen at its version (`revision.ts`). */
|
|
22
|
+
shown(): FigureRevision | undefined;
|
|
23
|
+
/** Whether the properties panel, where the forms are, is in sight: the edit middle, the properties tab. */
|
|
24
|
+
formsInSight(): boolean;
|
|
25
|
+
/** A refusal in the person's words. */
|
|
26
|
+
said(error: unknown): string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it
|
|
30
|
+
* names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone. And what taking
|
|
31
|
+
* the selected part off would do, worked out the same way. Both wait for the figure to rest; the editor taken down
|
|
32
|
+
* stops both.
|
|
33
|
+
*/
|
|
34
|
+
export declare class CommandPreview implements ReactiveController {
|
|
35
|
+
private readonly host;
|
|
36
|
+
private readonly of;
|
|
37
|
+
private preview?;
|
|
38
|
+
private drawn?;
|
|
39
|
+
private wordsSaid?;
|
|
40
|
+
private previewTimer?;
|
|
41
|
+
private detachTimer?;
|
|
42
|
+
private detachAsked?;
|
|
43
|
+
constructor(host: ReactiveControllerHost, of: CommandPreviewHost);
|
|
44
|
+
hostConnected(): void;
|
|
45
|
+
hostDisconnected(): void;
|
|
46
|
+
/** Drawn again only when it is another preview, as a state of the editor would be. */
|
|
47
|
+
private hold;
|
|
48
|
+
/**
|
|
49
|
+
* The fastening form's command, seen before it is done; null clears what was shown. What a command would do is worked
|
|
50
|
+
* out on a copy of the figure -- several compiles of it -- so it waits for the field to rest: a number typed digit by
|
|
51
|
+
* digit asks once, for the number it ends on. Clearing is at once, and the same question on the same version is not
|
|
52
|
+
* asked again.
|
|
53
|
+
*/
|
|
54
|
+
ask(candidate: PreviewCandidate | null): void;
|
|
55
|
+
private compute;
|
|
56
|
+
/** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
|
|
57
|
+
current(): (HeldPreview & {
|
|
58
|
+
part: string;
|
|
59
|
+
}) | undefined;
|
|
60
|
+
/** What the preview says, the same array while it says the same thing. */
|
|
61
|
+
words(): string[];
|
|
62
|
+
/** What the canvas draws of a preview, made once per preview: a new list every render would stand the board up each time. */
|
|
63
|
+
drawnOf(p: (HeldPreview & {
|
|
64
|
+
part: string;
|
|
65
|
+
}) | undefined): {
|
|
66
|
+
ghost: FigureRenderPart[];
|
|
67
|
+
highlight: FigureRenderPart | null;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
|
|
71
|
+
* edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
|
|
72
|
+
*/
|
|
73
|
+
detachWords(shown: FigureRevision): string[];
|
|
74
|
+
private computeDetachWords;
|
|
75
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { i18next } from '@operato/i18n';
|
|
2
|
+
import { figurePartFaceOf, figureRowOf } from '@hatiolab/figure-model';
|
|
3
|
+
import { faceHighlightOf, ghostPartsOf, previewWordsOf } from './command-preview.js';
|
|
4
|
+
import { partLabelOf } from './fields.js';
|
|
5
|
+
import { derivedHeld, derivedOf } from './revision.js';
|
|
6
|
+
/** The words a panel is given when there are none: one array, so it does not read as a change. */
|
|
7
|
+
export const NO_WORDS = [];
|
|
8
|
+
/** How long a command's candidate rests before its preview is worked out. */
|
|
9
|
+
const PREVIEW_SETTLE_MS = 150;
|
|
10
|
+
/**
|
|
11
|
+
* A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it
|
|
12
|
+
* names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone. And what taking
|
|
13
|
+
* the selected part off would do, worked out the same way. Both wait for the figure to rest; the editor taken down
|
|
14
|
+
* stops both.
|
|
15
|
+
*/
|
|
16
|
+
export class CommandPreview {
|
|
17
|
+
constructor(host, of) {
|
|
18
|
+
this.host = host;
|
|
19
|
+
this.of = of;
|
|
20
|
+
host.addController(this);
|
|
21
|
+
}
|
|
22
|
+
hostConnected() { }
|
|
23
|
+
hostDisconnected() {
|
|
24
|
+
clearTimeout(this.previewTimer);
|
|
25
|
+
clearTimeout(this.detachTimer);
|
|
26
|
+
}
|
|
27
|
+
/** Drawn again only when it is another preview, as a state of the editor would be. */
|
|
28
|
+
hold(preview) {
|
|
29
|
+
if (preview === this.preview)
|
|
30
|
+
return;
|
|
31
|
+
this.preview = preview;
|
|
32
|
+
this.host.requestUpdate();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The fastening form's command, seen before it is done; null clears what was shown. What a command would do is worked
|
|
36
|
+
* out on a copy of the figure -- several compiles of it -- so it waits for the field to rest: a number typed digit by
|
|
37
|
+
* digit asks once, for the number it ends on. Clearing is at once, and the same question on the same version is not
|
|
38
|
+
* asked again.
|
|
39
|
+
*/
|
|
40
|
+
ask(candidate) {
|
|
41
|
+
clearTimeout(this.previewTimer);
|
|
42
|
+
const session = this.of.session();
|
|
43
|
+
const selected = this.of.selected();
|
|
44
|
+
if (!candidate || !session || !selected) {
|
|
45
|
+
this.hold(undefined);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const view = session.read();
|
|
49
|
+
const key = JSON.stringify(candidate);
|
|
50
|
+
const same = this.preview &&
|
|
51
|
+
this.preview.key === key &&
|
|
52
|
+
this.preview.context === view.context &&
|
|
53
|
+
this.preview.part === selected &&
|
|
54
|
+
'revision' in this.preview &&
|
|
55
|
+
this.preview.revision === view.revision;
|
|
56
|
+
if (same)
|
|
57
|
+
return;
|
|
58
|
+
/*
|
|
59
|
+
What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed
|
|
60
|
+
on screen for the count chosen after it (:3300, 2026-10-11).
|
|
61
|
+
*/
|
|
62
|
+
if (this.preview && this.preview.key !== key)
|
|
63
|
+
this.hold(undefined);
|
|
64
|
+
this.previewTimer = setTimeout(() => this.compute(candidate), PREVIEW_SETTLE_MS);
|
|
65
|
+
}
|
|
66
|
+
compute(candidate) {
|
|
67
|
+
const session = this.of.session();
|
|
68
|
+
const selected = this.of.selected();
|
|
69
|
+
if (!session || !selected)
|
|
70
|
+
return;
|
|
71
|
+
const view = session.read();
|
|
72
|
+
const held = { context: view.context, revision: view.revision, key: JSON.stringify(candidate), part: selected };
|
|
73
|
+
try {
|
|
74
|
+
const seen = session.previewCommand(candidate.action);
|
|
75
|
+
let face;
|
|
76
|
+
if (candidate.face) {
|
|
77
|
+
try {
|
|
78
|
+
const shown = this.of.shown();
|
|
79
|
+
face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
face = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/* A row is read on the figure the command would make, by figure-model, as drawn. */
|
|
86
|
+
const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, selected, this.of.shown().inputs) : undefined;
|
|
87
|
+
this.hold({
|
|
88
|
+
...held,
|
|
89
|
+
context: seen.context,
|
|
90
|
+
revision: seen.revision,
|
|
91
|
+
geometry: seen.geometry,
|
|
92
|
+
moves: seen.moves,
|
|
93
|
+
added: seen.added,
|
|
94
|
+
...(row ? { row } : {}),
|
|
95
|
+
clipsWouldGo: seen.clipsWouldGo,
|
|
96
|
+
face,
|
|
97
|
+
faceUnknown: !!candidate.face && !face
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
/* Refused: nothing of an earlier result stays on screen, and the reason is said. */
|
|
102
|
+
this.hold({ ...held, refused: this.of.said(error) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
|
|
106
|
+
current() {
|
|
107
|
+
const p = this.preview;
|
|
108
|
+
const view = this.of.session()?.read();
|
|
109
|
+
/* Only where its form is: the properties panel stays in the page on another tab, and did not clear it (2026-09-27, :3391). */
|
|
110
|
+
if (!p || !view || p.context !== view.context || p.revision !== view.revision || p.part !== this.of.selected() || !this.of.formsInSight())
|
|
111
|
+
return undefined;
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
/** What the preview says, the same array while it says the same thing. */
|
|
115
|
+
words() {
|
|
116
|
+
const preview = this.current();
|
|
117
|
+
const selected = this.of.selected();
|
|
118
|
+
const language = i18next.language;
|
|
119
|
+
if (this.wordsSaid && this.wordsSaid.preview === preview && this.wordsSaid.part === selected && this.wordsSaid.language === language)
|
|
120
|
+
return this.wordsSaid.words;
|
|
121
|
+
const words = previewWordsOf(preview, selected, (key, params) => i18next.t(key, params));
|
|
122
|
+
this.wordsSaid = { preview, part: selected, language, words };
|
|
123
|
+
return words;
|
|
124
|
+
}
|
|
125
|
+
/** What the canvas draws of a preview, made once per preview: a new list every render would stand the board up each time. */
|
|
126
|
+
drawnOf(p) {
|
|
127
|
+
if (!p || p.refused)
|
|
128
|
+
return { ghost: [], highlight: null };
|
|
129
|
+
if (this.drawn?.of !== p)
|
|
130
|
+
this.drawn = { of: p, ghost: ghostPartsOf(p), highlight: faceHighlightOf(p.face) };
|
|
131
|
+
return this.drawn;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
|
|
135
|
+
* edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
|
|
136
|
+
*/
|
|
137
|
+
detachWords(shown) {
|
|
138
|
+
const part = this.of.selected();
|
|
139
|
+
if (!this.of.session() || !shown.asset.joints?.some(j => j.body1.part === part))
|
|
140
|
+
return NO_WORDS;
|
|
141
|
+
const key = `detach|${part}`;
|
|
142
|
+
const said = derivedHeld(shown, key);
|
|
143
|
+
if (said)
|
|
144
|
+
return said;
|
|
145
|
+
if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {
|
|
146
|
+
this.detachAsked = { shown, part };
|
|
147
|
+
clearTimeout(this.detachTimer);
|
|
148
|
+
this.detachTimer = setTimeout(() => {
|
|
149
|
+
if (this.of.shown() !== shown || this.of.selected() !== part)
|
|
150
|
+
return;
|
|
151
|
+
derivedOf(shown, key, () => this.computeDetachWords(shown, part));
|
|
152
|
+
this.host.requestUpdate();
|
|
153
|
+
}, PREVIEW_SETTLE_MS);
|
|
154
|
+
}
|
|
155
|
+
return NO_WORDS;
|
|
156
|
+
}
|
|
157
|
+
computeDetachWords(shown, part) {
|
|
158
|
+
const say = (k, params) => i18next.t(k, params);
|
|
159
|
+
let words;
|
|
160
|
+
try {
|
|
161
|
+
const seen = this.of.session().previewCommand({ kind: 'detach', part });
|
|
162
|
+
const own = seen.moves.some(m => m.body === part);
|
|
163
|
+
words = own
|
|
164
|
+
? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]
|
|
165
|
+
: /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ [];
|
|
166
|
+
const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part));
|
|
167
|
+
if (children.length)
|
|
168
|
+
words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }));
|
|
169
|
+
if (seen.clipsWouldGo.length)
|
|
170
|
+
words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
words = [say('figure.text.detach-refused', { reason: this.of.said(error) })];
|
|
174
|
+
}
|
|
175
|
+
return words;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=command-preview-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-preview-controller.js","sourceRoot":"","sources":["../../client/modeller/command-preview-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { figurePartFaceOf, figureRowOf, type FigureAuthoringAction, type FigureFaceArea } from '@hatiolab/figure-model'\nimport { faceHighlightOf, ghostPartsOf, previewWordsOf, type HeldPreview } from './command-preview.js'\nimport { partLabelOf } from './fields.js'\nimport type { FigureProductSession } from './product-session.js'\nimport type { FigureRenderPart } from './renderer.js'\nimport { derivedHeld, derivedOf, type FigureRevision } from './revision.js'\n\n/** The words a panel is given when there are none: one array, so it does not read as a change. */\nexport const NO_WORDS: string[] = []\n\n/** How long a command's candidate rests before its preview is worked out. */\nconst PREVIEW_SETTLE_MS = 150\n\n/** A command a form is trying, with the face it names when it names one. */\nexport type PreviewCandidate = { action: FigureAuthoringAction; face?: { part: string; face: string } }\n\n/** What the preview reads of the editor. */\nexport interface CommandPreviewHost {\n session(): FigureProductSession | undefined\n selected(): string\n /** The figure on screen at its version (`revision.ts`). */\n shown(): FigureRevision | undefined\n /** Whether the properties panel, where the forms are, is in sight: the edit middle, the properties tab. */\n formsInSight(): boolean\n /** A refusal in the person's words. */\n said(error: unknown): string\n}\n\n/**\n * A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it\n * names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone. And what taking\n * the selected part off would do, worked out the same way. Both wait for the figure to rest; the editor taken down\n * stops both.\n */\nexport class CommandPreview implements ReactiveController {\n private preview?: HeldPreview & { part: string }\n private drawn?: { of: HeldPreview; ghost: FigureRenderPart[]; highlight: FigureRenderPart | null }\n /*\n What a panel is handed is the same object while it says the same thing: a new array each render reads as a change to\n the panel, which draws itself again for nothing (sixteen times for one edit).\n */\n private wordsSaid?: { preview: unknown; part: string; language: string; words: string[] }\n private previewTimer?: ReturnType<typeof setTimeout>\n /*\n What taking the selected part off would do, worked out on a copy before the button is pressed (ADR-0097 §5.5): whether\n it stays where it stands now, what stays fastened to it, which clips would go. \"It stays where it is\" is said only\n when the copy shows it does.\n */\n private detachTimer?: ReturnType<typeof setTimeout>\n private detachAsked?: { shown: FigureRevision; part: string }\n\n constructor(\n private readonly host: ReactiveControllerHost,\n private readonly of: CommandPreviewHost\n ) {\n host.addController(this)\n }\n\n hostConnected() {}\n\n hostDisconnected() {\n clearTimeout(this.previewTimer)\n clearTimeout(this.detachTimer)\n }\n\n /** Drawn again only when it is another preview, as a state of the editor would be. */\n private hold(preview: (HeldPreview & { part: string }) | undefined) {\n if (preview === this.preview) return\n this.preview = preview\n this.host.requestUpdate()\n }\n\n /**\n * The fastening form's command, seen before it is done; null clears what was shown. What a command would do is worked\n * out on a copy of the figure -- several compiles of it -- so it waits for the field to rest: a number typed digit by\n * digit asks once, for the number it ends on. Clearing is at once, and the same question on the same version is not\n * asked again.\n */\n ask(candidate: PreviewCandidate | null) {\n clearTimeout(this.previewTimer)\n const session = this.of.session()\n const selected = this.of.selected()\n if (!candidate || !session || !selected) {\n this.hold(undefined)\n return\n }\n const view = session.read()\n const key = JSON.stringify(candidate)\n const same =\n this.preview &&\n this.preview.key === key &&\n this.preview.context === view.context &&\n this.preview.part === selected &&\n 'revision' in this.preview &&\n this.preview.revision === view.revision\n if (same) return\n /*\n What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed\n on screen for the count chosen after it (:3300, 2026-10-11).\n */\n if (this.preview && this.preview.key !== key) this.hold(undefined)\n this.previewTimer = setTimeout(() => this.compute(candidate), PREVIEW_SETTLE_MS)\n }\n\n private compute(candidate: PreviewCandidate) {\n const session = this.of.session()\n const selected = this.of.selected()\n if (!session || !selected) return\n const view = session.read()\n const held = { context: view.context!, revision: view.revision!, key: JSON.stringify(candidate), part: selected }\n try {\n const seen = session.previewCommand(candidate.action)\n let face: FigureFaceArea | undefined\n if (candidate.face) {\n try {\n const shown = this.of.shown()!\n face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs)\n } catch {\n face = undefined\n }\n }\n /* A row is read on the figure the command would make, by figure-model, as drawn. */\n const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, selected, this.of.shown()!.inputs) : undefined\n this.hold({\n ...held,\n context: seen.context,\n revision: seen.revision,\n geometry: seen.geometry,\n moves: seen.moves,\n added: seen.added,\n ...(row ? { row } : {}),\n clipsWouldGo: seen.clipsWouldGo,\n face,\n faceUnknown: !!candidate.face && !face\n })\n } catch (error) {\n /* Refused: nothing of an earlier result stays on screen, and the reason is said. */\n this.hold({ ...held, refused: this.of.said(error) })\n }\n }\n\n /** The preview, while it still speaks of what is on screen: this figure, this version, this part. */\n current(): (HeldPreview & { part: string }) | undefined {\n const p = this.preview\n const view = this.of.session()?.read()\n /* Only where its form is: the properties panel stays in the page on another tab, and did not clear it (2026-09-27, :3391). */\n if (!p || !view || p.context !== view.context || p.revision !== view.revision || p.part !== this.of.selected() || !this.of.formsInSight()) return undefined\n return p\n }\n\n /** What the preview says, the same array while it says the same thing. */\n words(): string[] {\n const preview = this.current()\n const selected = this.of.selected()\n const language = i18next.language\n if (this.wordsSaid && this.wordsSaid.preview === preview && this.wordsSaid.part === selected && this.wordsSaid.language === language) return this.wordsSaid.words\n const words = previewWordsOf(preview, selected, (key, params) => i18next.t(key, params))\n this.wordsSaid = { preview, part: selected, language, words }\n return words\n }\n\n /** What the canvas draws of a preview, made once per preview: a new list every render would stand the board up each time. */\n drawnOf(p: (HeldPreview & { part: string }) | undefined): { ghost: FigureRenderPart[]; highlight: FigureRenderPart | null } {\n if (!p || p.refused) return { ghost: [], highlight: null }\n if (this.drawn?.of !== p) this.drawn = { of: p, ghost: ghostPartsOf(p), highlight: faceHighlightOf(p.face) }\n return this.drawn\n }\n\n /**\n * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the\n * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.\n */\n detachWords(shown: FigureRevision): string[] {\n const part = this.of.selected()\n if (!this.of.session() || !shown.asset.joints?.some(j => j.body1.part === part)) return NO_WORDS\n const key = `detach|${part}`\n const said = derivedHeld<string[]>(shown, key)\n if (said) return said\n if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {\n this.detachAsked = { shown, part }\n clearTimeout(this.detachTimer)\n this.detachTimer = setTimeout(() => {\n if (this.of.shown() !== shown || this.of.selected() !== part) return\n derivedOf(shown, key, () => this.computeDetachWords(shown, part))\n this.host.requestUpdate()\n }, PREVIEW_SETTLE_MS)\n }\n return NO_WORDS\n }\n\n private computeDetachWords(shown: FigureRevision, part: string): string[] {\n const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)\n let words: string[]\n try {\n const seen = this.of.session()!.previewCommand({ kind: 'detach', part })\n const own = seen.moves.some(m => m.body === part)\n words = own\n ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]\n : /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []\n const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part))\n if (children.length) words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }))\n if (seen.clipsWouldGo.length) words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }))\n } catch (error) {\n words = [say('figure.text.detach-refused', { reason: this.of.said(error) })]\n }\n return words\n }\n}\n"]}
|