@1agh/maude 0.42.0 → 0.44.0
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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -330,6 +330,16 @@ export function isEditableTarget(t: EventTarget | null): boolean {
|
|
|
330
330
|
const tag = el.tagName;
|
|
331
331
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
332
332
|
if (el.isContentEditable) return true;
|
|
333
|
+
// Dogfood fix — `.isContentEditable` is a computed/inherited property whose
|
|
334
|
+
// handling of the `contenteditable="plaintext-only"` token (the value the
|
|
335
|
+
// element-text-edit system uses, canvas-shell.tsx) has had cross-engine/
|
|
336
|
+
// version inconsistencies. Check the raw attribute too so a tool-letter
|
|
337
|
+
// shortcut (R, T, N, …) can never fire while that editor has focus,
|
|
338
|
+
// regardless of whether isContentEditable correctly reflects it in a given
|
|
339
|
+
// runtime — this was the reported bug (typing "R" while editing in-canvas
|
|
340
|
+
// text switched to the Rectangle tool).
|
|
341
|
+
const raw = el.getAttribute?.('contenteditable');
|
|
342
|
+
if (raw === 'true' || raw === 'plaintext-only' || raw === '') return true;
|
|
333
343
|
return false;
|
|
334
344
|
}
|
|
335
345
|
|
package/apps/studio/inspect.ts
CHANGED
|
@@ -47,6 +47,19 @@ export interface SelectedElement {
|
|
|
47
47
|
* degrade to canvas-wide — never trust the positional id when stale.
|
|
48
48
|
*/
|
|
49
49
|
stale?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* feature-photo-editor (Task 14) — which photo-editing context this
|
|
52
|
+
* selection is (an artboard `<img>` vs. an annotation `ImageStroke`), and
|
|
53
|
+
* the resolved `assets/<sha8>.<ext>` source. Client-derived (dom-selection.ts
|
|
54
|
+
* re-reads the live DOM, including a `data-photo-asset` tag stamped after an
|
|
55
|
+
* edit bakes) — round-tripped here (not dropped like the OTHER client-only
|
|
56
|
+
* fields such as `authored`/`computed`/`attrs`) because, unlike those, there
|
|
57
|
+
* is no cheap way to re-derive it from a plain server-side restore: losing it
|
|
58
|
+
* silently drops the Inspector's Photo tab on every canvas switch / reconnect
|
|
59
|
+
* until a fresh click re-selects the element.
|
|
60
|
+
*/
|
|
61
|
+
photoKind?: 'artboard-img' | 'annotation-image';
|
|
62
|
+
photoAsset?: string;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/**
|
|
@@ -240,6 +253,19 @@ export function createInspect(
|
|
|
240
253
|
v,
|
|
241
254
|
canvas_mtime: mtimeFor(file),
|
|
242
255
|
...(id ? { id, canvas: deriveCanvasSlug(file) } : {}),
|
|
256
|
+
...(() => {
|
|
257
|
+
if (sel.photoKind !== 'artboard-img' && sel.photoKind !== 'annotation-image') return {};
|
|
258
|
+
// `photoAsset` traces back to client-derived DOM state (a
|
|
259
|
+
// `data-photo-asset` attribute inside the untrusted canvas iframe,
|
|
260
|
+
// DDR-054) — unlike the sibling `text` field it had no shape/length
|
|
261
|
+
// constraint before persisting to `_active.json` and broadcasting to
|
|
262
|
+
// every connected WS peer (security review finding). It's always a
|
|
263
|
+
// fixed-shape `assets/<sha8>.<ext>` reference, so an unshaped value
|
|
264
|
+
// is dropped outright rather than merely truncated.
|
|
265
|
+
const asset = String(sel.photoAsset || '');
|
|
266
|
+
if (!/^assets\/[0-9a-f]{8}\.[a-z0-9]+$/i.test(asset)) return {};
|
|
267
|
+
return { photoKind: sel.photoKind, photoAsset: asset };
|
|
268
|
+
})(),
|
|
243
269
|
};
|
|
244
270
|
}
|
|
245
271
|
|
|
@@ -366,6 +392,26 @@ const INSPECTOR_SCRIPT = `
|
|
|
366
392
|
'@keyframes dc-activity-glow { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.85; } }',
|
|
367
393
|
'@keyframes dc-activity-wave { from { transform: translateY(-100%); } to { transform: translateY(100%); } }',
|
|
368
394
|
'@media (prefers-reduced-motion: reduce) { html .dc-activity-rim { transition: none; } html .dc-activity-rim::after { animation: none; opacity: 0.55; } html .dc-activity-scan { display: none; } }',
|
|
395
|
+
/* feature-photo-editor — background-removal busy reveal. A data-photo-busy
|
|
396
|
+
attribute toggle on the real img/image element (set by canvas-lib.tsx's
|
|
397
|
+
PhotoPreviewBridge on a photo-busy postMessage from the shell), styled
|
|
398
|
+
here at the same single injection point as the .dc-activity-* agent-edit
|
|
399
|
+
chrome above — same "something is actively happening" rim + pulse
|
|
400
|
+
language. Deliberately NOT a floating tracked overlay: that's the exact
|
|
401
|
+
architecture DDR-161's addendum removed from the live-edit preview
|
|
402
|
+
(z-index/resize/hit-testing bugs) — an attribute on the element itself
|
|
403
|
+
moves for free with pan/zoom/resize, no separate rect sync needed.
|
|
404
|
+
Deliberately opacity-only (not a moving mask-position sweep, tried
|
|
405
|
+
first): the ML pass runs single-threaded WASM on the main thread
|
|
406
|
+
(confirmed live — env.wasm.numThreads falls back without
|
|
407
|
+
crossOriginIsolated), which blocks per-frame style/paint work for the
|
|
408
|
+
ENTIRE inference; a mask-position animation froze mid-sweep for that
|
|
409
|
+
whole window. opacity is one of the few properties browsers composite
|
|
410
|
+
off the main thread unconditionally, so the pulse keeps animating
|
|
411
|
+
exactly when the page is busiest — the one moment it needs to. */
|
|
412
|
+
'html [data-photo-busy] { outline: 2px solid var(--mdcc-activity, hsl(210 90% 58%)); outline-offset: 2px; animation: dc-photo-busy-pulse 1100ms ease-in-out infinite; will-change: opacity; }',
|
|
413
|
+
'@keyframes dc-photo-busy-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }',
|
|
414
|
+
'@media (prefers-reduced-motion: reduce) { html [data-photo-busy] { animation: none; opacity: 0.75; } }',
|
|
369
415
|
/* Phase 13.1 / DDR-077 — "holding last good render" toast, shown when an
|
|
370
416
|
agent edit produced a broken intermediate (build/render error) and the
|
|
371
417
|
canvas is held instead of flashing white. Amber = warn, distinct from the
|
|
@@ -482,7 +528,13 @@ const INSPECTOR_SCRIPT = `
|
|
|
482
528
|
return;
|
|
483
529
|
}
|
|
484
530
|
if (e.shiftKey) {
|
|
485
|
-
|
|
531
|
+
// ⌘⇧T (timeline) + ⌘⇧G (changes) were missing here, so with focus inside
|
|
532
|
+
// the canvas iframe they never reached the shell — opening the Timeline
|
|
533
|
+
// (and its Space/arrow transport, which needs the dock focused) silently
|
|
534
|
+
// stopped working after a canvas interaction moved focus into the iframe.
|
|
535
|
+
// ⌘⇧T is also the browser "reopen closed tab" chord, so the preventDefault
|
|
536
|
+
// below is doubly load-bearing.
|
|
537
|
+
var id = k === 'i' ? 'inspector' : k === 'm' ? 'comments' : k === 'e' ? 'export' : k === 'h' ? 'handoff' : k === 't' ? 'timeline' : k === 'g' ? 'changes' : null;
|
|
486
538
|
if (id) {
|
|
487
539
|
e.preventDefault();
|
|
488
540
|
try { window.parent.postMessage({ dgn: 'shell-shortcut', id: id }, '*'); } catch (err) {}
|
|
Binary file
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file media-commit-chain.ts — serialized accumulator chain for
|
|
3
|
+
* concurrent async media-intake commits.
|
|
4
|
+
* @scope apps/studio/media-commit-chain.ts
|
|
5
|
+
* @purpose Fixes the Phase-23 batch-drop data-loss bug: a Finder drop of
|
|
6
|
+
* N files fires N independent async completions (image decode /
|
|
7
|
+
* asset upload), each of which used to read a `strokesRef.current`
|
|
8
|
+
* mirror and call `commitStrokes(before, after)` on its own.
|
|
9
|
+
* `strokesRef` only catches up on the NEXT render, so two
|
|
10
|
+
* completions landing before a render commit both compute
|
|
11
|
+
* `before` from the same stale snapshot — the second commit's
|
|
12
|
+
* `setStrokesState` overwrites the first's, silently dropping it.
|
|
13
|
+
*
|
|
14
|
+
* This chain fixes it by threading the PREVIOUS link's resulting
|
|
15
|
+
* array forward as the next link's `before` while links are
|
|
16
|
+
* in flight (Promise-chained, mirroring `editApplyChainRef` /
|
|
17
|
+
* `structuralWrite` in client/app.jsx). The subtle part is what
|
|
18
|
+
* happens once the queue drains: a naive "go idle, trust a fresh
|
|
19
|
+
* `getSnapshot()` again" reset raced in practice — `getSnapshot()`
|
|
20
|
+
* (`strokesRef.current`) only catches up on React's NEXT render,
|
|
21
|
+
* which can take longer than the handful of microtask ticks this
|
|
22
|
+
* chain itself needs to settle a link, so a fast-idling chain
|
|
23
|
+
* could hand the next link a `getSnapshot()` that hasn't caught
|
|
24
|
+
* up yet — reproducing the same loss one level down (confirmed
|
|
25
|
+
* empirically against a real dev server: see the Task 5 write-up).
|
|
26
|
+
* Instead, every idle read is RECONCILED against `remembered`
|
|
27
|
+
* (the last array this chain itself produced): for every id the
|
|
28
|
+
* chain itself has ever touched, `remembered`'s value WINS
|
|
29
|
+
* outright, even if the fresh snapshot also contains that id —
|
|
30
|
+
* presence alone isn't enough, since fresh can hold a STALE
|
|
31
|
+
* value for an id the chain already updated (see `reconcile()`
|
|
32
|
+
* below and DDR-165). Only ids fresh has that the chain has
|
|
33
|
+
* never touched get folded in from it, which is what still lets
|
|
34
|
+
* an unrelated synchronous edit (pen/shape/drag, made while
|
|
35
|
+
* idle) through.
|
|
36
|
+
*
|
|
37
|
+
* Framework/DOM-free by design so the no-loss guarantee is
|
|
38
|
+
* testable without React or a real image decode/upload — see
|
|
39
|
+
* test/media-commit-chain.test.ts.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface MediaCommitResult<T> {
|
|
43
|
+
/** The chain's new accumulator value — what the NEXT queued link builds on. */
|
|
44
|
+
after: readonly T[];
|
|
45
|
+
/**
|
|
46
|
+
* Override the `before` passed to `onCommit` (defaults to this link's
|
|
47
|
+
* accumulator `before`). Lets a caller exclude an ephemeral, never-
|
|
48
|
+
* separately-committed entry (e.g. an optimistic blob: preview) from the
|
|
49
|
+
* undo record while still folding it into the accumulator correctly.
|
|
50
|
+
*/
|
|
51
|
+
commitBefore?: readonly T[];
|
|
52
|
+
label?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MediaCommitChain<T> {
|
|
56
|
+
/**
|
|
57
|
+
* Enqueue a synchronous mutation. `mutate` receives the current
|
|
58
|
+
* accumulated `before` (the previous link's `after` while a link is still
|
|
59
|
+
* in flight, or a reconciled `getSnapshot()` read once the queue has
|
|
60
|
+
* drained) and returns a result, or `null` to skip (no-op, `before` passes
|
|
61
|
+
* through unchanged). `onCommit` fires with `(before, after, label)` in
|
|
62
|
+
* strict queue order, once per non-null mutation.
|
|
63
|
+
*/
|
|
64
|
+
enqueue(
|
|
65
|
+
mutate: (before: readonly T[]) => MediaCommitResult<T> | null,
|
|
66
|
+
onCommit: (before: readonly T[], after: readonly T[], label?: string) => void
|
|
67
|
+
): Promise<readonly T[]>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createMediaCommitChain<T>(
|
|
71
|
+
getSnapshot: () => readonly T[],
|
|
72
|
+
keyOf: (item: T) => string
|
|
73
|
+
): MediaCommitChain<T> {
|
|
74
|
+
let pending: Promise<readonly T[]> | null = null;
|
|
75
|
+
// The last array this chain itself produced. Never cleared — only ever
|
|
76
|
+
// superseded by a fresh reconciled read once every id it carries shows up
|
|
77
|
+
// in `getSnapshot()` too (see reconcile()).
|
|
78
|
+
let remembered: readonly T[] | null = null;
|
|
79
|
+
|
|
80
|
+
function reconcile(): readonly T[] {
|
|
81
|
+
const fresh = getSnapshot();
|
|
82
|
+
if (!remembered) return fresh;
|
|
83
|
+
// Prefer `remembered`'s value for any id the CHAIN itself produced —
|
|
84
|
+
// not just fold in ids fresh is missing. `fresh` (getSnapshot) can
|
|
85
|
+
// contain a STALE value for an id the chain already swapped: e.g. an
|
|
86
|
+
// image stroke's optimistic (blob:) href is set via a separate, non-
|
|
87
|
+
// chain setState, and `fresh` catches up to THAT before it catches up
|
|
88
|
+
// to the chain's OWN later commit swapping it to the real href — same
|
|
89
|
+
// id, stale value. Checking "is the id present" (the previous version
|
|
90
|
+
// of this function) said "yes, already rendered" and used fresh's
|
|
91
|
+
// stale copy, silently reverting the swap — which then gets filtered
|
|
92
|
+
// out entirely as still-ephemeral at the persistence layer. `remembered`
|
|
93
|
+
// is always at least as fresh as anything the chain has itself
|
|
94
|
+
// touched, so it wins on overlap; anything fresh has that remembered
|
|
95
|
+
// doesn't (an external, non-chain edit made while idle) still comes
|
|
96
|
+
// through via the second spread.
|
|
97
|
+
const rememberedKeys = new Set(remembered.map(keyOf));
|
|
98
|
+
const notInRemembered = fresh.filter((item) => !rememberedKeys.has(keyOf(item)));
|
|
99
|
+
return [...remembered, ...notInRemembered];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const enqueue: MediaCommitChain<T>['enqueue'] = (mutate, onCommit) => {
|
|
103
|
+
const seed = pending ?? Promise.resolve(reconcile());
|
|
104
|
+
const link: Promise<readonly T[]> = seed.then((before) => {
|
|
105
|
+
const result = mutate(before);
|
|
106
|
+
if (!result) return before;
|
|
107
|
+
onCommit(result.commitBefore ?? before, result.after, result.label);
|
|
108
|
+
remembered = result.after;
|
|
109
|
+
return result.after;
|
|
110
|
+
});
|
|
111
|
+
pending = link;
|
|
112
|
+
void link.then(() => {
|
|
113
|
+
if (pending === link) pending = null;
|
|
114
|
+
});
|
|
115
|
+
return link;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return { enqueue };
|
|
119
|
+
}
|
package/apps/studio/paths.ts
CHANGED
|
@@ -48,6 +48,26 @@ export const CLIENT_DIR: string = join(DEV_SERVER_ROOT, 'client');
|
|
|
48
48
|
/** `<DEV_SERVER_ROOT>/dist/runtime/` — pre-built /_canvas-runtime/*.js bundles. */
|
|
49
49
|
export const RUNTIME_BUNDLES_DIR: string = join(DIST_DIR, 'runtime');
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* `<DEV_SERVER_ROOT>/stickers/` — bundled whiteboard sticker packs
|
|
53
|
+
* (feature-whiteboard-annotation-improvements, Phase 4). Ships with the
|
|
54
|
+
* `@1agh/maude` tarball (a subdir of `apps/studio`, already covered by the
|
|
55
|
+
* existing `files` entry — no separate packaging step). Resolved from
|
|
56
|
+
* DEV_SERVER_ROOT per DDR-045 — this is MAUDE's own bundled asset store, never
|
|
57
|
+
* the served project's.
|
|
58
|
+
*/
|
|
59
|
+
export const STICKERS_DIR: string = join(DEV_SERVER_ROOT, 'stickers');
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `<DEV_SERVER_ROOT>/media/` — bundled Maude-product media (the intro
|
|
63
|
+
* showreel, DDR-166 Phase 1 T2). Same shape as `STICKERS_DIR`: ships with the
|
|
64
|
+
* `@1agh/maude` tarball (a subdir of `apps/studio`, already covered by the
|
|
65
|
+
* existing `files` entry), resolved from DEV_SERVER_ROOT per DDR-045 — this is
|
|
66
|
+
* MAUDE's OWN asset store, never the served project's `.design/assets/`, so
|
|
67
|
+
* every user sees the same intro regardless of which project is open.
|
|
68
|
+
*/
|
|
69
|
+
export const MEDIA_DIR: string = join(DEV_SERVER_ROOT, 'media');
|
|
70
|
+
|
|
51
71
|
/**
|
|
52
72
|
* Absolute path to a bundled plugin's loadable tree (`commands/`, `agents/`,
|
|
53
73
|
* `skills/`, `hooks/`, `.claude-plugin/plugin.json`), or `null` when this layout
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file photo/filters.ts — pure photo-pipeline PLANNER (Stage B, Task 4)
|
|
3
|
+
* @scope apps/studio/photo/filters.ts
|
|
4
|
+
* @purpose Turn a `PhotoEdit` into an ordered, plain-data description of the
|
|
5
|
+
* compositing stack — `planPhotoPipeline(edit) → PhotoPipelineStep[]`.
|
|
6
|
+
* This is the deterministic, testable core: it decides WHAT filters/
|
|
7
|
+
* overlays/masks apply and in WHAT order, without ever touching a GPU.
|
|
8
|
+
*
|
|
9
|
+
* @invariant DEPENDENCY-FREE — like `schema.ts`, this file MUST NOT import
|
|
10
|
+
* `pixi.js`. The pixi realization (constructing real
|
|
11
|
+
* `PIXI.ColorMatrixFilter` / `NoiseFilter` / the duotone shader /
|
|
12
|
+
* `TilingSprite` / `Graphics` mask objects) lives in `pipeline.ts`,
|
|
13
|
+
* which is browser-only.
|
|
14
|
+
*
|
|
15
|
+
* WHY the split (deviation from the plan's single
|
|
16
|
+
* `buildFilterGraph(): PIXI.Filter[]`): pixi v8 eagerly compiles a
|
|
17
|
+
* `GlProgram` in every filter's constructor, which calls
|
|
18
|
+
* `document.createElement('canvas')` — so constructing a filter in a
|
|
19
|
+
* headless `bun test` throws `ReferenceError: document is not
|
|
20
|
+
* defined`. Returning plain-data STEP specs keeps the plan's stated
|
|
21
|
+
* Task-4 validation ("assert filter count/order, not pixel output")
|
|
22
|
+
* genuinely runnable, and mirrors how `draw/serialize.ts` produces a
|
|
23
|
+
* browser-free description of a render. `pipeline.ts` re-exports a
|
|
24
|
+
* `buildFilterGraph(edit)` that realizes these steps into pixi objects.
|
|
25
|
+
*
|
|
26
|
+
* @order Fixed, load-bearing pipeline order (filters are NOT commutative) —
|
|
27
|
+
* `PHOTO_PIPELINE_ORDER` from schema.ts:
|
|
28
|
+
* adjustments → duotone → grain → pattern → mask
|
|
29
|
+
* The adjustment SUB-order (within the single color-matrix step) is
|
|
30
|
+
* also fixed: brightness → exposure → contrast → saturation → hue →
|
|
31
|
+
* sepia → grayscale → invert (color-matrix ops compose, so order
|
|
32
|
+
* changes the result). Both orders are asserted by the unit tests.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type {
|
|
36
|
+
MaskPreset,
|
|
37
|
+
PatternBlend,
|
|
38
|
+
PatternType,
|
|
39
|
+
PhotoAdjustments,
|
|
40
|
+
PhotoEdit,
|
|
41
|
+
} from './schema.ts';
|
|
42
|
+
import { PHOTO_PIPELINE_ORDER } from './schema.ts';
|
|
43
|
+
|
|
44
|
+
// ── Step vocabulary (plain data — the realizer in pipeline.ts consumes these) ──
|
|
45
|
+
|
|
46
|
+
/** One color-matrix operation, with a NORMALIZED value (see schema.ts ranges).
|
|
47
|
+
* The realizer maps each op onto the corresponding pixi ColorMatrixFilter call. */
|
|
48
|
+
export type AdjustOpKind =
|
|
49
|
+
| 'brightness'
|
|
50
|
+
| 'exposure'
|
|
51
|
+
| 'contrast'
|
|
52
|
+
| 'saturation'
|
|
53
|
+
| 'hue'
|
|
54
|
+
| 'sepia'
|
|
55
|
+
| 'grayscale'
|
|
56
|
+
| 'invert';
|
|
57
|
+
|
|
58
|
+
export interface AdjustOp {
|
|
59
|
+
op: AdjustOpKind;
|
|
60
|
+
/** Normalized value straight from PhotoEdit.adjustments (−1…1, deg, or 0…1). */
|
|
61
|
+
value: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Adjustments compile to ONE color-matrix filter carrying an ordered op list. */
|
|
65
|
+
export interface AdjustmentsStep {
|
|
66
|
+
stage: 'adjustments';
|
|
67
|
+
kind: 'colorMatrix';
|
|
68
|
+
ops: AdjustOp[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DuotoneStep {
|
|
72
|
+
stage: 'duotone';
|
|
73
|
+
kind: 'duotone';
|
|
74
|
+
/** `#rrggbb` shadow color. */
|
|
75
|
+
colorA: string;
|
|
76
|
+
/** `#rrggbb` highlight color. */
|
|
77
|
+
colorB: string;
|
|
78
|
+
/** 0…1 crossfade between original and full duotone. */
|
|
79
|
+
intensity: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GrainStep {
|
|
83
|
+
stage: 'grain';
|
|
84
|
+
kind: 'noise';
|
|
85
|
+
amount: number;
|
|
86
|
+
size: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PatternStep {
|
|
90
|
+
stage: 'pattern';
|
|
91
|
+
kind: 'pattern';
|
|
92
|
+
type: PatternType;
|
|
93
|
+
scale: number;
|
|
94
|
+
opacity: number;
|
|
95
|
+
blend: PatternBlend;
|
|
96
|
+
/** `#rrggbb` ink color for the pattern tile. */
|
|
97
|
+
color: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MaskStep {
|
|
101
|
+
stage: 'mask';
|
|
102
|
+
kind: 'mask';
|
|
103
|
+
/** 'none' never reaches here — an unmasked edit omits the step entirely. */
|
|
104
|
+
preset: Exclude<MaskPreset, 'none'>;
|
|
105
|
+
strength: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type PhotoPipelineStep = AdjustmentsStep | DuotoneStep | GrainStep | PatternStep | MaskStep;
|
|
109
|
+
|
|
110
|
+
// ── Sensible visual defaults (used when a section is enabled but a field unset) ─
|
|
111
|
+
|
|
112
|
+
const DUOTONE_DEFAULT_A = '#111111';
|
|
113
|
+
const DUOTONE_DEFAULT_B = '#ffffff';
|
|
114
|
+
const DUOTONE_DEFAULT_INTENSITY = 1;
|
|
115
|
+
const GRAIN_DEFAULT_AMOUNT = 0.4;
|
|
116
|
+
const GRAIN_DEFAULT_SIZE = 1;
|
|
117
|
+
const PATTERN_DEFAULT_TYPE: PatternType = 'dots';
|
|
118
|
+
const PATTERN_DEFAULT_SCALE = 1;
|
|
119
|
+
const PATTERN_DEFAULT_OPACITY = 0.5;
|
|
120
|
+
const PATTERN_DEFAULT_BLEND: PatternBlend = 'normal';
|
|
121
|
+
const PATTERN_DEFAULT_COLOR = '#ffffff';
|
|
122
|
+
const MASK_DEFAULT_STRENGTH = 0.6;
|
|
123
|
+
|
|
124
|
+
const EPS = 1e-6;
|
|
125
|
+
const isSet = (v: number | undefined, origin = 0): boolean =>
|
|
126
|
+
v != null && Math.abs(v - origin) > EPS;
|
|
127
|
+
|
|
128
|
+
// Fixed adjustment sub-order. See @order above.
|
|
129
|
+
const ADJUST_FIELD_ORDER: readonly AdjustOpKind[] = [
|
|
130
|
+
'brightness',
|
|
131
|
+
'exposure',
|
|
132
|
+
'contrast',
|
|
133
|
+
'saturation',
|
|
134
|
+
'hue',
|
|
135
|
+
'sepia',
|
|
136
|
+
'grayscale',
|
|
137
|
+
'invert',
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function planAdjustments(a: PhotoAdjustments | undefined): AdjustmentsStep | null {
|
|
141
|
+
if (!a) return null;
|
|
142
|
+
const ops: AdjustOp[] = [];
|
|
143
|
+
for (const field of ADJUST_FIELD_ORDER) {
|
|
144
|
+
const v = a[field];
|
|
145
|
+
if (isSet(v)) ops.push({ op: field, value: v as number });
|
|
146
|
+
}
|
|
147
|
+
return ops.length ? { stage: 'adjustments', kind: 'colorMatrix', ops } : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The single source of truth for "what does this edit do." Returns the ordered
|
|
152
|
+
* list of pipeline steps — a neutral/empty edit returns `[]` (the compositor
|
|
153
|
+
* then skips pixi entirely, preserving the lazy-bundle guarantee). Steps always
|
|
154
|
+
* appear in `PHOTO_PIPELINE_ORDER`.
|
|
155
|
+
*/
|
|
156
|
+
export function planPhotoPipeline(edit: PhotoEdit | null | undefined): PhotoPipelineStep[] {
|
|
157
|
+
if (!edit) return [];
|
|
158
|
+
const byStage: Record<string, PhotoPipelineStep | null> = {
|
|
159
|
+
adjustments: planAdjustments(edit.adjustments),
|
|
160
|
+
duotone: null,
|
|
161
|
+
grain: null,
|
|
162
|
+
pattern: null,
|
|
163
|
+
mask: null,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const d = edit.duotone;
|
|
167
|
+
if (d?.enabled && (d.intensity ?? DUOTONE_DEFAULT_INTENSITY) > EPS) {
|
|
168
|
+
byStage.duotone = {
|
|
169
|
+
stage: 'duotone',
|
|
170
|
+
kind: 'duotone',
|
|
171
|
+
colorA: d.colorA ?? DUOTONE_DEFAULT_A,
|
|
172
|
+
colorB: d.colorB ?? DUOTONE_DEFAULT_B,
|
|
173
|
+
intensity: d.intensity ?? DUOTONE_DEFAULT_INTENSITY,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const g = edit.grain;
|
|
178
|
+
if (g?.enabled && (g.amount ?? GRAIN_DEFAULT_AMOUNT) > EPS) {
|
|
179
|
+
byStage.grain = {
|
|
180
|
+
stage: 'grain',
|
|
181
|
+
kind: 'noise',
|
|
182
|
+
amount: g.amount ?? GRAIN_DEFAULT_AMOUNT,
|
|
183
|
+
size: g.size ?? GRAIN_DEFAULT_SIZE,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const p = edit.pattern;
|
|
188
|
+
if (p?.enabled && (p.opacity ?? PATTERN_DEFAULT_OPACITY) > EPS) {
|
|
189
|
+
byStage.pattern = {
|
|
190
|
+
stage: 'pattern',
|
|
191
|
+
kind: 'pattern',
|
|
192
|
+
type: p.type ?? PATTERN_DEFAULT_TYPE,
|
|
193
|
+
scale: p.scale ?? PATTERN_DEFAULT_SCALE,
|
|
194
|
+
opacity: p.opacity ?? PATTERN_DEFAULT_OPACITY,
|
|
195
|
+
blend: p.blend ?? PATTERN_DEFAULT_BLEND,
|
|
196
|
+
color: p.color ?? PATTERN_DEFAULT_COLOR,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const m = edit.mask;
|
|
201
|
+
if (m?.preset && m.preset !== 'none' && (m.strength ?? MASK_DEFAULT_STRENGTH) > EPS) {
|
|
202
|
+
byStage.mask = {
|
|
203
|
+
stage: 'mask',
|
|
204
|
+
kind: 'mask',
|
|
205
|
+
preset: m.preset,
|
|
206
|
+
strength: m.strength ?? MASK_DEFAULT_STRENGTH,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Emit in the canonical, fixed order.
|
|
211
|
+
const out: PhotoPipelineStep[] = [];
|
|
212
|
+
for (const stage of PHOTO_PIPELINE_ORDER) {
|
|
213
|
+
const step = byStage[stage];
|
|
214
|
+
if (step) out.push(step);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Pure color helper (shared by the realizer + tests) ───────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `#rrggbb` → normalized `[r, g, b]` in 0…1. Throws on malformed input (the
|
|
223
|
+
* validator in schema.ts already rejects bad colors before persistence, so this
|
|
224
|
+
* is a belt-and-braces guard for the realizer). Kept here (pure, no pixi) so the
|
|
225
|
+
* duotone realization and its tests share one parser.
|
|
226
|
+
*/
|
|
227
|
+
export function hexToRgb01(hex: string): [number, number, number] {
|
|
228
|
+
const m = /^#([0-9a-fA-F]{6})$/.exec(hex);
|
|
229
|
+
if (!m) throw new Error(`hexToRgb01: not a #rrggbb color: ${hex}`);
|
|
230
|
+
const int = Number.parseInt(m[1], 16);
|
|
231
|
+
return [((int >> 16) & 0xff) / 255, ((int >> 8) & 0xff) / 255, (int & 0xff) / 255];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── GLSL for the custom duotone filter (a string — no pixi dependency) ────────
|
|
235
|
+
// Duotone is a per-pixel luminance → two-color gradient-map lerp, which a linear
|
|
236
|
+
// `ColorMatrixFilter` affine transform cannot express, so it needs its own
|
|
237
|
+
// fragment shader. `pipeline.ts` compiles these into a `PIXI.Filter` via
|
|
238
|
+
// `GlProgram.from({ vertex, fragment })`. WebGL (GLSL ES 1.00-ish, pixi v8
|
|
239
|
+
// high-precision) only — the Application is pinned to `preference: 'webgl'` so
|
|
240
|
+
// the WebGPU program is unnecessary (and WKWebView WebGPU is partial — Task 25).
|
|
241
|
+
|
|
242
|
+
export const DUOTONE_VERT_SOURCE = /* glsl */ `
|
|
243
|
+
in vec2 aPosition;
|
|
244
|
+
out vec2 vTextureCoord;
|
|
245
|
+
|
|
246
|
+
uniform vec4 uInputSize;
|
|
247
|
+
uniform vec4 uOutputFrame;
|
|
248
|
+
uniform vec4 uOutputTexture;
|
|
249
|
+
|
|
250
|
+
vec4 filterVertexPosition(void) {
|
|
251
|
+
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
|
|
252
|
+
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
|
|
253
|
+
position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
|
|
254
|
+
return vec4(position, 0.0, 1.0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
vec2 filterTextureCoord(void) {
|
|
258
|
+
return aPosition * (uOutputFrame.zw * uInputSize.zw);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
void main(void) {
|
|
262
|
+
gl_Position = filterVertexPosition();
|
|
263
|
+
vTextureCoord = filterTextureCoord();
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
export const DUOTONE_FRAG_SOURCE = /* glsl */ `
|
|
268
|
+
in vec2 vTextureCoord;
|
|
269
|
+
out vec4 finalColor;
|
|
270
|
+
|
|
271
|
+
uniform sampler2D uTexture;
|
|
272
|
+
uniform vec3 uColorA; // shadow color (0..1)
|
|
273
|
+
uniform vec3 uColorB; // highlight color (0..1)
|
|
274
|
+
uniform float uIntensity; // 0..1 crossfade with the original
|
|
275
|
+
|
|
276
|
+
void main(void) {
|
|
277
|
+
vec4 src = texture(uTexture, vTextureCoord);
|
|
278
|
+
// Rec. 601 luma over UN-premultiplied color.
|
|
279
|
+
vec3 rgb = src.a > 0.0 ? src.rgb / src.a : src.rgb;
|
|
280
|
+
float luma = dot(rgb, vec3(0.299, 0.587, 0.114));
|
|
281
|
+
vec3 duo = mix(uColorA, uColorB, luma);
|
|
282
|
+
vec3 outRgb = mix(rgb, duo, uIntensity);
|
|
283
|
+
finalColor = vec4(outRgb * src.a, src.a); // re-premultiply
|
|
284
|
+
}
|
|
285
|
+
`;
|