@dailies-studio/shot-editor 0.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.
Files changed (112) hide show
  1. package/dist/assets/index-B7notge7.css +1 -0
  2. package/dist/assets/index-BBE6Ta9L.js +52 -0
  3. package/dist/index.html +13 -0
  4. package/package.json +45 -0
  5. package/src/api.ts +289 -0
  6. package/src/app/App.tsx +394 -0
  7. package/src/audio/index.tsx +74 -0
  8. package/src/audio/preview.ts +364 -0
  9. package/src/bridge.ts +45 -0
  10. package/src/bridgeTypes.ts +582 -0
  11. package/src/channels.ts +40 -0
  12. package/src/main.tsx +15 -0
  13. package/src/panels/ResizeHandle.tsx +113 -0
  14. package/src/panels/agent/AgentPanel.tsx +478 -0
  15. package/src/panels/agent/agent.css +322 -0
  16. package/src/panels/export/ExportDialog.tsx +284 -0
  17. package/src/panels/files/FilePanel.tsx +564 -0
  18. package/src/panels/files/McpSection.tsx +66 -0
  19. package/src/panels/files/icons.tsx +118 -0
  20. package/src/panels/header/DocSwitcher.tsx +85 -0
  21. package/src/panels/header/Header.tsx +132 -0
  22. package/src/panels/header/HistoryControls.tsx +20 -0
  23. package/src/panels/header/StatusBadges.tsx +49 -0
  24. package/src/panels/header/TransportBar.tsx +86 -0
  25. package/src/panels/header/ZoomControls.tsx +38 -0
  26. package/src/panels/help/HelpSection.tsx +190 -0
  27. package/src/panels/inspector/FramingRows.tsx +69 -0
  28. package/src/panels/inspector/InspectorPanel.tsx +178 -0
  29. package/src/panels/inspector/MarkerSection.tsx +63 -0
  30. package/src/panels/inspector/ParamRow.tsx +71 -0
  31. package/src/panels/inspector/animate/BlockTiming.tsx +341 -0
  32. package/src/panels/inspector/animate/DuringBlocks.tsx +85 -0
  33. package/src/panels/inspector/animate/InOutPresets.tsx +57 -0
  34. package/src/panels/inspector/animate/ResolvedChannels.tsx +150 -0
  35. package/src/panels/inspector/animate/ShotSection.tsx +126 -0
  36. package/src/panels/inspector/animate/useBlocks.ts +43 -0
  37. package/src/panels/inspector/design/AlignRow.tsx +63 -0
  38. package/src/panels/inspector/design/AppearanceSection.tsx +49 -0
  39. package/src/panels/inspector/design/ClonerSection.tsx +142 -0
  40. package/src/panels/inspector/design/ComponentSection.tsx +94 -0
  41. package/src/panels/inspector/design/CornerSection.tsx +52 -0
  42. package/src/panels/inspector/design/FillSection.tsx +89 -0
  43. package/src/panels/inspector/design/HtmlSection.tsx +50 -0
  44. package/src/panels/inspector/design/InstanceSection.tsx +302 -0
  45. package/src/panels/inspector/design/MediaSection.tsx +251 -0
  46. package/src/panels/inspector/design/MeshSection.tsx +172 -0
  47. package/src/panels/inspector/design/ShadowSection.tsx +73 -0
  48. package/src/panels/inspector/design/StrokeSection.tsx +85 -0
  49. package/src/panels/inspector/design/TransformSection.tsx +91 -0
  50. package/src/panels/inspector/design/TypographySection.tsx +170 -0
  51. package/src/panels/inspector/scene/CameraSection.tsx +59 -0
  52. package/src/panels/inspector/scene/EffectsSection.tsx +123 -0
  53. package/src/panels/inspector/scene/SceneSection.tsx +71 -0
  54. package/src/panels/inspector/scene/TransitionSection.tsx +136 -0
  55. package/src/panels/inspector/useDocWrite.ts +268 -0
  56. package/src/panels/material/MaterialPicker.tsx +172 -0
  57. package/src/panels/stage/StageMenu.tsx +38 -0
  58. package/src/panels/stage/StagePanel.tsx +131 -0
  59. package/src/panels/timeline/AudioLane.tsx +340 -0
  60. package/src/panels/timeline/BlockRow.tsx +242 -0
  61. package/src/panels/timeline/ChannelRow.tsx +266 -0
  62. package/src/panels/timeline/LifespanRow.tsx +165 -0
  63. package/src/panels/timeline/MarkerLane.tsx +146 -0
  64. package/src/panels/timeline/SceneStrip.tsx +145 -0
  65. package/src/panels/timeline/TimelineDock.tsx +488 -0
  66. package/src/panels/timeline/model.ts +641 -0
  67. package/src/panels/timeline/snap.ts +191 -0
  68. package/src/panels/toolbar/AddMenu.tsx +230 -0
  69. package/src/panels/toolbar/CameraViewBar.tsx +70 -0
  70. package/src/panels/toolbar/ShadingBar.tsx +42 -0
  71. package/src/panels/toolbar/ToolDockBar.tsx +110 -0
  72. package/src/panels/toolbar/ViewModeBar.tsx +49 -0
  73. package/src/shotTokens.css +131 -0
  74. package/src/stage/FrustumGizmo.tsx +184 -0
  75. package/src/stage/GridPlane.tsx +274 -0
  76. package/src/stage/MoveGizmo.tsx +411 -0
  77. package/src/stage/PerfHud.tsx +206 -0
  78. package/src/stage/SplitRenderer.tsx +511 -0
  79. package/src/stage/StageApp.tsx +3423 -0
  80. package/src/stage/StageTools.tsx +581 -0
  81. package/src/stage/bridgeClient.ts +278 -0
  82. package/src/stage/feed.ts +243 -0
  83. package/src/stage/frustumGeometry.ts +172 -0
  84. package/src/stage/gizmoMath.ts +494 -0
  85. package/src/stage/handles.ts +151 -0
  86. package/src/stage/instanceModules.ts +152 -0
  87. package/src/stage/interactions/move.ts +172 -0
  88. package/src/stage/interactions/panZoom.ts +60 -0
  89. package/src/stage/isolation.ts +155 -0
  90. package/src/stage/main.tsx +110 -0
  91. package/src/stage/modalState.ts +427 -0
  92. package/src/stage/overlays/Overlays.tsx +1046 -0
  93. package/src/stage/panes.ts +121 -0
  94. package/src/stage/penState.ts +112 -0
  95. package/src/stage/project.ts +402 -0
  96. package/src/stage/shading.ts +55 -0
  97. package/src/stage/tokenColor.ts +26 -0
  98. package/src/stage/view.ts +182 -0
  99. package/src/stage/workingView.ts +636 -0
  100. package/src/state/agentActions.ts +203 -0
  101. package/src/state/clientId.ts +25 -0
  102. package/src/state/context.tsx +33 -0
  103. package/src/state/insert.ts +507 -0
  104. package/src/state/keymap.ts +560 -0
  105. package/src/state/menu.ts +141 -0
  106. package/src/state/panelSize.ts +26 -0
  107. package/src/state/renderEvents.ts +100 -0
  108. package/src/state/store.ts +4094 -0
  109. package/src/state/triangles.ts +70 -0
  110. package/src/state/types.ts +384 -0
  111. package/src/styles.css +2948 -0
  112. package/src/tools.ts +181 -0
@@ -0,0 +1,155 @@
1
+ // Looking at one subtree, straight on.
2
+ //
3
+ // Isolation is an editor MODE, not a document state: nothing here is ever
4
+ // written to a Doc3D and nothing reaches an export. What it needs is a camera
5
+ // pose, and that pose is derived from the subtree itself rather than orbited to
6
+ // by hand — "enter this card and see it flat" has exactly one right answer.
7
+ //
8
+ // ── WHY NOT THE WORKING CAMERA ───────────────────────────────────────────
9
+ //
10
+ // The working pane's camera is a spherical orbit rig: its `up` is reset to
11
+ // world +y every frame (so it cannot roll) and its polar angle is clamped off
12
+ // both poles (so it cannot look straight down). A card rolled 30 degrees and a
13
+ // card lying flat are both perfectly ordinary, and neither can be framed by
14
+ // that rig. So the pose is built here from the subtree's own basis instead.
15
+ //
16
+ // ── AND WHY ORTHOGRAPHIC ─────────────────────────────────────────────────
17
+ //
18
+ // Editing flat means no foreshortening: two children the same size have to
19
+ // measure the same on screen, and a nudge has to move the same number of pixels
20
+ // wherever it lands. A perspective camera at a derived distance is 1:1 only on
21
+ // the plane it was derived for — which is the plane the subtree's ROOT sits in,
22
+ // not the plane its children sit in once any of them has a z.
23
+ //
24
+ // The frustum is sized from the subtree's own bounds, measured in the ROOT's
25
+ // frame, so entering a subtree frames it whatever the scene camera was doing.
26
+ import { toLocal, worldStateAt } from "@dailies-studio/shot/doc";
27
+ import type { Mat3, Scene3D, Vec3 } from "@dailies-studio/shot/doc";
28
+ import type { Rational } from "@dailies-studio/engine";
29
+
30
+ /** How much of the pane is margin around the framed subtree, per side. */
31
+ export const ISOLATION_MARGIN = 0.12;
32
+
33
+ /** A subtree with no extent still needs a frustum — a single zero-size layer is
34
+ * a real document, and a zero-width ortho camera projects everything to NaN. */
35
+ const MIN_HALF_EXTENT = 1;
36
+
37
+ export interface IsolationFrame {
38
+ /** Where to look, in THREE world space. */
39
+ target: Vec3;
40
+ /** The subtree's own basis: column 2 is its normal, column 1 its up. */
41
+ rotation: Mat3;
42
+ /** Half-extents of the subtree's bounds IN ITS OWN FRAME, design px. */
43
+ halfWidth: number;
44
+ halfHeight: number;
45
+ /** Half-depth, for near/far. A flat subtree still gets a usable slab. */
46
+ halfDepth: number;
47
+ }
48
+
49
+ /**
50
+ * The pose and extent that frame `rootId`'s subtree head-on at a frame.
51
+ *
52
+ * The RESOLVED chain, not the authored one: an author entering a card whose
53
+ * parent is mid-rotation is looking at where it is now. Returns null when the
54
+ * layer is not in this scene, which is what "the isolation went stale" looks
55
+ * like from here.
56
+ */
57
+ export function isolationFrame(
58
+ scene: Scene3D,
59
+ rootId: string,
60
+ subtree: ReadonlySet<string>,
61
+ sceneFrame: number,
62
+ fps: Rational,
63
+ ): IsolationFrame | null {
64
+ const root = scene.layersById[rootId];
65
+ if (root === undefined) return null;
66
+ const rootPose = worldStateAt(scene, rootId, sceneFrame, fps);
67
+
68
+ let minX = Infinity;
69
+ let maxX = -Infinity;
70
+ let minY = Infinity;
71
+ let maxY = -Infinity;
72
+ let minZ = Infinity;
73
+ let maxZ = -Infinity;
74
+ for (const id of subtree) {
75
+ const layer = scene.layersById[id];
76
+ if (layer === undefined) continue;
77
+ // Each member's centre in the ROOT's frame, with its own axis-aligned box
78
+ // around it. The box ignores the member's own tilt and roll, the same
79
+ // simplification `groupLayers` makes for the same reason: a rotation-aware
80
+ // hull would frame a rolled child looser than the box the author sees.
81
+ const local = toLocal(rootPose, worldStateAt(scene, id, sceneFrame, fps));
82
+ const [w, h] = layer.transform.size;
83
+ minX = Math.min(minX, local.position[0] - w / 2);
84
+ maxX = Math.max(maxX, local.position[0] + w / 2);
85
+ minY = Math.min(minY, local.position[1] - h / 2);
86
+ maxY = Math.max(maxY, local.position[1] + h / 2);
87
+ minZ = Math.min(minZ, local.position[2]);
88
+ maxZ = Math.max(maxZ, local.position[2]);
89
+ }
90
+ if (!Number.isFinite(minX)) return null;
91
+
92
+ // The bounds' CENTRE, not the root's origin: a group whose children all sit
93
+ // to one side would otherwise be framed off-centre, which is the state the
94
+ // grouping arithmetic exists to make impossible to author but which a
95
+ // hand-written document can still hold.
96
+ const centre: Vec3 = [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2];
97
+ const world = worldOf(rootPose, centre);
98
+
99
+ return {
100
+ target: world,
101
+ rotation: rootPose.rotation,
102
+ halfWidth: Math.max(MIN_HALF_EXTENT, (maxX - minX) / 2),
103
+ halfHeight: Math.max(MIN_HALF_EXTENT, (maxY - minY) / 2),
104
+ halfDepth: Math.max(MIN_HALF_EXTENT, (maxZ - minZ) / 2),
105
+ };
106
+ }
107
+
108
+ /** A point in the root's DESIGN frame, out in three world space. Design +y is
109
+ * down and +z is away, so both flip on the way through the one mapping. */
110
+ function worldOf(pose: { position: Vec3; rotation: Mat3 }, local: Vec3): Vec3 {
111
+ const d: Vec3 = [local[0], -local[1], -local[2]];
112
+ return [
113
+ pose.position[0] + pose.rotation[0][0] * d[0] + pose.rotation[0][1] * d[1] + pose.rotation[0][2] * d[2],
114
+ pose.position[1] + pose.rotation[1][0] * d[0] + pose.rotation[1][1] * d[1] + pose.rotation[1][2] * d[2],
115
+ pose.position[2] + pose.rotation[2][0] * d[0] + pose.rotation[2][1] * d[1] + pose.rotation[2][2] * d[2],
116
+ ];
117
+ }
118
+
119
+ /**
120
+ * The orthographic half-extents that show the whole subtree in a pane of this
121
+ * aspect, with a margin.
122
+ *
123
+ * Both axes take the SAME scale — the larger of the two requirements — because
124
+ * an ortho camera with different x and y scales is a squashed picture, and the
125
+ * point of isolation is that a square measures square.
126
+ */
127
+ export function isolationExtent(
128
+ frame: IsolationFrame,
129
+ aspect: number,
130
+ ): { halfWidth: number; halfHeight: number } {
131
+ const safe = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
132
+ const grow = 1 / (1 - 2 * ISOLATION_MARGIN);
133
+ const halfHeight = Math.max(frame.halfHeight, frame.halfWidth / safe) * grow;
134
+ return { halfWidth: halfHeight * safe, halfHeight };
135
+ }
136
+
137
+ /** Camera eye for an isolation frame: back along the subtree's own normal, far
138
+ * enough that the ortho slab clears the deepest member either way. */
139
+ export function isolationEye(frame: IsolationFrame): { eye: Vec3; up: Vec3; far: number } {
140
+ const normal: Vec3 = [frame.rotation[0][2], frame.rotation[1][2], frame.rotation[2][2]];
141
+ const up: Vec3 = [frame.rotation[0][1], frame.rotation[1][1], frame.rotation[2][1]];
142
+ // Distance is arbitrary for an orthographic projection, so it is chosen for
143
+ // the CLIP planes: far enough that nothing in the subtree is behind the eye,
144
+ // near enough that the depth range stays a sane number.
145
+ const back = frame.halfDepth * 4 + 1000;
146
+ return {
147
+ eye: [
148
+ frame.target[0] + normal[0] * back,
149
+ frame.target[1] + normal[1] * back,
150
+ frame.target[2] + normal[2] * back,
151
+ ],
152
+ up,
153
+ far: back * 2 + frame.halfDepth * 4,
154
+ };
155
+ }
@@ -0,0 +1,110 @@
1
+ // Stage entry — served ONLY through the project's Vite (/shot-stage.html +
2
+ // /@fs, package.json "./stage" export) so the three stack resolves to the
3
+ // STUDIO's pinned copies and the project's asset route is same-origin; never
4
+ // bundled into the chrome dist.
5
+ //
6
+ // INSTALL-BEFORE-PAINT (the canvas stage's posture): the chrome polls
7
+ // `iframe.contentWindow.__shotStudio` on load and must never observe a
8
+ // rendered-but-bridgeless stage, so the handle installs SYNCHRONOUSLY in
9
+ // `boot()`. StageApp itself renders on the chrome's `connect()` — there is
10
+ // nothing worth painting before a chrome exists, and the document arrives
11
+ // immediately after.
12
+ import { createRoot } from "react-dom/client";
13
+ import { StageAssetsContext, StudioSurfaceContext } from "@dailies-studio/runtime";
14
+ import type { StageAssetResolver } from "@dailies-studio/runtime";
15
+ // The stage is its OWN document and inherits nothing from the chrome's
16
+ // stylesheet, so it adopts the design tokens by importing them. tokens.css and
17
+ // NOT styles.css: the palette is inert until referenced, while the widget rules
18
+ // and the global box-sizing reset would repaint the author's own content
19
+ // differently here than in the export — which is exactly the preview != runtime
20
+ // split this whole lane refuses.
21
+ import "@dailies-studio/ui/tokens.css";
22
+ // Then THIS LANE's palette on top, because the overlays drawn over the stage
23
+ // read --accent, --guide and --frame-border, and the house values are not the
24
+ // ones the chrome around them is using. Without it the selection quad and the
25
+ // tilt handles paint in the house rose while every control beside them is
26
+ // blue — the overlays were never wrong, they were reading a token nobody had
27
+ // overridden for this document. Same file the chrome imports; one definition.
28
+ import "../shotTokens.css";
29
+ import { createStageClient } from "./bridgeClient.js";
30
+ import { StageApp } from "./StageApp.js";
31
+
32
+ // WITHOUT THIS PROVIDER, IMAGE AND VIDEO LAYERS ARE BLANK. `useStageAssetResolver`
33
+ // falls back to the bare content-addressed filename, which resolves against
34
+ // /shot-stage.html and 404s — the dev server serves the asset store only at
35
+ // /studio/assets/ (same origin here, since this iframe rides the project's
36
+ // Vite). The harness hit this exact failure class before us; mirror its
37
+ // resolver (cli/src/harnessRuntime.ts).
38
+ const stageAssets: StageAssetResolver = (storePath) =>
39
+ "/studio/assets/" + encodeURIComponent(storePath);
40
+
41
+ export function boot(): void {
42
+ if (window.__shotStudio) {
43
+ console.warn("[shot-editor stage] boot() called twice — ignoring");
44
+ return;
45
+ }
46
+ document.documentElement.style.height = "100%";
47
+ document.body.style.cssText = "margin:0;height:100%;overflow:hidden;background:#000";
48
+ const container = document.createElement("div");
49
+ container.style.cssText = "position:fixed;inset:0";
50
+ document.body.appendChild(container);
51
+
52
+ const client = createStageClient();
53
+ // Install BEFORE first paint, and before a render is even scheduled.
54
+ window.__shotStudio = client.bridge;
55
+
56
+ const root = createRoot(container);
57
+ client.setDisposer(() => {
58
+ root.unmount();
59
+ container.remove();
60
+ delete window.__shotStudio;
61
+ });
62
+
63
+ // First render on connect: `connect()` flips `connected`, so subscribing
64
+ // catches it whether the chrome connects on the next poll tick or already
65
+ // did between install and subscribe.
66
+ let rendered = false;
67
+ const renderOnConnect = (): void => {
68
+ if (rendered || !client.getState().connected) return;
69
+ rendered = true;
70
+ unsubscribe();
71
+ // THE STUDIO-SURFACE MARKER, on the ROOT and never on a branch (the
72
+ // harness's posture). This stage mounts the shot runtime with an explicit
73
+ // frame and no wall clock; anything that reads "no ambient clock" as "a
74
+ // real page" and arms an animation would be animating under the selection
75
+ // handles while a gesture reads live geometry off a moving target.
76
+ root.render(
77
+ <StudioSurfaceContext.Provider value={true}>
78
+ <StageAssetsContext.Provider value={stageAssets}>
79
+ <StageApp client={client} />
80
+ </StageAssetsContext.Provider>
81
+ </StudioSurfaceContext.Provider>,
82
+ );
83
+ };
84
+ const unsubscribe = client.subscribe(renderOnConnect);
85
+ renderOnConnect();
86
+ forwardShotDocChanged();
87
+ }
88
+
89
+ /**
90
+ * The server's "this shot changed under you" push, RELAYED to the chrome.
91
+ *
92
+ * THIS IFRAME IS THE ONLY HALF WITH AN HMR CLIENT. The server pushes over the
93
+ * Vite WS, but the chrome is served as a PREBUILT STATIC bundle from dist/ —
94
+ * Vite strips its `import.meta.hot` branch at build time, so a listener there
95
+ * would be dead in every shipped build. The stage is served through the
96
+ * project's Vite and has a live client, so it forwards: same-origin
97
+ * postMessage to the parent, which the chrome turns back into the event its
98
+ * handler already takes. Exactly the relay the canvas stage performs.
99
+ */
100
+ function forwardShotDocChanged(): void {
101
+ const hot = typeof import.meta.hot?.on === "function" ? import.meta.hot : null;
102
+ if (hot === null || window.parent === window) return;
103
+ hot.on("studio:shot-doc-changed", (data: unknown) => {
104
+ try {
105
+ window.parent.postMessage({ type: "studio:shot-doc-changed", data }, location.origin);
106
+ } catch {
107
+ // A relay must never take the stage down.
108
+ }
109
+ });
110
+ }