@litfamily/litgrok 1.0.26 → 1.0.28
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/.grok/hooks/lit-mark.mjs +42 -0
- package/.grok/hooks/lit-status-line.mjs +174 -21
- package/.grok/hooks/plan-gate.mjs +13 -3
- package/.grok/hooks/record-passive-event.mjs +40 -6
- package/.grok/hooks/session-end.json +15 -0
- package/.grok/hooks/session-end.mjs +6 -0
- package/.grok/hooks/session-liveness.mjs +102 -0
- package/.grok/hooks/session-rows.mjs +57 -0
- package/.grok/hooks/session-start.mjs +17 -0
- package/.grok/hooks/workflow-stage.mjs +5 -2
- package/.grok/rules/00-litgrok.md +14 -0
- package/.grok/skills/frontend-ui-ux/ATTRIBUTION.md +23 -0
- package/.grok/skills/frontend-ui-ux/SKILL.md +4 -1
- package/.grok/skills/frontend-ui-ux/examples/phase-3-readme-ab/README.md +9 -0
- package/.grok/skills/frontend-ui-ux/kit/components.css +414 -0
- package/.grok/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/.grok/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/.grok/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/.grok/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/.grok/skills/frontend-ui-ux/references/adaptive-layout.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/brand-and-imagery.md +11 -7
- package/.grok/skills/frontend-ui-ux/references/complete-contract.md +21 -0
- package/.grok/skills/frontend-ui-ux/references/composition.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/craft-floor.md +256 -93
- package/.grok/skills/frontend-ui-ux/references/creative-directions.md +9 -6
- package/.grok/skills/frontend-ui-ux/references/default-editorial-pixel.json +28 -0
- package/.grok/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/.grok/skills/frontend-ui-ux/references/implementation-platforms.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/inclusive-interface.md +11 -5
- package/.grok/skills/frontend-ui-ux/references/interaction-motion.md +34 -78
- package/.grok/skills/frontend-ui-ux/references/motion-guide.md +10 -7
- package/.grok/skills/frontend-ui-ux/references/performance-delivery.md +11 -8
- package/.grok/skills/frontend-ui-ux/references/probe-loop.md +1 -1
- package/.grok/skills/frontend-ui-ux/references/product-direction.md +9 -4
- package/.grok/skills/frontend-ui-ux/references/production-interview.md +17 -0
- package/.grok/skills/frontend-ui-ux/references/redesign-playbook.md +10 -6
- package/.grok/skills/frontend-ui-ux/references/slop-register.md +148 -73
- package/.grok/skills/frontend-ui-ux/references/system-foundations.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/visual-language.md +10 -4
- package/.grok/skills/frontend-ui-ux/references/visual-reconstruction.md +39 -72
- package/.grok/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-driver.mjs +33 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +1555 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +153 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-server.mjs +57 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +139 -0
- package/.grok/skills/frontend-ui-ux/scripts/interface-probe.mjs +363 -0
- package/.grok/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/.grok/skills/lit-adhd/SKILL.md +155 -0
- package/.grok/skills/lit-adhd/references/card-examples.md +131 -0
- package/.grok/skills/lit-adhd/references/status-taxonomy.md +50 -0
- package/.grok/skills/lit-adhd/scripts/lib/brief.mjs +421 -0
- package/.grok/skills/lit-adhd/scripts/lib/collect.mjs +395 -0
- package/.grok/skills/lit-adhd/scripts/lib/gauge.mjs +87 -0
- package/.grok/skills/lit-adhd/scripts/lib/next-step.mjs +22 -0
- package/.grok/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
- package/.grok/skills/lit-adhd/scripts/lib/project-state-root.mjs +55 -0
- package/.grok/skills/lit-adhd/scripts/lib/redact.mjs +64 -0
- package/.grok/skills/lit-adhd/scripts/lib/suggest.mjs +140 -0
- package/.grok/skills/lit-adhd/scripts/lib/transcripts.mjs +232 -0
- package/.grok/skills/lit-adhd/scripts/rewind.mjs +6 -0
- package/.grok/skills/lit-diagram-drawer/SKILL.md +17 -5
- package/.grok/skills/lit-diagram-drawer/references/output-spec.md +2 -2
- package/.grok/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/.grok/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/.grok/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/.grok/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/.grok/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/.grok/skills/lit-diagram-drawer/references/verifier-guide.md +4 -0
- package/.grok/skills/lit-diagram-drawer/scripts/visual-quality.mjs +2 -2
- package/.grok/skills/lit-handoff/SKILL.md +2 -0
- package/.grok/skills/lit-hijack/SKILL.md +459 -0
- package/.grok/skills/lit-hijack/references/capture-and-provenance.md +124 -0
- package/.grok/skills/lit-hijack/references/clean-room-build.md +130 -0
- package/.grok/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/.grok/skills/lit-hijack/references/gap-and-port-design.md +150 -0
- package/.grok/skills/lit-hijack/references/parity-and-report.md +130 -0
- package/.grok/skills/lit-hijack/scripts/hijack.mjs +890 -0
- package/.grok/skills/lit-hijack/scripts/lib/safe-fetch.mjs +213 -0
- package/.grok/skills/lit-plan/SKILL.md +3 -1
- package/.grok/skills/lit-plan/references/plan-schema.md +4 -0
- package/.grok/skills/lit-plan/scripts/scaffold-plan.mjs +9 -2
- package/.grok/skills/lit-plan/scripts/scope-roots.mjs +83 -0
- package/.grok/skills/lit-pptx/SKILL.md +10 -5
- package/.grok/skills/lit-pptx/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/.grok/skills/lit-pptx/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/.grok/skills/lit-pptx/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/.grok/skills/lit-pptx/examples/16-community-programme-riso-ko.md +158 -0
- package/.grok/skills/lit-pptx/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/.grok/skills/lit-pptx/fonts/jetbrains-mono/provenance.json +1 -0
- package/.grok/skills/lit-pptx/references/direction-step.md +34 -8
- package/.grok/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/.grok/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/.grok/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/.grok/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/.grok/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/.grok/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/.grok/skills/lit-pptx/scripts/craft_extras.py +61 -3
- package/.grok/skills/lit-pptx/scripts/deck_output.py +76 -62
- package/.grok/skills/lit-pptx/scripts/embed_fonts.py +14 -1
- package/.grok/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/.grok/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/.grok/skills/lit-pptx/scripts/lib/render-pack.js +389 -582
- package/.grok/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/.grok/skills/lit-pptx/scripts/lib/template-registry.js +60 -8
- package/.grok/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/.grok/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/.grok/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/.grok/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/night/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/paper/pack.yaml +2 -0
- package/.grok/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/.grok/skills/lit-pptx/templates/tonalities/signal/pack.yaml +3 -0
- package/.grok/skills/lit-pptx/templates/tonalities/studio/pack.yaml +2 -0
- package/.grok/skills/lit-recap/SKILL.md +1 -1
- package/.grok/skills/lit-schematic/SKILL.md +140 -0
- package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/.grok/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/.grok/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/.grok/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/.grok/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/.grok/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/.grok/skills/lit-schematic/references/journal-figures.md +72 -0
- package/.grok/skills/lit-schematic/references/kit-api.md +134 -0
- package/.grok/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/.grok/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/.grok/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/.grok/skills/lit-schematic/references/verification.md +100 -0
- package/.grok/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/.grok/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/.grok/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
- package/.grok/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/.grok/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/.grok/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/.grok/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/.grok/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/.grok/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/.grok/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/.grok/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/.grok/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/.grok/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/.grok/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/.grok/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/.grok/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/.grok/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/.grok/skills/lit-schematic/scripts/render.mjs +233 -0
- package/.grok/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/.grok/skills/lit-scientific-visualization/SKILL.md +36 -12
- package/.grok/skills/lit-typographic-motion/SKILL.md +7 -4
- package/.grok/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +11 -0
- package/.grok/skills/lit-typographic-motion/recipes/hero-3d/expected.json +62 -0
- package/.grok/skills/lit-typographic-motion/recipes/hero-3d/index.html +82 -0
- package/.grok/skills/lit-typographic-motion/references/choice-first.md +30 -0
- package/.grok/skills/lit-typographic-motion/references/cover.md +10 -1
- package/.grok/skills/lit-typographic-motion/references/depth.md +119 -0
- package/.grok/skills/lit-typographic-motion/references/treatment.md +9 -1
- package/.grok/skills/lit-typographic-motion/scripts/blender/runner.py +218 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/assets.mjs +230 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/blender.mjs +136 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/board.mjs +153 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/chrome.mjs +61 -32
- package/.grok/skills/lit-typographic-motion/scripts/lib/constants.mjs +4 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-capture.mjs +20 -11
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover-treatment.mjs +26 -2
- package/.grok/skills/lit-typographic-motion/scripts/lib/cover.mjs +12 -5
- package/.grok/skills/lit-typographic-motion/scripts/lib/frame-pool.mjs +78 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/gallery.mjs +160 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/house.mjs +43 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/look.mjs +39 -1
- package/.grok/skills/lit-typographic-motion/scripts/lib/scaffold.mjs +174 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/score.mjs +640 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/sound.mjs +143 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/soundtrack.mjs +12 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-gate.mjs +44 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-qa.mjs +132 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage-session.mjs +51 -3
- package/.grok/skills/lit-typographic-motion/scripts/lib/stage.mjs +74 -9
- package/.grok/skills/lit-typographic-motion/scripts/lib/styles.mjs +224 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/treatment.mjs +198 -4
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/LICENSE +21 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/.grok/skills/lit-typographic-motion/scripts/lib/vendor/three/three.module.js +54571 -0
- package/.grok/skills/lit-typographic-motion/scripts/motion.mjs +56 -6
- package/.grok/skills/lit-typographic-motion/scripts/stage/cover-init.js +20 -8
- package/.grok/skills/lit-typographic-motion/scripts/stage/loop-kit.js +2 -2
- package/.grok/skills/lit-typographic-motion/scripts/stage/stage-kit.js +25 -0
- package/.grok/skills/lit-typographic-motion/styles/paper.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/swiss-signal.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/terminalcore.json +1 -0
- package/.grok/skills/lit-typographic-motion/styles/tidal.json +1 -0
- package/.grok/skills/litgrok/SKILL.md +1 -1
- package/.grok/skills/litresearch/SKILL.md +2 -0
- package/.grok/skills/readme-studio/SKILL.md +14 -12
- package/.grok/skills/readme-studio/references/choice-first.md +24 -0
- package/.grok/skills/start-work/SKILL.md +8 -1
- package/.grok-plugin/plugin.json +4 -1
- package/CHANGELOG.md +18 -0
- package/README.md +11 -11
- package/README_ko-KR.md +11 -11
- package/TRADEMARK.md +12 -0
- package/bin/litgrok.mjs +2 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/showcase/brief.webp +0 -0
- package/docs/assets/showcase/deck.webp +0 -0
- package/docs/assets/showcase/page.webp +0 -0
- package/docs/assets/showcase/pid.webp +0 -0
- package/docs/assets/skills/lit-adhd.webp +0 -0
- package/docs/assets/skills/lit-hijack.webp +0 -0
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference.md +11 -7
- package/docs/reference_ko-KR.md +11 -7
- package/hooks/hooks.json +11 -0
- package/package.json +6 -1
- package/plugin.json +1 -1
- package/.grok/skills/frontend-ui-ux/scripts/page-probe.mjs +0 -105
- package/.grok/skills/frontend-ui-ux/scripts/probe.mjs +0 -91
- package/.grok/skills/frontend-ui-ux/scripts/rule-data.mjs +0 -56
- package/.grok/skills/frontend-ui-ux/scripts/static-probe.mjs +0 -27
- package/.grok/skills/frontend-ui-ux/scripts/static-server.mjs +0 -19
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// treatment.json: the film's plan, written before any render on either path (brief section 5).
|
|
2
2
|
// validateTreatment() is pure and returns the first failing field; loadTreatment() turns that into
|
|
3
3
|
// exit 16 (BLOCKED_TREATMENT_INVALID). Every string in a treatment is inert data.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { BlockedError, DIRECTOR, EXIT } from './constants.mjs';
|
|
7
8
|
import { SKILL_ROOT } from './fonts.mjs';
|
|
9
|
+
import { SCORE_GENRES } from './score.mjs';
|
|
10
|
+
import { EVENT_SOUNDS } from './sound.mjs';
|
|
11
|
+
import { LAYOUT_IDS, STYLE_IDS, TRANSITIONS as STYLE_TRANSITIONS, pickStyle, readStyle, styleColors, styleFamilies } from './styles.mjs';
|
|
8
12
|
|
|
9
13
|
const EPS = 1e-9;
|
|
10
14
|
|
|
@@ -63,9 +67,23 @@ export function restateLimit(request) {
|
|
|
63
67
|
export const sentenceCount = (text) => String(text ?? '').trim().split(/(?<=[.!?。?!])\s+/u).filter((part) => part.trim()).length;
|
|
64
68
|
|
|
65
69
|
const PLACEHOLDER = /<[^<>\n]{1,80}>/u;
|
|
70
|
+
// A cut and an event within a tenth of a second read as the same moment; half the events must sit elsewhere.
|
|
71
|
+
const EVENT_OFF_CUT = 0.1;
|
|
72
|
+
|
|
73
|
+
// A rich stage film plans at least this much world: depth layers, labelled parts, a share of moving shots.
|
|
74
|
+
const RICH_WORLD = Object.freeze({ layers: 3, labels: 6, labelWords: 4, cameraShare: 0.5 });
|
|
75
|
+
const LOCKED_CAMERA = /^\s*(?:none|locked|static|still|hold|fixed|no move)\s*\.?\s*$/iu;
|
|
76
|
+
const SCREEN_ROLES = new Set(['screenshot', 'ui', 'readme-image']);
|
|
77
|
+
|
|
78
|
+
const URL_IN_REQUEST = /\bhttps?:\/\/[^\s"'<>)\]]+/giu;
|
|
79
|
+
|
|
80
|
+
// A link in the request, normalized for comparison: lowercase, no trailing punctuation, slash or .git.
|
|
81
|
+
export const normUrl = (value) => String(value ?? '').trim().replace(/[.,;:!?]+$/u, '').replace(/\/+$/u, '').replace(/\.git$/u, '').toLowerCase();
|
|
82
|
+
export const requestUrls = (request) => [...String(request ?? '').matchAll(URL_IN_REQUEST)].map((m) => normUrl(m[0]));
|
|
66
83
|
|
|
67
84
|
function freeTextLeaves(t) {
|
|
68
|
-
const leaves = [t?.idea, t?.audience, t?.channel, t?.ambition];
|
|
85
|
+
const leaves = [t?.idea, t?.audience, t?.channel, t?.ambition, t?.story?.expect, t?.story?.turn, t?.story?.payoff, t?.protagonist?.look];
|
|
86
|
+
for (const act of Array.isArray(t?.protagonist?.acts) ? t.protagonist.acts : []) leaves.push(act?.action);
|
|
69
87
|
for (const beat of Array.isArray(t?.beats) ? t.beats : []) leaves.push(beat?.purpose, beat?.onScreen, beat?.motion, beat?.sound);
|
|
70
88
|
for (const line of Array.isArray(t?.copy?.lines) ? t.copy.lines : []) leaves.push(line);
|
|
71
89
|
for (const entry of Array.isArray(t?.palette) ? t.palette : []) leaves.push(entry?.role);
|
|
@@ -105,10 +123,10 @@ function coveredSeconds(beats, indices) {
|
|
|
105
123
|
return total;
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
126
|
+
export function validateTreatment(t, { skillRoot = SKILL_ROOT, house = null } = {}) {
|
|
109
127
|
if (!t || typeof t !== 'object' || Array.isArray(t)) return bad('treatment', 'treatment.json must hold one JSON object');
|
|
110
128
|
if (t.mode === 'cover') return bad('mode', 'this is a cover treatment (mode: cover); render it with cover --out <dir> (references/cover.md)');
|
|
111
|
-
const required = ['request', 'genre', 'path', 'pathReason', 'idea', 'audience', 'channel', 'format', 'formatReason', 'durationSec', 'beats', 'subject', 'visualDevices', 'typePlan', 'palette', 'sound', 'copy', 'inventions', 'ambition'];
|
|
129
|
+
const required = ['request', 'genre', 'path', 'pathReason', 'idea', 'audience', 'channel', 'format', 'formatReason', 'durationSec', 'beats', 'subject', 'visualDevices', 'typePlan', 'palette', 'sound', 'copy', 'inventions', 'ambition', 'story'];
|
|
112
130
|
for (const field of required) if (!(field in t) || t[field] === null || t[field] === '') return bad(field, `${field} is missing`);
|
|
113
131
|
// The user's own words may contain angle brackets; everything the model wrote may not.
|
|
114
132
|
const authored = { ...t, request: undefined, copy: t.copy?.source === 'user' ? undefined : t.copy };
|
|
@@ -125,6 +143,17 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
125
143
|
if (!['type', 'stage'].includes(t.path)) return bad('path', 'path must be type or stage');
|
|
126
144
|
if (!['16:9', '9:16'].includes(t.format)) return bad('format', 'format must be 16:9 or 9:16');
|
|
127
145
|
|
|
146
|
+
// The house style: a style the request names wins; then the project house file; then keywords;
|
|
147
|
+
// then the genre. A declared style must be the rule's pick; an undeclared film takes the pick.
|
|
148
|
+
const pick = pickStyle({ request: t.request, copy: t.copy?.lines ?? [], path: t.path, house, genre: t.genre });
|
|
149
|
+
let style = null;
|
|
150
|
+
if ('style' in t && t.style !== undefined && t.style !== null) {
|
|
151
|
+
if (!STYLE_IDS.includes(t.style)) return bad('style', `style must be one of ${STYLE_IDS.join(', ')}`);
|
|
152
|
+
if (t.path === 'type' && !readStyle(t.style).typePreset) return bad('style', 'the type path needs a style with a type preset; paper is stage-only');
|
|
153
|
+
if (t.style !== pick.id) return bad('style', `${pick.reason}; this film takes the ${pick.id} style`);
|
|
154
|
+
style = readStyle(t.style);
|
|
155
|
+
}
|
|
156
|
+
|
|
128
157
|
const limit = restateLimit(t.request);
|
|
129
158
|
const stripped = normalize(stripQuoted(t.request));
|
|
130
159
|
if (!text(t.idea) || sentenceCount(t.idea) !== 1) return bad('idea', 'idea must be one sentence');
|
|
@@ -142,6 +171,16 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
142
171
|
for (const [i, beat] of t.beats.entries()) {
|
|
143
172
|
if (!beat || typeof beat.t0 !== 'number' || typeof beat.t1 !== 'number' || !(beat.t1 > beat.t0)) return bad(`beats[${i}]`, 'each beat needs numeric t0 < t1');
|
|
144
173
|
for (const key of ['purpose', 'onScreen', 'motion', 'sound']) if (!text(beat[key])) return bad(`beats[${i}].${key}`, `beats[${i}].${key} must be a non-empty string`);
|
|
174
|
+
// Shot fields are optional; when present they follow the layout grid.
|
|
175
|
+
if (beat.layout !== undefined && !LAYOUT_IDS.includes(beat.layout)) return bad(`beats[${i}].layout`, `layout must be one of ${LAYOUT_IDS.join(', ')}`);
|
|
176
|
+
if (beat.transition !== undefined && beat.transition !== 'end' && !(style ? style.transitions : STYLE_TRANSITIONS).includes(beat.transition)) return bad(`beats[${i}].transition`, `transition must be one of ${(style ? style.transitions : STYLE_TRANSITIONS).join(', ')} or end`);
|
|
177
|
+
if (i === t.beats.length - 1 && beat.transition !== undefined && beat.transition !== 'end') return bad(`beats[${i}].transition`, 'the last beat hands off to nothing; its transition is end');
|
|
178
|
+
if (i < t.beats.length - 1 && beat.transition === 'end') return bad(`beats[${i}].transition`, 'only the last beat ends; a middle beat hands off with cut, wipe, match, push, iris, dissolve or morph');
|
|
179
|
+
if (beat.hero !== undefined && !text(beat.hero)) return bad(`beats[${i}].hero`, 'a named hero says what moves in one line');
|
|
180
|
+
if (beat.lines !== undefined) {
|
|
181
|
+
if (!Array.isArray(beat.lines) || beat.lines.some((n) => !Number.isInteger(n) || n < 0 || n >= t.copy.lines.length)) return bad(`beats[${i}].lines`, `lines lists copy-line indices 0..${t.copy.lines.length - 1}`);
|
|
182
|
+
if (beat.layout === 'full' && beat.lines.length) return bad(`beats[${i}].lines`, 'a full-frame shot carries no copy lines; the drawing fills the frame');
|
|
183
|
+
}
|
|
145
184
|
}
|
|
146
185
|
const arc = t.genre === 'type-led' ? Math.max(1, t.copy.source === 'user' ? t.copy.lines.length : 1) : DIRECTOR.arcStages[t.genre];
|
|
147
186
|
if (t.beats.length < arc) return bad('beats', `a ${t.genre} film needs at least ${arc} beats (its arc); found ${t.beats.length}`);
|
|
@@ -167,6 +206,11 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
167
206
|
if (!['subject', 'support', 'texture'].includes(device.role)) return bad(`visualDevices[${i}].role`, 'role must be subject, support or texture');
|
|
168
207
|
if (DIRECTOR.textureKinds.includes(device.kind) && device.role !== 'texture') return bad(`visualDevices[${i}].role`, `${device.kind} is always a texture`);
|
|
169
208
|
if (!Array.isArray(device.beats) || !device.beats.every((b) => Number.isInteger(b) && b >= 0 && b < t.beats.length)) return bad(`visualDevices[${i}].beats`, 'beats must list beat indices');
|
|
209
|
+
if (device.kind === 'screen') {
|
|
210
|
+
const assets = Array.isArray(t.product?.assets) ? t.product.assets : [];
|
|
211
|
+
const real = assets.find((a) => a.path === device.asset);
|
|
212
|
+
if (!real || !SCREEN_ROLES.has(real.role)) return bad(`visualDevices[${i}].asset`, 'a product screen shows the product\'s real screenshot, UI or README image: name one listed in product.assets, never draw an invented one');
|
|
213
|
+
}
|
|
170
214
|
}
|
|
171
215
|
if (t.path === 'stage') {
|
|
172
216
|
const subjects = t.visualDevices.filter((d) => d.role === 'subject');
|
|
@@ -177,6 +221,98 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
177
221
|
if (kinds.size < 2) return bad('visualDevices', 'the stage path needs at least 2 distinct non-texture device kinds');
|
|
178
222
|
}
|
|
179
223
|
|
|
224
|
+
// The story: what the viewer expects, the turn they do not, the shot where it lands, the payoff.
|
|
225
|
+
if (!('story' in t) || t.story === null || t.story === '') return bad('story', 'story is missing: { expect, turn, beat, payoff }: what the viewer expects, what happens instead, the shot where it lands, what the ending gives');
|
|
226
|
+
{
|
|
227
|
+
const story = t.story;
|
|
228
|
+
if (!story || typeof story !== 'object') return bad('story', 'story must be an object { expect, turn, beat, payoff }');
|
|
229
|
+
for (const key of ['expect', 'turn', 'payoff']) {
|
|
230
|
+
if (!text(story[key])) return bad(`story.${key}`, `story.${key} must be a non-empty string`);
|
|
231
|
+
if (PLACEHOLDER.test(story[key])) return bad(`story.${key}`, `story.${key} holds a placeholder value; write this film's own`);
|
|
232
|
+
if (sentenceCount(story[key]) > 2) return bad(`story.${key}`, `story.${key} is at most 2 sentences`);
|
|
233
|
+
}
|
|
234
|
+
if (normalize(story.turn) === normalize(story.expect)) return bad('story.turn', 'the turn is what the viewer does not expect; it cannot restate story.expect');
|
|
235
|
+
if (!Number.isInteger(story.beat) || story.beat < 1 || story.beat >= t.beats.length) return bad('story.beat', `story.beat is the index of the shot where the turn lands: after the opening, before the end (1-${t.beats.length - 1})`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The protagonist: one drawn character or hero object that acts across a stage film.
|
|
239
|
+
if (t.path === 'stage') {
|
|
240
|
+
if (!('protagonist' in t) || t.protagonist === null || t.protagonist === '') return bad('protagonist', 'protagonist is missing: { name, kind, look, acts[] }: the character or hero object that carries the film');
|
|
241
|
+
const p = t.protagonist;
|
|
242
|
+
if (!p || typeof p !== 'object') return bad('protagonist', 'protagonist must be an object { name, kind, look, acts }');
|
|
243
|
+
if (!text(p.name) || PLACEHOLDER.test(p.name)) return bad('protagonist.name', 'protagonist.name must name it');
|
|
244
|
+
if (!DIRECTOR.protagonistKinds.includes(p.kind)) return bad('protagonist.kind', `protagonist.kind must be ${DIRECTOR.protagonistKinds.join(' or ')}`);
|
|
245
|
+
if (!text(p.look) || PLACEHOLDER.test(p.look)) return bad('protagonist.look', 'protagonist.look says what it looks like on screen: shape, material, colour role, scale in the frame');
|
|
246
|
+
if (!Array.isArray(p.acts) || !p.acts.length) return bad('protagonist.acts', 'protagonist.acts lists what it does, shot by shot: [{ beat, action }]');
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
for (const [i, act] of p.acts.entries()) {
|
|
249
|
+
if (!Number.isInteger(act?.beat) || act.beat < 0 || act.beat >= t.beats.length) return bad(`protagonist.acts[${i}].beat`, 'protagonist.acts[].beat must be a beat index');
|
|
250
|
+
if (seen.has(act.beat)) return bad(`protagonist.acts[${i}].beat`, 'one act per shot');
|
|
251
|
+
seen.add(act.beat);
|
|
252
|
+
if (!text(act.action) || PLACEHOLDER.test(act.action)) return bad(`protagonist.acts[${i}].action`, 'protagonist.acts[].action says what it does in this shot, as a viewer would see it');
|
|
253
|
+
}
|
|
254
|
+
const carried = coveredSeconds(t.beats, [...seen]);
|
|
255
|
+
if (carried < 0.5 * t.durationSec - EPS) return bad('protagonist.acts', `it acts for ${carried.toFixed(1)} s, under half the film; it carries the film, not one shot`);
|
|
256
|
+
if (!seen.has(t.story.beat)) return bad('protagonist.acts', `the turn lands at shot ${t.story.beat + 1}, and the protagonist has no act there`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// A named product: its real logo, images, UI and words, never invented ones.
|
|
260
|
+
{
|
|
261
|
+
const urls = requestUrls(t.request);
|
|
262
|
+
if (!('product' in t) || t.product === null) {
|
|
263
|
+
if (urls.length) return bad('product', `product is missing: the request links ${urls[0]}; fetch its real assets with motion.mjs assets --out <dir> --url <link> and list them`);
|
|
264
|
+
} else {
|
|
265
|
+
const p = t.product;
|
|
266
|
+
if (!p || typeof p !== 'object') return bad('product', 'product must be an object { name, url, assets }');
|
|
267
|
+
if (!text(p.name) || PLACEHOLDER.test(p.name)) return bad('product.name', 'product.name must name the product');
|
|
268
|
+
if (urls.length && !urls.includes(normUrl(p.url))) return bad('product.url', `product.url must be the link the request gives (${urls.join(', ')})`);
|
|
269
|
+
if (!('unreachable' in p) || p.unreachable === undefined || p.unreachable === null) {
|
|
270
|
+
if (!Array.isArray(p.assets) || !p.assets.length) return bad('product.assets', 'product.assets lists the real assets the film uses: [{ path, role, source, sha256 }]; when none can be reached set unreachable and ask the user');
|
|
271
|
+
for (const [i, asset] of p.assets.entries()) {
|
|
272
|
+
const field = `product.assets[${i}]`;
|
|
273
|
+
if (typeof asset?.path !== 'string' || !asset.path.startsWith('stage/assets/') || asset.path.split('/').includes('..')) return bad(`${field}.path`, 'product asset paths are files under stage/assets/, relative to the output directory');
|
|
274
|
+
if (!DIRECTOR.assetRoles.includes(asset.role)) return bad(`${field}.role`, `product asset roles: ${DIRECTOR.assetRoles.join(', ')}`);
|
|
275
|
+
if (!['fetched', 'user'].includes(asset.source)) return bad(`${field}.source`, 'product asset source is fetched (by motion.mjs assets) or user (supplied)');
|
|
276
|
+
if (!/^[0-9a-f]{64}$/u.test(asset.sha256 ?? '')) return bad(`${field}.sha256`, 'product asset sha256 must be the file\'s SHA-256');
|
|
277
|
+
}
|
|
278
|
+
if (t.subject?.source === 'invented') return bad('subject.source', 'the product is real: the subject is the product (source user), its specifics from its own words');
|
|
279
|
+
} else if (!text(p.unreachable)) return bad('product.unreachable', 'product.unreachable says what could not be reached and why');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// The 3D route: a depth3d subject is drawn with the pinned three.js on the GPU rung.
|
|
284
|
+
if ('depth' in t && t.depth !== undefined && t.depth !== null && !['2d', '3d'].includes(t.depth)) return bad('depth', 'depth is 2d (the default) or 3d');
|
|
285
|
+
{
|
|
286
|
+
const solid = t.visualDevices.some((d) => d.kind === 'depth3d' && d.role === 'subject');
|
|
287
|
+
if (t.path !== 'type') {
|
|
288
|
+
if (solid && t.depth !== '3d') return bad('depth', 'the subject is a depth3d device: set depth to 3d, so the film takes the 3D route');
|
|
289
|
+
if (t.depth === '3d' && !solid) return bad('depth', 'depth 3d needs the subject drawn as a depth3d device (role subject)');
|
|
290
|
+
} else if (t.depth === '3d') return bad('depth', 'depth 3d is a stage-path route');
|
|
291
|
+
if ('render3d' in t && t.render3d !== undefined && t.render3d !== null && !['three', 'blender'].includes(t.render3d)) return bad('render3d', 'render3d is three (the GPU rung, the default) or blender (the offline Cycles rung)');
|
|
292
|
+
if (t.render3d === 'blender' && t.depth !== '3d') return bad('render3d', 'the Blender rung renders a 3D world: set depth to 3d with a depth3d subject');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// A rich stage film plans its density before any page exists: the depth layers of its world,
|
|
296
|
+
// the labelled parts the viewer reads in it and the shots where the camera moves.
|
|
297
|
+
if (t.path === 'stage' && t.register !== 'minimal') {
|
|
298
|
+
const world = t.world;
|
|
299
|
+
if (!world || typeof world !== 'object') return bad('world', 'world is missing: { layers, labels, camera }: a rich film plans its layered world, its labelled parts and its camera moves');
|
|
300
|
+
if (!Array.isArray(world.layers) || world.layers.filter(text).length < RICH_WORLD.layers) return bad('world.layers', `world.layers holds at least ${RICH_WORLD.layers} depth layers, back to front, each saying what lives there`);
|
|
301
|
+
for (const [i, label] of (Array.isArray(world.labels) ? world.labels : []).entries()) {
|
|
302
|
+
if (!text(label) || PLACEHOLDER.test(label)) return bad(`world.labels[${i}]`, 'world.labels[] is the label as it appears on screen');
|
|
303
|
+
if (label.trim().split(/\s+/u).length > RICH_WORLD.labelWords) return bad(`world.labels[${i}]`, `a label is at most ${RICH_WORLD.labelWords} words; longer text is copy`);
|
|
304
|
+
}
|
|
305
|
+
if (!Array.isArray(world.labels) || world.labels.length < RICH_WORLD.labels) return bad('world.labels', `world.labels holds at least ${RICH_WORLD.labels} labelled parts the viewer reads on screen`);
|
|
306
|
+
if (!Array.isArray(world.camera)) return bad('world.camera', 'world.camera lists the camera moves: [{ beat, move }]');
|
|
307
|
+
const moving = new Set();
|
|
308
|
+
for (const [i, move] of world.camera.entries()) {
|
|
309
|
+
if (!Number.isInteger(move?.beat) || move.beat < 0 || move.beat >= t.beats.length) return bad(`world.camera[${i}].beat`, 'world.camera[].beat must be a beat index');
|
|
310
|
+
if (!text(move.move) || PLACEHOLDER.test(move.move) || LOCKED_CAMERA.test(move.move)) return bad(`world.camera[${i}].move`, 'name the move (push-in, truck, crane, orbit, rack focus); a locked shot is not listed');
|
|
311
|
+
moving.add(move.beat);
|
|
312
|
+
}
|
|
313
|
+
if (moving.size < RICH_WORLD.cameraShare * t.beats.length - EPS) return bad('world.camera', `the camera moves in ${moving.size} of ${t.beats.length} shots; a rich film moves it in at least half (keep the flash audit with slow light changes, not a locked camera)`);
|
|
314
|
+
}
|
|
315
|
+
|
|
180
316
|
const plan = t.typePlan;
|
|
181
317
|
if (!plan || typeof plan !== 'object' || !Array.isArray(plan.faces) || plan.faces.length === 0) return bad('typePlan', 'typePlan must be { faces: [...], hierarchy, maxWordsOnScreen }');
|
|
182
318
|
const unknownFace = plan.faces.find((face) => !DIRECTOR.faces.includes(face));
|
|
@@ -190,19 +326,50 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
190
326
|
if (!entry || !/^#[0-9a-f]{6}$/iu.test(entry.hex ?? '')) return bad(`palette[${i}].hex`, 'hex must look like #1A2B3C');
|
|
191
327
|
if (!text(entry.role)) return bad(`palette[${i}].role`, 'each colour needs a role');
|
|
192
328
|
}
|
|
329
|
+
// A declared style owns the look: palette, faces and generated bed come from it.
|
|
330
|
+
if (style) {
|
|
331
|
+
const allowed = new Set(styleColors(style));
|
|
332
|
+
const offStyle = t.palette.find((entry) => !allowed.has(String(entry.hex).toLowerCase()));
|
|
333
|
+
if (offStyle) return bad('palette', `"${offStyle.hex}" is not on the ${style.id} style; copy may only use ${style.textRoles.join(', ')}`);
|
|
334
|
+
const families = styleFamilies(style);
|
|
335
|
+
const unknownFace = t.typePlan.faces.find((face) => !families.has(face));
|
|
336
|
+
if (unknownFace !== undefined) return bad('typePlan.faces', `"${unknownFace}" is not a ${style.id} face; use ${[...families].join(', ')}`);
|
|
337
|
+
}
|
|
193
338
|
|
|
194
339
|
const sound = t.sound;
|
|
195
340
|
if (!sound || typeof sound !== 'object' || !DIRECTOR.soundModes.includes(sound.mode)) return bad('sound.mode', `sound.mode must be one of ${DIRECTOR.soundModes.join(', ')}`);
|
|
196
341
|
if (!text(sound.plan)) return bad('sound.plan', 'sound.plan must be a non-empty string');
|
|
197
|
-
if (sound.mode === 'none' && !silenceAsked(t.request) && !mutedChannel(t.channel)) return bad('sound.mode', 'none is allowed only when the user asked for silence or the channel plays muted by design');
|
|
342
|
+
if (sound.mode === 'none' && !silenceAsked(t.request) && !mutedChannel(t.channel)) return bad('sound.mode', 'none is allowed only when the user asked for silence or the channel plays muted by design; the default is a score');
|
|
343
|
+
if (sound.mode === 'generated' && t.register === 'rich') return bad('sound.mode', 'a rich film scores its own music (mode score); the style beds are for minimal films');
|
|
344
|
+
if ('genre' in sound && sound.genre !== undefined && sound.genre !== null && !SCORE_GENRES.includes(sound.genre)) return bad('sound.genre', `sound.genre must be one of ${SCORE_GENRES.join(', ')}`);
|
|
198
345
|
if (sound.mode === 'generated') {
|
|
199
346
|
const timbre = typeof sound.palette === 'string' ? sound.palette : sound.palette?.timbre;
|
|
200
347
|
if (!DIRECTOR.timbres.includes(timbre)) return bad('sound.palette', `a generated bed needs a timbre palette: ${DIRECTOR.timbres.join(', ')}`);
|
|
201
348
|
const tempo = typeof sound.palette === 'object' ? sound.palette.tempo : undefined;
|
|
202
349
|
if (tempo !== undefined && !(Number.isFinite(tempo) && tempo >= 50 && tempo <= 180)) return bad('sound.palette', 'tempo must be 50-180 BPM');
|
|
350
|
+
// A styled film's bed is one of the style's own; a minimal film's bed is the style default.
|
|
351
|
+
if (style && typeof sound.palette === 'object') {
|
|
352
|
+
const same = (b) => ['timbre', 'key', 'mode', 'tempo'].every((k) => String(b[k]).toLowerCase() === String(sound.palette[k]).toLowerCase());
|
|
353
|
+
if (!style.beds.some(same)) return bad('sound.palette', `a ${style.id} bed is one of its own: ${style.beds.map((b) => `${b.timbre} ${b.key} ${b.mode} ${b.tempo}`).join('; ')}`);
|
|
354
|
+
}
|
|
203
355
|
}
|
|
204
356
|
if ((sound.mode === 'supplied' || sound.mode === 'authored') && !text(sound.file)) return bad('sound.file', `${sound.mode} sound names its WAV or audio file in sound.file`);
|
|
205
357
|
if (sound.mode === 'authored' && !/^stage\//u.test(sound.file)) return bad('sound.file', 'an authored track is a WAV the model wrote under stage/');
|
|
358
|
+
if ((sound.mode === 'generated' || sound.mode === 'score') && t.path === 'stage' && !('events' in sound)) return bad('sound.events', 'list the on-screen events that make a sound: [{ t, on, kind }], at least 2, half of them away from the cuts');
|
|
359
|
+
if ('events' in sound) {
|
|
360
|
+
const events = sound.events;
|
|
361
|
+
if (!Array.isArray(events) || events.length < 2) return bad('sound.events', 'at least 2 on-screen events that make a sound: [{ t, on, kind }]');
|
|
362
|
+
const cuts = t.beats.slice(1).map((b) => b.t0);
|
|
363
|
+
let offCut = 0;
|
|
364
|
+
for (let i = 0; i < events.length; i += 1) {
|
|
365
|
+
const e = events[i], f = `sound.events[${i}]`;
|
|
366
|
+
if (typeof e?.t !== 'number' || !(e.t > 0 && e.t < t.durationSec)) return bad(`${f}.t`, `the second the event happens on screen, inside the film (0-${t.durationSec})`);
|
|
367
|
+
if (!text(e.on) || PLACEHOLDER.test(e.on)) return bad(`${f}.on`, 'what happens on screen at that moment, as a viewer would see it');
|
|
368
|
+
if (!EVENT_SOUNDS.includes(e.kind)) return bad(`${f}.kind`, `must be one of ${EVENT_SOUNDS.join(', ')}`);
|
|
369
|
+
if (cuts.every((c) => Math.abs(c - e.t) >= EVENT_OFF_CUT - 1e-9)) offCut += 1;
|
|
370
|
+
}
|
|
371
|
+
if (offCut * 2 < events.length) return bad('sound.events', `${events.length - offCut} of ${events.length} events sit on a cut; at least half sync to something moving inside a shot`);
|
|
372
|
+
}
|
|
206
373
|
|
|
207
374
|
if (t.copy.source === 'user') {
|
|
208
375
|
const whole = normalize(t.request);
|
|
@@ -218,6 +385,19 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
218
385
|
if (t.copy.source !== 'user' && !typeLedCue(t.request)) return bad('path', 'the type path needs the user\'s own words or a type-led cue in the request');
|
|
219
386
|
}
|
|
220
387
|
|
|
388
|
+
// The register defaults to rich: a full frame unless the film opts into negative space.
|
|
389
|
+
if ('register' in t && t.register !== undefined && t.register !== null) {
|
|
390
|
+
if (!['rich', 'minimal'].includes(t.register)) return bad('register', 'register is rich (the default) or minimal');
|
|
391
|
+
} else {
|
|
392
|
+
t.register = 'rich';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// preview: true only when the request asks for a preview or storyboard first.
|
|
396
|
+
if ('preview' in t && t.preview !== undefined && t.preview !== null) {
|
|
397
|
+
if (typeof t.preview !== 'boolean') return bad('preview', 'preview is a boolean');
|
|
398
|
+
if (t.preview && !/preview|storyboard|스토리보드|미리보기/iu.test(t.request)) return bad('preview', 'preview is true only when the request asks for a preview or storyboard');
|
|
399
|
+
}
|
|
400
|
+
|
|
221
401
|
if (!Array.isArray(t.inventions) || !t.inventions.every(text)) return bad('inventions', 'inventions must be an array of strings');
|
|
222
402
|
const invented = t.copy.source === 'invented' || s.source === 'invented';
|
|
223
403
|
if (invented && t.inventions.length === 0) return bad('inventions', 'list every invention: the subject, its specifics and invented copy');
|
|
@@ -231,6 +411,19 @@ export function validateTreatment(t, { skillRoot = SKILL_ROOT } = {}) {
|
|
|
231
411
|
|
|
232
412
|
export function treatmentPath(outDir) { return join(outDir, 'treatment.json'); }
|
|
233
413
|
|
|
414
|
+
// The product's files on disk: each listed asset exists and still has its hash. An unreachable
|
|
415
|
+
// product is exit 22: the film stops and the reply asks the user for the logo, screens and words.
|
|
416
|
+
export function checkProductFiles(outDir, t) {
|
|
417
|
+
const product = t.product;
|
|
418
|
+
if (!product) return;
|
|
419
|
+
if (product.unreachable) throw new BlockedError(EXIT.ASSET_MISSING, `BLOCKED_ASSET_MISSING: ${product.name}'s real assets could not be reached (${product.unreachable}); ask the user for its logo, a screenshot or the README images, put them in ${join(outDir, 'stage', 'assets')} and list them; never draw invented product screens`);
|
|
420
|
+
for (const asset of product.assets ?? []) {
|
|
421
|
+
const file = join(outDir, asset.path);
|
|
422
|
+
if (!existsSync(file)) throw new BlockedError(EXIT.ASSET_MISSING, `BLOCKED_ASSET_MISSING: ${asset.path} is listed in product.assets and missing; fetch or supply it, never draw a stand-in`);
|
|
423
|
+
if (createHash('sha256').update(readFileSync(file)).digest('hex') !== asset.sha256) throw new BlockedError(EXIT.STAGE_CONTRACT, `STAGE_CONTRACT_ERROR: ${asset.path} does not match its sha256 in product.assets; list it again after changing it`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
234
427
|
// Exit 16 with the field name: no render runs without a valid treatment in the output directory.
|
|
235
428
|
export function loadTreatment(outDir, options = {}) {
|
|
236
429
|
const path = treatmentPath(outDir);
|
|
@@ -239,6 +432,7 @@ export function loadTreatment(outDir, options = {}) {
|
|
|
239
432
|
try { parsed = JSON.parse(readFileSync(path, 'utf8')); } catch (error) { throw new BlockedError(EXIT.TREATMENT_INVALID, `BLOCKED_TREATMENT_INVALID field=treatment: treatment.json is not valid JSON (${error.message})`); }
|
|
240
433
|
const result = validateTreatment(parsed, options);
|
|
241
434
|
if (!result.ok) throw new BlockedError(EXIT.TREATMENT_INVALID, `BLOCKED_TREATMENT_INVALID field=${result.field}: ${result.message}`);
|
|
435
|
+
checkProductFiles(outDir, parsed);
|
|
242
436
|
return parsed;
|
|
243
437
|
}
|
|
244
438
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2010-2024 three.js authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BackSide,
|
|
7
|
+
BoxGeometry,
|
|
8
|
+
Mesh,
|
|
9
|
+
MeshBasicMaterial,
|
|
10
|
+
MeshStandardMaterial,
|
|
11
|
+
PointLight,
|
|
12
|
+
Scene,
|
|
13
|
+
} from 'three';
|
|
14
|
+
|
|
15
|
+
class RoomEnvironment extends Scene {
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
const geometry = new BoxGeometry();
|
|
22
|
+
geometry.deleteAttribute( 'uv' );
|
|
23
|
+
|
|
24
|
+
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
|
|
25
|
+
const boxMaterial = new MeshStandardMaterial();
|
|
26
|
+
|
|
27
|
+
const mainLight = new PointLight( 0xffffff, 900, 28, 2 );
|
|
28
|
+
mainLight.position.set( 0.418, 16.199, 0.300 );
|
|
29
|
+
this.add( mainLight );
|
|
30
|
+
|
|
31
|
+
const room = new Mesh( geometry, roomMaterial );
|
|
32
|
+
room.position.set( - 0.757, 13.219, 0.717 );
|
|
33
|
+
room.scale.set( 31.713, 28.305, 28.591 );
|
|
34
|
+
this.add( room );
|
|
35
|
+
|
|
36
|
+
const box1 = new Mesh( geometry, boxMaterial );
|
|
37
|
+
box1.position.set( - 10.906, 2.009, 1.846 );
|
|
38
|
+
box1.rotation.set( 0, - 0.195, 0 );
|
|
39
|
+
box1.scale.set( 2.328, 7.905, 4.651 );
|
|
40
|
+
this.add( box1 );
|
|
41
|
+
|
|
42
|
+
const box2 = new Mesh( geometry, boxMaterial );
|
|
43
|
+
box2.position.set( - 5.607, - 0.754, - 0.758 );
|
|
44
|
+
box2.rotation.set( 0, 0.994, 0 );
|
|
45
|
+
box2.scale.set( 1.970, 1.534, 3.955 );
|
|
46
|
+
this.add( box2 );
|
|
47
|
+
|
|
48
|
+
const box3 = new Mesh( geometry, boxMaterial );
|
|
49
|
+
box3.position.set( 6.167, 0.857, 7.803 );
|
|
50
|
+
box3.rotation.set( 0, 0.561, 0 );
|
|
51
|
+
box3.scale.set( 3.927, 6.285, 3.687 );
|
|
52
|
+
this.add( box3 );
|
|
53
|
+
|
|
54
|
+
const box4 = new Mesh( geometry, boxMaterial );
|
|
55
|
+
box4.position.set( - 2.017, 0.018, 6.124 );
|
|
56
|
+
box4.rotation.set( 0, 0.333, 0 );
|
|
57
|
+
box4.scale.set( 2.002, 4.566, 2.064 );
|
|
58
|
+
this.add( box4 );
|
|
59
|
+
|
|
60
|
+
const box5 = new Mesh( geometry, boxMaterial );
|
|
61
|
+
box5.position.set( 2.291, - 0.756, - 2.621 );
|
|
62
|
+
box5.rotation.set( 0, - 0.286, 0 );
|
|
63
|
+
box5.scale.set( 1.546, 1.552, 1.496 );
|
|
64
|
+
this.add( box5 );
|
|
65
|
+
|
|
66
|
+
const box6 = new Mesh( geometry, boxMaterial );
|
|
67
|
+
box6.position.set( - 2.193, - 0.369, - 5.547 );
|
|
68
|
+
box6.rotation.set( 0, 0.516, 0 );
|
|
69
|
+
box6.scale.set( 3.875, 3.487, 2.986 );
|
|
70
|
+
this.add( box6 );
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// -x right
|
|
74
|
+
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
|
|
75
|
+
light1.position.set( - 16.116, 14.37, 8.208 );
|
|
76
|
+
light1.scale.set( 0.1, 2.428, 2.739 );
|
|
77
|
+
this.add( light1 );
|
|
78
|
+
|
|
79
|
+
// -x left
|
|
80
|
+
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
|
|
81
|
+
light2.position.set( - 16.109, 18.021, - 8.207 );
|
|
82
|
+
light2.scale.set( 0.1, 2.425, 2.751 );
|
|
83
|
+
this.add( light2 );
|
|
84
|
+
|
|
85
|
+
// +x
|
|
86
|
+
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
|
|
87
|
+
light3.position.set( 14.904, 12.198, - 1.832 );
|
|
88
|
+
light3.scale.set( 0.15, 4.265, 6.331 );
|
|
89
|
+
this.add( light3 );
|
|
90
|
+
|
|
91
|
+
// +z
|
|
92
|
+
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
|
|
93
|
+
light4.position.set( - 0.462, 8.89, 14.520 );
|
|
94
|
+
light4.scale.set( 4.38, 5.441, 0.088 );
|
|
95
|
+
this.add( light4 );
|
|
96
|
+
|
|
97
|
+
// -z
|
|
98
|
+
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
|
|
99
|
+
light5.position.set( 3.235, 11.486, - 12.541 );
|
|
100
|
+
light5.scale.set( 2.5, 2.0, 0.1 );
|
|
101
|
+
this.add( light5 );
|
|
102
|
+
|
|
103
|
+
// +y
|
|
104
|
+
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
|
|
105
|
+
light6.position.set( 0.0, 20.0, 0.0 );
|
|
106
|
+
light6.scale.set( 1.0, 0.1, 1.0 );
|
|
107
|
+
this.add( light6 );
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose() {
|
|
112
|
+
|
|
113
|
+
const resources = new Set();
|
|
114
|
+
|
|
115
|
+
this.traverse( ( object ) => {
|
|
116
|
+
|
|
117
|
+
if ( object.isMesh ) {
|
|
118
|
+
|
|
119
|
+
resources.add( object.geometry );
|
|
120
|
+
resources.add( object.material );
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
for ( const resource of resources ) {
|
|
127
|
+
|
|
128
|
+
resource.dispose();
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createAreaLightMaterial( intensity ) {
|
|
137
|
+
|
|
138
|
+
const material = new MeshBasicMaterial();
|
|
139
|
+
material.color.setScalar( intensity );
|
|
140
|
+
return material;
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { RoomEnvironment };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxGeometry,
|
|
3
|
+
Vector3
|
|
4
|
+
} from 'three';
|
|
5
|
+
|
|
6
|
+
const _tempNormal = new Vector3();
|
|
7
|
+
|
|
8
|
+
function getUv( faceDirVector, normal, uvAxis, projectionAxis, radius, sideLength ) {
|
|
9
|
+
|
|
10
|
+
const totArcLength = 2 * Math.PI * radius / 4;
|
|
11
|
+
|
|
12
|
+
// length of the planes between the arcs on each axis
|
|
13
|
+
const centerLength = Math.max( sideLength - 2 * radius, 0 );
|
|
14
|
+
const halfArc = Math.PI / 4;
|
|
15
|
+
|
|
16
|
+
// Get the vector projected onto the Y plane
|
|
17
|
+
_tempNormal.copy( normal );
|
|
18
|
+
_tempNormal[ projectionAxis ] = 0;
|
|
19
|
+
_tempNormal.normalize();
|
|
20
|
+
|
|
21
|
+
// total amount of UV space alloted to a single arc
|
|
22
|
+
const arcUvRatio = 0.5 * totArcLength / ( totArcLength + centerLength );
|
|
23
|
+
|
|
24
|
+
// the distance along one arc the point is at
|
|
25
|
+
const arcAngleRatio = 1.0 - ( _tempNormal.angleTo( faceDirVector ) / halfArc );
|
|
26
|
+
|
|
27
|
+
if ( Math.sign( _tempNormal[ uvAxis ] ) === 1 ) {
|
|
28
|
+
|
|
29
|
+
return arcAngleRatio * arcUvRatio;
|
|
30
|
+
|
|
31
|
+
} else {
|
|
32
|
+
|
|
33
|
+
// total amount of UV space alloted to the plane between the arcs
|
|
34
|
+
const lenUv = centerLength / ( totArcLength + centerLength );
|
|
35
|
+
return lenUv + arcUvRatio + arcUvRatio * ( 1.0 - arcAngleRatio );
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class RoundedBoxGeometry extends BoxGeometry {
|
|
42
|
+
|
|
43
|
+
constructor( width = 1, height = 1, depth = 1, segments = 2, radius = 0.1 ) {
|
|
44
|
+
|
|
45
|
+
// ensure segments is odd so we have a plane connecting the rounded corners
|
|
46
|
+
segments = segments * 2 + 1;
|
|
47
|
+
|
|
48
|
+
// ensure radius isn't bigger than shortest side
|
|
49
|
+
radius = Math.min( width / 2, height / 2, depth / 2, radius );
|
|
50
|
+
|
|
51
|
+
super( 1, 1, 1, segments, segments, segments );
|
|
52
|
+
|
|
53
|
+
// if we just have one segment we're the same as a regular box
|
|
54
|
+
if ( segments === 1 ) return;
|
|
55
|
+
|
|
56
|
+
const geometry2 = this.toNonIndexed();
|
|
57
|
+
|
|
58
|
+
this.index = null;
|
|
59
|
+
this.attributes.position = geometry2.attributes.position;
|
|
60
|
+
this.attributes.normal = geometry2.attributes.normal;
|
|
61
|
+
this.attributes.uv = geometry2.attributes.uv;
|
|
62
|
+
|
|
63
|
+
//
|
|
64
|
+
|
|
65
|
+
const position = new Vector3();
|
|
66
|
+
const normal = new Vector3();
|
|
67
|
+
|
|
68
|
+
const box = new Vector3( width, height, depth ).divideScalar( 2 ).subScalar( radius );
|
|
69
|
+
|
|
70
|
+
const positions = this.attributes.position.array;
|
|
71
|
+
const normals = this.attributes.normal.array;
|
|
72
|
+
const uvs = this.attributes.uv.array;
|
|
73
|
+
|
|
74
|
+
const faceTris = positions.length / 6;
|
|
75
|
+
const faceDirVector = new Vector3();
|
|
76
|
+
const halfSegmentSize = 0.5 / segments;
|
|
77
|
+
|
|
78
|
+
for ( let i = 0, j = 0; i < positions.length; i += 3, j += 2 ) {
|
|
79
|
+
|
|
80
|
+
position.fromArray( positions, i );
|
|
81
|
+
normal.copy( position );
|
|
82
|
+
normal.x -= Math.sign( normal.x ) * halfSegmentSize;
|
|
83
|
+
normal.y -= Math.sign( normal.y ) * halfSegmentSize;
|
|
84
|
+
normal.z -= Math.sign( normal.z ) * halfSegmentSize;
|
|
85
|
+
normal.normalize();
|
|
86
|
+
|
|
87
|
+
positions[ i + 0 ] = box.x * Math.sign( position.x ) + normal.x * radius;
|
|
88
|
+
positions[ i + 1 ] = box.y * Math.sign( position.y ) + normal.y * radius;
|
|
89
|
+
positions[ i + 2 ] = box.z * Math.sign( position.z ) + normal.z * radius;
|
|
90
|
+
|
|
91
|
+
normals[ i + 0 ] = normal.x;
|
|
92
|
+
normals[ i + 1 ] = normal.y;
|
|
93
|
+
normals[ i + 2 ] = normal.z;
|
|
94
|
+
|
|
95
|
+
const side = Math.floor( i / faceTris );
|
|
96
|
+
|
|
97
|
+
switch ( side ) {
|
|
98
|
+
|
|
99
|
+
case 0: // right
|
|
100
|
+
|
|
101
|
+
// generate UVs along Z then Y
|
|
102
|
+
faceDirVector.set( 1, 0, 0 );
|
|
103
|
+
uvs[ j + 0 ] = getUv( faceDirVector, normal, 'z', 'y', radius, depth );
|
|
104
|
+
uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'z', radius, height );
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 1: // left
|
|
108
|
+
|
|
109
|
+
// generate UVs along Z then Y
|
|
110
|
+
faceDirVector.set( - 1, 0, 0 );
|
|
111
|
+
uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'z', 'y', radius, depth );
|
|
112
|
+
uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'z', radius, height );
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 2: // top
|
|
116
|
+
|
|
117
|
+
// generate UVs along X then Z
|
|
118
|
+
faceDirVector.set( 0, 1, 0 );
|
|
119
|
+
uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'z', radius, width );
|
|
120
|
+
uvs[ j + 1 ] = getUv( faceDirVector, normal, 'z', 'x', radius, depth );
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case 3: // bottom
|
|
124
|
+
|
|
125
|
+
// generate UVs along X then Z
|
|
126
|
+
faceDirVector.set( 0, - 1, 0 );
|
|
127
|
+
uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'z', radius, width );
|
|
128
|
+
uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'z', 'x', radius, depth );
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 4: // front
|
|
132
|
+
|
|
133
|
+
// generate UVs along X then Y
|
|
134
|
+
faceDirVector.set( 0, 0, 1 );
|
|
135
|
+
uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'y', radius, width );
|
|
136
|
+
uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'x', radius, height );
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case 5: // back
|
|
140
|
+
|
|
141
|
+
// generate UVs along X then Y
|
|
142
|
+
faceDirVector.set( 0, 0, - 1 );
|
|
143
|
+
uvs[ j + 0 ] = getUv( faceDirVector, normal, 'x', 'y', radius, width );
|
|
144
|
+
uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'x', radius, height );
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { RoundedBoxGeometry };
|