cozyclay 2.0.2 → 2.1.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/CHANGELOG.md +56 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +789 -263
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +113 -28
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +94 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* Unlayered and rooted like src/panels/details.css: the Foldout's v2 Details primitives
|
|
2
|
+
(.v2-details-section.v2-details-section .btn / .inspector-hint) are unlayered too, so the
|
|
3
|
+
overrides below repeat that root to win on specificity. */
|
|
4
|
+
/* One shrinkable column: an image's intrinsic width must not widen the Details column. */
|
|
5
|
+
.sculpt-panel { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-1); min-width: 0; padding: 4px var(--sp-2) var(--sp-2); }
|
|
6
|
+
.sculpt-panel > *, .sculpt-sheet > * { min-width: 0; }
|
|
7
|
+
.v2-details-section.v2-details-section .sculpt-panel .btn.full { margin-top: 0; }
|
|
8
|
+
.v2-details-section.v2-details-section .sculpt-panel .btn:disabled { opacity: .45; cursor: not-allowed; }
|
|
9
|
+
.sculpt-label { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-1); color: var(--text-2); font-size: 11px; font-weight: 500; white-space: nowrap; }
|
|
10
|
+
.sculpt-label small { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 11px; font-variant-numeric: tabular-nums; color: var(--text-3); }
|
|
11
|
+
|
|
12
|
+
/* Reference well */
|
|
13
|
+
.sculpt-well { position: relative; border-radius: var(--r-2); border: 1px solid var(--row-divider); background: var(--field); overflow: hidden; }
|
|
14
|
+
.sculpt-well-empty { display: grid; justify-items: center; gap: 4px; padding: var(--sp-3) var(--sp-2); border-style: dashed; border-color: var(--divider-strong); color: var(--text-2); text-align: center; cursor: pointer; transition: border-color var(--fast), background var(--fast); }
|
|
15
|
+
.sculpt-well-empty:hover, .sculpt-well-empty[data-dragging="true"] { border-color: var(--select); background: var(--select-tint); color: var(--text-1); }
|
|
16
|
+
.sculpt-well-empty:focus-visible { outline: none; border-color: var(--select); box-shadow: 0 0 0 2px var(--select-tint); }
|
|
17
|
+
.sculpt-well-title { font-size: 12px; font-weight: 500; color: var(--text-1); }
|
|
18
|
+
.sculpt-well-sub { font-size: 11px; color: var(--text-3); }
|
|
19
|
+
.v2-details-section.v2-details-section .sculpt-well-empty .btn.ghost { width: auto; margin-top: 4px; pointer-events: none; }
|
|
20
|
+
.sculpt-well-filled { display: grid; justify-items: center; background: var(--surface-0); }
|
|
21
|
+
/* The frame shrink-wraps the picture, so the measured slices land on the subject. */
|
|
22
|
+
.sculpt-ref-frame { position: relative; max-width: 100%; }
|
|
23
|
+
.sculpt-ref-frame img { display: block; max-width: 100%; max-height: 220px; }
|
|
24
|
+
.v2-details-section.v2-details-section .sculpt-well-replace { position: absolute; right: 6px; bottom: 6px; width: auto; background: var(--overlay); color: var(--text-1); }
|
|
25
|
+
|
|
26
|
+
/* Primary action */
|
|
27
|
+
.v2-details-section.v2-details-section .sculpt-cancel { color: var(--danger); background: none; box-shadow: inset 0 0 0 1px var(--danger); }
|
|
28
|
+
.v2-details-section.v2-details-section .sculpt-cancel:hover { background: var(--surface-active); color: var(--danger); }
|
|
29
|
+
.v2-details-section.v2-details-section .sculpt-refusal { color: var(--text-2); }
|
|
30
|
+
|
|
31
|
+
/* Progress */
|
|
32
|
+
.sculpt-progress { display: grid; gap: var(--sp-1); padding: var(--sp-1); border-radius: var(--r-2); background: var(--surface-raised); }
|
|
33
|
+
.sculpt-stepper { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; padding: 0; list-style: none; }
|
|
34
|
+
.sculpt-step { position: relative; display: grid; justify-items: center; gap: 4px; color: var(--text-3); font-size: 10.5px; white-space: nowrap; letter-spacing: -.01em; }
|
|
35
|
+
/* Connector from the previous step's dot to this one. */
|
|
36
|
+
.sculpt-step + .sculpt-step::before { content: ""; position: absolute; top: 4px; right: 50%; width: 100%; height: 1px; background: var(--divider-strong); }
|
|
37
|
+
.sculpt-step[data-state="complete"]::before, .sculpt-step[data-state="current"]::before { background: var(--select); }
|
|
38
|
+
.sculpt-step-dot { position: relative; z-index: 1; width: 9px; height: 9px; border-radius: 50%; border: 1px solid var(--divider-strong); background: var(--surface-raised); }
|
|
39
|
+
.sculpt-step[data-state="complete"] { color: var(--text-2); }
|
|
40
|
+
.sculpt-step[data-state="complete"] .sculpt-step-dot { background: var(--select); border-color: var(--select); }
|
|
41
|
+
.sculpt-step[data-state="current"] { color: var(--text-1); font-weight: 600; }
|
|
42
|
+
.sculpt-step[data-state="current"] .sculpt-step-dot { border-color: var(--select); background: var(--select); box-shadow: 0 0 0 3px var(--select-tint); animation: sculpt-pulse 1.4s var(--ease) infinite; }
|
|
43
|
+
@keyframes sculpt-pulse { 50% { box-shadow: 0 0 0 5px var(--select-tint); } }
|
|
44
|
+
@media (prefers-reduced-motion: reduce) { .sculpt-step[data-state="current"] .sculpt-step-dot { animation: none; } }
|
|
45
|
+
|
|
46
|
+
.sculpt-stats { display: flex; justify-content: space-between; color: var(--text-2); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
47
|
+
.sculpt-iou { display: grid; gap: 4px; }
|
|
48
|
+
.sculpt-iou .sculpt-label small { color: var(--text-1); font-family: var(--mono); }
|
|
49
|
+
.sculpt-iou-target { color: var(--text-3); }
|
|
50
|
+
.sculpt-iou-bar { position: relative; height: 6px; border-radius: 3px; background: var(--field); }
|
|
51
|
+
.sculpt-iou-fill { position: absolute; inset: 0 auto 0 0; width: calc(var(--sculpt-iou) * 100%); border-radius: inherit; background: var(--primary); transition: width var(--med) var(--ease); }
|
|
52
|
+
.sculpt-iou-bar[data-reached="true"] .sculpt-iou-fill { background: var(--ok); }
|
|
53
|
+
.sculpt-iou-marker { position: absolute; top: -3px; bottom: -3px; left: calc(var(--sculpt-target) * 100%); width: 2px; margin-left: -1px; border-radius: 1px; background: var(--select); }
|
|
54
|
+
.v2-details-section.v2-details-section .sculpt-done { color: var(--ok); }
|
|
55
|
+
.v2-details-section.v2-details-section .sculpt-error { color: var(--danger); }
|
|
56
|
+
|
|
57
|
+
/* Overlay sheet */
|
|
58
|
+
.sculpt-sheet { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; }
|
|
59
|
+
.v2-details-section.v2-details-section .sculpt-sheet .btn[aria-pressed="true"] { color: var(--text-1); box-shadow: inset 0 0 0 1px var(--select); }
|
|
60
|
+
.sculpt-sheet-image { display: block; width: 100%; border-radius: var(--r-1); border: 1px solid var(--row-divider); background: var(--surface-0); }
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/* --------------------------------------------------- live narration (#739) ---
|
|
64
|
+
Everything below says "it is working": a lump of clay being kneaded while we
|
|
65
|
+
wait, a bar against the usual wait, the score trail, the candidate race and
|
|
66
|
+
the log. Motion stops under prefers-reduced-motion; the words stay. */
|
|
67
|
+
|
|
68
|
+
/* What we wait on now */
|
|
69
|
+
.sculpt-now { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; column-gap: 8px; row-gap: 4px; padding: 6px 8px; border-radius: var(--r-1); background: var(--field); font-size: 12px; color: var(--text-1); }
|
|
70
|
+
.sculpt-now-label { min-width: 0; overflow-wrap: anywhere; line-height: 1.35; font-weight: 500; }
|
|
71
|
+
.sculpt-now-time { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--text-2); }
|
|
72
|
+
.sculpt-now-expect { grid-column: 2 / 4; font-size: 11px; color: var(--text-3); }
|
|
73
|
+
.sculpt-now[data-over="true"] .sculpt-now-expect { color: var(--warn, var(--text-2)); }
|
|
74
|
+
.sculpt-now-bar { grid-column: 1 / -1; position: relative; height: 3px; border-radius: 2px; background: var(--surface-active); overflow: hidden; }
|
|
75
|
+
.sculpt-now-bar > span { position: absolute; inset: 0 auto 0 0; width: calc(var(--sculpt-wait) * 100%); background: var(--select); border-radius: inherit; transition: width .5s linear; }
|
|
76
|
+
.sculpt-now[data-over="true"] .sculpt-now-bar > span { width: 100%; background: linear-gradient(90deg, var(--select-tint), var(--select), var(--select-tint)); background-size: 200% 100%; animation: sculpt-shimmer 1.2s linear infinite; }
|
|
77
|
+
@keyframes sculpt-shimmer { to { background-position: -200% 0; } }
|
|
78
|
+
|
|
79
|
+
/* A lump of clay being kneaded: squash, stretch, a thumb press. */
|
|
80
|
+
.sculpt-clay { width: 16px; height: 16px; margin-top: 1px; border-radius: 52% 48% 46% 54% / 55% 50% 50% 45%; background: radial-gradient(circle at 35% 30%, #e9c9a6, #c08a5c 70%); box-shadow: inset -2px -2px 0 rgba(0, 0, 0, .12); animation: sculpt-knead 1.1s ease-in-out infinite; }
|
|
81
|
+
@keyframes sculpt-knead {
|
|
82
|
+
0%, 100% { transform: scale(1, 1) rotate(0deg); border-radius: 52% 48% 46% 54% / 55% 50% 50% 45%; }
|
|
83
|
+
30% { transform: scale(1.18, .82) rotate(-6deg); border-radius: 60% 40% 55% 45% / 45% 55% 45% 55%; }
|
|
84
|
+
60% { transform: scale(.86, 1.14) rotate(5deg); border-radius: 45% 55% 40% 60% / 60% 40% 60% 40%; }
|
|
85
|
+
}
|
|
86
|
+
.sculpt-now[data-what="score"] .sculpt-clay, .sculpt-now[data-what="measure"] .sculpt-clay { animation-duration: .6s; }
|
|
87
|
+
|
|
88
|
+
/* IoU trail */
|
|
89
|
+
.sculpt-trail { position: relative; height: 34px; }
|
|
90
|
+
.sculpt-trail svg { display: block; width: 100%; height: 100%; overflow: visible; }
|
|
91
|
+
.sculpt-trail-target { stroke: var(--select); stroke-width: 1; stroke-dasharray: 2 2; vector-effect: non-scaling-stroke; opacity: .7; }
|
|
92
|
+
.sculpt-trail-line { fill: none; stroke: var(--primary); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
|
|
93
|
+
.sculpt-trail-dot { fill: var(--primary); }
|
|
94
|
+
.sculpt-trail-dot[data-last="true"] { fill: var(--ok); animation: sculpt-dot 1.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
|
|
95
|
+
@keyframes sculpt-dot { 50% { transform: scale(1.6); } }
|
|
96
|
+
.sculpt-delta { position: absolute; right: 0; top: -4px; padding: 1px 6px; border-radius: 9px; font: 600 11px/1.4 var(--mono); background: var(--surface-raised); box-shadow: 0 0 0 1px var(--row-divider); animation: sculpt-float 1.8s var(--ease) both; }
|
|
97
|
+
.sculpt-delta[data-sign="up"] { color: var(--ok); }
|
|
98
|
+
.sculpt-delta[data-sign="down"] { color: var(--danger); }
|
|
99
|
+
@keyframes sculpt-float { from { opacity: 0; transform: translateY(8px) scale(.9); } 25% { opacity: 1; transform: none; } }
|
|
100
|
+
|
|
101
|
+
/* Candidate race */
|
|
102
|
+
.sculpt-race { display: grid; gap: 3px; margin: 0; padding: 6px 8px; list-style: none; border-radius: var(--r-1); background: var(--field); }
|
|
103
|
+
.sculpt-race-row { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 6fr) auto; align-items: center; gap: 6px; font-size: 11px; color: var(--text-2); }
|
|
104
|
+
.sculpt-race-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
105
|
+
.sculpt-race-bar { height: 5px; border-radius: 3px; background: var(--surface-active); overflow: hidden; }
|
|
106
|
+
.sculpt-race-bar > span { display: block; height: 100%; width: calc(var(--sculpt-race) * 100%); border-radius: inherit; background: var(--text-3); transform-origin: left; animation: sculpt-grow .7s var(--ease) both; animation-delay: var(--sculpt-race-delay); }
|
|
107
|
+
@keyframes sculpt-grow { from { transform: scaleX(0); } }
|
|
108
|
+
.sculpt-race-score { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
109
|
+
.sculpt-race-row[data-leader="true"] { color: var(--text-1); font-weight: 600; }
|
|
110
|
+
.sculpt-race-row[data-leader="true"] .sculpt-race-bar > span { background: var(--ok); }
|
|
111
|
+
.sculpt-race-row[data-leader="true"] .sculpt-race-label::before { content: "\2605 "; color: var(--ok); }
|
|
112
|
+
.sculpt-race-row[data-refused="true"] { opacity: .5; text-decoration: line-through; }
|
|
113
|
+
|
|
114
|
+
/* Activity log */
|
|
115
|
+
.sculpt-log { display: grid; gap: 2px; max-height: 168px; margin: 0; padding: 4px 2px; list-style: none; overflow-y: auto; overscroll-behavior: contain; scroll-behavior: smooth; }
|
|
116
|
+
.sculpt-log-row { display: grid; grid-template-columns: 30px 8px minmax(0, 1fr); align-items: baseline; gap: 6px; font-size: 11px; line-height: 1.4; color: var(--text-2); animation: sculpt-in .35s var(--ease) both; }
|
|
117
|
+
.sculpt-log-row:last-child { color: var(--text-1); }
|
|
118
|
+
@keyframes sculpt-in { from { opacity: 0; transform: translateY(4px); } }
|
|
119
|
+
.sculpt-log-time { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--text-3); }
|
|
120
|
+
.sculpt-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-3); align-self: center; }
|
|
121
|
+
.sculpt-log-row[data-kind="agent"] .sculpt-log-dot { background: var(--select); }
|
|
122
|
+
.sculpt-log-row[data-kind="ok"] .sculpt-log-dot, .sculpt-log-row[data-kind="done"] .sculpt-log-dot { background: var(--ok); }
|
|
123
|
+
.sculpt-log-row[data-kind="score"] .sculpt-log-dot { background: var(--primary); }
|
|
124
|
+
.sculpt-log-row[data-kind="warn"] .sculpt-log-dot { background: var(--warn, #d9a441); }
|
|
125
|
+
.sculpt-log-row[data-kind="error"] .sculpt-log-dot { background: var(--danger); }
|
|
126
|
+
.sculpt-log-row[data-kind="done"] { color: var(--ok); font-weight: 600; }
|
|
127
|
+
.sculpt-log-text { min-width: 0; overflow-wrap: anywhere; }
|
|
128
|
+
|
|
129
|
+
/* Measuring the reference: a scan line, then the slices it found */
|
|
130
|
+
.sculpt-scanline { position: absolute; left: 0; right: 0; top: 0; height: 2px; background: var(--select); box-shadow: 0 0 8px 2px var(--select-tint); animation: sculpt-scan 1.1s ease-in-out infinite alternate; pointer-events: none; }
|
|
131
|
+
@keyframes sculpt-scan { to { top: calc(100% - 2px); } }
|
|
132
|
+
.sculpt-scan { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
133
|
+
.sculpt-scan-span { fill: var(--select); opacity: .8; animation: sculpt-slice .45s var(--ease) both; }
|
|
134
|
+
@keyframes sculpt-slice { from { opacity: 0; transform: scaleX(0); } }
|
|
135
|
+
.sculpt-scan-span { transform-box: fill-box; transform-origin: center; }
|
|
136
|
+
|
|
137
|
+
@media (prefers-reduced-motion: reduce) {
|
|
138
|
+
.sculpt-clay, .sculpt-trail-dot, .sculpt-delta, .sculpt-race-bar > span, .sculpt-log-row, .sculpt-scanline, .sculpt-scan-span, .sculpt-now[data-over="true"] .sculpt-now-bar > span { animation: none; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Model and effort */
|
|
142
|
+
.sculpt-model { display: grid; gap: 4px; }
|
|
143
|
+
.sculpt-model-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 4px; }
|
|
144
|
+
.sculpt-select { min-width: 0; height: 26px; padding: 0 6px; border-radius: var(--r-1); border: 1px solid var(--row-divider); background: var(--field); color: var(--text-1); font: inherit; font-size: 12px; }
|
|
145
|
+
.sculpt-select:disabled { opacity: .6; }
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
/* Blueprint (model sheet) */
|
|
149
|
+
.sculpt-blueprint { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; min-width: 0; padding: 8px; border-radius: var(--r-2); background: var(--surface-raised); }
|
|
150
|
+
.sculpt-blueprint-row { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; }
|
|
151
|
+
.sculpt-blueprint > * { min-width: 0; }
|
|
152
|
+
.sculpt-blueprint .sculpt-label small { overflow: hidden; text-overflow: ellipsis; }
|
|
153
|
+
.sculpt-notes { box-sizing: border-box; width: 100%; min-width: 0; resize: vertical; padding: 6px; border-radius: var(--r-1); border: 1px solid var(--row-divider); background: var(--field); color: var(--text-1); font: inherit; font-size: 12px; }
|
|
154
|
+
.sculpt-blueprint .sculpt-now .btn { grid-column: 1 / -1; justify-self: end; width: auto; }
|
|
155
|
+
.sculpt-sheet-figure { display: grid; gap: 4px; margin: 0; }
|
|
156
|
+
.sculpt-sheet-figure > a { position: relative; display: block; border-radius: var(--r-1); overflow: hidden; border: 1px solid var(--row-divider); background: #fff; }
|
|
157
|
+
.sculpt-sheet-figure img { display: block; width: 100%; }
|
|
158
|
+
.sculpt-sheet-tag { position: absolute; margin: 3px; padding: 0 4px; border-radius: 3px; font-size: 9.5px; line-height: 1.5; color: #fff; background: rgba(20, 22, 28, .62); pointer-events: none; }
|
|
159
|
+
.sculpt-sheet-figure figcaption { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-3); }
|
|
160
|
+
.sculpt-sheet-figure figcaption a { color: var(--select); }
|
|
161
|
+
.sculpt-prompt summary { cursor: pointer; font-size: 11px; color: var(--text-2); }
|
|
162
|
+
.sculpt-prompt pre { max-height: 180px; margin: 4px 0 0; padding: 6px; overflow: auto; white-space: pre-wrap; border-radius: var(--r-1); background: var(--field); font: 10.5px/1.45 var(--mono); color: var(--text-2); }
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// The Top View the Studio agent reads (verify_result visual "plan",
|
|
2
|
+
// capture_plan_png): a 2D data diagram drawn from editor state, not a render.
|
|
3
|
+
// planDiagramLayout computes the world->pixel transform and a list of
|
|
4
|
+
// primitives; drawPlanDiagram paints them on any 2D canvas context.
|
|
5
|
+
// Pure: no renderer, no React, no DOM.
|
|
6
|
+
import { isEffectivelyHidden } from './scene-objects.js';
|
|
7
|
+
import { shotAtFrame } from './cuts.js';
|
|
8
|
+
import { occludersOf, primarySubjects, shotCamera, sideOfAxis } from './studio-geometry-facts.js';
|
|
9
|
+
import { entityWorldBounds } from './studio-agent-commands.js';
|
|
10
|
+
|
|
11
|
+
export const PLAN_DIAGRAM = Object.freeze({ marginM: 1.5, maxSpanM: 30, minSpanM: 6, characterRadiusM: 0.3, frustumM: 3, defaultFovDeg: 40 });
|
|
12
|
+
const SIDE_LABEL = { left: 'L', right: 'R', 'on-axis': 'on-axis' };
|
|
13
|
+
const GROUP_HUES = ['#c9a46a', '#a8c47a', '#d39a8c', '#8fb7c9', '#c0a0d0', '#d6c06a'];
|
|
14
|
+
const round = v => Math.round(v * 1000) / 1000 + 0;
|
|
15
|
+
const flat = p => ({ x: round(p.x), z: round(p.z) });
|
|
16
|
+
|
|
17
|
+
function groupRoot(object, objects) {
|
|
18
|
+
let node = object;
|
|
19
|
+
for (let guard = 0; node?.parent && guard < 64; guard += 1) {
|
|
20
|
+
const parent = objects.find(o => o.id === node.parent);
|
|
21
|
+
if (!parent) break;
|
|
22
|
+
node = parent;
|
|
23
|
+
}
|
|
24
|
+
return node;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Footprint corners in world x/z: width*scaleX by depth*scaleZ, turned by rot (yaw deg, 0 = +Z, positive toward +X). */
|
|
28
|
+
function footprintCorners(o) {
|
|
29
|
+
const w = (o.footprint?.width ?? 0) * (o.scaleX ?? 1) / 2, d = (o.footprint?.depth ?? 0) * (o.scaleZ ?? 1) / 2;
|
|
30
|
+
const yaw = (o.rot ?? 0) * Math.PI / 180, c = Math.cos(yaw), s = Math.sin(yaw);
|
|
31
|
+
return [[-w, -d], [w, -d], [w, d], [-w, d]].map(([lx, lz]) => flat({ x: o.x + lx * c + lz * s, z: o.z - lx * s + lz * c }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The diagram's content and framing. `options`: width/height of the output in
|
|
36
|
+
* pixels, `ports` for world bounds (occluders), `currentShotId` (defaults to
|
|
37
|
+
* the shot under state.frame), `aspect` of the shot frame for the horizontal FOV.
|
|
38
|
+
*/
|
|
39
|
+
export function planDiagramLayout(state, { width = 1280, height = 720, ports, currentShotId, aspect = 16 / 9 } = {}) {
|
|
40
|
+
const visible = e => !isEffectivelyHidden(e, state.objects, state.characters);
|
|
41
|
+
const characters = state.characters.filter(visible);
|
|
42
|
+
// A carried prop's numbers are local to the bone it rides, so it has no floor spot of its own.
|
|
43
|
+
const objects = state.objects.filter(o => visible(o) && !o.attach);
|
|
44
|
+
const shots = [...(state.shotDocument?.shots ?? [])].sort((a, b) => a.startFrame - b.startFrame);
|
|
45
|
+
const currentId = currentShotId ?? shotAtFrame(shots, state.frame ?? 0)?.id ?? null;
|
|
46
|
+
const primitives = [];
|
|
47
|
+
|
|
48
|
+
// Largest footprints first, so a ground plane never covers the props standing on it.
|
|
49
|
+
const area = o => (o.footprint?.width ?? 0) * (o.scaleX ?? 1) * (o.footprint?.depth ?? 0) * (o.scaleZ ?? 1);
|
|
50
|
+
for (const o of [...objects].sort((p, q) => area(q) - area(p))) {
|
|
51
|
+
const root = groupRoot(o, state.objects), grouped = root !== o || state.objects.some(child => child.parent === o.id);
|
|
52
|
+
const hue = grouped ? GROUP_HUES[[...root.id].reduce((sum, ch) => sum + ch.charCodeAt(0), 0) % GROUP_HUES.length] : '#d8c39a';
|
|
53
|
+
primitives.push({ kind: 'object', id: o.id, label: o.name || o.id, center: flat(o), corners: footprintCorners(o), groupId: grouped ? root.id : null, fill: hue });
|
|
54
|
+
}
|
|
55
|
+
for (const c of characters) {
|
|
56
|
+
const yaw = (c.rot ?? 0) * Math.PI / 180;
|
|
57
|
+
primitives.push({ kind: 'character', id: c.id, label: c.subject || c.id, x: round(c.x), z: round(c.z), radius: PLAN_DIAGRAM.characterRadiusM, facing: flat({ x: Math.sin(yaw), z: Math.cos(yaw) }) });
|
|
58
|
+
}
|
|
59
|
+
shots.forEach((shot, index) => {
|
|
60
|
+
const camera = shotCamera(shot, state);
|
|
61
|
+
if (!camera) return;
|
|
62
|
+
const fovDeg = shot.cameraKeys?.[0]?.framing?.fovDeg ?? PLAN_DIAGRAM.defaultFovDeg;
|
|
63
|
+
const halfFov = Math.atan(Math.tan(fovDeg * Math.PI / 360) * aspect), heading = Math.atan2(camera.lookAt.x - camera.position.x, camera.lookAt.z - camera.position.z);
|
|
64
|
+
const edge = angle => flat({ x: camera.position.x + Math.sin(angle) * PLAN_DIAGRAM.frustumM, z: camera.position.z + Math.cos(angle) * PLAN_DIAGRAM.frustumM });
|
|
65
|
+
const current = shot.id === currentId, entry = { kind: 'camera', shotId: shot.id, label: `S${index + 1}`, current, position: flat(camera.position), lookAt: flat(camera.lookAt),
|
|
66
|
+
halfFovRad: round(halfFov), frustum: [edge(heading - halfFov), edge(heading + halfFov)], cameraSide: null };
|
|
67
|
+
primitives.push(entry);
|
|
68
|
+
if (!current) return;
|
|
69
|
+
const pair = primarySubjects(shot.subjectIds ?? [], camera.position, state);
|
|
70
|
+
if (pair.length !== 2) return;
|
|
71
|
+
entry.cameraSide = SIDE_LABEL[sideOfAxis(pair[0], pair[1], camera.position)] ?? null;
|
|
72
|
+
primitives.push({ kind: 'actionLine', shotId: shot.id, subjectIds: pair.map(c => c.id), a: flat(pair[0]), b: flat(pair[1]), distanceM: round(Math.hypot(pair[1].x - pair[0].x, pair[1].z - pair[0].z)), cameraSide: entry.cameraSide });
|
|
73
|
+
if (!ports) return;
|
|
74
|
+
for (const subject of pair) {
|
|
75
|
+
const box = entityWorldBounds(subject, state, ports);
|
|
76
|
+
if (!box) continue;
|
|
77
|
+
for (const hit of occludersOf(subject, camera.position, state, ports, box)) {
|
|
78
|
+
const blocker = state.objects.find(o => o.id === hit.byObjectId);
|
|
79
|
+
primitives.push({ kind: 'occluder', shotId: shot.id, subjectId: hit.subjectId, byObjectId: hit.byObjectId, corners: blocker && !blocker.attach ? footprintCorners(blocker) : [],
|
|
80
|
+
segment: { from: flat(camera.position), to: flat({ x: (box.min.x + box.max.x) / 2, z: (box.min.z + box.max.z) / 2 }) } });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Fit everything visible plus a margin, keep the output aspect, cap the span.
|
|
86
|
+
// A capped view centres on the cast and cameras, so the action stays in frame.
|
|
87
|
+
const box = points => {
|
|
88
|
+
const xs = points.map(p => p.x), zs = points.map(p => p.z);
|
|
89
|
+
return xs.length ? { min: { x: Math.min(...xs), z: Math.min(...zs) }, max: { x: Math.max(...xs), z: Math.max(...zs) } } : null;
|
|
90
|
+
};
|
|
91
|
+
const action = box(primitives.flatMap(p => p.kind === 'character' ? [p] : p.kind === 'camera' ? [p.position] : []));
|
|
92
|
+
const { min, max } = box(primitives.flatMap(p => p.kind === 'object' ? [p.center, ...p.corners] : p.kind === 'character' ? [p] : p.kind === 'camera' ? [p.position] : [])) ?? { min: { x: 0, z: 0 }, max: { x: 0, z: 0 } };
|
|
93
|
+
const pixelAspect = width / height;
|
|
94
|
+
let spanX = max.x - min.x + 2 * PLAN_DIAGRAM.marginM, spanZ = max.z - min.z + 2 * PLAN_DIAGRAM.marginM;
|
|
95
|
+
spanZ = Math.max(spanZ, PLAN_DIAGRAM.minSpanM);
|
|
96
|
+
if (spanX / spanZ < pixelAspect) spanX = spanZ * pixelAspect; else spanZ = spanX / pixelAspect;
|
|
97
|
+
const cap = Math.min(1, PLAN_DIAGRAM.maxSpanM / Math.max(spanX, spanZ));
|
|
98
|
+
spanX *= cap; spanZ *= cap;
|
|
99
|
+
const focus = cap < 1 && action ? action : { min, max }, centre = { x: (focus.min.x + focus.max.x) / 2, z: (focus.min.z + focus.max.z) / 2 };
|
|
100
|
+
const scale = width / spanX, view = { minX: centre.x - spanX / 2, maxX: centre.x + spanX / 2, minZ: centre.z - spanZ / 2, maxZ: centre.z + spanZ / 2 };
|
|
101
|
+
primitives.unshift({ kind: 'grid', stepM: 1, labelStepM: 2, ...Object.fromEntries(Object.entries(view).map(([k, v]) => [k, round(v)])) });
|
|
102
|
+
// +X to the right, +Z down the image.
|
|
103
|
+
const toPixel = p => [(p.x - view.minX) * scale, (p.z - view.minZ) * scale];
|
|
104
|
+
return { width, height, scale: round(scale), view, toPixel, currentShotId: currentId, primitives };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Paint the plan diagram on a 2D context of the output size; returns the layout. */
|
|
108
|
+
export function drawPlanDiagram(ctx, state, options = {}) {
|
|
109
|
+
const layout = planDiagramLayout(state, options), { width, height, scale, view, toPixel } = layout;
|
|
110
|
+
const path = (corners, close = true) => {
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
corners.forEach((p, i) => { const [x, y] = toPixel(p); i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); });
|
|
113
|
+
if (close) ctx.closePath();
|
|
114
|
+
};
|
|
115
|
+
const text = (value, x, y, colour = '#222', align = 'left') => {
|
|
116
|
+
ctx.textAlign = align; ctx.lineWidth = 3; ctx.strokeStyle = 'rgba(250, 248, 242, 0.9)'; ctx.fillStyle = colour;
|
|
117
|
+
ctx.strokeText(value, x, y); ctx.fillText(value, x, y);
|
|
118
|
+
};
|
|
119
|
+
ctx.fillStyle = '#f7f5ef'; ctx.fillRect(0, 0, width, height);
|
|
120
|
+
ctx.font = '500 13px system-ui, sans-serif'; ctx.textBaseline = 'middle';
|
|
121
|
+
for (const p of layout.primitives) {
|
|
122
|
+
if (p.kind === 'grid') {
|
|
123
|
+
for (let x = Math.ceil(view.minX); x <= view.maxX; x += 1) {
|
|
124
|
+
const [px] = toPixel({ x, z: 0 });
|
|
125
|
+
ctx.strokeStyle = x === 0 ? '#9a968c' : x % 2 ? '#e8e5dc' : '#d9d5ca'; ctx.lineWidth = x === 0 ? 1.5 : 1;
|
|
126
|
+
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, height); ctx.stroke();
|
|
127
|
+
if (x % 2 === 0) text(`x ${x}`, px + 3, height - 10, '#6d695f');
|
|
128
|
+
}
|
|
129
|
+
for (let z = Math.ceil(view.minZ); z <= view.maxZ; z += 1) {
|
|
130
|
+
const [, py] = toPixel({ x: 0, z });
|
|
131
|
+
ctx.strokeStyle = z === 0 ? '#9a968c' : z % 2 ? '#e8e5dc' : '#d9d5ca'; ctx.lineWidth = z === 0 ? 1.5 : 1;
|
|
132
|
+
ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(width, py); ctx.stroke();
|
|
133
|
+
if (z % 2 === 0) text(`z ${z}`, 4, py - 8, '#6d695f');
|
|
134
|
+
}
|
|
135
|
+
const [ox, oy] = toPixel({ x: 0, z: 0 });
|
|
136
|
+
ctx.strokeStyle = '#5c584f'; ctx.lineWidth = 2;
|
|
137
|
+
ctx.beginPath(); ctx.arc(ox, oy, 5, 0, Math.PI * 2); ctx.stroke();
|
|
138
|
+
text('0', ox + 8, oy + 10, '#5c584f');
|
|
139
|
+
} else if (p.kind === 'object') {
|
|
140
|
+
ctx.fillStyle = p.fill; ctx.strokeStyle = '#8a7350'; ctx.lineWidth = 1.5;
|
|
141
|
+
if (p.corners.some((c, i) => c.x !== p.corners[(i + 2) % 4].x || c.z !== p.corners[(i + 2) % 4].z)) { path(p.corners); ctx.fill(); ctx.stroke(); }
|
|
142
|
+
const [x, y] = toPixel(p.center);
|
|
143
|
+
text(p.label.length > 24 ? `${p.label.slice(0, 23)}\u2026` : p.label, x, y, '#4a3b22', 'center');
|
|
144
|
+
} else if (p.kind === 'character') {
|
|
145
|
+
const [x, y] = toPixel(p), r = p.radius * scale, [fx, fy] = toPixel({ x: p.x + p.facing.x * (p.radius + 0.35), z: p.z + p.facing.z * (p.radius + 0.35) });
|
|
146
|
+
ctx.fillStyle = '#6ea8e0'; ctx.strokeStyle = '#2f6aa8'; ctx.lineWidth = 2;
|
|
147
|
+
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
148
|
+
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(fx, fy); ctx.stroke();
|
|
149
|
+
const head = Math.atan2(fy - y, fx - x);
|
|
150
|
+
ctx.beginPath(); ctx.moveTo(fx, fy); ctx.lineTo(fx - 8 * Math.cos(head - 0.5), fy - 8 * Math.sin(head - 0.5)); ctx.lineTo(fx - 8 * Math.cos(head + 0.5), fy - 8 * Math.sin(head + 0.5)); ctx.closePath(); ctx.fillStyle = '#2f6aa8'; ctx.fill();
|
|
151
|
+
// The name goes behind the character, so two people facing each other keep their labels apart.
|
|
152
|
+
const behind = p.facing.x >= 0 ? -1 : 1, name = p.label.length > 24 ? `${p.label.slice(0, 23)}\u2026` : p.label;
|
|
153
|
+
text(name, x + behind * (r + 4), y - r - 6, '#1f4f80', behind < 0 ? 'right' : 'left');
|
|
154
|
+
} else if (p.kind === 'camera') {
|
|
155
|
+
const [cx, cy] = toPixel(p.position), [lx, ly] = toPixel(p.lookAt);
|
|
156
|
+
ctx.fillStyle = p.current ? 'rgba(214, 64, 52, 0.22)' : 'rgba(120, 120, 120, 0.14)'; ctx.strokeStyle = p.current ? '#d64034' : '#7d7a73'; ctx.lineWidth = p.current ? 2 : 1.25;
|
|
157
|
+
path([p.position, ...p.frustum]); ctx.fill(); ctx.stroke();
|
|
158
|
+
ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(lx, ly); ctx.stroke();
|
|
159
|
+
ctx.beginPath(); ctx.arc(cx, cy, 6, 0, Math.PI * 2);
|
|
160
|
+
if (p.current) { ctx.fillStyle = '#d64034'; ctx.fill(); } else { ctx.fillStyle = '#ffffff'; ctx.fill(); ctx.stroke(); }
|
|
161
|
+
text(p.cameraSide ? `${p.label} (${p.cameraSide})` : p.label, cx + 9, cy + 12, p.current ? '#b02d22' : '#55524b');
|
|
162
|
+
} else if (p.kind === 'actionLine') {
|
|
163
|
+
const [ax, ay] = toPixel(p.a), [bx, by] = toPixel(p.b);
|
|
164
|
+
ctx.strokeStyle = '#3c7d3a'; ctx.lineWidth = 2; ctx.setLineDash([8, 6]);
|
|
165
|
+
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke(); ctx.setLineDash([]);
|
|
166
|
+
text(`${p.distanceM.toFixed(2)} m`, (ax + bx) / 2, (ay + by) / 2 - 12, '#2d5f2b', 'center');
|
|
167
|
+
} else if (p.kind === 'occluder') {
|
|
168
|
+
ctx.strokeStyle = '#e0241b'; ctx.lineWidth = 3;
|
|
169
|
+
if (p.corners.length) { path(p.corners); ctx.stroke(); }
|
|
170
|
+
const [fx, fy] = toPixel(p.segment.from), [tx, ty] = toPixel(p.segment.to);
|
|
171
|
+
ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(fx, fy); ctx.lineTo(tx, ty); ctx.stroke();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Legend, top-right.
|
|
175
|
+
const rows = [['character', '#6ea8e0'], ['object', '#d8c39a'], ['camera (filled: current shot)', '#d64034'], ['action line', '#3c7d3a'], ['blocked sight line', '#e0241b']];
|
|
176
|
+
const lx = width - 236, ly = 12;
|
|
177
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.92)'; ctx.strokeStyle = '#c9c4b8'; ctx.lineWidth = 1;
|
|
178
|
+
ctx.fillRect(lx, ly, 224, rows.length * 20 + 12); ctx.strokeRect(lx, ly, 224, rows.length * 20 + 12);
|
|
179
|
+
rows.forEach(([name, colour], i) => {
|
|
180
|
+
ctx.fillStyle = colour; ctx.fillRect(lx + 10, ly + 12 + i * 20, 14, 10);
|
|
181
|
+
ctx.fillStyle = '#333'; ctx.textAlign = 'left'; ctx.fillText(name, lx + 32, ly + 17 + i * 20);
|
|
182
|
+
});
|
|
183
|
+
return layout;
|
|
184
|
+
}
|