<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="/assets/github-CoHtXIyb.svg"/><link rel="preload" as="image" href="/assets/x-_YOlxEmy.svg"/><link rel="icon" href="https://firtoz.com/media/81b50cc6-d514-4e22-a984-e72746e63078.png"/><title>firtoz | notes on building</title><meta name="description" content="Notes on building real things: AI, web, games, and the systems behind them."/><meta property="og:title" content="firtoz | notes on building"/><meta property="og:description" content="Notes on building real things: AI, web, games, and the systems behind them."/><meta property="og:site_name" content="firtoz"/><meta property="og:type" content="website"/><meta property="og:url" content="https://firtoz.com/"/><meta property="og:image" content="https://firtoz.com/og.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:site" content="@firtoz"/><meta name="twitter:title" content="firtoz | notes on building"/><meta name="twitter:description" content="Notes on building real things: AI, web, games, and the systems behind them."/><meta name="twitter:image" content="https://firtoz.com/og.png"/><link rel="preconnect" href="https://eu-assets.i.posthog.com" crossorigin="anonymous"/><link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/feed.xml"/><link rel="modulepreload" href="/assets/entry.client-D3_8OtZa.js"/><link rel="modulepreload" href="/assets/root-DZSIzbIP.js"/><link rel="modulepreload" href="/assets/entry.client-D3_8OtZa.js"/><link rel="modulepreload" href="/assets/ConcurrentSubmitterProvider-BpGDvZ4b.js"/><link rel="modulepreload" href="/assets/vendor-zod-BDmUoUzW.js"/><link rel="modulepreload" href="/assets/BlogLayout-D2-XJNc5.js"/><link rel="modulepreload" href="/assets/PostCard-D7tBjRHp.js"/><link rel="modulepreload" href="/assets/vendor-posthog-DTHv5gVh.js"/><link rel="modulepreload" href="/assets/isAdmin-COlvcvZU.js"/><link rel="modulepreload" href="/assets/github-DS4be7QB.js"/><link rel="modulepreload" href="/assets/x-CUb4TX-7.js"/><link rel="modulepreload" href="/assets/BlogContentRenderer-ewM_QnPn.js"/><link rel="modulepreload" href="/assets/shared-extensions-DbeyeYZK.js"/><link rel="modulepreload" href="/assets/nodeUtils-C14IIQH4.js"/><link rel="modulepreload" href="/assets/code-highlighter-DZNs8-E7.js"/><link rel="modulepreload" href="/assets/vendor-refractor-BEGDkSND.js"/><link rel="modulepreload" href="/assets/dialog-CcWRISlu.js"/><link rel="modulepreload" href="/assets/button-D-YIgNus.js"/><link rel="modulepreload" href="/assets/vendor-utils-Ce1ZTWB2.js"/><link rel="modulepreload" href="/assets/utils-BPQb07fs.js"/><link rel="modulepreload" href="/assets/PostEditorContext-BzkB-NDb.js"/><link rel="modulepreload" href="/assets/select-B_NaCOnR.js"/><link rel="modulepreload" href="/assets/label-DiCFLVIW.js"/><link rel="modulepreload" href="/assets/tiptap-utils-mA0cZ5w4.js"/><link rel="modulepreload" href="/assets/_layout-B2VWF10v.js"/><link rel="modulepreload" href="/assets/og-urls-BbsTksUP.js"/><link rel="modulepreload" href="/assets/cache-BAr-CJqI.js"/><link rel="modulepreload" href="/assets/_-BPrHGPHx.js"/><style>
							html { color-scheme: light dark; }
							html, body { 
								background-color: #fff; 
								color: #111; 
							}
							@media (prefers-color-scheme: dark) {
								html, body { 
									background-color: #030712; 
									color: #f6f3f4; 
								}
							}
						</style><style>/**
 * Critical CSS for prose content - inlined in <head> for fast initial render
 * This file can be imported as raw text: import criticalCss from './prose-critical.css?raw'
 */

.prose-content {
	color: var(--color-prose-body);
	font-size: 1.0625rem;
	line-height: 1.75;
}
.ProseMirror h1,
.prose-content h1 {
	color: var(--color-prose-headings);
	font-family: var(--font-display);
	font-weight: 600;
	letter-spacing: -0.025em;
	font-size: 2rem;
	line-height: 1.2;
	margin-top: 2em;
	margin-bottom: 0.5em;
}
.ProseMirror h2,
.prose-content h2 {
	color: var(--color-prose-headings);
	font-family: var(--font-display);
	font-weight: 600;
	letter-spacing: -0.025em;
	font-size: 1.5rem;
	line-height: 1.3;
	margin-top: 2em;
	margin-bottom: 0.5em;
	padding-bottom: 0.5em;
	border-bottom: 1px solid var(--color-line);
}
.ProseMirror h3,
.prose-content h3 {
	color: var(--color-prose-headings);
	font-family: var(--font-display);
	font-weight: 600;
	letter-spacing: -0.025em;
	font-size: 1.25rem;
	line-height: 1.4;
	margin-top: 1.5em;
	margin-bottom: 0.5em;
}
.ProseMirror h4,
.prose-content h4 {
	color: var(--color-prose-headings);
	font-family: var(--font-display);
	font-weight: 600;
	letter-spacing: -0.025em;
	font-size: 1.125rem;
	margin-top: 1.5em;
	margin-bottom: 0.5em;
}
.ProseMirror :is(h1, h2, h3, h4, p):first-child,
.prose-content :is(h1, h2, h3, h4, p):first-child {
	margin-top: 0;
}
.ProseMirror :is(h1, h2, h3, h4, p):last-child,
.prose-content :is(h1, h2, h3, h4, p):last-child {
	margin-bottom: 0;
}
.ProseMirror p,
.prose-content p {
	margin-bottom: 1.5em;
}
.prose-content a {
	color: var(--color-prose-links);
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 2px;
	transition:
		color 0.15s,
		text-decoration-color 0.15s;
}
.prose-content a:hover {
	text-decoration-thickness: 2px;
}
.ProseMirror strong,
.ProseMirror b,
.prose-content strong,
.prose-content b {
	font-weight: 600;
	color: var(--color-prose-headings);
}
.ProseMirror em,
.prose-content em {
	font-style: italic;
}
.prose-content code:not(pre code),
.ProseMirror code:not(pre code) {
	color: var(--color-prose-code);
	background: var(--color-prose-code-bg);
	padding: 0.2em 0.4em;
	border-radius: 0.25rem;
	font-size: 0.875em;
	font-family: var(--font-mono);
	font-weight: 500;
	border: 1px solid var(--color-line);
}
.ProseMirror blockquote,
.prose-content blockquote {
	border-left: 3px solid var(--color-accent);
	margin: 1.5em 0;
	padding-left: 1.5em;
	font-style: italic;
	color: var(--color-muted);
}
.ProseMirror ul,
.prose-content ul,
.ProseMirror ol,
.prose-content ol {
	margin-bottom: 1.5em;
	padding-left: 1.5em;
}
.ProseMirror ul,
.prose-content ul {
	list-style-type: disc;
}
.ProseMirror ol,
.prose-content ol {
	list-style-type: decimal;
}
.ProseMirror li,
.prose-content li {
	display: list-item;
	margin-bottom: 0.5em;
	padding-left: 0.375em;
}
.prose-content li::marker {
	color: var(--color-muted);
}
.ProseMirror li p,
.prose-content li p {
	margin: 0;
}
.ProseMirror ul ul,
.prose-content ul ul {
	list-style-type: circle;
	margin-top: 0.5em;
	margin-bottom: 0.5em;
}
.ProseMirror ul ul ul,
.prose-content ul ul ul {
	list-style-type: square;
}
.prose-content li > ul,
.prose-content li > ol {
	margin-top: 0.5em;
	margin-bottom: 0.5em;
}
.ProseMirror hr,
.prose-content hr {
	border: none;
	border-top: 1px solid var(--color-line);
	margin: 3em 0;
}
.prose-content pre {
	border-radius: 0.5rem;
	padding: 1rem 1.25rem;
	margin: 1.5em 0;
	overflow-x: auto;
	font-size: 0.875rem;
	line-height: 1.7;
	background: #0d1117;
	border: 1px solid rgba(255, 255, 255, 0.1);
}
.prose-content pre code {
	background: transparent;
	padding: 0;
	border-radius: 0;
	font-size: inherit;
	font-family: var(--font-mono);
	color: #c9d1d9;
	display: block;
}
@media (prefers-color-scheme: light) {
	.prose-content pre {
		background: #0d1117;
		border: 1px solid rgba(0, 0, 0, 0.15);
	}
}
</style><link rel="stylesheet" href="/assets/root-B4HDV-KX.css"/><link rel="stylesheet" href="/assets/vendor-refractor-rCx-ggvl.css#"/><link rel="stylesheet" href="/assets/prose-content-Bj1YCACO.css" media="all"/></head><body><div class="min-h-screen bg-page text-ink"><script>
						(function() {
							const theme = 'system';
							document.documentElement.classList.remove('light', 'dark');
							if (theme !== 'system') {
								document.documentElement.classList.add(theme);
							}
						})();
					</script><style>
							:root {
								--color-accent-light: #ff7300;
								--color-accent-dark: #ff7300;
							}
							:root.light {
								--accent: #ff7300;
								--accent-foreground: #ffffff;
								--color-accent: #ff7300;
							}
							:root.dark {
								--accent: #ff7300;
								--accent-foreground: #0c0a09;
								--color-accent: #ff7300;
							}
						</style><div class="fixed inset-0 -z-10 bg-grid opacity-[0.02] dark:opacity-[0.04]"></div><header class="sticky top-0 z-50 border-b border-line/50 bg-page/80 backdrop-blur-xl"><nav class="mx-auto flex max-w-3xl lg:max-w-5xl xl:max-w-6xl items-center justify-between px-4 sm:px-6 py-4"><div class="flex items-center gap-2"><a class="font-display text-lg sm:text-xl font-semibold tracking-tight text-ink transition-colors hover:text-accent" href="/" data-discover="true">firtoz</a></div><div class="flex items-center gap-3 sm:gap-4 lg:gap-6"><a class="text-sm font-medium transition-colors text-muted hover:text-ink" href="/" data-discover="true">Writing</a><a class="text-sm font-medium transition-colors text-muted hover:text-ink" href="/about" data-discover="true">About</a><a href="https://github.com/firtoz" target="_blank" rel="noopener noreferrer" class="text-muted transition-colors hover:text-ink hidden sm:block" aria-label="GitHub"><img src="/assets/github-CoHtXIyb.svg" alt="" class="h-5 w-5 social-icon"/></a><a href="https://x.com/firtoz" target="_blank" rel="noopener noreferrer" class="text-muted transition-colors hover:text-ink hidden sm:block" aria-label="X"><img src="/assets/x-_YOlxEmy.svg" alt="" class="h-5 w-5"/></a><a href="/feed.xml" class="text-muted transition-colors hover:text-ink hidden sm:block" aria-label="RSS Feed"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rss" aria-hidden="true"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a><button type="button" class="text-muted transition-colors hover:text-ink" aria-label="Light mode. Click to override."><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun" aria-hidden="true"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></svg></button></div></nav></header><main class="mx-auto max-w-3xl lg:max-w-5xl xl:max-w-6xl px-6 py-12"><div class="max-w-3xl mx-auto space-y-12"><div class="text-center space-y-4"><div class="space-y-4"><h1 class="font-display text-8xl sm:text-9xl font-bold tracking-tight text-accent/20">404</h1><h2 class="font-display text-2xl sm:text-3xl font-semibold tracking-tight text-ink">Page Not Found</h2><p class="text-lg text-muted max-w-md mx-auto">The page you&#x27;re looking for doesn&#x27;t exist or has been moved.</p></div></div><div class="border-t border-line/50 pt-12"><h2 class="font-display text-2xl font-semibold text-ink mb-8 text-center">Recent Posts</h2><div class="space-y-8"><article class="border-b border-line/50 pb-12 mb-12 last:border-0"><div class="flex flex-wrap items-center gap-2 text-xs text-muted mb-4"><time dateTime="2026-04-20T19:18:13.000Z">April 20, 2026</time></div><div class="flex gap-2 mb-6 items-center"><a href="/post/ja-ti-know-who-arrives-know-who-leaves" data-discover="true"><h2 class="font-display text-3xl font-bold text-ink hover:text-accent transition-colors">Ja-ti: know who arrives, know who leaves</h2></a></div><div class="prose-content mb-6"><div class="prose-content"><p>I’ve been shipping <a href="https://ja-ti.com/" class="prose-link" target="_blank" rel="noopener noreferrer">Ja-ti</a>, a small product that tracks <strong>follower changes on X</strong>: who followed, who left, and the messy middle (suspended accounts, “gone” profiles, that sort of thing). The pitch fits on a line — <em>know who arrives, know who leaves</em> — but the implementation is where it gets interesting, because “compare two follower lists” sounds trivial until you’re doing it <strong>repeatedly</strong>, <strong>fairly across subscribers</strong>, <strong>within API and platform limits</strong>, and <strong>with billing that matches how expensive an account actually is to track</strong>.</p><p>At a high level, Ja-ti takes the accounts you care about and checks them on a cadence you choose (within feasibility rules driven by audience size — you can’t promise a 15-minute sweep on a seven-figure follower count and mean it). It stores <strong>snapshots</strong> and <strong>diffs</strong> so you see history, not just a one-off delta. It sends <strong>digest email</strong> when something changed: new follows, unfollows, VIP-tier movements, account-status shifts — with tier-aware truncation so the entry experience doesn’t pretend every account gets a novel.</p><p>Billing is <strong>usage-shaped</strong>: list price scales with <strong>follower-count bands</strong>, there’s a <strong>card-required trial</strong>, and <strong>manual checks</strong> are gated differently from automatic runs so power users don’t accidentally erode margin. None of that needs a manifesto; it’s the boring truth of consumer infra products — <strong>the price has to track the cost</strong>, and the UX has to <strong>fail loudly</strong> when someone asks for the impossible.</p><div class="w-full text-center my-4"><img src="https://firtoz.com/media/1375f29b-57fa-4e10-b9d0-d93a7173eec9.png" alt="Ja-ti product UI: dashboard or follower tracking view." width="1682" height="1174" loading="lazy" class="max-w-full h-auto rounded-lg inline-block"/></div></div></div><a class="inline-flex items-center gap-2 text-sm font-medium text-accent hover:underline" href="/post/ja-ti-know-who-arrives-know-who-leaves#read-more" data-discover="true">Continue reading<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg></a></article><article class="border-b border-line/50 pb-12 mb-12 last:border-0"><div class="flex flex-wrap items-center gap-2 text-xs text-muted mb-4"><time dateTime="2026-02-22T14:45:00.000Z">February 22, 2026</time></div><div class="flex gap-2 mb-6 items-center"><a href="/post/tab-canopy-what-it-is-and-why-it-exists" data-discover="true"><h2 class="font-display text-3xl font-bold text-ink hover:text-accent transition-colors">Tab Canopy: what it is and why it exists</h2></a></div><div class="prose-content mb-6"><div class="prose-content"><p>Your tabs don’t think in a straight line. You open a main article, then sources, then references from those. <strong>Tab Canopy</strong> organises them in a <strong>tree</strong> instead of a flat list. Nest tabs under others, drag to reorder, search with Ctrl+F. It lives in the side panel, works across windows, and stays in sync. Everything is stored locally in your browser.</p><div class="w-full text-center my-4"><div class="image-crop-wrapper overflow-hidden rounded-lg max-w-full min-w-0 inline-block" style="width:822.22px;aspect-ratio:822.22 / 371.25;height:auto"><img src="https://firtoz.com/media/e2e27d9d-fe84-4639-a2b4-4a1ac1fb661d.png" alt="Tab Canopy side panel showing a tree of tabs: parent tabs with nested children, expand and collapse controls, and indentation showing the hierarchy. Tabs organised in a tree instead of a flat list." loading="lazy" class="block rounded-lg max-w-none align-top" style="width:102.04081632653062%;height:101.01010101010101%;transform:translate(-2%, -1%)"/></div></div><p>It’s <a href="https://chromewebstore.google.com/detail/tab-canopy/kghaoebcnfieahcepdmalkjhdnfnlodg" class="prose-link" target="_blank" rel="noopener noreferrer">on the Chrome Web Store</a> and <a href="https://addons.mozilla.org/en-GB/firefox/addon/tab-canopy/" class="prose-link" target="_blank" rel="noopener noreferrer">Firefox Add-ons</a>, and <a href="https://github.com/firtoz/tab-canopy" class="prose-link" target="_blank" rel="noopener noreferrer">open source (MIT) on GitHub</a>. <a href="https://wxt.dev/" class="prose-link" target="_blank" rel="noopener noreferrer">WXT</a> made it straightforward to build one codebase for both. Plenty of extensions already do tree-style tabs; I built this one to have a real use case for the Firtoz collection packages (<a href="https://github.com/firtoz/fullstack-toolkit/tree/main/packages/drizzle-indexeddb" class="prose-link" target="_blank" rel="noopener noreferrer">IndexedDB + TanStack collections</a>). The experiment turned into something I use every day.</p><p>Then the cracks showed. </p><p>Moving a parent tab in the browser could scramble the tree; closing one could leave orphans; an update would sometimes overwrite the structure the UI had just set. Fixing one bug broke another. So I fixed the collections layer, rewired the extension around a single reconciler, and got the tests green. Here’s what changed and why it matters.</p></div></div><a class="inline-flex items-center gap-2 text-sm font-medium text-accent hover:underline" href="/post/tab-canopy-what-it-is-and-why-it-exists#read-more" data-discover="true">Continue reading<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg></a></article><article class="border-b border-line/50 pb-12 mb-12 last:border-0"><div class="flex flex-wrap items-center gap-2 text-xs text-muted mb-4"><time dateTime="2026-02-14T20:08:00.000Z">February 14, 2026</time></div><div class="flex gap-2 mb-6 items-center"><a href="/post/per-object-clipping-planes-shader-in-unity" data-discover="true"><h2 class="font-display text-3xl font-bold text-ink hover:text-accent transition-colors">Per object clipping planes shader in Unity</h2></a></div><div class="prose-content mb-6"><div class="prose-content"><h2>Unity Standard Shader with Custom Clipping Planes</h2><blockquote><h3>2026 note</h3><p>I’m in the process of migrating posts from my old blog into this new system.</p><p>Rather than rewriting everything, I’m keeping the original structure and tone where possible, and adding light context where it helps.</p></blockquote><p>For a project of mine, I wanted to have custom clipping planes for objects, so that if an object is intersection with another, it would hide any part after the intersection.</p><p>It looks like this:</p><div class="relative my-6 w-full max-w-full overflow-hidden rounded-lg" style="aspect-ratio:16/9"><div class="rounded-lg bg-surface-alt animate-[pulse_2.5s_ease-in-out_infinite] w-full" style="aspect-ratio:16/9" aria-hidden="true"></div></div><div class="relative my-6 w-full max-w-full overflow-hidden rounded-lg" style="aspect-ratio:16/9"><div class="rounded-lg bg-surface-alt animate-[pulse_2.5s_ease-in-out_infinite] w-full" style="aspect-ratio:16/9" aria-hidden="true"></div></div><p>I decided to extend the Standard shader provided by Unity3D to achieve this effect.</p></div></div><a class="inline-flex items-center gap-2 text-sm font-medium text-accent hover:underline" href="/post/per-object-clipping-planes-shader-in-unity#read-more" data-discover="true">Continue reading<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg></a></article></div></div><div class="flex justify-center items-center"><a class="inline-flex items-center gap-2 px-6 py-3 bg-accent text-accent-foreground rounded-lg font-medium transition-colors hover:bg-accent/90" href="/" data-discover="true"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-house" aria-hidden="true"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"></path><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>Go to Homepage</a></div><div class="border-t border-line/50 pt-8 space-y-4"><div class="flex items-center justify-center gap-2 text-muted"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search" aria-hidden="true"><path d="m21 21-4.34-4.34"></path><circle cx="11" cy="11" r="8"></circle></svg><span class="text-sm font-medium">Looking for something?</span></div><div class="flex flex-wrap gap-3 justify-center text-sm"><a class="px-4 py-2 rounded-full bg-surface border border-line text-ink hover:bg-accent/10 hover:border-accent/50 transition-colors" href="/" data-discover="true">All Posts</a><a class="px-4 py-2 rounded-full bg-surface border border-line text-ink hover:bg-accent/10 hover:border-accent/50 transition-colors" href="/about" data-discover="true">About</a><a href="/feed.xml" class="px-4 py-2 rounded-full bg-surface border border-line text-ink hover:bg-accent/10 hover:border-accent/50 transition-colors">RSS Feed</a><a href="/sitemap.xml" class="px-4 py-2 rounded-full bg-surface border border-line text-ink hover:bg-accent/10 hover:border-accent/50 transition-colors">Sitemap</a></div></div></div></main><footer class="border-t border-line/50 bg-page/50"><div class="mx-auto max-w-3xl lg:max-w-5xl xl:max-w-6xl px-6 py-8"><div class="flex flex-col items-center justify-between gap-4 sm:flex-row"><p class="text-sm text-muted">© <!-- -->2026<!-- --> firtoz<!-- -->. All rights reserved.</p><div class="flex items-center gap-4 text-sm text-muted"><a href="/feed.xml" class="hover:text-ink transition-colors">RSS</a><span class="text-line">·</span><a href="/sitemap.xml" class="hover:text-ink transition-colors">Sitemap</a></div></div></div></footer></div><script>((c,u)=>{if(!window.history.state||!window.history.state.key){let f=Math.random().toString(32).slice(2);window.history.replaceState({key:f},"")}try{let g=JSON.parse(sessionStorage.getItem(c)||"{}")[u||window.history.state.key];typeof g=="number"&&window.scrollTo(0,g)}catch(f){console.error(f),sessionStorage.removeItem(c)}})("react-router-scroll-positions", null)</script><script>window.__reactRouterContext = {"basename":"/","future":{"unstable_optimizeDeps":false,"unstable_subResourceIntegrity":false,"unstable_trailingSlashAwareDataRequests":false,"unstable_previewServerPrerendering":false,"v8_middleware":false,"v8_splitRouteModules":false,"v8_viteEnvironmentApi":true},"routeDiscovery":{"mode":"lazy","manifestPath":"/__manifest"},"ssr":true,"isSpaMode":false};window.__reactRouterContext.stream = new ReadableStream({start(controller){window.__reactRouterContext.streamController = controller;}}).pipeThrough(new TextEncoderStream());</script><script type="module" async="">;
import * as route0 from "/assets/root-DZSIzbIP.js";
import * as route1 from "/assets/_layout-B2VWF10v.js";
import * as route2 from "/assets/_-BPrHGPHx.js";
  window.__reactRouterManifest = {
  "entry": {
    "module": "/assets/entry.client-D3_8OtZa.js",
    "imports": [],
    "css": []
  },
  "routes": {
    "root": {
      "id": "root",
      "path": "",
      "hasAction": false,
      "hasLoader": true,
      "hasClientAction": false,
      "hasClientLoader": false,
      "hasClientMiddleware": false,
      "hasDefaultExport": true,
      "hasErrorBoundary": true,
      "module": "/assets/root-DZSIzbIP.js",
      "imports": [
        "/assets/entry.client-D3_8OtZa.js",
        "/assets/ConcurrentSubmitterProvider-BpGDvZ4b.js",
        "/assets/vendor-zod-BDmUoUzW.js",
        "/assets/BlogLayout-D2-XJNc5.js",
        "/assets/PostCard-D7tBjRHp.js",
        "/assets/vendor-posthog-DTHv5gVh.js",
        "/assets/isAdmin-COlvcvZU.js",
        "/assets/github-DS4be7QB.js",
        "/assets/x-CUb4TX-7.js",
        "/assets/BlogContentRenderer-ewM_QnPn.js",
        "/assets/shared-extensions-DbeyeYZK.js",
        "/assets/nodeUtils-C14IIQH4.js",
        "/assets/code-highlighter-DZNs8-E7.js",
        "/assets/vendor-refractor-BEGDkSND.js",
        "/assets/dialog-CcWRISlu.js",
        "/assets/button-D-YIgNus.js",
        "/assets/vendor-utils-Ce1ZTWB2.js",
        "/assets/utils-BPQb07fs.js",
        "/assets/PostEditorContext-BzkB-NDb.js",
        "/assets/select-B_NaCOnR.js",
        "/assets/label-DiCFLVIW.js",
        "/assets/tiptap-utils-mA0cZ5w4.js"
      ],
      "css": [
        "/assets/root-B4HDV-KX.css",
        "/assets/vendor-refractor-rCx-ggvl.css#"
      ]
    },
    "routes/blog/_layout": {
      "id": "routes/blog/_layout",
      "parentId": "root",
      "hasAction": false,
      "hasLoader": true,
      "hasClientAction": false,
      "hasClientLoader": false,
      "hasClientMiddleware": false,
      "hasDefaultExport": true,
      "hasErrorBoundary": false,
      "module": "/assets/_layout-B2VWF10v.js",
      "imports": [
        "/assets/entry.client-D3_8OtZa.js",
        "/assets/BlogLayout-D2-XJNc5.js",
        "/assets/isAdmin-COlvcvZU.js",
        "/assets/og-urls-BbsTksUP.js",
        "/assets/cache-BAr-CJqI.js",
        "/assets/github-DS4be7QB.js",
        "/assets/x-CUb4TX-7.js"
      ],
      "css": []
    },
    "routes/blog/$": {
      "id": "routes/blog/$",
      "parentId": "routes/blog/_layout",
      "path": "*",
      "hasAction": false,
      "hasLoader": true,
      "hasClientAction": false,
      "hasClientLoader": false,
      "hasClientMiddleware": false,
      "hasDefaultExport": true,
      "hasErrorBoundary": false,
      "module": "/assets/_-BPrHGPHx.js",
      "imports": [
        "/assets/entry.client-D3_8OtZa.js",
        "/assets/PostCard-D7tBjRHp.js",
        "/assets/BlogContentRenderer-ewM_QnPn.js",
        "/assets/shared-extensions-DbeyeYZK.js",
        "/assets/nodeUtils-C14IIQH4.js",
        "/assets/code-highlighter-DZNs8-E7.js",
        "/assets/vendor-refractor-BEGDkSND.js",
        "/assets/dialog-CcWRISlu.js",
        "/assets/button-D-YIgNus.js",
        "/assets/vendor-utils-Ce1ZTWB2.js",
        "/assets/utils-BPQb07fs.js",
        "/assets/PostEditorContext-BzkB-NDb.js",
        "/assets/select-B_NaCOnR.js",
        "/assets/label-DiCFLVIW.js",
        "/assets/tiptap-utils-mA0cZ5w4.js",
        "/assets/isAdmin-COlvcvZU.js"
      ],
      "css": [
        "/assets/vendor-refractor-rCx-ggvl.css#"
      ]
    },
    "routes/blog/index": {
      "id": "routes/blog/index",
      "parentId": "routes/blog/_layout",
      "index": true,
      "hasAction": false,
      "hasLoader": true,
      "hasClientAction": false,
      "hasClientLoader": false,
      "hasClientMiddleware": false,
      "hasDefaultExport": true,
      "hasErrorBoundary": false,
      "module": "/assets/index-BaOEe_lg.js",
      "imports": [
        "/assets/entry.client-D3_8OtZa.js",
        "/assets/github-DS4be7QB.js",
        "/assets/x-CUb4TX-7.js",
        "/assets/BlogContentRenderer-ewM_QnPn.js",
        "/assets/PostCard-D7tBjRHp.js",
        "/assets/og-urls-BbsTksUP.js",
        "/assets/tiptap-utils-mA0cZ5w4.js",
        "/assets/cache-BAr-CJqI.js",
        "/assets/shared-extensions-DbeyeYZK.js",
        "/assets/nodeUtils-C14IIQH4.js",
        "/assets/code-highlighter-DZNs8-E7.js",
        "/assets/vendor-refractor-BEGDkSND.js",
        "/assets/dialog-CcWRISlu.js",
        "/assets/button-D-YIgNus.js",
        "/assets/vendor-utils-Ce1ZTWB2.js",
        "/assets/utils-BPQb07fs.js",
        "/assets/PostEditorContext-BzkB-NDb.js",
        "/assets/select-B_NaCOnR.js",
        "/assets/label-DiCFLVIW.js",
        "/assets/isAdmin-COlvcvZU.js"
      ],
      "css": [
        "/assets/vendor-refractor-rCx-ggvl.css#"
      ]
    }
  },
  "url": "/assets/manifest-519781f9.js",
  "version": "519781f9"
};
  window.__reactRouterRouteModules = {"root":route0,"routes/blog/_layout":route1,"routes/blog/$":route2};

import("/assets/entry.client-D3_8OtZa.js");</script><!--$--><script>window.__reactRouterContext.streamController.enqueue("[{\"_1\":2,\"_125\":-5,\"_126\":-5},\"loaderData\",{\"_3\":4,\"_25\":26,\"_94\":95},\"root\",{\"_5\":6,\"_23\":24},\"analyticsConfig\",{\"_7\":8},\"posthog\",{\"_9\":10,\"_11\":12,\"_13\":14,\"_15\":16,\"_17\":18,\"_19\":20,\"_21\":22},\"enabled\",true,\"key\",\"phc_vXuG9GRmiSXMCwmVBQpZ4i9mj9MifBNJwm2TAizFMhhx\",\"host\",\"https://d.lunix.ai\",\"deployStage\",\"local\",\"logsEnvironment\",\"local_development\",\"releaseName\",\"firtoz-blog-web-prod\",\"serviceVersion\",\"19001d269148962dddc3485b514163e0d02c7767+25\",\"faviconUrl\",\"https://firtoz.com/media/81b50cc6-d514-4e22-a984-e72746e63078.png\",\"routes/blog/_layout\",{\"_27\":28,\"_85\":86,\"_87\":88,\"_89\":90},\"config\",{\"_29\":30,\"_31\":32,\"_33\":34,\"_35\":36,\"_37\":38,\"_39\":40,\"_41\":42,\"_43\":44,\"_45\":46,\"_55\":56,\"_23\":24,\"_57\":58,\"_59\":58,\"_60\":58,\"_61\":58,\"_62\":58,\"_63\":64,\"_65\":58,\"_66\":67,\"_68\":38,\"_69\":67,\"_70\":71,\"_72\":73,\"_74\":58,\"_75\":76,\"_77\":76,\"_78\":79,\"_80\":81,\"_82\":83,\"_84\":81},\"url\",\"https://firtoz.com\",\"name\",\"firtoz\",\"description\",\"Notes on building real things: AI, web, games, and the systems behind them.\",\"descriptionJson\",\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Notes on building real things: AI, web, games, and the systems behind them.\\\"}]}]}\",\"tagline\",\"notes on building\",\"heroJson\",\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":1},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Notes on Building\\\"}]},{\\\"type\\\":\\\"paragraph\\\"}]}\",\"aboutContent\",\"Hello! I’m Firtina Ozbalikchi, a full-stack engineer and creative technologist based in London.\\nI’ve spent over a decade building software across games, 3D graphics, and the web, from engine and tooling work at Unity, Improbable, and EA to early-stage startups and open-source projects used by thousands. I was formerly the CTO of Greybox, a small founding team building a collaborative 3D world editor with agentic, multimodal AI.\\nThis blog is where I think in public about building tools, WebGL and graphics, AI systems, startups, and the realities of shipping products as a small team. I care about how technology shapes creativity and collaboration, and I enjoy breaking down complex technical ideas into something useful and concrete.\",\"aboutContentJson\",\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Hello! I’m Firtina Ozbalikchi, a full-stack engineer and creative technologist based in London.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"I’ve spent over a decade building software across games, 3D graphics, and the web, from engine and tooling work at Unity, Improbable, and EA to early-stage startups and open-source projects used by thousands. I was formerly the CTO of Greybox, a small founding team building a collaborative 3D world editor with agentic, multimodal AI.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"This blog is where I think in public about building tools, WebGL and graphics, AI systems, startups, and the realities of shipping products as a small team. I care about how technology shapes creativity and collaboration, and I enjoy breaking down complex technical ideas into something useful and concrete.\\\"}]}]}\",\"author\",{\"_31\":32,\"_47\":48,\"_49\":50,\"_51\":52,\"_53\":32,\"_54\":32},\"bio\",\"Firtina Ozbalikchi is a full-stack engineer and creative technologist based in London. He builds software across graphics, AI, and the web, and writes about systems, tools, and shipping real products.\",\"bioJson\",\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Firtina Ozbalikchi is a full-stack engineer and creative technologist based in London. He builds software across graphics, AI, and the web, and writes about systems, tools, and shipping real products.\\\"}]}]}\",\"email\",\"hello@firtoz.com\",\"x\",\"github\",\"accentColor\",\"#ff7300\",\"ogTitle\",\"\",\"ogDescription\",\"ogSiteName\",\"twitterTitle\",\"twitterDescription\",\"ogImageTitleMode\",\"include\",\"ogImageTitle\",\"ogImageTaglineMode\",\"override\",\"ogImageTagline\",\"ogImageDescriptionMode\",\"ogImageDescription\",\"Things I've learned from building real stuff\",\"ogImageUrl\",\"https://firtoz.com/media/49b73384-5efd-4800-9f31-8281ab980ce6.png\",\"ogCtaLabel\",\"ogFaviconHidden\",false,\"ogSplitTagline\",\"ogImagePaddingTop\",32,\"ogImagePaddingRight\",40,\"ogImagePaddingBottom\",48,\"ogImagePaddingLeft\",\"adminInfoPromise\",[\"P\",86],\"theme\",\"system\",\"ogMeta\",{\"_91\":92,\"_33\":34,\"_93\":32,\"_61\":92,\"_62\":34},\"title\",\"firtoz | notes on building\",\"siteName\",\"routes/blog/$\",{\"_96\":97},\"recentPosts\",[98,111,118],{\"_99\":100,\"_101\":102,\"_91\":103,\"_104\":-5,\"_105\":106,\"_107\":108,\"_109\":110},\"id\",\"dd58947c-c6ac-4a08-9626-3add030c59d1\",\"slug\",\"ja-ti-know-who-arrives-know-who-leaves\",\"Ja-ti: know who arrives, know who leaves\",\"excerpt\",\"publishedAt\",[\"D\",1776712693000],\"contentJson\",\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"I’ve been shipping \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://ja-ti.com/\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Ja-ti\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", a small product that tracks \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"follower changes on X\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\": who followed, who left, and the messy middle (suspended accounts, “gone” profiles, that sort of thing). The pitch fits on a line — \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"italic\\\"}],\\\"text\\\":\\\"know who arrives, know who leaves\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" — but the implementation is where it gets interesting, because “compare two follower lists” sounds trivial until you’re doing it \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"repeatedly\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"fairly across subscribers\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"within API and platform limits\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"with billing that matches how expensive an account actually is to track\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"At a high level, Ja-ti takes the accounts you care about and checks them on a cadence you choose (within feasibility rules driven by audience size — you can’t promise a 15-minute sweep on a seven-figure follower count and mean it). It stores \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"snapshots\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"diffs\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" so you see history, not just a one-off delta. It sends \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"digest email\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" when something changed: new follows, unfollows, VIP-tier movements, account-status shifts — with tier-aware truncation so the entry experience doesn’t pretend every account gets a novel.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Billing is \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"usage-shaped\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\": list price scales with \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"follower-count bands\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", there’s a \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"card-required trial\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"manual checks\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" are gated differently from automatic runs so power users don’t accidentally erode margin. None of that needs a manifesto; it’s the boring truth of consumer infra products — \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"the price has to track the cost\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and the UX has to \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"fail loudly\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" when someone asks for the impossible.\\\"}]},{\\\"type\\\":\\\"image\\\",\\\"attrs\\\":{\\\"src\\\":\\\"https://firtoz.com/media/1375f29b-57fa-4e10-b9d0-d93a7173eec9.png\\\",\\\"alt\\\":\\\"Ja-ti product UI: dashboard or follower tracking view.\\\",\\\"title\\\":null,\\\"width\\\":\\\"1682\\\",\\\"height\\\":\\\"1174\\\",\\\"mediaRefId\\\":null,\\\"cropX\\\":null,\\\"cropY\\\":null,\\\"cropW\\\":null,\\\"cropH\\\":null}},{\\\"type\\\":\\\"MoreNode\\\"},{\\\"type\\\":\\\"horizontalRule\\\"},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Why the architecture isn’t “one big Worker”\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The tempting v0 is: cron job, fetch followers, diff in memory, send email. That works until:\\\"}]},{\\\"type\\\":\\\"bulletList\\\",\\\"content\\\":[{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Multiple people\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" track the same large account (you don’t want N redundant crawls).\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Alarms and scheduling\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" need \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"per-canonical-user\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" state, not a global queue that forgets whose interval is whose.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Follower enumeration\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" is \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"paginated and long\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" — too long for a single request handler to own naïvely.\\\"}]}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"So Ja-ti splits responsibilities:\\\"}]},{\\\"type\\\":\\\"bulletList\\\",\\\"content\\\":[{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Postgres\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" (via \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://developers.cloudflare.com/hyperdrive/\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Cloudflare Hyperdrive\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\") holds the durable truth: tracked accounts, snapshots, staging for big crawls, profile versions, billing-adjacent fields, webhook idempotency — the usual “this must survive a deploy” stuff.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"A \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Durable Object per tracked X identity\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" owns \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"scheduling\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"subscriber-facing coordination\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" — shared alarms, plan sync, the kind of state that’s painful to get right if you smear it across KV and hope.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://developers.cloudflare.com/workflows/\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Cloudflare Workflows\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" run the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"follower-check pipeline\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\": paginated fetch + persist, then finalize into a consistent snapshot, then diff / notify — with step budgets and safety caps so a pathological account becomes an \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"ops problem\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", not a silent wrong answer.\\\"}]}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The split matches how the platform wants you to think: \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Workflows for orchestration\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"DOs for strongly-owned singleton state\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Postgres for queryable history\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"horizontalRule\\\"},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The part that’s easy to underestimate: finalize\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The crawl is only half the story. The other half is making \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"partial progress invisible\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" to readers: snapshots that aren’t complete shouldn’t look like the latest truth. Ja-ti leans on explicit \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"complete\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" flags, staging reads, and batched writes so finalize doesn’t devolve into thousands of round-trips or a single step that blows the Worker limit.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"If you’ve ever watched a “simple” ETL catch fire on cardinality, this is the same genre — \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"correctness and throughput are the same feature\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"horizontalRule\\\"},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Typed boundaries (and why I care)\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"One meta-choice worth naming: the repo is intentionally \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"strict at the edges\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". Typed contracts between the web app and the DO HTTP surface (via \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"@firtoz/hono-fetcher\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" — same idea as the Tab Canopy work: one clear contract) mean \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"await res.json()\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" carries the server’s Hono app types — no \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"as\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" as a coping strategy. That sounds precious until you’re iterating with \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"agents and humans\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" in the same codebase: \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"the compiler becomes the checklist\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"In-app billing uses \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://docs.useautumn.com\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Autumn\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"; email goes out through \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Resend\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"; X data through a provider API. The interesting glue is all in \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"when\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" you check, \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"who\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" pays for \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"which\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" cadence, and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"how\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" you keep multi-subscriber accounts from fighting.\\\"}]},{\\\"type\\\":\\\"horizontalRule\\\"},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Where things stand\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"v1-shaped work is largely in place — band pricing, trials, VIP thresholds, crossover handling when someone’s audience moves bands, dashboard and history UX, optional \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"PostHog\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", the whole “ship a real SaaS” package. The open threads are the usual second-order product work: finer pricing above large bands, optional email-density add-ons, and the long tail of \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"mega-account\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" honesty (disclosed caps, incremental strategies, or polite “out of scope” copy).\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Ja-ti started as a straightforward idea and grew into a \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"systems\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" problem: fairness across subscribers, correctness under pagination, and pricing that tracks reality. That’s a good sign. The boring products are often the ones that teach you the most about platforms.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"If you’re building something in this space — \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"diffing social graphs under constraints\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" — this is where I’ve been spending that energy lately.\\\"}]}]}\",\"revisionTags\",[],{\"_99\":112,\"_101\":113,\"_91\":114,\"_104\":-5,\"_105\":115,\"_107\":116,\"_109\":117},\"492d26c7-6629-4777-a94e-50bfc699c4fb\",\"tab-canopy-what-it-is-and-why-it-exists\",\"Tab Canopy: what it is and why it exists\",[\"D\",1771771500000],\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Your tabs don’t think in a straight line. You open a main article, then sources, then references from those. \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Tab Canopy\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" organises them in a \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"tree\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" instead of a flat list. Nest tabs under others, drag to reorder, search with Ctrl+F. It lives in the side panel, works across windows, and stays in sync. Everything is stored locally in your browser.\\\"}]},{\\\"type\\\":\\\"image\\\",\\\"attrs\\\":{\\\"src\\\":\\\"https://firtoz.com/media/e2e27d9d-fe84-4639-a2b4-4a1ac1fb661d.png\\\",\\\"alt\\\":\\\"Tab Canopy side panel showing a tree of tabs: parent tabs with nested children, expand and collapse controls, and indentation showing the hierarchy. Tabs organised in a tree instead of a flat list.\\\",\\\"title\\\":null,\\\"width\\\":839,\\\"height\\\":375,\\\"cropX\\\":0.02,\\\"cropY\\\":0.01,\\\"cropW\\\":0.98,\\\"cropH\\\":0.99}},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"It’s \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://chromewebstore.google.com/detail/tab-canopy/kghaoebcnfieahcepdmalkjhdnfnlodg\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"on the Chrome Web Store\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://addons.mozilla.org/en-GB/firefox/addon/tab-canopy/\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Firefox Add-ons\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://github.com/firtoz/tab-canopy\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"open source (MIT) on GitHub\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://wxt.dev/\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"WXT\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" made it straightforward to build one codebase for both. Plenty of extensions already do tree-style tabs; I built this one to have a real use case for the Firtoz collection packages (\\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://github.com/firtoz/fullstack-toolkit/tree/main/packages/drizzle-indexeddb\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"IndexedDB + TanStack collections\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"). The experiment turned into something I use every day.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Then the cracks showed. \\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Moving a parent tab in the browser could scramble the tree; closing one could leave orphans; an update would sometimes overwrite the structure the UI had just set. Fixing one bug broke another. So I fixed the collections layer, rewired the extension around a single reconciler, and got the tests green. Here’s what changed and why it matters.\\\"}]},{\\\"type\\\":\\\"MoreNode\\\"},{\\\"type\\\":\\\"horizontalRule\\\"},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Why the big rewrite: bugs that wouldn’t go away\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The bugs were the kind that don’t show up in demos. In real use: wrong order after moving a parent tab, inconsistent tree after closing one, and, the sneakiest one, browser update events overwriting the tree shape the UI had just written. Fix one thing, another regressed. \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Tab events and DB writes were scattered across many handlers\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", with races and no clear ownership of who writes what.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Two things had to change.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"1. Collections layer\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The extension sits on \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://github.com/firtoz/fullstack-toolkit/tree/main/packages/drizzle-indexeddb\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"@firtoz/drizzle-indexeddb\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and related packages. The bugs exposed gaps in syncing and multi-client access. In a Chrome extension the side panel runs in a different context from the background; it can’t open IndexedDB in the same way. The old approach was an \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"IDB proxy\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\": the background held the real DB and the panel talked to it through a proxy client/server and sync adapter, so both sides were effectively sharing one DB over the messaging layer. That layer was \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://github.com/firtoz/fullstack-toolkit/commit/3c7ce1dbca5c5396386db9927ae7f5e19a562cf6\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"removed from the package\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" in favour of a simpler model: native IndexedDB only where it’s available (the background), and in contexts that can’t use it (the panel), a \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"memory collection\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" that receives explicit \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"SyncMessage[]\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" over your own transport. I bumped to ^1.0.0 for \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"drizzle-indexeddb\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"drizzle-utils\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", added \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"@firtoz/db-helpers\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and wired the panel to a memory collection. Background emits sync on put/delete and on load; panel applies it via \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"collection.utils.receiveSync(messages)\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". One clear, testable contract instead of the old shared-proxy setup.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"2. Extension architecture\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"With a reliable sync story, the next step was to stop every handler writing to the DB. I introduced a \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"single reconciliation loop\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\": tab events become a small set of event types, get \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"enqueued\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and one \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"reconciler\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" drains the queue and is the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"only\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" writer. Tree logic lives in a pure module; the reconciler just “apply event → compute tree → write once.” I also fixed the “second update overwrites parent” bug: tree shape is owned only by create/move/remove; \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"handleTabUpdated\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" no longer overwrites \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"parentTabId\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" when it would incorrectly flatten a tab. Title overrides from the UI persist via \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"patchTab\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"/\\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"patchWindow\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and sync correctly.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Why keep the browser and the tree in sync both ways?\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" It would have been easier to treat the tree as the only source of truth and ignore the browser’s native tab order. I deliberately didn’t. When you move a tab in the browser strip, the tree should reflect that (e.g. “parent moved after its child” → child flattens to root); when you drag in the side panel, the browser strip should reorder to match. So we need two directions: \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"flat list → tree\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" (infer parent/order from browser events) and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"tree → flat list\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" (depth-first order to tell the browser where to put tabs). The core of that lives in a handful of pure functions.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"What those functions do\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" (for anyone reading the code or building something similar):\\\"}]},{\\\"type\\\":\\\"bulletList\\\",\\\"content\\\":[{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"**inferTreeFromBrowserMove(tabs, movedTabId, newIndex)**\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". You moved a tab in the browser strip; the flat order changed. This takes the new order and figures out the new tree: who is the moved tab’s parent now (the parent of the tab immediately after it, or root if it’s at the end), which of its children ended up \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"italic\\\"}],\\\"text\\\":\\\"before\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" it in the list and should be “flattened” to the same level, and new \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"treeOrder\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" keys (fractional indexing) so order is stable.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"**promoteOnRemove(tabs, removedTabId)**\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". A tab was closed. Its direct children need a new parent (the removed tab’s parent) and new order among their new siblings; grandchildren stay where they are. Returns a map of tab id → \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"{ parentTabId, treeOrder }\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" for those direct children only.\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"**inferTreeFromBrowserCreate(tabsInWindow, newTabIndex, newTabId)**\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". A new tab appeared at a given index in the window. Decide its \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"parentTabId\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"treeOrder\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" so it slots into the tree (same parent as the tab after it, order between the siblings that are before/after it in the strip).\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"**flattenTreeToBrowserOrder(tabs)**\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". The other direction: given the tree, produce the depth-first list of tab ids. The reconciler uses this to call \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"tabs.move()\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" so the browser strip matches the tree after a UI drag.\\\"}]}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The flow: browser event or UI action → event enqueued → reconciler runs one of these (or applies a patch), gets updated tabs → single write to DB and sync to the panel. No handler writes on its own.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Fix the collections and sync model first, then rewire the extension around a single writer and pure tree logic.\\\"}]},{\\\"type\\\":\\\"horizontalRule\\\"},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Where things stand now\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The issues we were chasing are fixed and the test suite is passing. E2E covers “moving parent tab in native browser after its child,” “moving parent tab between its child and the next tab,” and “db sync” (sidepanel receives windows and tabs from background). There are still flaky edges (e.g. promotion timing), but the core behaviour is stable.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"I’m using Tab Canopy daily again with more confidence, fewer surprises, less fragility, and room to iterate without fighting the old architecture. If you’re trying it from the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://chromewebstore.google.com/detail/tab-canopy/kghaoebcnfieahcepdmalkjhdnfnlodg\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Chrome Web Store\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://addons.mozilla.org/en-GB/firefox/addon/tab-canopy/\\\",\\\"target\\\":null,\\\"rel\\\":null,\\\"class\\\":null,\\\"title\\\":null}}],\\\"text\\\":\\\"Firefox Add-ons\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", or from source, this is the release where the tree and sync actually hold up.\\\"}]}]}\",[],{\"_99\":119,\"_101\":120,\"_91\":121,\"_104\":-5,\"_105\":122,\"_107\":123,\"_109\":124},\"6e61f7e0-2ee7-4dc2-a988-8f7411198906\",\"per-object-clipping-planes-shader-in-unity\",\"Per object clipping planes shader in Unity\",[\"D\",1771099680000],\"{\\\"type\\\":\\\"doc\\\",\\\"content\\\":[{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Unity Standard Shader with Custom Clipping Planes\\\"}]},{\\\"type\\\":\\\"blockquote\\\",\\\"content\\\":[{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"2026 note\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"I’m in the process of migrating posts from my old blog into this new system.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Rather than rewriting everything, I’m keeping the original structure and tone where possible, and adding light context where it helps.\\\"}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"For a project of mine, I wanted to have custom clipping planes for objects, so that if an object is intersection with another, it would hide any part after the intersection.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"It looks like this:\\\"}]},{\\\"type\\\":\\\"VideoNode\\\",\\\"attrs\\\":{\\\"sources\\\":[],\\\"tracks\\\":[],\\\"width\\\":null,\\\"height\\\":null,\\\"autoplay\\\":true,\\\"muted\\\":true,\\\"loop\\\":true,\\\"playsinline\\\":true,\\\"class\\\":null,\\\"mediaRefId\\\":\\\"c6202935-4db9-4906-8f60-c855e2c47995\\\"}},{\\\"type\\\":\\\"VideoNode\\\",\\\"attrs\\\":{\\\"sources\\\":[],\\\"tracks\\\":[],\\\"width\\\":null,\\\"height\\\":null,\\\"autoplay\\\":true,\\\"muted\\\":true,\\\"loop\\\":true,\\\"playsinline\\\":true,\\\"class\\\":null,\\\"mediaRefId\\\":\\\"0852479f-3436-40c9-8e54-aecee7126667\\\"}},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"I decided to extend the Standard shader provided by Unity3D to achieve this effect.\\\"}]},{\\\"type\\\":\\\"MoreNode\\\"},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"If you do not care about the technical details, \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"#the-result\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"skip to the bottom\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"!\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Some Background\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Warning, this post will get quite technical.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"You can find the Unity3D shader sources from the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"http://unity3d.com/get-unity/download/archive\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"Unity3D Download Archive\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"A clipping plane can be defined by 2 vectors, a position and a normal.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"These two vectors can be used to check whether parts of shapes are in front or behind the plane. We keep the parts in front, and hide the parts behind.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Wolfram Mathworld \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"http://mathworld.wolfram.com/Point-PlaneDistance.html\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"describes the algorithm\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" to get the distance of a point to a plane. Here it is in code:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Point to plane distance calculation:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"glsl\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"//http://mathworld.wolfram.com/Point-PlaneDistance.html\\\\nfloat distanceToPlane(float3 planePosition, float3 planeNormal, float3 pointInWorld)\\\\n{\\\\n    //w = vector from plane to point\\\\n    float3 w = - ( planePosition - pointInWorld );\\\\n    return ( \\\\n        planeNormal.x * w.x + \\\\n        planeNormal.y * w.y + \\\\n        planeNormal.z * w.z \\\\n    ) / sqrt ( \\\\n        planeNormal.x * planeNormal.x +\\\\n        planeNormal.y * planeNormal.y +\\\\n        planeNormal.z * planeNormal.z \\\\n    );\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"In order to find which parts of the objects are to be clipped, we need to extract the world coordinate of all points to be rendered. This is already done for most of the standard shader's vertex programs:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Extract world coordinates in vertex programs:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"glsl\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"float4 posWorld = mul(_Object2World, v.vertex);\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"In the fragment programs, we can then use the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"http://http.developer.nvidia.com/Cg/clip.html\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"clip\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" function with the distance to the plane as the parameter. If the clip function is called with any number less than zero, it will discard the current pixel. This is perfect, because if the distance to the plane is less than zero, a point is behind the plane.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Using the clip function:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"glsl\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"float4 _planePos;\\\\nfloat4 _planeNorm;\\\\n\\\\nvoid PlaneClip(float3 posWorld) {\\\\n    clip(distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld));\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"If you have more planes, you can call clip with float2, float3, float4 parameters, or call clip multiple times. For example:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Multiple clipping planes:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"glsl\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"float4 _planePos;\\\\nfloat4 _planeNorm;\\\\n\\\\n#if (CLIP_TWO || CLIP_THREE)\\\\n    float4 _planePos2;\\\\n    float4 _planeNorm2;\\\\n#endif\\\\n\\\\n#if (CLIP_THREE)\\\\n    float4 _planePos3;\\\\n    float4 _planeNorm3;\\\\n#endif\\\\n\\\\nvoid PlaneClip(float3 posWorld) {\\\\n    #if CLIP_THREE\\\\n        clip(float3(\\\\n            distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld),\\\\n            distanceToPlane(_planePos2.xyz, _planeNorm2.xyz, posWorld),\\\\n            distanceToPlane(_planePos3.xyz, _planeNorm3.xyz, posWorld)\\\\n        ));\\\\n    #else //CLIP_THREE\\\\n        #if CLIP_TWO\\\\n            clip(float2(\\\\n                distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld),\\\\n                distanceToPlane(_planePos2.xyz, _planeNorm2.xyz, posWorld)\\\\n            ));\\\\n        #else //CLIP_TWO\\\\n            clip(distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld));\\\\n        #endif //CLIP_TWO\\\\n    #endif //CLIP_THREE\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"All we need to do now is change all passes of the Standard shader and modify the vertex and fragment programs to call this function.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"We will use Unity3D's wonderful \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"http://docs.unity3d.com/Manual/SL-MultipleProgramVariants.html\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"shader program variants feature\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" for this, so that if we do not want any clipping planes it will not cause any performance hits as the code will just be eliminated in that case. The CLIP_TWO and CLIP_THREE definitions are produced by the shader variant system, because in each pass we will have this directive:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Shader variant pragma directive:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"c\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#pragma multi_compile __ CLIP_ONE CLIP_TWO CLIP_THREE\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"It basically tells Unity3D's shader compiler to generate four variants of the shader, a variant with no clipping planes, a variant with one clipping plane, another variant with two clipping planes, and the last one with three. We can choose how many clipping planes we want to use, by for example enabling the CLIP_ONE keyword, or the CLIP_TWO keyword. The method to enable keywords is: \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"http://docs.unity3d.com/ScriptReference/Material.EnableKeyword.html\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"Material.EnableKeyword\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Let's go!\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Create a new shader called \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"StandardClippable.shader\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and place it in your project's Assets/Shaders directory. Copy the contents of the Standard.shader file in the builtin_shaders zip, which can be found inside the DefaultResourcesExtra directory. Paste into the StandardClippable.shader. Change the first line to be:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"Shader \\\\\\\"Custom/StandardClippable\\\\\\\"\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Add the properties for the plane positions and normals, so that the properties block will look like this:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Shader properties for clipping planes:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Properties\\\\n{\\\\n    _Color(\\\\\\\"Color\\\\\\\", Color) = (1,1,1,1)\\\\n    _MainTex(\\\\\\\"Albedo\\\\\\\", 2D) = \\\\\\\"white\\\\\\\" {}\\\\n\\\\n    _Cutoff(\\\\\\\"Alpha Cutoff\\\\\\\", Range(0.0, 1.0)) = 0.5\\\\n\\\\n    _Glossiness(\\\\\\\"Smoothness\\\\\\\", Range(0.0, 1.0)) = 0.5\\\\n    [Gamma] _Metallic(\\\\\\\"Metallic\\\\\\\", Range(0.0, 1.0)) = 0.0\\\\n    _MetallicGlossMap(\\\\\\\"Metallic\\\\\\\", 2D) = \\\\\\\"white\\\\\\\" {}\\\\n\\\\n    _BumpScale(\\\\\\\"Scale\\\\\\\", Float) = 1.0\\\\n    _BumpMap(\\\\\\\"Normal Map\\\\\\\", 2D) = \\\\\\\"bump\\\\\\\" {}\\\\n\\\\n    _Parallax (\\\\\\\"Height Scale\\\\\\\", Range (0.005, 0.08)) = 0.02\\\\n    _ParallaxMap (\\\\\\\"Height Map\\\\\\\", 2D) = \\\\\\\"black\\\\\\\" {}\\\\n\\\\n    _OcclusionStrength(\\\\\\\"Strength\\\\\\\", Range(0.0, 1.0)) = 1.0\\\\n    _OcclusionMap(\\\\\\\"Occlusion\\\\\\\", 2D) = \\\\\\\"white\\\\\\\" {}\\\\n\\\\n    _EmissionColor(\\\\\\\"Color\\\\\\\", Color) = (0,0,0)\\\\n    _EmissionMap(\\\\\\\"Emission\\\\\\\", 2D) = \\\\\\\"white\\\\\\\" {}\\\\n\\\\n    _DetailMask(\\\\\\\"Detail Mask\\\\\\\", 2D) = \\\\\\\"white\\\\\\\" {}\\\\n\\\\n    _DetailAlbedoMap(\\\\\\\"Detail Albedo x2\\\\\\\", 2D) = \\\\\\\"grey\\\\\\\" {}\\\\n    _DetailNormalMapScale(\\\\\\\"Scale\\\\\\\", Float) = 1.0\\\\n    _DetailNormalMap(\\\\\\\"Normal Map\\\\\\\", 2D) = \\\\\\\"bump\\\\\\\" {}\\\\n\\\\n    [Enum(UV0,0,UV1,1)] _UVSec (\\\\\\\"UV Set for secondary textures\\\\\\\", Float) = 0\\\\n\\\\n    // UI-only data\\\\n    [HideInInspector] _EmissionScaleUI(\\\\\\\"Scale\\\\\\\", Float) = 0.0\\\\n    [HideInInspector] _EmissionColorUI(\\\\\\\"Color\\\\\\\", Color) = (1,1,1)\\\\n\\\\n    // Blending state\\\\n    [HideInInspector] _Mode (\\\\\\\"__mode\\\\\\\", Float) = 0.0\\\\n    [HideInInspector] _SrcBlend (\\\\\\\"__src\\\\\\\", Float) = 1.0\\\\n    [HideInInspector] _DstBlend (\\\\\\\"__dst\\\\\\\", Float) = 0.0\\\\n    [HideInInspector] _ZWrite (\\\\\\\"__zw\\\\\\\", Float) = 1.0\\\\n\\\\n    _planePos (\\\\\\\"Clipping Plane Position\\\\\\\", Vector) = ( 0, 0, 0, 1 )\\\\n    _planePos2 (\\\\\\\"Clipping Plane Position 2\\\\\\\", Vector) = ( 0, 0, 0, 1 )\\\\n    _planePos3 (\\\\\\\"Clipping Plane Position 3\\\\\\\", Vector) = ( 0, 0, 0, 1 )\\\\n\\\\n    _planeNorm (\\\\\\\"Clipping Plane Normal\\\\\\\", Vector) = ( 0, 1, 0, 1 )\\\\n    _planeNorm2 (\\\\\\\"Clipping Plane Normal 2\\\\\\\", Vector) = ( 0, 1, 0, 1 )\\\\n    _planeNorm3 (\\\\\\\"Clipping Plane Normal 3\\\\\\\", Vector) = ( 0, 1, 0, 1 )\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"We just added the lines after 41.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"We will create a helper .cginc file named \\\\\\\"plane_clipping.cginc\\\\\\\". Here is its contents:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"plane_clipping.cginc helper file:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#ifndef PLANE_CLIPPING_INCLUDED\\\\n#define PLANE_CLIPPING_INCLUDED\\\\n\\\\n//Plane clipping definitions. Uses three planes for clipping, but this can be increased if necessary.\\\\n\\\\n#if CLIP_ONE || CLIP_TWO || CLIP_THREE\\\\n    //If we have 1, 2 or 3 clipping planes, PLANE_CLIPPING_ENABLED will be defined.\\\\n    //This makes it easier to check if this feature is available or not.\\\\n    #define PLANE_CLIPPING_ENABLED 1\\\\n\\\\n    //http://mathworld.wolfram.com/Point-PlaneDistance.html\\\\n    float distanceToPlane(float3 planePosition, float3 planeNormal, float3 pointInWorld)\\\\n    {\\\\n        //w = vector from plane to point\\\\n        float3 w = - ( planePosition - pointInWorld );\\\\n        float res = ( planeNormal.x * w.x + \\\\n            planeNormal.y * w.y + \\\\n            planeNormal.z * w.z ) \\\\n            / sqrt( planeNormal.x * planeNormal.x +\\\\n                planeNormal.y * planeNormal.y +\\\\n                planeNormal.z * planeNormal.z );\\\\n        return res;\\\\n    }\\\\n\\\\n    //we will have at least one plane.\\\\n    float4 _planePos;\\\\n    float4 _planeNorm;\\\\n\\\\n    //at least two planes.\\\\n    #if (CLIP_TWO || CLIP_THREE)\\\\n        float4 _planePos2;\\\\n        float4 _planeNorm2;\\\\n    #endif\\\\n\\\\n    //at least three planes.\\\\n    #if (CLIP_THREE)\\\\n        float4 _planePos3;\\\\n        float4 _planeNorm3;\\\\n    #endif\\\\n\\\\n    //discard drawing of a point in the world if it is behind any one of the planes.\\\\n    void PlaneClip(float3 posWorld) {\\\\n        #if CLIP_THREE\\\\n            clip(float3(\\\\n                distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld),\\\\n                distanceToPlane(_planePos2.xyz, _planeNorm2.xyz, posWorld),\\\\n                distanceToPlane(_planePos3.xyz, _planeNorm3.xyz, posWorld)\\\\n            ));\\\\n        #else //CLIP_THREE\\\\n            #if CLIP_TWO\\\\n                clip(float2(\\\\n                    distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld),\\\\n                    distanceToPlane(_planePos2.xyz, _planeNorm2.xyz, posWorld)\\\\n                ));\\\\n            #else //CLIP_TWO\\\\n                clip(distanceToPlane(_planePos.xyz, _planeNorm.xyz, posWorld));\\\\n            #endif //CLIP_TWO\\\\n        #endif //CLIP_THREE\\\\n    }\\\\n\\\\n    //preprocessor macro that will produce an empty block if no clipping planes are used.\\\\n    #define PLANE_CLIP(posWorld) PlaneClip(posWorld);\\\\n    \\\\n#else\\\\n    //empty definition\\\\n    #define PLANE_CLIP(s)\\\\n#endif\\\\n\\\\n#endif // PLANE_CLIPPING_INCLUDED\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The comments in the above file should explain what it is doing.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The next step is to use the PLANE_CLIP macro in the fragment programs of all passes.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The First Pass\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Let's look at the FORWARD pass for example:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Original forward pass:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"// ------------------------------------------------------------------\\\\n// Base forward pass (directional light, emission, lightmaps, ...)\\\\nPass\\\\n{\\\\n    Name \\\\\\\"FORWARD\\\\\\\" \\\\n    Tags { \\\\\\\"LightMode\\\\\\\" = \\\\\\\"ForwardBase\\\\\\\" }\\\\n\\\\n    Blend [_SrcBlend] [_DstBlend]\\\\n    ZWrite [_ZWrite]\\\\n\\\\n    CGPROGRAM\\\\n    #pragma target 3.0\\\\n    // TEMPORARY: GLES2.0 temporarily disabled to prevent errors spam on devices without textureCubeLodEXT\\\\n    #pragma exclude_renderers gles\\\\n    \\\\n    // -------------------------------------\\\\n    \\\\n    #pragma shader_feature _NORMALMAP\\\\n    #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON\\\\n    #pragma shader_feature _EMISSION\\\\n    #pragma shader_feature _METALLICGLOSSMAP \\\\n    #pragma shader_feature ___ _DETAIL_MULX2\\\\n    #pragma shader_feature _PARALLAXMAP\\\\n    \\\\n    #pragma multi_compile_fwdbase\\\\n    #pragma multi_compile_fog\\\\n    \\\\n    #pragma vertex vertForwardBase\\\\n    #pragma fragment fragForwardBase\\\\n\\\\n    #include \\\\\\\"UnityStandardCore.cginc\\\\\\\"\\\\n\\\\n    ENDCG\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The lines which are important are 28, 29 and 31. This pass uses the vertForwardBase vertex program, and the fragForwardBase fragment program. These programs are defined in the UnityStandardCore.cginc file.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"So, find the UnityStandardCore.cginc file in the default shaders zip. Make a copy of it, and save it as \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_clipped.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" next to our \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"StandardClippable.shader\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" file.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Change all references of \\\\\\\"\\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"UnityStandardCore.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"\\\\\\\" to be \\\\\\\"\\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_clipped.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"\\\\\\\" instead.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Also, add the line\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"#pragma multi_compile __ CLIP_ONE CLIP_TWO CLIP_THREE\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"just above the include lines. The forward pass should now look like this:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Modified forward pass:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"// ------------------------------------------------------------------\\\\n// Base forward pass (directional light, emission, lightmaps, ...)\\\\nPass\\\\n{\\\\n    Name \\\\\\\"FORWARD\\\\\\\" \\\\n    Tags { \\\\\\\"LightMode\\\\\\\" = \\\\\\\"ForwardBase\\\\\\\" }\\\\n\\\\n    Blend [_SrcBlend] [_DstBlend]\\\\n    ZWrite [_ZWrite]\\\\n\\\\n    CGPROGRAM\\\\n    #pragma target 3.0\\\\n    // TEMPORARY: GLES2.0 temporarily disabled to prevent errors spam on devices without textureCubeLodEXT\\\\n    #pragma exclude_renderers gles\\\\n    \\\\n    // -------------------------------------\\\\n    \\\\n    #pragma shader_feature _NORMALMAP\\\\n    #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON\\\\n    #pragma shader_feature _EMISSION\\\\n    #pragma shader_feature _METALLICGLOSSMAP \\\\n    #pragma shader_feature ___ _DETAIL_MULX2\\\\n    #pragma shader_feature _PARALLAXMAP\\\\n    \\\\n    #pragma multi_compile_fwdbase\\\\n    #pragma multi_compile_fog\\\\n    \\\\n    #pragma vertex vertForwardBase\\\\n    #pragma fragment fragForwardBase\\\\n\\\\n    #pragma multi_compile __ CLIP_ONE CLIP_TWO CLIP_THREE\\\\n\\\\n    #include \\\\\\\"standard_clipped.cginc\\\\\\\"\\\\n\\\\n    ENDCG\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And your Shaders folder should now have three files:\\\"}]},{\\\"type\\\":\\\"bulletList\\\",\\\"content\\\":[{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"StandardClippable.shader\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"plane_clipping.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" and\\\"}]}]},{\\\"type\\\":\\\"listItem\\\",\\\"content\\\":[{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_clipped.cginc\\\"}]}]}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Let's open the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_clipped.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" file. Add this line to the top of the file:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"#include \\\\\\\"plane_clipping.cginc\\\\\\\"\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Place it just below the include for \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"AutoLight.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". This now allows us to use the functions and macros defined in that file.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"We will be editing the vertex program first. Here it is as copied from the file:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Original vertex program:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"struct VertexOutputForwardBase\\\\n{\\\\n    float4 pos : SV_POSITION;\\\\n    float4 tex : TEXCOORD0;\\\\n    half3 eyeVec : TEXCOORD1;\\\\n    half4 tangentToWorldAndParallax[3] : TEXCOORD2; // [3x3:tangentToWorld | 1x3:viewDirForParallax]\\\\n    half4 ambientOrLightmapUV : TEXCOORD5; // SH or Lightmap UV\\\\n    SHADOW_COORDS(6)\\\\n    UNITY_FOG_COORDS(7)\\\\n\\\\n    // next ones would not fit into SM2.0 limits, but they are always for SM3.0+\\\\n    #if UNITY_SPECCUBE_BOX_PROJECTION\\\\n        float3 posWorld : TEXCOORD8;\\\\n    #endif\\\\n};\\\\n\\\\nVertexOutputForwardBase vertForwardBase (VertexInput v)\\\\n{\\\\n    VertexOutputForwardBase o;\\\\n    UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o);\\\\n\\\\n    float4 posWorld = mul(_Object2World, v.vertex);\\\\n    #if UNITY_SPECCUBE_BOX_PROJECTION\\\\n        o.posWorld = posWorld.xyz;\\\\n    #endif\\\\n    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);\\\\n    o.tex = TexCoords(v);\\\\n    o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos);\\\\n    float3 normalWorld = UnityObjectToWorldNormal(v.normal);\\\\n    #ifdef _TANGENT_TO_WORLD\\\\n        float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);\\\\n\\\\n        float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);\\\\n        o.tangentToWorldAndParallax[0].xyz = tangentToWorld[0];\\\\n        o.tangentToWorldAndParallax[1].xyz = tangentToWorld[1];\\\\n        o.tangentToWorldAndParallax[2].xyz = tangentToWorld[2];\\\\n    #else\\\\n        o.tangentToWorldAndParallax[0].xyz = 0;\\\\n        o.tangentToWorldAndParallax[1].xyz = 0;\\\\n        o.tangentToWorldAndParallax[2].xyz = normalWorld;\\\\n    #endif\\\\n    //We need this for shadow receving\\\\n    TRANSFER_SHADOW(o);\\\\n\\\\n    // Static lightmaps\\\\n    #ifndef LIGHTMAP_OFF\\\\n        o.ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw;\\\\n        o.ambientOrLightmapUV.zw = 0;\\\\n    // Sample light probe for Dynamic objects only (no static or dynamic lightmaps)\\\\n    #elif UNITY_SHOULD_SAMPLE_SH\\\\n        #if UNITY_SAMPLE_FULL_SH_PER_PIXEL\\\\n            o.ambientOrLightmapUV.rgb = 0;\\\\n        #elif (SHADER_TARGET \u003c 30)\\\\n            o.ambientOrLightmapUV.rgb = ShadeSH9(half4(normalWorld, 1.0));\\\\n        #else\\\\n            // Optimization: L2 per-vertex, L0..L1 per-pixel\\\\n            o.ambientOrLightmapUV.rgb = ShadeSH3Order(half4(normalWorld, 1.0));\\\\n        #endif\\\\n        // Add approximated illumination from non-important point lights\\\\n        #ifdef VERTEXLIGHT_ON\\\\n            o.ambientOrLightmapUV.rgb += Shade4PointLights (\\\\n                unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,\\\\n                unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,\\\\n                unity_4LightAtten0, posWorld, normalWorld);\\\\n        #endif\\\\n    #endif\\\\n\\\\n    #ifdef DYNAMICLIGHTMAP_ON\\\\n        o.ambientOrLightmapUV.zw = v.uv2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;\\\\n    #endif\\\\n    \\\\n    #ifdef _PARALLAXMAP\\\\n        TANGENT_SPACE_ROTATION;\\\\n        half3 viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex));\\\\n        o.tangentToWorldAndParallax[0].w = viewDirForParallax.x;\\\\n        o.tangentToWorldAndParallax[1].w = viewDirForParallax.y;\\\\n        o.tangentToWorldAndParallax[2].w = viewDirForParallax.z;\\\\n    #endif\\\\n    \\\\n    UNITY_TRANSFER_FOG(o,o.pos);\\\\n    return o;\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The vertex program will need to pass the world position to the fragment program. It currently does so only if \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"UNITY_SPECCUBE_BOX_PROJECTION\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" is defined (relevant lines in above snippet: 12 and 23).\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Change the lines\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"#if UNITY_SPECCUBE_BOX_PROJECTION\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"to be:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"#if UNITY_SPECCUBE_BOX_PROJECTION || PLANE_CLIPPING_ENABLED\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"There should be one inside the struct definition just above the function, and one within the function. This way, the posWorld vector will be passed onto the fragment shader to be used by plane clipping.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The next step is the fragment program:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Original fragment program:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"half4 fragForwardBase (VertexOutputForwardBase i) : SV_Target\\\\n{\\\\n    FRAGMENT_SETUP(s)\\\\n    UnityLight mainLight = MainLight (s.normalWorld);\\\\n    half atten = SHADOW_ATTENUATION(i);\\\\n    \\\\n    half occlusion = Occlusion(i.tex.xy);\\\\n    UnityGI gi = FragmentGI (\\\\n        s.posWorld, occlusion, i.ambientOrLightmapUV, atten, s.oneMinusRoughness, s.normalWorld, s.eyeVec, mainLight);\\\\n\\\\n    half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);\\\\n    c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);\\\\n    c.rgb += Emission(i.tex.xy);\\\\n\\\\n    UNITY_APPLY_FOG(i.fogCoord, c.rgb);\\\\n    return OutputForward (c, s.alpha);\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"This uses the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"FRAGMENT_SETUP\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" macro:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"FRAGMENT_SETUP macro:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#if UNITY_SPECCUBE_BOX_PROJECTION\\\\n    #define IN_WORLDPOS(i) i.posWorld\\\\n#else\\\\n    #define IN_WORLDPOS(i) half3(0,0,0)\\\\n#endif\\\\n\\\\n#define IN_LIGHTDIR_FWDADD(i) half3(i.tangentToWorldAndLightDir[0].w, i.tangentToWorldAndLightDir[1].w, i.tangentToWorldAndLightDir[2].w)\\\\n\\\\n#define FRAGMENT_SETUP(x) FragmentCommonData x = \\\\\\\\\\\\n    FragmentSetup(i.tex, i.eyeVec, WorldNormal(i.tangentToWorldAndParallax), IN_VIEWDIR4PARALLAX(i), ExtractTangentToWorldPerPixel(i.tangentToWorldAndParallax), IN_WORLDPOS(i));\\\\n\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Which uses the IN_WORLDPOS macro in order to get the world position if necessary.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The world position is acquired only if UNITY_SPECCUBE_BOX_PROJECTION (similar to above) is defined, so change the line\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"#if UNITY_SPECCUBE_BOX_PROJECTION\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"to be:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"#if UNITY_SPECCUBE_BOX_PROJECTION || PLANE_CLIPPING_ENABLED\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The lines up to FRAGMENT_SETUP should now look like:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Modified fragment setup:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#if UNITY_SPECCUBE_BOX_PROJECTION || PLANE_CLIPPING_ENABLED\\\\n    #define IN_WORLDPOS(i) i.posWorld\\\\n#else\\\\n    #define IN_WORLDPOS(i) half3(0,0,0)\\\\n#endif\\\\n\\\\n#define IN_LIGHTDIR_FWDADD(i) half3(i.tangentToWorldAndLightDir[0].w, i.tangentToWorldAndLightDir[1].w, i.tangentToWorldAndLightDir[2].w)\\\\n\\\\n#define FRAGMENT_SETUP(x) FragmentCommonData x = \\\\\\\\\\\\n    FragmentSetup(i.tex, i.eyeVec, WorldNormal(i.tangentToWorldAndParallax), IN_VIEWDIR4PARALLAX(i), ExtractTangentToWorldPerPixel(i.tangentToWorldAndParallax), IN_WORLDPOS(i));\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And finally, let's add the plane clipping to the fragment shader. Place this line\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"PLANE_CLIP(s.posWorld)\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"just below\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"FRAGMENT_SETUP(s)\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Your fragment shader code should now look like this:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Complete modified fragment program:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"half4 fragForwardBase (VertexOutputForwardBase i) : SV_Target\\\\n{\\\\n    FRAGMENT_SETUP(s)\\\\n    PLANE_CLIP(s.posWorld)\\\\n\\\\n    UnityLight mainLight = MainLight (s.normalWorld);\\\\n    half atten = SHADOW_ATTENUATION(i);\\\\n    \\\\n    half occlusion = Occlusion(i.tex.xy);\\\\n    UnityGI gi = FragmentGI (\\\\n        s.posWorld, occlusion, i.ambientOrLightmapUV, atten, s.oneMinusRoughness, s.normalWorld, s.eyeVec, mainLight);\\\\n\\\\n    half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);\\\\n    c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);\\\\n    c.rgb += Emission(i.tex.xy);\\\\n\\\\n    UNITY_APPLY_FOG(i.fogCoord, c.rgb);\\\\n    return OutputForward (c, s.alpha);\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"This fixes the FORWARD pass!\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Other passes\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The next pass is the FORWARD_ADD.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Let's make the vertex shader pass the world position to the fragment shader:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Add these lines before the closing brace of the VertexOutputForwardAdd struct:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"VertexOutputForwardAdd struct additions:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#if PLANE_CLIPPING_ENABLED\\\\n    float3 posWorld : TEXCOORD9;\\\\n#endif\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"We use TEXCOORD9 because 8 was used just above it.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And in the vertForwardAdd function, add the lines:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"vertForwardAdd modifications:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#if PLANE_CLIPPING_ENABLED\\\\n    o.posWorld = posWorld.xyz;\\\\n#endif\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Just after this line:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"float4 posWorld = mul(_Object2World, v.vertex);\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The fragment shader uses the FRAGMENT_SETUP_FWDADD macro, which is defined just below FRAGMENT_SETUP.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Above the FRAGMENT_SETUP_FWDADD macro, add these lines:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":null},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#if PLANE_CLIPPING_ENABLED\\\\n    #define IN_WORLDPOS_FWDADD(i) i.posWorld\\\\n#else\\\\n    #define IN_WORLDPOS_FWDADD(i) half3(0,0,0)\\\\n#endif\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And use the newly created \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"IN_WORLDPOS_FWDADD\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" macro instead of the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"half3(0,0,0)\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" as the last parameter for FragmentSetup. Here's how the relevant lines should look:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"FRAGMENT_SETUP_FWDADD macro changes:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#if PLANE_CLIPPING_ENABLED\\\\n    #define IN_WORLDPOS_FWDADD(i) i.posWorld\\\\n#else\\\\n    #define IN_WORLDPOS_FWDADD(i) half3(0,0,0)\\\\n#endif\\\\n\\\\n#define FRAGMENT_SETUP_FWDADD(x) FragmentCommonData x = \\\\\\\\\\\\n    FragmentSetup(i.tex, i.eyeVec, WorldNormal(i.tangentToWorldAndLightDir), IN_VIEWDIR4PARALLAX_FWDADD(i), ExtractTangentToWorldPerPixel(i.tangentToWorldAndLightDir), IN_WORLDPOS_FWDADD(i));\\\\n\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And finally, you can now call the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"PLANE_CLIP(s.posWorld)\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" macro right after \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"code\\\"}],\\\"text\\\":\\\"FRAGMENT_SETUP_FWDADD(s)\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" inside fragForwardAdd.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Here's the code for the VertexOutputForwardAdd struct, vertForwardAdd function and fragForwardAdd function altogether:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Complete forward add implementation:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"// ------------------------------------------------------------------\\\\n// Additive forward pass (one light per pass)\\\\nstruct VertexOutputForwardAdd\\\\n{\\\\n    float4 pos : SV_POSITION;\\\\n    float4 tex : TEXCOORD0;\\\\n    half3 eyeVec : TEXCOORD1;\\\\n    half4 tangentToWorldAndLightDir[3] : TEXCOORD2; // [3x3:tangentToWorld | 1x3:lightDir]\\\\n    LIGHTING_COORDS(5,6)\\\\n    UNITY_FOG_COORDS(7)\\\\n\\\\n    // next ones would not fit into SM2.0 limits, but they are always for SM3.0+\\\\n    #if defined(_PARALLAXMAP)\\\\n        half3 viewDirForParallax : TEXCOORD8;\\\\n    #endif\\\\n\\\\n    #if PLANE_CLIPPING_ENABLED\\\\n        float3 posWorld : TEXCOORD9;\\\\n    #endif\\\\n};\\\\n\\\\nVertexOutputForwardAdd vertForwardAdd (VertexInput v)\\\\n{\\\\n    VertexOutputForwardAdd o;\\\\n    UNITY_INITIALIZE_OUTPUT(VertexOutputForwardAdd, o);\\\\n\\\\n    float4 posWorld = mul(_Object2World, v.vertex);\\\\n    #if PLANE_CLIPPING_ENABLED\\\\n        o.posWorld = posWorld.xyz;\\\\n    #endif\\\\n    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);\\\\n    o.tex = TexCoords(v);\\\\n    o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos);\\\\n    float3 normalWorld = UnityObjectToWorldNormal(v.normal);\\\\n    #ifdef _TANGENT_TO_WORLD\\\\n        float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w);\\\\n\\\\n        float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w);\\\\n        o.tangentToWorldAndLightDir[0].xyz = tangentToWorld[0];\\\\n        o.tangentToWorldAndLightDir[1].xyz = tangentToWorld[1];\\\\n        o.tangentToWorldAndLightDir[2].xyz = tangentToWorld[2];\\\\n    #else\\\\n        o.tangentToWorldAndLightDir[0].xyz = 0;\\\\n        o.tangentToWorldAndLightDir[1].xyz = 0;\\\\n        o.tangentToWorldAndLightDir[2].xyz = normalWorld;\\\\n    #endif\\\\n    //We need this for shadow receving\\\\n    TRANSFER_VERTEX_TO_FRAGMENT(o);\\\\n\\\\n    float3 lightDir = _WorldSpaceLightPos0.xyz - posWorld.xyz * _WorldSpaceLightPos0.w;\\\\n    #ifndef USING_DIRECTIONAL_LIGHT\\\\n        lightDir = NormalizePerVertexNormal(lightDir);\\\\n    #endif\\\\n    o.tangentToWorldAndLightDir[0].w = lightDir.x;\\\\n    o.tangentToWorldAndLightDir[1].w = lightDir.y;\\\\n    o.tangentToWorldAndLightDir[2].w = lightDir.z;\\\\n\\\\n    #ifdef _PARALLAXMAP\\\\n        TANGENT_SPACE_ROTATION;\\\\n        o.viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex));\\\\n    #endif\\\\n    \\\\n    UNITY_TRANSFER_FOG(o,o.pos);\\\\n    return o;\\\\n}\\\\n\\\\nhalf4 fragForwardAdd (VertexOutputForwardAdd i) : SV_Target\\\\n{\\\\n    FRAGMENT_SETUP_FWDADD(s)\\\\n    PLANE_CLIP(s.posWorld)\\\\n\\\\n    UnityLight light = AdditiveLight (s.normalWorld, IN_LIGHTDIR_FWDADD(i), LIGHT_ATTENUATION(i));\\\\n    UnityIndirect noIndirect = ZeroIndirect ();\\\\n\\\\n    half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, light, noIndirect);\\\\n    \\\\n    UNITY_APPLY_FOG_COLOR(i.fogCoord, c.rgb, half4(0,0,0,0)); // fog towards black in additive pass\\\\n    return OutputForward (c, s.alpha);\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And here's what the pass definition in StandardClippable.shader should look like:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Forward add shader pass:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"// ------------------------------------------------------------------\\\\n// Additive forward pass (one light per pass)\\\\nPass\\\\n{\\\\n    Name \\\\\\\"FORWARD_DELTA\\\\\\\"\\\\n    Tags { \\\\\\\"LightMode\\\\\\\" = \\\\\\\"ForwardAdd\\\\\\\" }\\\\n    Blend [_SrcBlend] One\\\\n    Fog { Color (0,0,0,0) } // in additive pass fog should be black\\\\n    ZWrite Off\\\\n    ZTest LEqual\\\\n\\\\n    CGPROGRAM\\\\n    #pragma target 3.0\\\\n    // GLES2.0 temporarily disabled to prevent errors spam on devices without textureCubeLodEXT\\\\n    #pragma exclude_renderers gles\\\\n\\\\n    // -------------------------------------\\\\n\\\\n    \\\\n    #pragma shader_feature _NORMALMAP\\\\n    #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON\\\\n    #pragma shader_feature _METALLICGLOSSMAP\\\\n    #pragma shader_feature ___ _DETAIL_MULX2\\\\n    #pragma shader_feature _PARALLAXMAP\\\\n    \\\\n    #pragma multi_compile_fwdadd_fullshadows\\\\n    #pragma multi_compile_fog\\\\n    \\\\n    #pragma vertex vertForwardAdd\\\\n    #pragma fragment fragForwardAdd\\\\n\\\\n    #pragma multi_compile __ CLIP_ONE CLIP_TWO CLIP_THREE\\\\n\\\\n    #include \\\\\\\"standard_clipped.cginc\\\\\\\"\\\\n\\\\n    ENDCG\\\\n}\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Shadow pass\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The shadow pass uses another file, \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"UnityStandardShadow.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". Make a copy of this file and save it as \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_shadow_clipped.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\". In the shadow pass definition, include the \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_shadow_clipped.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" instead of \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"UnityStandardShadow.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", and don't forget the #pragma declarations!\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Inside \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"standard_shadow_clipped.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\", include \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"plane_clipping.cginc\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" as usual.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"In some conditions, the shadow vertex shader does not use an output struct. We want to ensure that we have the output struct so that we can pass the world position. Around line 27, change the code so that it looks like this:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Shadow vertex output struct:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"// Has a non-empty shadow caster output struct (it's an error to have empty structs on some platforms...)\\\\n#if PLANE_CLIPPING_ENABLED || !defined(V2F_SHADOW_CASTER_NOPOS_IS_EMPTY) || defined(UNITY_STANDARD_USE_SHADOW_UVS)\\\\n    #define UNITY_STANDARD_USE_SHADOW_OUTPUT_STRUCT 1\\\\n#endif\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Inside the VertexOutputShadowCaster struct (around line 50), add the posWorld parameter:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"VertexOutputShadowCaster modifications:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"#ifdef UNITY_STANDARD_USE_SHADOW_OUTPUT_STRUCT\\\\nstruct VertexOutputShadowCaster\\\\n{\\\\n    V2F_SHADOW_CASTER_NOPOS\\\\n    #if defined(UNITY_STANDARD_USE_SHADOW_UVS)\\\\n        float2 tex : TEXCOORD1;\\\\n    #endif\\\\n\\\\n    #if PLANE_CLIPPING_ENABLED\\\\n        float3 posWorld : TEXCOORD2;\\\\n    #endif\\\\n};\\\\n#endif\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And finally, just after it, here's the modified vertShadowCaster and fragShadowCaster functions:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Complete shadow pass implementation:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"shader\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"void vertShadowCaster (VertexInput v,\\\\n    #ifdef UNITY_STANDARD_USE_SHADOW_OUTPUT_STRUCT\\\\n        out VertexOutputShadowCaster o,\\\\n    #endif\\\\n    out float4 opos : SV_POSITION)\\\\n{\\\\n    #if PLANE_CLIPPING_ENABLED\\\\n        float4 posWorld = mul(_Object2World, v.vertex);\\\\n        o.posWorld = posWorld.xyz;\\\\n    #endif\\\\n    TRANSFER_SHADOW_CASTER_NOPOS(o,opos)\\\\n    #if defined(UNITY_STANDARD_USE_SHADOW_UVS)\\\\n        o.tex = TRANSFORM_TEX(v.uv0, _MainTex);\\\\n    #endif\\\\n}\\\\n\\\\n\\\\nhalf4 fragShadowCaster (\\\\n    #ifdef UNITY_STANDARD_USE_SHADOW_OUTPUT_STRUCT\\\\n        VertexOutputShadowCaster i\\\\n    #endif\\\\n    #ifdef UNITY_STANDARD_USE_DITHER_MASK\\\\n        , UNITY_VPOS_TYPE vpos : VPOS\\\\n    #endif\\\\n    ) : SV_Target\\\\n{\\\\n    PLANE_CLIP(i.posWorld)\\\\n\\\\n    #if defined(UNITY_STANDARD_USE_SHADOW_UVS)\\\\n        half alpha = tex2D(_MainTex, i.tex).a * _Color.a;\\\\n        #if defined(_ALPHATEST_ON)\\\\n            clip (alpha - _Cutoff);\\\\n        #endif\\\\n        #if defined(_ALPHABLEND_ON) || defined(_ALPHAPREMULTIPLY_ON)\\\\n            #if defined(UNITY_STANDARD_USE_DITHER_MASK)\\\\n                // Use dither mask for alpha blended shadows, based on pixel position xy\\\\n                // and alpha level. Our dither texture is 4x4x16.\\\\n                half alphaRef = tex3D(_DitherMaskLOD, float3(vpos.xy*0.25,alpha*0.9375)).a;\\\\n                clip (alphaRef - 0.01);\\\\n            #else\\\\n                clip (alpha - _Cutoff);\\\\n            #endif\\\\n        #endif\\\\n    #endif // #if defined(UNITY_STANDARD_USE_SHADOW_UVS)\\\\n\\\\n    SHADOW_CASTER_FRAGMENT(i)\\\\n}\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Guess what we added ;)\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Deferred pass\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The deferred pass is very similar to the forward pass. Try to do it yourself :) If you have any trouble, write a comment below, and I will help you!\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Lod 150 passes\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"You just need to use the correct include files and the #pragma declarations in these passes.\\\"}]},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":3},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"An example script\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Here's an example that shows how to use the EnableKeyword method correctly and define the clipping planes for the shader:\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"bold\\\"}],\\\"text\\\":\\\"Example script for enabling clipping planes:\\\"}]},{\\\"type\\\":\\\"codeBlock\\\",\\\"attrs\\\":{\\\"language\\\":\\\"cs\\\"},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"using UnityEngine;\\\\nusing System.Collections;\\\\nusing System.Linq;\\\\n\\\\n[ExecuteInEditMode]\\\\npublic class ClippableObject : MonoBehaviour {\\\\n    public void OnEnable() {\\\\n        //let's just create a new material instance.\\\\n        GetComponent\u003cRenderer\u003e().sharedMaterial = new Material(Shader.Find(\\\\\\\"Custom/StandardClippable\\\\\\\")) {\\\\n            hideFlags = HideFlags.HideAndDontSave\\\\n        };\\\\n    }\\\\n\\\\n    public void Start() { }\\\\n\\\\n    //only 3 clip planes for now, will need to modify the shader for more.\\\\n    [Range(0, 3)]\\\\n    public int clipPlanes = 0;\\\\n\\\\n    //preview size for the planes. Shown when the object is selected.\\\\n    public float planePreviewSize = 5.0f;\\\\n\\\\n    //Positions and rotations for the planes. The rotations will be converted into normals to be used by the shaders.\\\\n    public Vector3 plane1Position = Vector3.zero;\\\\n    public Vector3 plane1Rotation = new Vector3(0, 0, 0);\\\\n\\\\n    public Vector3 plane2Position = Vector3.zero;\\\\n    public Vector3 plane2Rotation = new Vector3(0, 90, 90);\\\\n\\\\n    public Vector3 plane3Position = Vector3.zero;\\\\n    public Vector3 plane3Rotation = new Vector3(0, 0, 90);\\\\n\\\\n    //Only used for previewing a plane. Draws diagonals and edges of a limited flat plane.\\\\n    private void DrawPlane(Vector3 position, Vector3 euler) {\\\\n        var forward = Quaternion.Euler(euler) * Vector3.forward;\\\\n        var left = Quaternion.Euler(euler) * Vector3.left;\\\\n\\\\n        var forwardLeft = position + forward * planePreviewSize * 0.5f + left * planePreviewSize * 0.5f;\\\\n        var forwardRight = forwardLeft - left * planePreviewSize;\\\\n        var backRight = forwardRight - forward * planePreviewSize;\\\\n        var backLeft = forwardLeft - forward * planePreviewSize;\\\\n\\\\n        Gizmos.DrawLine(position, forwardLeft);\\\\n        Gizmos.DrawLine(position, forwardRight);\\\\n        Gizmos.DrawLine(position, backRight);\\\\n        Gizmos.DrawLine(position, backLeft);\\\\n\\\\n        Gizmos.DrawLine(forwardLeft, forwardRight);\\\\n        Gizmos.DrawLine(forwardRight, backRight);\\\\n        Gizmos.DrawLine(backRight, backLeft);\\\\n        Gizmos.DrawLine(backLeft, forwardLeft);\\\\n    }\\\\n\\\\n    private void OnDrawGizmosSelected() {\\\\n        if (clipPlanes \u003e= 1) {\\\\n            DrawPlane(plane1Position, plane1Rotation);\\\\n        }\\\\n        if (clipPlanes \u003e= 2) {\\\\n            DrawPlane(plane2Position, plane2Rotation);\\\\n        }\\\\n        if (clipPlanes \u003e= 3) {\\\\n            DrawPlane(plane3Position, plane3Rotation);\\\\n        }\\\\n    }\\\\n\\\\n    //Ideally the planes do not need to be updated every frame, but we'll just keep the logic here for simplicity purposes.\\\\n    public void Update()\\\\n    {\\\\n        var sharedMaterial = GetComponent\u003cRenderer\u003e().sharedMaterial;\\\\n\\\\n        //Only should enable one keyword. If you want to enable any one of them, you actually need to disable the others. \\\\n        //This may be a bug...\\\\n        switch (clipPlanes) {\\\\n            case 0:\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_ONE\\\\\\\");\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_TWO\\\\\\\");\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_THREE\\\\\\\");\\\\n                break;\\\\n            case 1:\\\\n                sharedMaterial.EnableKeyword(\\\\\\\"CLIP_ONE\\\\\\\");\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_TWO\\\\\\\");\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_THREE\\\\\\\");\\\\n                break;\\\\n            case 2:\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_ONE\\\\\\\");\\\\n                sharedMaterial.EnableKeyword(\\\\\\\"CLIP_TWO\\\\\\\");\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_THREE\\\\\\\");\\\\n                break;\\\\n            case 3:\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_ONE\\\\\\\");\\\\n                sharedMaterial.DisableKeyword(\\\\\\\"CLIP_TWO\\\\\\\");\\\\n                sharedMaterial.EnableKeyword(\\\\\\\"CLIP_THREE\\\\\\\");\\\\n                break;\\\\n        }\\\\n\\\\n        //pass the planes to the shader if necessary.\\\\n        if (clipPlanes \u003e= 1)\\\\n        {\\\\n            sharedMaterial.SetVector(\\\\\\\"_planePos\\\\\\\", plane1Position);\\\\n            //plane normal vector is the rotated 'up' vector.\\\\n            sharedMaterial.SetVector(\\\\\\\"_planeNorm\\\\\\\", Quaternion.Euler(plane1Rotation) * Vector3.up);\\\\n        }\\\\n\\\\n        if (clipPlanes \u003e= 2)\\\\n        {\\\\n            sharedMaterial.SetVector(\\\\\\\"_planePos2\\\\\\\", plane2Position);\\\\n            sharedMaterial.SetVector(\\\\\\\"_planeNorm2\\\\\\\", Quaternion.Euler(plane2Rotation) * Vector3.up);\\\\n        }\\\\n\\\\n        if (clipPlanes \u003e= 3)\\\\n        {\\\\n            sharedMaterial.SetVector(\\\\\\\"_planePos3\\\\\\\", plane3Position);\\\\n            sharedMaterial.SetVector(\\\\\\\"_planeNorm3\\\\\\\", Quaternion.Euler(plane3Rotation) * Vector3.up);\\\\n        }\\\\n    }\\\\n}\\\"}]},{\\\"type\\\":\\\"AnchorNode\\\",\\\"attrs\\\":{\\\"id\\\":\\\"the-result\\\"}},{\\\"type\\\":\\\"heading\\\",\\\"attrs\\\":{\\\"level\\\":2},\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"The Result!\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"You can find all the code on Github, in \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://github.com/firtoz/Unity3D-Plane-Clipping\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"Unity3D-Plane-Clipping\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\" project I created.\\\"}]},{\\\"type\\\":\\\"paragraph\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"And \\\"},{\\\"type\\\":\\\"text\\\",\\\"marks\\\":[{\\\"type\\\":\\\"link\\\",\\\"attrs\\\":{\\\"href\\\":\\\"https://github.com/firtoz/Unity3D-Plane-Clipping/releases/download/2015.74.0/plane_clipping.unitypackage\\\",\\\"target\\\":\\\"_blank\\\",\\\"rel\\\":\\\"noopener noreferrer nofollow\\\",\\\"class\\\":\\\"prose-link\\\",\\\"title\\\":null}}],\\\"text\\\":\\\"here's the unitypackage file\\\"},{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\".\\\"}]}]}\",[],\"actionData\",\"errors\"]\n");</script><!--$--><script>window.__reactRouterContext.streamController.enqueue("P86:[{\"_128\":76},\"isAdmin\"]\n");</script><!--$--><script>window.__reactRouterContext.streamController.close();</script><!--/$--><!--/$--><!--/$--></body></html>