mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
feat(card): LED strips in View — lazy chunk, stripe, linear light (#780)
Stage 3a of #780. - src/led-strip-gate.ts is the only initial-graph foothold (ТЗ §13.1): which markers a space shows as a strip (active and bound), the half-length anchor that replaces their icon position, and one page-wide load of the lazy led-strip-runtime chunk (fingerprint handshake, a hashed-URL retry on the next explicit entry, never in a loop). - The card: a represented marker takes no auto slot, draws no icon, casts no round pool and is placed at the anchor; two call sites render the stripe layer and the linear field from the chunk. The space model carries the stored strips untouched; scaling, validation and geometry are in the chunk. - src/led-strip-runtime.ts: the stripe is two strokes of one derived path (outline #383838 t, core t/2, round joins and caps, t = 0.08/0.12 D), white off, white core + field on with Glow, source-colour core without Glow, grey dashed unavailable without a field. The hit path takes the card's own device handlers (one action path) with radius max(22 px, t/2) and the nearest stripe as owner. The linear field is the exact distance field with the shared falloff: opaque grey bands of a luminance mask per piece, pieces joined by lighten (the maximum), each piece clipped to what its own emitters see, so a hidden part never lights through another part's visibility; buried emitters emit nothing; a failed clip is dark. A bounded per-space cache (50) counts geometry rebuilds. - The initial View graph had 501 B of headroom. The furniture library copy (furn.*, 104 keys × 4 languages, editor-only) moves into a new lazy `tools` namespace (#627 mechanism) that the editor runtime awaits; the initial View graph is 298 686 B gzip with the LED gate in it (−1 879 B vs the base). Tests: test/led-strip-runtime.test.mjs (6: representation, gate anchor = geometry anchor, falloff, states and radius, per-piece clipping and buried strips, bounded cache), bundle and i18n fixtures for the LED chunk and the fourth namespace. Issue: #780 User-Visible: no
This commit is contained in:
@@ -469,6 +469,7 @@ export const NAMESPACE_ENGLISH_CONSUMERS = {
|
||||
settings: ['lazyEditorFiles', 'lazyOnboardingFiles'],
|
||||
support: ['lazyEditorFiles'],
|
||||
topology: ['lazyEditorFiles'],
|
||||
tools: ['lazyEditorFiles'],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -775,7 +776,7 @@ export function assertBundleBudget(
|
||||
const namespaceLocales = manifest.lazyNamespaceLocaleFiles;
|
||||
if (!Array.isArray(namespaceLocales) || namespaceLocales.length !== NAMESPACE_LOCALE_CHUNKS.length) {
|
||||
throw new Error(`lazy namespace locale graph has ${namespaceLocales?.length ?? 0} files,`
|
||||
+ ` expected ${NAMESPACE_LOCALE_CHUNKS.length} (settings/support/topology × ru/de/fr)`);
|
||||
+ ` expected ${NAMESPACE_LOCALE_CHUNKS.length} (settings/support/topology/tools × ru/de/fr)`);
|
||||
}
|
||||
for (const [graph, label] of [
|
||||
['initialViewFiles', 'initial View graph'],
|
||||
|
||||
@@ -12,13 +12,14 @@ const FR_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FR_RETRY_ASSET__';
|
||||
const FURNITURE_ART_RETRY_ASSET_TOKEN = '__HOUSEPLAN_FURNITURE_ART_RETRY_ASSET__';
|
||||
const PDF_RETRY_ASSET_TOKEN = '__HOUSEPLAN_PDF_RETRY_ASSET__';
|
||||
const MOON_RETRY_ASSET_TOKEN = '__HOUSEPLAN_MOON_RETRY_ASSET__';
|
||||
const LED_RETRY_ASSET_TOKEN = '__HOUSEPLAN_LED_RETRY_ASSET__';
|
||||
|
||||
/**
|
||||
* #627: ru/de/fr of the three lazy dictionary namespaces are one chunk per
|
||||
* namespace × language. Their loader modules are the recognition marker for
|
||||
* the manifest graph and the target of each second-attempt retry token.
|
||||
*/
|
||||
export const NAMESPACE_LOCALE_CHUNKS = ['settings', 'support', 'topology']
|
||||
export const NAMESPACE_LOCALE_CHUNKS = ['settings', 'support', 'topology', 'tools']
|
||||
.flatMap((namespace) => ['ru', 'de', 'fr'].map((language) => ({
|
||||
namespace,
|
||||
language,
|
||||
@@ -114,6 +115,10 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
// #661: the moon's math, art and template — loaded only at night.
|
||||
: modules.some((id) => id.endsWith('/src/moon-runtime.ts'))
|
||||
? 'moon'
|
||||
// #780: LED strip geometry, light field and hit paths — loaded
|
||||
// only when a shown space has an active strip.
|
||||
: modules.some((id) => id.endsWith('/src/led-strip-runtime.ts'))
|
||||
? 'led'
|
||||
// #474: designer furniture artwork — its own lazy chunk, shared by
|
||||
// the editor (static import) and the View runtime (dynamic).
|
||||
: modules.some((id) => id.endsWith('/src/furniture-plan-art.generated.ts'))
|
||||
@@ -159,6 +164,8 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
|| path.includes('pdf-export-'));
|
||||
const moonRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'moon'
|
||||
|| path.includes('moon-runtime-'));
|
||||
const ledRoots = dynamicRoots.filter((path) => byPath.get(path)?._role === 'led'
|
||||
|| path.includes('led-strip-runtime-'));
|
||||
// #627: namespace dictionaries are dynamic imports of LAZY chunks, never of
|
||||
// the initial graph, so they are found by their module, not as a root.
|
||||
const namespaceLocaleRoots = files.filter((file) => file._role === 'namespace-locale')
|
||||
@@ -177,6 +184,7 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
const lazyFurnitureArt = graphFrom(furnitureArtRoots);
|
||||
const lazyPdf = graphFrom(pdfRoots);
|
||||
const lazyMoon = graphFrom(moonRoots);
|
||||
const lazyLed = graphFrom(ledRoots);
|
||||
const lazyNamespaceLocale = graphFrom(namespaceLocaleRoots);
|
||||
const sum = (paths) => [...paths]
|
||||
.reduce((total, path) => total + (byPath.get(path)?.gzipBytes || 0), 0);
|
||||
@@ -207,6 +215,8 @@ export function buildBundleManifest(bundle, fingerprint) {
|
||||
lazyPdfGzipBytes: sum(lazyPdf),
|
||||
lazyMoonFiles: [...lazyMoon].sort(),
|
||||
lazyMoonGzipBytes: sum(lazyMoon),
|
||||
lazyLedFiles: [...lazyLed].sort(),
|
||||
lazyLedGzipBytes: sum(lazyLed),
|
||||
lazyNamespaceLocaleFiles: [...lazyNamespaceLocale].sort(),
|
||||
lazyNamespaceLocaleGzipBytes: sum(lazyNamespaceLocale),
|
||||
files: files.map(({ _role, ...file }) => file),
|
||||
@@ -276,6 +286,9 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
if (!furnitureArt) throw new Error('furniture artwork chunk was not emitted');
|
||||
if (!pdf) throw new Error('PDF export runtime chunk was not emitted');
|
||||
if (!moon) throw new Error('moon runtime chunk was not emitted');
|
||||
const led = chunks.find((chunk) => Object.keys(chunk.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith('/src/led-strip-runtime.ts')));
|
||||
if (!led) throw new Error('LED strip runtime chunk was not emitted');
|
||||
const namespaceLocales = NAMESPACE_LOCALE_CHUNKS.map((entry) => {
|
||||
const chunk = chunks.find((candidate) => Object.keys(candidate.modules)
|
||||
.some((id) => id.replaceAll('\\', '/').endsWith(entry.module)));
|
||||
@@ -292,6 +305,7 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
let frenchReplacements = 0;
|
||||
let pdfReplacements = 0;
|
||||
let moonReplacements = 0;
|
||||
let ledReplacements = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.code.includes(EDITOR_RETRY_ASSET_TOKEN)) {
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), editor.fileName);
|
||||
@@ -341,6 +355,12 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
moonReplacements += chunk.code.split(MOON_RETRY_ASSET_TOKEN).length - 1;
|
||||
chunk.code = chunk.code.replaceAll(MOON_RETRY_ASSET_TOKEN, asset);
|
||||
}
|
||||
if (chunk.code.includes(LED_RETRY_ASSET_TOKEN)) {
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), led.fileName);
|
||||
if (!asset.startsWith('.')) asset = `./${asset}`;
|
||||
ledReplacements += chunk.code.split(LED_RETRY_ASSET_TOKEN).length - 1;
|
||||
chunk.code = chunk.code.replaceAll(LED_RETRY_ASSET_TOKEN, asset);
|
||||
}
|
||||
for (const entry of namespaceLocales) {
|
||||
if (!chunk.code.includes(entry.token)) continue;
|
||||
let asset = posix.relative(posix.dirname(chunk.fileName), entry.chunk.fileName);
|
||||
@@ -351,9 +371,9 @@ export function editorRuntimeRetryUrlPlugin() {
|
||||
}
|
||||
if (editorReplacements !== 1 || onboardingReplacements !== 1 || isometricReplacements !== 1
|
||||
|| germanReplacements !== 1 || frenchReplacements !== 1 || furnitureArtReplacements !== 1
|
||||
|| pdfReplacements !== 1 || moonReplacements !== 1) {
|
||||
|| pdfReplacements !== 1 || moonReplacements !== 1 || ledReplacements !== 1) {
|
||||
throw new Error('lazy retry URL placeholder counts are '
|
||||
+ `${editorReplacements}/${onboardingReplacements}/${isometricReplacements}/${germanReplacements}/${frenchReplacements}/${furnitureArtReplacements}/${pdfReplacements}/${moonReplacements}, expected 1/1/1/1/1/1/1/1`);
|
||||
+ `${editorReplacements}/${onboardingReplacements}/${isometricReplacements}/${germanReplacements}/${frenchReplacements}/${furnitureArtReplacements}/${pdfReplacements}/${moonReplacements}/${ledReplacements}, expected 1/1/1/1/1/1/1/1/1`);
|
||||
}
|
||||
// #627: the same strict rule for every namespace × language token —
|
||||
// exactly one second-attempt URL each, never zero and never two.
|
||||
|
||||
@@ -361,7 +361,8 @@ export const SMOKE_LINKS = [
|
||||
symbols: [
|
||||
'namespaceLanguageRuntime', 'composeLanguageRuntimes', 'surfaceLanguageRuntime',
|
||||
'SETTINGS_LANGUAGE_RUNTIME', 'SUPPORT_LANGUAGE_RUNTIME', 'TOPOLOGY_LANGUAGE_RUNTIME',
|
||||
'EDITOR_LANGUAGE_RUNTIME', 'ONBOARDING_LANGUAGE_RUNTIME', 'settingsT', 'supportT', 'topologyT',
|
||||
'TOOLS_LANGUAGE_RUNTIME', 'EDITOR_LANGUAGE_RUNTIME', 'ONBOARDING_LANGUAGE_RUNTIME',
|
||||
'settingsT', 'supportT', 'topologyT', 'toolsT',
|
||||
],
|
||||
smokes: ['smoke_lazy_admin_locale.mjs', 'smoke_dialog_segments_i18n.mjs'],
|
||||
because: 'the ru/de/fr namespace dictionaries are separate lazy chunks: only the production '
|
||||
|
||||
Reference in New Issue
Block a user