@selvajs/visualization 1.0.0-beta.4 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"scene.cjs","names":["THREE","hiddenKey"],"sources":["../src/scene/objects.ts","../src/scene/layers.ts","../src/scene/identity.ts","../src/scene/visibility.ts","../src/scene/selection.ts","../src/scene/outliner.ts"],"sourcesContent":["// ============================================================================\n// Scene content: which objects are user content, and how they are labelled\n// ============================================================================\n//\n// A live THREE.Scene holds more than the solve's output — the renderer also adds a camera, lights,\n// and viewer aids (grid, floor, measurement overlay, CSS2D label layer). Anything presenting the\n// scene to a user has to filter those out the same way.\n\nimport * as THREE from 'three';\n\n// Viewer aids tagged by `userData.id` in `render/`; live in the scene graph but are not solve output.\nexport const HELPER_IDS: ReadonlySet<string> = new Set(['grid', 'floor', 'label-layer', 'measure']);\n\nexport function isSceneContent(object: THREE.Object3D): boolean {\n\treturn (\n\t\t!(object instanceof THREE.Camera) &&\n\t\t!(object instanceof THREE.Light) &&\n\t\t!HELPER_IDS.has(object.userData?.id)\n\t);\n}\n\n// Only top-level children: a mesh's own sub-objects (edge overlays, labels) are governed by it,\n// not listed alongside it.\nexport function getSceneObjects(scene: THREE.Scene): THREE.Object3D[] {\n\treturn scene.children.filter(isSceneContent);\n}\n\n// `Line2`/`LineSegments2` are how curves are rendered — an implementation detail no user should\n// have to decode.\nexport function prettyType(type: string): string {\n\treturn (\n\t\ttype\n\t\t\t.replace(/^Line(Segments)?2$/, 'Curve')\n\t\t\t.replace('Mesh', '')\n\t\t\t.replace('Object3D', 'Obj') || type\n\t);\n}\n\nexport function getObjectLabel(object: THREE.Object3D): string {\n\treturn (\n\t\tobject.userData?.name || object.userData?.fileName || object.name || prettyType(object.type)\n\t);\n}\n\nexport function getTypeLabel(object: THREE.Object3D): string {\n\treturn prettyType(object.type);\n}\n","// ============================================================================\n// Layer grouping and search filtering\n// ============================================================================\n\nimport type * as THREE from 'three';\nimport { getObjectLabel } from './objects.js';\n\nexport const DEFAULT_LAYER = 'Default';\n\n/**\n * Group content objects by their Grasshopper layer.\n *\n * `userData.layer` wins over `userData.category`; objects with neither land in {@link DEFAULT_LAYER}.\n * Insertion order is preserved, so the grouping follows scene-graph order rather than sorting\n * alphabetically — the order geometry was baked in is meaningful to the author.\n */\nexport function groupByLayer(objects: THREE.Object3D[]): Map<string, THREE.Object3D[]> {\n\tconst groups = new Map<string, THREE.Object3D[]>();\n\tfor (const obj of objects) {\n\t\tconst layer: string = obj.userData?.layer || obj.userData?.category || DEFAULT_LAYER;\n\t\tlet bucket = groups.get(layer);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tgroups.set(layer, bucket);\n\t\t}\n\t\tbucket.push(obj);\n\t}\n\treturn groups;\n}\n\n/**\n * Filter grouped layers by a free-text query.\n *\n * A layer whose *name* matches keeps all its objects — searching for a layer means wanting to see\n * what's on it. Otherwise the layer keeps only the objects whose labels match, and drops out\n * entirely when none do. An empty query returns the input untouched.\n */\nexport function filterLayerGroups(\n\tgroups: Map<string, THREE.Object3D[]>,\n\tquery: string\n): Map<string, THREE.Object3D[]> {\n\tif (!query.trim()) return groups;\n\tconst q = query.toLowerCase();\n\tconst filtered = new Map<string, THREE.Object3D[]>();\n\tfor (const [layerName, objects] of groups) {\n\t\tconst matching = layerName.toLowerCase().includes(q)\n\t\t\t? objects\n\t\t\t: objects.filter((obj) => getObjectLabel(obj).toLowerCase().includes(q));\n\t\tif (matching.length > 0) filtered.set(layerName, matching);\n\t}\n\treturn filtered;\n}\n","// ============================================================================\n// Stable object identity across solves\n// ============================================================================\n//\n// A solve discards all content and rebuilds it, so `THREE.Object3D.uuid` (assigned per instance)\n// cannot answer \"is this the same wall I hid a minute ago\". Anything that must outlive a solve —\n// hidden state, selection, per-object overrides — has to key on what the geometry *is*.\n//\n// Grasshopper gives no object GUIDs, so identity is synthesized from `userData`, in descending\n// order of trustworthiness.\n\nimport type * as THREE from 'three';\n\n// Unit separator, not a printable character: layer/object names may contain anything a user can\n// type, and 'a' + ':' + 'b:c' must not collide with 'a:b' + ':' + 'c'.\nconst SEP = String.fromCharCode(31);\n\n/**\n * Tries, in order: `userData.id` (display items arrive with a pre-built pick key), then\n * `sourceComponentId` + `originalIndex` (component GUID is stable across solves), then\n * `name` + `layer` as a weaker fallback — two unnamed meshes on one layer collide under it.\n */\nexport function getStableKey(object: THREE.Object3D): string | null {\n\tconst data = object.userData;\n\tif (!data) return null;\n\n\tif (typeof data.id === 'string' && data.id) return data.id;\n\n\tif (typeof data.sourceComponentId === 'string' && data.sourceComponentId) {\n\t\t// `originalIndex` is 0 for the first mesh of a component, so check presence, not truthiness.\n\t\tconst index = typeof data.originalIndex === 'number' ? data.originalIndex : 0;\n\t\treturn `gh${SEP}${data.sourceComponentId}${SEP}${index}`;\n\t}\n\n\tconst name = typeof data.name === 'string' ? data.name : object.name;\n\tconst layer = typeof data.layer === 'string' ? data.layer : '';\n\tif (name) return `name${SEP}${layer}${SEP}${name}`;\n\n\treturn null;\n}\n\n/** Falls back to the instance uuid when the object has no stable identity, unlike `getStableKey`. */\nexport function getTrackingKey(object: THREE.Object3D): string {\n\treturn getStableKey(object) ?? object.uuid;\n}\n","// ============================================================================\n// Visibility: hidden-set bookkeeping over live THREE objects\n// ============================================================================\n//\n// Hiding an object flips `.visible` on the object *and its whole subtree* (edge overlays and CSS2D\n// labels are children, and three does not propagate `.visible` down for the label renderer), and\n// records the fact in a hidden-set so the UI can render an eye-off state and compute per-layer\n// tri-state.\n//\n// The set is keyed by stable identity (see `identity.ts`), not `uuid`, so hiding survives a solve —\n// a solve regenerates uuids but not the Grasshopper source the key is derived from. Objects with no\n// identifying userData fall back to their uuid, so their hidden state lasts only until the next solve.\n\nimport type * as THREE from 'three';\nimport { getTrackingKey as hiddenKey } from './identity.js';\n\n// Backed by a caller-supplied `Set` so a reactive host can pass a framework-observable set\n// (Svelte's `SvelteSet`) and get re-renders for free.\nexport interface VisibilityState {\n\treadonly hidden: Set<string>;\n\tisHidden(object: THREE.Object3D): boolean;\n\tsetVisible(object: THREE.Object3D, visible: boolean): void;\n\tisLayerHidden(objects: THREE.Object3D[]): boolean;\n\t/** Drives the tri-state eye icon. */\n\tisLayerPartial(objects: THREE.Object3D[]): boolean;\n\ttoggleLayer(objects: THREE.Object3D[]): void;\n\t/** Restores `.visible` on everything the user had hidden before the solve. Call after each solve. */\n\tapplyTo(objects: THREE.Object3D[]): void;\n\treset(): void;\n}\n\nexport function createVisibilityState(hidden: Set<string> = new Set()): VisibilityState {\n\tconst state: VisibilityState = {\n\t\thidden,\n\n\t\tisHidden: (object) => hidden.has(hiddenKey(object)),\n\n\t\tsetVisible(object, visible) {\n\t\t\tobject.visible = visible;\n\t\t\tobject.traverse((child) => {\n\t\t\t\tchild.visible = visible;\n\t\t\t});\n\t\t\tconst key = hiddenKey(object);\n\t\t\tif (visible) hidden.delete(key);\n\t\t\telse hidden.add(key);\n\t\t},\n\n\t\tisLayerHidden: (objects) => objects.length > 0 && objects.every((obj) => state.isHidden(obj)),\n\n\t\tisLayerPartial(objects) {\n\t\t\tconst count = objects.filter((obj) => state.isHidden(obj)).length;\n\t\t\treturn count > 0 && count < objects.length;\n\t\t},\n\n\t\ttoggleLayer(objects) {\n\t\t\t// A partially hidden layer hides the rest — only a fully hidden layer comes back.\n\t\t\tconst show = state.isLayerHidden(objects);\n\t\t\tfor (const obj of objects) state.setVisible(obj, show);\n\t\t},\n\n\t\tapplyTo(objects) {\n\t\t\tfor (const object of objects) {\n\t\t\t\t// Only push objects down. Anything not in the set keeps whatever visibility the render\n\t\t\t\t// layer gave it, so this never fights another feature that hid something for its own\n\t\t\t\t// reasons (isolate mode, section-box culling).\n\t\t\t\tif (!hidden.has(hiddenKey(object))) continue;\n\t\t\t\tobject.visible = false;\n\t\t\t\tobject.traverse((child) => {\n\t\t\t\t\tchild.visible = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\n\t\treset: () => hidden.clear()\n\t};\n\treturn state;\n}\n","// ============================================================================\n// Selection: click / ctrl-click / shift-range over a flat list\n// ============================================================================\n//\n// Standard file-explorer selection semantics. Range selection needs the *flattened, currently\n// visible* order — what the user actually sees after search filtering and layer collapse — which\n// only the caller knows, so it is passed in per click rather than derived here.\n\nexport interface SelectionModifiers {\n\tshiftKey: boolean;\n\t/** Ctrl (Windows/Linux) or Meta (macOS) — the caller normalizes the platform difference. */\n\ttoggleKey: boolean;\n}\n\nexport interface SelectionState {\n\treadonly selected: Set<string>;\n\t/** The click that anchors the next shift-range. */\n\treadonly anchor: string | null;\n\tisSelected(uuid: string): boolean;\n\t/**\n\t * Apply a click.\n\t *\n\t * @param flatOrder The uuids currently visible, in display order — the span a shift-range walks.\n\t */\n\tselect(uuid: string, modifiers: SelectionModifiers, flatOrder: () => string[]): void;\n\tclear(): void;\n\t/**\n\t * Observe anchor moves. The anchor is a scalar, so unlike `selected` it cannot be watched by\n\t * handing in an observable set — a host that renders it mirrors it through this instead.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n}\n\nexport function createSelectionState(selected: Set<string> = new Set()): SelectionState {\n\tlet anchor: string | null = null;\n\tconst anchorListeners = new Set<(anchor: string | null) => void>();\n\n\tconst setAnchor = (next: string | null) => {\n\t\tanchor = next;\n\t\tfor (const listener of anchorListeners) listener(next);\n\t};\n\n\treturn {\n\t\tselected,\n\t\tget anchor() {\n\t\t\treturn anchor;\n\t\t},\n\n\t\tisSelected: (uuid) => selected.has(uuid),\n\n\t\tselect(uuid, modifiers, flatOrder) {\n\t\t\tif (modifiers.shiftKey && anchor) {\n\t\t\t\tconst flat = flatOrder();\n\t\t\t\tconst a = flat.indexOf(anchor);\n\t\t\t\tconst b = flat.indexOf(uuid);\n\t\t\t\tif (a !== -1 && b !== -1) {\n\t\t\t\t\tconst [lo, hi] = a < b ? [a, b] : [b, a];\n\t\t\t\t\t// Ctrl+Shift extends the selection; plain Shift replaces it.\n\t\t\t\t\tif (!modifiers.toggleKey) selected.clear();\n\t\t\t\t\tfor (let i = lo; i <= hi; i++) selected.add(flat[i]);\n\t\t\t\t}\n\t\t\t\t// The anchor stays put, so dragging the shift-click around pivots on the same origin.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (modifiers.toggleKey) {\n\t\t\t\tif (selected.has(uuid)) selected.delete(uuid);\n\t\t\t\telse selected.add(uuid);\n\t\t\t} else {\n\t\t\t\tselected.clear();\n\t\t\t\tselected.add(uuid);\n\t\t\t}\n\t\t\tsetAnchor(uuid);\n\t\t},\n\n\t\tclear() {\n\t\t\tselected.clear();\n\t\t\tsetAnchor(null);\n\t\t},\n\n\t\tonAnchorChange(listener) {\n\t\t\tanchorListeners.add(listener);\n\t\t\treturn () => anchorListeners.delete(listener);\n\t\t}\n\t};\n}\n","// ============================================================================\n// SceneOutliner: the object-list state machine over a live THREE.Scene\n// ============================================================================\n//\n// Everything an outliner panel needs — content filtering, layer grouping, search, collapse,\n// visibility and selection — with no DOM and no framework. A host renders `layerGroups()` and\n// forwards clicks; see `SceneManager.svelte` in `@selvajs/ui` for the Svelte binding.\n\nimport type * as THREE from 'three';\nimport { getSceneObjects } from './objects.js';\nimport { filterLayerGroups, groupByLayer } from './layers.js';\nimport { createVisibilityState, type VisibilityState } from './visibility.js';\nimport { createSelectionState, type SelectionModifiers, type SelectionState } from './selection.js';\n\nexport interface SceneOutlinerOptions {\n\t/**\n\t * Backing sets for the mutable state. Supply framework-observable sets (e.g. Svelte's\n\t * `SvelteSet`) to make a host re-render on mutation; omit for plain `Set`s.\n\t */\n\tsets?: {\n\t\thidden?: Set<string>;\n\t\tselected?: Set<string>;\n\t\tcollapsed?: Set<string>;\n\t};\n}\n\nexport interface SceneOutliner {\n\treadonly visibility: VisibilityState;\n\treadonly selection: SelectionState;\n\treadonly collapsed: Set<string>;\n\n\t/** Free-text search over layer and object names. */\n\tsearchQuery: string;\n\n\t/** Not memoized — recomputes from the scene on every call. */\n\tobjects(): THREE.Object3D[];\n\t/** Content grouped by layer, after the search filter. */\n\tlayerGroups(): Map<string, THREE.Object3D[]>;\n\n\tisCollapsed(layerName: string): boolean;\n\ttoggleCollapsed(layerName: string): void;\n\n\t/**\n\t * Toggle one object's visibility. When the object is part of a multi-selection, the whole\n\t * selection follows it — hiding one of five selected meshes hides all five.\n\t */\n\ttoggleObject(object: THREE.Object3D): void;\n\n\t/** Shift-ranges resolve against `flatVisibleUuids()`, not scene-graph order. */\n\tselect(uuid: string, modifiers: SelectionModifiers): void;\n\n\t/**\n\t * Observe shift-range anchor moves, for hosts that mirror it into their own state.\n\t * Convenience passthrough to `selection.onAnchorChange`.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n\n\t/**\n\t * The uuids currently visible in the panel, in display order: every object of every\n\t * non-collapsed layer that survived the search filter. This is the span a shift-range walks.\n\t */\n\tflatVisibleUuids(): string[];\n\n\t/**\n\t * Re-apply hidden state to freshly built scene content. **Call after every solve.**\n\t *\n\t * A solve discards all content and rebuilds it, so whatever the user had hidden comes back\n\t * visible unless it is hidden again. Hidden state is keyed by stable identity rather than\n\t * instance uuid, so this restores it (see `identity.ts`). Selection is dropped: it refers to\n\t * object instances that no longer exist, and a persistent selection across solves is not a\n\t * behaviour anyone asked for.\n\t */\n\tapplyTo(): void;\n\n\t/** Drop all hidden and selected state, showing everything. Objects are left as they are. */\n\treset(): void;\n}\n\nexport function createSceneOutliner(\n\tscene: THREE.Scene,\n\toptions: SceneOutlinerOptions = {}\n): SceneOutliner {\n\tconst collapsed = options.sets?.collapsed ?? new Set<string>();\n\tconst visibility = createVisibilityState(options.sets?.hidden);\n\tconst selection = createSelectionState(options.sets?.selected);\n\n\tconst outliner: SceneOutliner = {\n\t\tvisibility,\n\t\tselection,\n\t\tcollapsed,\n\t\tsearchQuery: '',\n\n\t\tobjects: () => getSceneObjects(scene),\n\n\t\tlayerGroups: () =>\n\t\t\tfilterLayerGroups(groupByLayer(getSceneObjects(scene)), outliner.searchQuery),\n\n\t\tisCollapsed: (layerName) => collapsed.has(layerName),\n\n\t\ttoggleCollapsed(layerName) {\n\t\t\tif (collapsed.has(layerName)) collapsed.delete(layerName);\n\t\t\telse collapsed.add(layerName);\n\t\t},\n\n\t\ttoggleObject(object) {\n\t\t\tif (selection.isSelected(object.uuid) && selection.selected.size > 1) {\n\t\t\t\tconst chosen = outliner.objects().filter((o) => selection.isSelected(o.uuid));\n\t\t\t\t// Show the group only once every member is hidden, matching the per-layer rule.\n\t\t\t\tconst allHidden = chosen.every((o) => visibility.isHidden(o));\n\t\t\t\tfor (const o of chosen) visibility.setVisible(o, allHidden);\n\t\t\t} else {\n\t\t\t\tvisibility.setVisible(object, visibility.isHidden(object));\n\t\t\t}\n\t\t},\n\n\t\tselect(uuid, modifiers) {\n\t\t\tselection.select(uuid, modifiers, () => outliner.flatVisibleUuids());\n\t\t},\n\n\t\tonAnchorChange: (listener) => selection.onAnchorChange(listener),\n\n\t\tflatVisibleUuids() {\n\t\t\tconst result: string[] = [];\n\t\t\tfor (const [layerName, objects] of outliner.layerGroups()) {\n\t\t\t\tif (collapsed.has(layerName)) continue;\n\t\t\t\tfor (const obj of objects) result.push(obj.uuid);\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\n\t\tapplyTo() {\n\t\t\tvisibility.applyTo(outliner.objects());\n\t\t\tselection.clear();\n\t\t},\n\n\t\treset() {\n\t\t\tvisibility.reset();\n\t\t\tselection.clear();\n\t\t}\n\t};\n\n\treturn outliner;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scene.cjs","names":["THREE","hiddenKey"],"sources":["../src/scene/objects.ts","../src/scene/layers.ts","../src/scene/identity.ts","../src/scene/visibility.ts","../src/scene/selection.ts","../src/scene/outliner.ts"],"sourcesContent":["// ============================================================================\n// Scene content: which objects are user content, and how they are labelled\n// ============================================================================\n//\n// A live THREE.Scene holds more than the solve's output — the renderer also adds a camera, lights,\n// and viewer aids (grid, floor, measurement overlay, CSS2D label layer). Anything presenting the\n// scene to a user has to filter those out the same way.\n\nimport * as THREE from 'three';\n\n// Viewer aids tagged by `userData.id` in `render/`; live in the scene graph but are not solve output.\nexport const HELPER_IDS: ReadonlySet<string> = new Set(['grid', 'floor', 'label-layer', 'measure']);\n\nexport function isSceneContent(object: THREE.Object3D): boolean {\n\treturn (\n\t\t!(object instanceof THREE.Camera) &&\n\t\t!(object instanceof THREE.Light) &&\n\t\t!HELPER_IDS.has(object.userData?.id)\n\t);\n}\n\n// Only top-level children: a mesh's own sub-objects (edge overlays, labels) are governed by it,\n// not listed alongside it.\nexport function getSceneObjects(scene: THREE.Scene): THREE.Object3D[] {\n\treturn scene.children.filter(isSceneContent);\n}\n\n// `Line2`/`LineSegments2` are how curves are rendered — an implementation detail no user should\n// have to decode.\nexport function prettyType(type: string): string {\n\treturn (\n\t\ttype\n\t\t\t.replace(/^Line(Segments)?2$/, 'Curve')\n\t\t\t.replace('Mesh', '')\n\t\t\t.replace('Object3D', 'Obj') || type\n\t);\n}\n\nexport function getObjectLabel(object: THREE.Object3D): string {\n\treturn (\n\t\tobject.userData?.name || object.userData?.fileName || object.name || prettyType(object.type)\n\t);\n}\n\nexport function getTypeLabel(object: THREE.Object3D): string {\n\treturn prettyType(object.type);\n}\n","// ============================================================================\n// Layer grouping and search filtering\n// ============================================================================\n\nimport type * as THREE from 'three';\nimport { getObjectLabel } from './objects.js';\n\nexport const DEFAULT_LAYER = 'Default';\n\n/**\n * Group content objects by their Grasshopper layer.\n *\n * `userData.layer` wins over `userData.category`; objects with neither land in {@link DEFAULT_LAYER}.\n * Insertion order is preserved, so the grouping follows scene-graph order rather than sorting\n * alphabetically — the order geometry was baked in is meaningful to the author.\n */\nexport function groupByLayer(objects: THREE.Object3D[]): Map<string, THREE.Object3D[]> {\n\tconst groups = new Map<string, THREE.Object3D[]>();\n\tfor (const obj of objects) {\n\t\tconst layer: string = obj.userData?.layer || obj.userData?.category || DEFAULT_LAYER;\n\t\tlet bucket = groups.get(layer);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tgroups.set(layer, bucket);\n\t\t}\n\t\tbucket.push(obj);\n\t}\n\treturn groups;\n}\n\n/**\n * Filter grouped layers by a free-text query.\n *\n * A layer whose *name* matches keeps all its objects — searching for a layer means wanting to see\n * what's on it. Otherwise the layer keeps only the objects whose labels match, and drops out\n * entirely when none do. An empty query returns the input untouched.\n */\nexport function filterLayerGroups(\n\tgroups: Map<string, THREE.Object3D[]>,\n\tquery: string\n): Map<string, THREE.Object3D[]> {\n\tif (!query.trim()) return groups;\n\tconst q = query.toLowerCase();\n\tconst filtered = new Map<string, THREE.Object3D[]>();\n\tfor (const [layerName, objects] of groups) {\n\t\tconst matching = layerName.toLowerCase().includes(q)\n\t\t\t? objects\n\t\t\t: objects.filter((obj) => getObjectLabel(obj).toLowerCase().includes(q));\n\t\tif (matching.length > 0) filtered.set(layerName, matching);\n\t}\n\treturn filtered;\n}\n","// ============================================================================\n// Stable object identity across solves\n// ============================================================================\n//\n// A solve discards all content and rebuilds it, so `THREE.Object3D.uuid` (assigned per instance)\n// cannot answer \"is this the same wall I hid a minute ago\". Anything that must outlive a solve —\n// hidden state, selection, per-object overrides — has to key on what the geometry *is*.\n//\n// Grasshopper gives no object GUIDs, so identity is synthesized from `userData`, in descending\n// order of trustworthiness.\n\nimport type * as THREE from 'three';\n\n// Unit separator, not a printable character: layer/object names may contain anything a user can\n// type, and 'a' + ':' + 'b:c' must not collide with 'a:b' + ':' + 'c'.\nconst SEP = String.fromCharCode(31);\n\n/**\n * Tries, in order: `userData.id` (display items arrive with a pre-built pick key), then\n * `sourceComponentId` + `originalIndex` (component GUID is stable across solves), then\n * `name` + `layer` as a weaker fallback — two unnamed meshes on one layer collide under it.\n */\nexport function getStableKey(object: THREE.Object3D): string | null {\n\tconst data = object.userData;\n\tif (!data) return null;\n\n\tif (typeof data.id === 'string' && data.id) return data.id;\n\n\tif (typeof data.sourceComponentId === 'string' && data.sourceComponentId) {\n\t\t// A merged mesh covers several source meshes and its `originalIndex` is only the first\n\t\t// member's, which collides with every other merge starting at the same index. Key on the\n\t\t// whole (sorted, so material grouping order can't change it) member set when there is one.\n\t\tif (Array.isArray(data.mergedIndices) && data.mergedIndices.length > 0) {\n\t\t\tconst members = [...data.mergedIndices].sort((a, b) => a - b).join(',');\n\t\t\treturn `gh${SEP}${data.sourceComponentId}${SEP}m${SEP}${members}`;\n\t\t}\n\t\t// `originalIndex` is 0 for the first mesh of a component, so check presence, not truthiness.\n\t\tconst index = typeof data.originalIndex === 'number' ? data.originalIndex : 0;\n\t\treturn `gh${SEP}${data.sourceComponentId}${SEP}${index}`;\n\t}\n\n\tconst name = typeof data.name === 'string' ? data.name : object.name;\n\tconst layer = typeof data.layer === 'string' ? data.layer : '';\n\tif (name) return `name${SEP}${layer}${SEP}${name}`;\n\n\treturn null;\n}\n\n/** Falls back to the instance uuid when the object has no stable identity, unlike `getStableKey`. */\nexport function getTrackingKey(object: THREE.Object3D): string {\n\treturn getStableKey(object) ?? object.uuid;\n}\n","// ============================================================================\n// Visibility: hidden-set bookkeeping over live THREE objects\n// ============================================================================\n//\n// Hiding an object flips `.visible` on the object *and its whole subtree* (edge overlays and CSS2D\n// labels are children, and three does not propagate `.visible` down for the label renderer), and\n// records the fact in a hidden-set so the UI can render an eye-off state and compute per-layer\n// tri-state.\n//\n// The set is keyed by stable identity (see `identity.ts`), not `uuid`, so hiding survives a solve —\n// a solve regenerates uuids but not the Grasshopper source the key is derived from. Objects with no\n// identifying userData fall back to their uuid, so their hidden state lasts only until the next solve.\n\nimport type * as THREE from 'three';\nimport { getTrackingKey as hiddenKey } from './identity.js';\n\n// Backed by a caller-supplied `Set` so a reactive host can pass a framework-observable set\n// (Svelte's `SvelteSet`) and get re-renders for free.\nexport interface VisibilityState {\n\treadonly hidden: Set<string>;\n\tisHidden(object: THREE.Object3D): boolean;\n\tsetVisible(object: THREE.Object3D, visible: boolean): void;\n\tisLayerHidden(objects: THREE.Object3D[]): boolean;\n\t/** Drives the tri-state eye icon. */\n\tisLayerPartial(objects: THREE.Object3D[]): boolean;\n\ttoggleLayer(objects: THREE.Object3D[]): void;\n\t/** Restores `.visible` on everything the user had hidden before the solve. Call after each solve. */\n\tapplyTo(objects: THREE.Object3D[]): void;\n\treset(): void;\n}\n\nexport function createVisibilityState(hidden: Set<string> = new Set()): VisibilityState {\n\tconst state: VisibilityState = {\n\t\thidden,\n\n\t\tisHidden: (object) => hidden.has(hiddenKey(object)),\n\n\t\tsetVisible(object, visible) {\n\t\t\tobject.visible = visible;\n\t\t\tobject.traverse((child) => {\n\t\t\t\tchild.visible = visible;\n\t\t\t});\n\t\t\tconst key = hiddenKey(object);\n\t\t\tif (visible) hidden.delete(key);\n\t\t\telse hidden.add(key);\n\t\t},\n\n\t\tisLayerHidden: (objects) => objects.length > 0 && objects.every((obj) => state.isHidden(obj)),\n\n\t\tisLayerPartial(objects) {\n\t\t\tconst count = objects.filter((obj) => state.isHidden(obj)).length;\n\t\t\treturn count > 0 && count < objects.length;\n\t\t},\n\n\t\ttoggleLayer(objects) {\n\t\t\t// A partially hidden layer hides the rest — only a fully hidden layer comes back.\n\t\t\tconst show = state.isLayerHidden(objects);\n\t\t\tfor (const obj of objects) state.setVisible(obj, show);\n\t\t},\n\n\t\tapplyTo(objects) {\n\t\t\tfor (const object of objects) {\n\t\t\t\t// Only push objects down. Anything not in the set keeps whatever visibility the render\n\t\t\t\t// layer gave it, so this never fights another feature that hid something for its own\n\t\t\t\t// reasons (isolate mode, section-box culling).\n\t\t\t\tif (!hidden.has(hiddenKey(object))) continue;\n\t\t\t\tobject.visible = false;\n\t\t\t\tobject.traverse((child) => {\n\t\t\t\t\tchild.visible = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\n\t\treset: () => hidden.clear()\n\t};\n\treturn state;\n}\n","// ============================================================================\n// Selection: click / ctrl-click / shift-range over a flat list\n// ============================================================================\n//\n// Standard file-explorer selection semantics. Range selection needs the *flattened, currently\n// visible* order — what the user actually sees after search filtering and layer collapse — which\n// only the caller knows, so it is passed in per click rather than derived here.\n\nexport interface SelectionModifiers {\n\tshiftKey: boolean;\n\t/** Ctrl (Windows/Linux) or Meta (macOS) — the caller normalizes the platform difference. */\n\ttoggleKey: boolean;\n}\n\nexport interface SelectionState {\n\treadonly selected: Set<string>;\n\t/** The click that anchors the next shift-range. */\n\treadonly anchor: string | null;\n\tisSelected(uuid: string): boolean;\n\t/**\n\t * Apply a click.\n\t *\n\t * @param flatOrder The uuids currently visible, in display order — the span a shift-range walks.\n\t */\n\tselect(uuid: string, modifiers: SelectionModifiers, flatOrder: () => string[]): void;\n\tclear(): void;\n\t/**\n\t * Observe anchor moves. The anchor is a scalar, so unlike `selected` it cannot be watched by\n\t * handing in an observable set — a host that renders it mirrors it through this instead.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n}\n\nexport function createSelectionState(selected: Set<string> = new Set()): SelectionState {\n\tlet anchor: string | null = null;\n\tconst anchorListeners = new Set<(anchor: string | null) => void>();\n\n\tconst setAnchor = (next: string | null) => {\n\t\tanchor = next;\n\t\tfor (const listener of anchorListeners) listener(next);\n\t};\n\n\treturn {\n\t\tselected,\n\t\tget anchor() {\n\t\t\treturn anchor;\n\t\t},\n\n\t\tisSelected: (uuid) => selected.has(uuid),\n\n\t\tselect(uuid, modifiers, flatOrder) {\n\t\t\tif (modifiers.shiftKey && anchor) {\n\t\t\t\tconst flat = flatOrder();\n\t\t\t\tconst a = flat.indexOf(anchor);\n\t\t\t\tconst b = flat.indexOf(uuid);\n\t\t\t\tif (a !== -1 && b !== -1) {\n\t\t\t\t\tconst [lo, hi] = a < b ? [a, b] : [b, a];\n\t\t\t\t\t// Ctrl+Shift extends the selection; plain Shift replaces it.\n\t\t\t\t\tif (!modifiers.toggleKey) selected.clear();\n\t\t\t\t\tfor (let i = lo; i <= hi; i++) selected.add(flat[i]);\n\t\t\t\t}\n\t\t\t\t// The anchor stays put, so dragging the shift-click around pivots on the same origin.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (modifiers.toggleKey) {\n\t\t\t\tif (selected.has(uuid)) selected.delete(uuid);\n\t\t\t\telse selected.add(uuid);\n\t\t\t} else {\n\t\t\t\tselected.clear();\n\t\t\t\tselected.add(uuid);\n\t\t\t}\n\t\t\tsetAnchor(uuid);\n\t\t},\n\n\t\tclear() {\n\t\t\tselected.clear();\n\t\t\tsetAnchor(null);\n\t\t},\n\n\t\tonAnchorChange(listener) {\n\t\t\tanchorListeners.add(listener);\n\t\t\treturn () => anchorListeners.delete(listener);\n\t\t}\n\t};\n}\n","// ============================================================================\n// SceneOutliner: the object-list state machine over a live THREE.Scene\n// ============================================================================\n//\n// Everything an outliner panel needs — content filtering, layer grouping, search, collapse,\n// visibility and selection — with no DOM and no framework. A host renders `layerGroups()` and\n// forwards clicks; see `SceneManager.svelte` in `@selvajs/ui` for the Svelte binding.\n\nimport type * as THREE from 'three';\nimport { getSceneObjects } from './objects.js';\nimport { filterLayerGroups, groupByLayer } from './layers.js';\nimport { createVisibilityState, type VisibilityState } from './visibility.js';\nimport { createSelectionState, type SelectionModifiers, type SelectionState } from './selection.js';\n\nexport interface SceneOutlinerOptions {\n\t/**\n\t * Backing sets for the mutable state. Supply framework-observable sets (e.g. Svelte's\n\t * `SvelteSet`) to make a host re-render on mutation; omit for plain `Set`s.\n\t */\n\tsets?: {\n\t\thidden?: Set<string>;\n\t\tselected?: Set<string>;\n\t\tcollapsed?: Set<string>;\n\t};\n}\n\nexport interface SceneOutliner {\n\treadonly visibility: VisibilityState;\n\treadonly selection: SelectionState;\n\treadonly collapsed: Set<string>;\n\n\t/** Free-text search over layer and object names. */\n\tsearchQuery: string;\n\n\t/** Not memoized — recomputes from the scene on every call. */\n\tobjects(): THREE.Object3D[];\n\t/** Content grouped by layer, after the search filter. */\n\tlayerGroups(): Map<string, THREE.Object3D[]>;\n\n\tisCollapsed(layerName: string): boolean;\n\ttoggleCollapsed(layerName: string): void;\n\n\t/**\n\t * Toggle one object's visibility. When the object is part of a multi-selection, the whole\n\t * selection follows it — hiding one of five selected meshes hides all five.\n\t */\n\ttoggleObject(object: THREE.Object3D): void;\n\n\t/** Shift-ranges resolve against `flatVisibleUuids()`, not scene-graph order. */\n\tselect(uuid: string, modifiers: SelectionModifiers): void;\n\n\t/**\n\t * Observe shift-range anchor moves, for hosts that mirror it into their own state.\n\t * Convenience passthrough to `selection.onAnchorChange`.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n\n\t/**\n\t * The uuids currently visible in the panel, in display order: every object of every\n\t * non-collapsed layer that survived the search filter. This is the span a shift-range walks.\n\t */\n\tflatVisibleUuids(): string[];\n\n\t/**\n\t * Re-apply hidden state to freshly built scene content. **Call after every solve.**\n\t *\n\t * A solve discards all content and rebuilds it, so whatever the user had hidden comes back\n\t * visible unless it is hidden again. Hidden state is keyed by stable identity rather than\n\t * instance uuid, so this restores it (see `identity.ts`). Selection is dropped: it refers to\n\t * object instances that no longer exist, and a persistent selection across solves is not a\n\t * behaviour anyone asked for.\n\t */\n\tapplyTo(): void;\n\n\t/** Drop all hidden and selected state, showing everything. Objects are left as they are. */\n\treset(): void;\n}\n\nexport function createSceneOutliner(\n\tscene: THREE.Scene,\n\toptions: SceneOutlinerOptions = {}\n): SceneOutliner {\n\tconst collapsed = options.sets?.collapsed ?? new Set<string>();\n\tconst visibility = createVisibilityState(options.sets?.hidden);\n\tconst selection = createSelectionState(options.sets?.selected);\n\n\tconst outliner: SceneOutliner = {\n\t\tvisibility,\n\t\tselection,\n\t\tcollapsed,\n\t\tsearchQuery: '',\n\n\t\tobjects: () => getSceneObjects(scene),\n\n\t\tlayerGroups: () =>\n\t\t\tfilterLayerGroups(groupByLayer(getSceneObjects(scene)), outliner.searchQuery),\n\n\t\tisCollapsed: (layerName) => collapsed.has(layerName),\n\n\t\ttoggleCollapsed(layerName) {\n\t\t\tif (collapsed.has(layerName)) collapsed.delete(layerName);\n\t\t\telse collapsed.add(layerName);\n\t\t},\n\n\t\ttoggleObject(object) {\n\t\t\tif (selection.isSelected(object.uuid) && selection.selected.size > 1) {\n\t\t\t\tconst chosen = outliner.objects().filter((o) => selection.isSelected(o.uuid));\n\t\t\t\t// Show the group only once every member is hidden, matching the per-layer rule.\n\t\t\t\tconst allHidden = chosen.every((o) => visibility.isHidden(o));\n\t\t\t\tfor (const o of chosen) visibility.setVisible(o, allHidden);\n\t\t\t} else {\n\t\t\t\tvisibility.setVisible(object, visibility.isHidden(object));\n\t\t\t}\n\t\t},\n\n\t\tselect(uuid, modifiers) {\n\t\t\tselection.select(uuid, modifiers, () => outliner.flatVisibleUuids());\n\t\t},\n\n\t\tonAnchorChange: (listener) => selection.onAnchorChange(listener),\n\n\t\tflatVisibleUuids() {\n\t\t\tconst result: string[] = [];\n\t\t\tfor (const [layerName, objects] of outliner.layerGroups()) {\n\t\t\t\tif (collapsed.has(layerName)) continue;\n\t\t\t\tfor (const obj of objects) result.push(obj.uuid);\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\n\t\tapplyTo() {\n\t\t\tvisibility.applyTo(outliner.objects());\n\t\t\tselection.clear();\n\t\t},\n\n\t\treset() {\n\t\t\tvisibility.reset();\n\t\t\tselection.clear();\n\t\t}\n\t};\n\n\treturn outliner;\n}\n"],"mappings":"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"}
package/dist/scene.js CHANGED
@@ -1,2 +1,2 @@
1
- import*as e from"three";const t=new Set([`grid`,`floor`,`label-layer`,`measure`]);function n(n){return!(n instanceof e.Camera)&&!(n instanceof e.Light)&&!t.has(n.userData?.id)}function r(e){return e.children.filter(n)}function i(e){return e.replace(/^Line(Segments)?2$/,`Curve`).replace(`Mesh`,``).replace(`Object3D`,`Obj`)||e}function a(e){return e.userData?.name||e.userData?.fileName||e.name||i(e.type)}function o(e){return i(e.type)}function s(e){let t=new Map;for(let n of e){let e=n.userData?.layer||n.userData?.category||`Default`,r=t.get(e);r||(r=[],t.set(e,r)),r.push(n)}return t}function c(e,t){if(!t.trim())return e;let n=t.toLowerCase(),r=new Map;for(let[t,i]of e){let e=t.toLowerCase().includes(n)?i:i.filter(e=>a(e).toLowerCase().includes(n));e.length>0&&r.set(t,e)}return r}function l(e){let t=e.userData;if(!t)return null;if(typeof t.id==`string`&&t.id)return t.id;if(typeof t.sourceComponentId==`string`&&t.sourceComponentId){let e=typeof t.originalIndex==`number`?t.originalIndex:0;return`gh${t.sourceComponentId}${e}`}let n=typeof t.name==`string`?t.name:e.name,r=typeof t.layer==`string`?t.layer:``;return n?`name${r}${n}`:null}function u(e){return l(e)??e.uuid}function d(e=new Set){let t={hidden:e,isHidden:t=>e.has(u(t)),setVisible(t,n){t.visible=n,t.traverse(e=>{e.visible=n});let r=u(t);n?e.delete(r):e.add(r)},isLayerHidden:e=>e.length>0&&e.every(e=>t.isHidden(e)),isLayerPartial(e){let n=e.filter(e=>t.isHidden(e)).length;return n>0&&n<e.length},toggleLayer(e){let n=t.isLayerHidden(e);for(let r of e)t.setVisible(r,n)},applyTo(t){for(let n of t)e.has(u(n))&&(n.visible=!1,n.traverse(e=>{e.visible=!1}))},reset:()=>e.clear()};return t}function f(e=new Set){let t=null,n=new Set,r=e=>{t=e;for(let t of n)t(e)};return{selected:e,get anchor(){return t},isSelected:t=>e.has(t),select(n,i,a){if(i.shiftKey&&t){let r=a(),o=r.indexOf(t),s=r.indexOf(n);if(o!==-1&&s!==-1){let[t,n]=o<s?[o,s]:[s,o];i.toggleKey||e.clear();for(let i=t;i<=n;i++)e.add(r[i])}return}i.toggleKey?e.has(n)?e.delete(n):e.add(n):(e.clear(),e.add(n)),r(n)},clear(){e.clear(),r(null)},onAnchorChange(e){return n.add(e),()=>n.delete(e)}}}function p(e,t={}){let n=t.sets?.collapsed??new Set,i=d(t.sets?.hidden),a=f(t.sets?.selected),o={visibility:i,selection:a,collapsed:n,searchQuery:``,objects:()=>r(e),layerGroups:()=>c(s(r(e)),o.searchQuery),isCollapsed:e=>n.has(e),toggleCollapsed(e){n.has(e)?n.delete(e):n.add(e)},toggleObject(e){if(a.isSelected(e.uuid)&&a.selected.size>1){let e=o.objects().filter(e=>a.isSelected(e.uuid)),t=e.every(e=>i.isHidden(e));for(let n of e)i.setVisible(n,t)}else i.setVisible(e,i.isHidden(e))},select(e,t){a.select(e,t,()=>o.flatVisibleUuids())},onAnchorChange:e=>a.onAnchorChange(e),flatVisibleUuids(){let e=[];for(let[t,r]of o.layerGroups())if(!n.has(t))for(let t of r)e.push(t.uuid);return e},applyTo(){i.applyTo(o.objects()),a.clear()},reset(){i.reset(),a.clear()}};return o}export{p as createSceneOutliner,a as getObjectLabel,u as getTrackingKey,o as getTypeLabel};
1
+ import*as e from"three";const t=new Set([`grid`,`floor`,`label-layer`,`measure`]);function n(n){return!(n instanceof e.Camera)&&!(n instanceof e.Light)&&!t.has(n.userData?.id)}function r(e){return e.children.filter(n)}function i(e){return e.replace(/^Line(Segments)?2$/,`Curve`).replace(`Mesh`,``).replace(`Object3D`,`Obj`)||e}function a(e){return e.userData?.name||e.userData?.fileName||e.name||i(e.type)}function o(e){return i(e.type)}function s(e){let t=new Map;for(let n of e){let e=n.userData?.layer||n.userData?.category||`Default`,r=t.get(e);r||(r=[],t.set(e,r)),r.push(n)}return t}function c(e,t){if(!t.trim())return e;let n=t.toLowerCase(),r=new Map;for(let[t,i]of e){let e=t.toLowerCase().includes(n)?i:i.filter(e=>a(e).toLowerCase().includes(n));e.length>0&&r.set(t,e)}return r}function l(e){let t=e.userData;if(!t)return null;if(typeof t.id==`string`&&t.id)return t.id;if(typeof t.sourceComponentId==`string`&&t.sourceComponentId){if(Array.isArray(t.mergedIndices)&&t.mergedIndices.length>0){let e=[...t.mergedIndices].sort((e,t)=>e-t).join(`,`);return`gh${t.sourceComponentId}m${e}`}let e=typeof t.originalIndex==`number`?t.originalIndex:0;return`gh${t.sourceComponentId}${e}`}let n=typeof t.name==`string`?t.name:e.name,r=typeof t.layer==`string`?t.layer:``;return n?`name${r}${n}`:null}function u(e){return l(e)??e.uuid}function d(e=new Set){let t={hidden:e,isHidden:t=>e.has(u(t)),setVisible(t,n){t.visible=n,t.traverse(e=>{e.visible=n});let r=u(t);n?e.delete(r):e.add(r)},isLayerHidden:e=>e.length>0&&e.every(e=>t.isHidden(e)),isLayerPartial(e){let n=e.filter(e=>t.isHidden(e)).length;return n>0&&n<e.length},toggleLayer(e){let n=t.isLayerHidden(e);for(let r of e)t.setVisible(r,n)},applyTo(t){for(let n of t)e.has(u(n))&&(n.visible=!1,n.traverse(e=>{e.visible=!1}))},reset:()=>e.clear()};return t}function f(e=new Set){let t=null,n=new Set,r=e=>{t=e;for(let t of n)t(e)};return{selected:e,get anchor(){return t},isSelected:t=>e.has(t),select(n,i,a){if(i.shiftKey&&t){let r=a(),o=r.indexOf(t),s=r.indexOf(n);if(o!==-1&&s!==-1){let[t,n]=o<s?[o,s]:[s,o];i.toggleKey||e.clear();for(let i=t;i<=n;i++)e.add(r[i])}return}i.toggleKey?e.has(n)?e.delete(n):e.add(n):(e.clear(),e.add(n)),r(n)},clear(){e.clear(),r(null)},onAnchorChange(e){return n.add(e),()=>n.delete(e)}}}function p(e,t={}){let n=t.sets?.collapsed??new Set,i=d(t.sets?.hidden),a=f(t.sets?.selected),o={visibility:i,selection:a,collapsed:n,searchQuery:``,objects:()=>r(e),layerGroups:()=>c(s(r(e)),o.searchQuery),isCollapsed:e=>n.has(e),toggleCollapsed(e){n.has(e)?n.delete(e):n.add(e)},toggleObject(e){if(a.isSelected(e.uuid)&&a.selected.size>1){let e=o.objects().filter(e=>a.isSelected(e.uuid)),t=e.every(e=>i.isHidden(e));for(let n of e)i.setVisible(n,t)}else i.setVisible(e,i.isHidden(e))},select(e,t){a.select(e,t,()=>o.flatVisibleUuids())},onAnchorChange:e=>a.onAnchorChange(e),flatVisibleUuids(){let e=[];for(let[t,r]of o.layerGroups())if(!n.has(t))for(let t of r)e.push(t.uuid);return e},applyTo(){i.applyTo(o.objects()),a.clear()},reset(){i.reset(),a.clear()}};return o}export{p as createSceneOutliner,a as getObjectLabel,u as getTrackingKey,o as getTypeLabel};
2
2
  //# sourceMappingURL=scene.js.map
package/dist/scene.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scene.js","names":["hiddenKey"],"sources":["../src/scene/objects.ts","../src/scene/layers.ts","../src/scene/identity.ts","../src/scene/visibility.ts","../src/scene/selection.ts","../src/scene/outliner.ts"],"sourcesContent":["// ============================================================================\n// Scene content: which objects are user content, and how they are labelled\n// ============================================================================\n//\n// A live THREE.Scene holds more than the solve's output — the renderer also adds a camera, lights,\n// and viewer aids (grid, floor, measurement overlay, CSS2D label layer). Anything presenting the\n// scene to a user has to filter those out the same way.\n\nimport * as THREE from 'three';\n\n// Viewer aids tagged by `userData.id` in `render/`; live in the scene graph but are not solve output.\nexport const HELPER_IDS: ReadonlySet<string> = new Set(['grid', 'floor', 'label-layer', 'measure']);\n\nexport function isSceneContent(object: THREE.Object3D): boolean {\n\treturn (\n\t\t!(object instanceof THREE.Camera) &&\n\t\t!(object instanceof THREE.Light) &&\n\t\t!HELPER_IDS.has(object.userData?.id)\n\t);\n}\n\n// Only top-level children: a mesh's own sub-objects (edge overlays, labels) are governed by it,\n// not listed alongside it.\nexport function getSceneObjects(scene: THREE.Scene): THREE.Object3D[] {\n\treturn scene.children.filter(isSceneContent);\n}\n\n// `Line2`/`LineSegments2` are how curves are rendered — an implementation detail no user should\n// have to decode.\nexport function prettyType(type: string): string {\n\treturn (\n\t\ttype\n\t\t\t.replace(/^Line(Segments)?2$/, 'Curve')\n\t\t\t.replace('Mesh', '')\n\t\t\t.replace('Object3D', 'Obj') || type\n\t);\n}\n\nexport function getObjectLabel(object: THREE.Object3D): string {\n\treturn (\n\t\tobject.userData?.name || object.userData?.fileName || object.name || prettyType(object.type)\n\t);\n}\n\nexport function getTypeLabel(object: THREE.Object3D): string {\n\treturn prettyType(object.type);\n}\n","// ============================================================================\n// Layer grouping and search filtering\n// ============================================================================\n\nimport type * as THREE from 'three';\nimport { getObjectLabel } from './objects.js';\n\nexport const DEFAULT_LAYER = 'Default';\n\n/**\n * Group content objects by their Grasshopper layer.\n *\n * `userData.layer` wins over `userData.category`; objects with neither land in {@link DEFAULT_LAYER}.\n * Insertion order is preserved, so the grouping follows scene-graph order rather than sorting\n * alphabetically — the order geometry was baked in is meaningful to the author.\n */\nexport function groupByLayer(objects: THREE.Object3D[]): Map<string, THREE.Object3D[]> {\n\tconst groups = new Map<string, THREE.Object3D[]>();\n\tfor (const obj of objects) {\n\t\tconst layer: string = obj.userData?.layer || obj.userData?.category || DEFAULT_LAYER;\n\t\tlet bucket = groups.get(layer);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tgroups.set(layer, bucket);\n\t\t}\n\t\tbucket.push(obj);\n\t}\n\treturn groups;\n}\n\n/**\n * Filter grouped layers by a free-text query.\n *\n * A layer whose *name* matches keeps all its objects — searching for a layer means wanting to see\n * what's on it. Otherwise the layer keeps only the objects whose labels match, and drops out\n * entirely when none do. An empty query returns the input untouched.\n */\nexport function filterLayerGroups(\n\tgroups: Map<string, THREE.Object3D[]>,\n\tquery: string\n): Map<string, THREE.Object3D[]> {\n\tif (!query.trim()) return groups;\n\tconst q = query.toLowerCase();\n\tconst filtered = new Map<string, THREE.Object3D[]>();\n\tfor (const [layerName, objects] of groups) {\n\t\tconst matching = layerName.toLowerCase().includes(q)\n\t\t\t? objects\n\t\t\t: objects.filter((obj) => getObjectLabel(obj).toLowerCase().includes(q));\n\t\tif (matching.length > 0) filtered.set(layerName, matching);\n\t}\n\treturn filtered;\n}\n","// ============================================================================\n// Stable object identity across solves\n// ============================================================================\n//\n// A solve discards all content and rebuilds it, so `THREE.Object3D.uuid` (assigned per instance)\n// cannot answer \"is this the same wall I hid a minute ago\". Anything that must outlive a solve —\n// hidden state, selection, per-object overrides — has to key on what the geometry *is*.\n//\n// Grasshopper gives no object GUIDs, so identity is synthesized from `userData`, in descending\n// order of trustworthiness.\n\nimport type * as THREE from 'three';\n\n// Unit separator, not a printable character: layer/object names may contain anything a user can\n// type, and 'a' + ':' + 'b:c' must not collide with 'a:b' + ':' + 'c'.\nconst SEP = String.fromCharCode(31);\n\n/**\n * Tries, in order: `userData.id` (display items arrive with a pre-built pick key), then\n * `sourceComponentId` + `originalIndex` (component GUID is stable across solves), then\n * `name` + `layer` as a weaker fallback — two unnamed meshes on one layer collide under it.\n */\nexport function getStableKey(object: THREE.Object3D): string | null {\n\tconst data = object.userData;\n\tif (!data) return null;\n\n\tif (typeof data.id === 'string' && data.id) return data.id;\n\n\tif (typeof data.sourceComponentId === 'string' && data.sourceComponentId) {\n\t\t// `originalIndex` is 0 for the first mesh of a component, so check presence, not truthiness.\n\t\tconst index = typeof data.originalIndex === 'number' ? data.originalIndex : 0;\n\t\treturn `gh${SEP}${data.sourceComponentId}${SEP}${index}`;\n\t}\n\n\tconst name = typeof data.name === 'string' ? data.name : object.name;\n\tconst layer = typeof data.layer === 'string' ? data.layer : '';\n\tif (name) return `name${SEP}${layer}${SEP}${name}`;\n\n\treturn null;\n}\n\n/** Falls back to the instance uuid when the object has no stable identity, unlike `getStableKey`. */\nexport function getTrackingKey(object: THREE.Object3D): string {\n\treturn getStableKey(object) ?? object.uuid;\n}\n","// ============================================================================\n// Visibility: hidden-set bookkeeping over live THREE objects\n// ============================================================================\n//\n// Hiding an object flips `.visible` on the object *and its whole subtree* (edge overlays and CSS2D\n// labels are children, and three does not propagate `.visible` down for the label renderer), and\n// records the fact in a hidden-set so the UI can render an eye-off state and compute per-layer\n// tri-state.\n//\n// The set is keyed by stable identity (see `identity.ts`), not `uuid`, so hiding survives a solve —\n// a solve regenerates uuids but not the Grasshopper source the key is derived from. Objects with no\n// identifying userData fall back to their uuid, so their hidden state lasts only until the next solve.\n\nimport type * as THREE from 'three';\nimport { getTrackingKey as hiddenKey } from './identity.js';\n\n// Backed by a caller-supplied `Set` so a reactive host can pass a framework-observable set\n// (Svelte's `SvelteSet`) and get re-renders for free.\nexport interface VisibilityState {\n\treadonly hidden: Set<string>;\n\tisHidden(object: THREE.Object3D): boolean;\n\tsetVisible(object: THREE.Object3D, visible: boolean): void;\n\tisLayerHidden(objects: THREE.Object3D[]): boolean;\n\t/** Drives the tri-state eye icon. */\n\tisLayerPartial(objects: THREE.Object3D[]): boolean;\n\ttoggleLayer(objects: THREE.Object3D[]): void;\n\t/** Restores `.visible` on everything the user had hidden before the solve. Call after each solve. */\n\tapplyTo(objects: THREE.Object3D[]): void;\n\treset(): void;\n}\n\nexport function createVisibilityState(hidden: Set<string> = new Set()): VisibilityState {\n\tconst state: VisibilityState = {\n\t\thidden,\n\n\t\tisHidden: (object) => hidden.has(hiddenKey(object)),\n\n\t\tsetVisible(object, visible) {\n\t\t\tobject.visible = visible;\n\t\t\tobject.traverse((child) => {\n\t\t\t\tchild.visible = visible;\n\t\t\t});\n\t\t\tconst key = hiddenKey(object);\n\t\t\tif (visible) hidden.delete(key);\n\t\t\telse hidden.add(key);\n\t\t},\n\n\t\tisLayerHidden: (objects) => objects.length > 0 && objects.every((obj) => state.isHidden(obj)),\n\n\t\tisLayerPartial(objects) {\n\t\t\tconst count = objects.filter((obj) => state.isHidden(obj)).length;\n\t\t\treturn count > 0 && count < objects.length;\n\t\t},\n\n\t\ttoggleLayer(objects) {\n\t\t\t// A partially hidden layer hides the rest — only a fully hidden layer comes back.\n\t\t\tconst show = state.isLayerHidden(objects);\n\t\t\tfor (const obj of objects) state.setVisible(obj, show);\n\t\t},\n\n\t\tapplyTo(objects) {\n\t\t\tfor (const object of objects) {\n\t\t\t\t// Only push objects down. Anything not in the set keeps whatever visibility the render\n\t\t\t\t// layer gave it, so this never fights another feature that hid something for its own\n\t\t\t\t// reasons (isolate mode, section-box culling).\n\t\t\t\tif (!hidden.has(hiddenKey(object))) continue;\n\t\t\t\tobject.visible = false;\n\t\t\t\tobject.traverse((child) => {\n\t\t\t\t\tchild.visible = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\n\t\treset: () => hidden.clear()\n\t};\n\treturn state;\n}\n","// ============================================================================\n// Selection: click / ctrl-click / shift-range over a flat list\n// ============================================================================\n//\n// Standard file-explorer selection semantics. Range selection needs the *flattened, currently\n// visible* order — what the user actually sees after search filtering and layer collapse — which\n// only the caller knows, so it is passed in per click rather than derived here.\n\nexport interface SelectionModifiers {\n\tshiftKey: boolean;\n\t/** Ctrl (Windows/Linux) or Meta (macOS) — the caller normalizes the platform difference. */\n\ttoggleKey: boolean;\n}\n\nexport interface SelectionState {\n\treadonly selected: Set<string>;\n\t/** The click that anchors the next shift-range. */\n\treadonly anchor: string | null;\n\tisSelected(uuid: string): boolean;\n\t/**\n\t * Apply a click.\n\t *\n\t * @param flatOrder The uuids currently visible, in display order — the span a shift-range walks.\n\t */\n\tselect(uuid: string, modifiers: SelectionModifiers, flatOrder: () => string[]): void;\n\tclear(): void;\n\t/**\n\t * Observe anchor moves. The anchor is a scalar, so unlike `selected` it cannot be watched by\n\t * handing in an observable set — a host that renders it mirrors it through this instead.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n}\n\nexport function createSelectionState(selected: Set<string> = new Set()): SelectionState {\n\tlet anchor: string | null = null;\n\tconst anchorListeners = new Set<(anchor: string | null) => void>();\n\n\tconst setAnchor = (next: string | null) => {\n\t\tanchor = next;\n\t\tfor (const listener of anchorListeners) listener(next);\n\t};\n\n\treturn {\n\t\tselected,\n\t\tget anchor() {\n\t\t\treturn anchor;\n\t\t},\n\n\t\tisSelected: (uuid) => selected.has(uuid),\n\n\t\tselect(uuid, modifiers, flatOrder) {\n\t\t\tif (modifiers.shiftKey && anchor) {\n\t\t\t\tconst flat = flatOrder();\n\t\t\t\tconst a = flat.indexOf(anchor);\n\t\t\t\tconst b = flat.indexOf(uuid);\n\t\t\t\tif (a !== -1 && b !== -1) {\n\t\t\t\t\tconst [lo, hi] = a < b ? [a, b] : [b, a];\n\t\t\t\t\t// Ctrl+Shift extends the selection; plain Shift replaces it.\n\t\t\t\t\tif (!modifiers.toggleKey) selected.clear();\n\t\t\t\t\tfor (let i = lo; i <= hi; i++) selected.add(flat[i]);\n\t\t\t\t}\n\t\t\t\t// The anchor stays put, so dragging the shift-click around pivots on the same origin.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (modifiers.toggleKey) {\n\t\t\t\tif (selected.has(uuid)) selected.delete(uuid);\n\t\t\t\telse selected.add(uuid);\n\t\t\t} else {\n\t\t\t\tselected.clear();\n\t\t\t\tselected.add(uuid);\n\t\t\t}\n\t\t\tsetAnchor(uuid);\n\t\t},\n\n\t\tclear() {\n\t\t\tselected.clear();\n\t\t\tsetAnchor(null);\n\t\t},\n\n\t\tonAnchorChange(listener) {\n\t\t\tanchorListeners.add(listener);\n\t\t\treturn () => anchorListeners.delete(listener);\n\t\t}\n\t};\n}\n","// ============================================================================\n// SceneOutliner: the object-list state machine over a live THREE.Scene\n// ============================================================================\n//\n// Everything an outliner panel needs — content filtering, layer grouping, search, collapse,\n// visibility and selection — with no DOM and no framework. A host renders `layerGroups()` and\n// forwards clicks; see `SceneManager.svelte` in `@selvajs/ui` for the Svelte binding.\n\nimport type * as THREE from 'three';\nimport { getSceneObjects } from './objects.js';\nimport { filterLayerGroups, groupByLayer } from './layers.js';\nimport { createVisibilityState, type VisibilityState } from './visibility.js';\nimport { createSelectionState, type SelectionModifiers, type SelectionState } from './selection.js';\n\nexport interface SceneOutlinerOptions {\n\t/**\n\t * Backing sets for the mutable state. Supply framework-observable sets (e.g. Svelte's\n\t * `SvelteSet`) to make a host re-render on mutation; omit for plain `Set`s.\n\t */\n\tsets?: {\n\t\thidden?: Set<string>;\n\t\tselected?: Set<string>;\n\t\tcollapsed?: Set<string>;\n\t};\n}\n\nexport interface SceneOutliner {\n\treadonly visibility: VisibilityState;\n\treadonly selection: SelectionState;\n\treadonly collapsed: Set<string>;\n\n\t/** Free-text search over layer and object names. */\n\tsearchQuery: string;\n\n\t/** Not memoized — recomputes from the scene on every call. */\n\tobjects(): THREE.Object3D[];\n\t/** Content grouped by layer, after the search filter. */\n\tlayerGroups(): Map<string, THREE.Object3D[]>;\n\n\tisCollapsed(layerName: string): boolean;\n\ttoggleCollapsed(layerName: string): void;\n\n\t/**\n\t * Toggle one object's visibility. When the object is part of a multi-selection, the whole\n\t * selection follows it — hiding one of five selected meshes hides all five.\n\t */\n\ttoggleObject(object: THREE.Object3D): void;\n\n\t/** Shift-ranges resolve against `flatVisibleUuids()`, not scene-graph order. */\n\tselect(uuid: string, modifiers: SelectionModifiers): void;\n\n\t/**\n\t * Observe shift-range anchor moves, for hosts that mirror it into their own state.\n\t * Convenience passthrough to `selection.onAnchorChange`.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n\n\t/**\n\t * The uuids currently visible in the panel, in display order: every object of every\n\t * non-collapsed layer that survived the search filter. This is the span a shift-range walks.\n\t */\n\tflatVisibleUuids(): string[];\n\n\t/**\n\t * Re-apply hidden state to freshly built scene content. **Call after every solve.**\n\t *\n\t * A solve discards all content and rebuilds it, so whatever the user had hidden comes back\n\t * visible unless it is hidden again. Hidden state is keyed by stable identity rather than\n\t * instance uuid, so this restores it (see `identity.ts`). Selection is dropped: it refers to\n\t * object instances that no longer exist, and a persistent selection across solves is not a\n\t * behaviour anyone asked for.\n\t */\n\tapplyTo(): void;\n\n\t/** Drop all hidden and selected state, showing everything. Objects are left as they are. */\n\treset(): void;\n}\n\nexport function createSceneOutliner(\n\tscene: THREE.Scene,\n\toptions: SceneOutlinerOptions = {}\n): SceneOutliner {\n\tconst collapsed = options.sets?.collapsed ?? new Set<string>();\n\tconst visibility = createVisibilityState(options.sets?.hidden);\n\tconst selection = createSelectionState(options.sets?.selected);\n\n\tconst outliner: SceneOutliner = {\n\t\tvisibility,\n\t\tselection,\n\t\tcollapsed,\n\t\tsearchQuery: '',\n\n\t\tobjects: () => getSceneObjects(scene),\n\n\t\tlayerGroups: () =>\n\t\t\tfilterLayerGroups(groupByLayer(getSceneObjects(scene)), outliner.searchQuery),\n\n\t\tisCollapsed: (layerName) => collapsed.has(layerName),\n\n\t\ttoggleCollapsed(layerName) {\n\t\t\tif (collapsed.has(layerName)) collapsed.delete(layerName);\n\t\t\telse collapsed.add(layerName);\n\t\t},\n\n\t\ttoggleObject(object) {\n\t\t\tif (selection.isSelected(object.uuid) && selection.selected.size > 1) {\n\t\t\t\tconst chosen = outliner.objects().filter((o) => selection.isSelected(o.uuid));\n\t\t\t\t// Show the group only once every member is hidden, matching the per-layer rule.\n\t\t\t\tconst allHidden = chosen.every((o) => visibility.isHidden(o));\n\t\t\t\tfor (const o of chosen) visibility.setVisible(o, allHidden);\n\t\t\t} else {\n\t\t\t\tvisibility.setVisible(object, visibility.isHidden(object));\n\t\t\t}\n\t\t},\n\n\t\tselect(uuid, modifiers) {\n\t\t\tselection.select(uuid, modifiers, () => outliner.flatVisibleUuids());\n\t\t},\n\n\t\tonAnchorChange: (listener) => selection.onAnchorChange(listener),\n\n\t\tflatVisibleUuids() {\n\t\t\tconst result: string[] = [];\n\t\t\tfor (const [layerName, objects] of outliner.layerGroups()) {\n\t\t\t\tif (collapsed.has(layerName)) continue;\n\t\t\t\tfor (const obj of objects) result.push(obj.uuid);\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\n\t\tapplyTo() {\n\t\t\tvisibility.applyTo(outliner.objects());\n\t\t\tselection.clear();\n\t\t},\n\n\t\treset() {\n\t\t\tvisibility.reset();\n\t\t\tselection.clear();\n\t\t}\n\t};\n\n\treturn outliner;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scene.js","names":["hiddenKey"],"sources":["../src/scene/objects.ts","../src/scene/layers.ts","../src/scene/identity.ts","../src/scene/visibility.ts","../src/scene/selection.ts","../src/scene/outliner.ts"],"sourcesContent":["// ============================================================================\n// Scene content: which objects are user content, and how they are labelled\n// ============================================================================\n//\n// A live THREE.Scene holds more than the solve's output — the renderer also adds a camera, lights,\n// and viewer aids (grid, floor, measurement overlay, CSS2D label layer). Anything presenting the\n// scene to a user has to filter those out the same way.\n\nimport * as THREE from 'three';\n\n// Viewer aids tagged by `userData.id` in `render/`; live in the scene graph but are not solve output.\nexport const HELPER_IDS: ReadonlySet<string> = new Set(['grid', 'floor', 'label-layer', 'measure']);\n\nexport function isSceneContent(object: THREE.Object3D): boolean {\n\treturn (\n\t\t!(object instanceof THREE.Camera) &&\n\t\t!(object instanceof THREE.Light) &&\n\t\t!HELPER_IDS.has(object.userData?.id)\n\t);\n}\n\n// Only top-level children: a mesh's own sub-objects (edge overlays, labels) are governed by it,\n// not listed alongside it.\nexport function getSceneObjects(scene: THREE.Scene): THREE.Object3D[] {\n\treturn scene.children.filter(isSceneContent);\n}\n\n// `Line2`/`LineSegments2` are how curves are rendered — an implementation detail no user should\n// have to decode.\nexport function prettyType(type: string): string {\n\treturn (\n\t\ttype\n\t\t\t.replace(/^Line(Segments)?2$/, 'Curve')\n\t\t\t.replace('Mesh', '')\n\t\t\t.replace('Object3D', 'Obj') || type\n\t);\n}\n\nexport function getObjectLabel(object: THREE.Object3D): string {\n\treturn (\n\t\tobject.userData?.name || object.userData?.fileName || object.name || prettyType(object.type)\n\t);\n}\n\nexport function getTypeLabel(object: THREE.Object3D): string {\n\treturn prettyType(object.type);\n}\n","// ============================================================================\n// Layer grouping and search filtering\n// ============================================================================\n\nimport type * as THREE from 'three';\nimport { getObjectLabel } from './objects.js';\n\nexport const DEFAULT_LAYER = 'Default';\n\n/**\n * Group content objects by their Grasshopper layer.\n *\n * `userData.layer` wins over `userData.category`; objects with neither land in {@link DEFAULT_LAYER}.\n * Insertion order is preserved, so the grouping follows scene-graph order rather than sorting\n * alphabetically — the order geometry was baked in is meaningful to the author.\n */\nexport function groupByLayer(objects: THREE.Object3D[]): Map<string, THREE.Object3D[]> {\n\tconst groups = new Map<string, THREE.Object3D[]>();\n\tfor (const obj of objects) {\n\t\tconst layer: string = obj.userData?.layer || obj.userData?.category || DEFAULT_LAYER;\n\t\tlet bucket = groups.get(layer);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tgroups.set(layer, bucket);\n\t\t}\n\t\tbucket.push(obj);\n\t}\n\treturn groups;\n}\n\n/**\n * Filter grouped layers by a free-text query.\n *\n * A layer whose *name* matches keeps all its objects — searching for a layer means wanting to see\n * what's on it. Otherwise the layer keeps only the objects whose labels match, and drops out\n * entirely when none do. An empty query returns the input untouched.\n */\nexport function filterLayerGroups(\n\tgroups: Map<string, THREE.Object3D[]>,\n\tquery: string\n): Map<string, THREE.Object3D[]> {\n\tif (!query.trim()) return groups;\n\tconst q = query.toLowerCase();\n\tconst filtered = new Map<string, THREE.Object3D[]>();\n\tfor (const [layerName, objects] of groups) {\n\t\tconst matching = layerName.toLowerCase().includes(q)\n\t\t\t? objects\n\t\t\t: objects.filter((obj) => getObjectLabel(obj).toLowerCase().includes(q));\n\t\tif (matching.length > 0) filtered.set(layerName, matching);\n\t}\n\treturn filtered;\n}\n","// ============================================================================\n// Stable object identity across solves\n// ============================================================================\n//\n// A solve discards all content and rebuilds it, so `THREE.Object3D.uuid` (assigned per instance)\n// cannot answer \"is this the same wall I hid a minute ago\". Anything that must outlive a solve —\n// hidden state, selection, per-object overrides — has to key on what the geometry *is*.\n//\n// Grasshopper gives no object GUIDs, so identity is synthesized from `userData`, in descending\n// order of trustworthiness.\n\nimport type * as THREE from 'three';\n\n// Unit separator, not a printable character: layer/object names may contain anything a user can\n// type, and 'a' + ':' + 'b:c' must not collide with 'a:b' + ':' + 'c'.\nconst SEP = String.fromCharCode(31);\n\n/**\n * Tries, in order: `userData.id` (display items arrive with a pre-built pick key), then\n * `sourceComponentId` + `originalIndex` (component GUID is stable across solves), then\n * `name` + `layer` as a weaker fallback — two unnamed meshes on one layer collide under it.\n */\nexport function getStableKey(object: THREE.Object3D): string | null {\n\tconst data = object.userData;\n\tif (!data) return null;\n\n\tif (typeof data.id === 'string' && data.id) return data.id;\n\n\tif (typeof data.sourceComponentId === 'string' && data.sourceComponentId) {\n\t\t// A merged mesh covers several source meshes and its `originalIndex` is only the first\n\t\t// member's, which collides with every other merge starting at the same index. Key on the\n\t\t// whole (sorted, so material grouping order can't change it) member set when there is one.\n\t\tif (Array.isArray(data.mergedIndices) && data.mergedIndices.length > 0) {\n\t\t\tconst members = [...data.mergedIndices].sort((a, b) => a - b).join(',');\n\t\t\treturn `gh${SEP}${data.sourceComponentId}${SEP}m${SEP}${members}`;\n\t\t}\n\t\t// `originalIndex` is 0 for the first mesh of a component, so check presence, not truthiness.\n\t\tconst index = typeof data.originalIndex === 'number' ? data.originalIndex : 0;\n\t\treturn `gh${SEP}${data.sourceComponentId}${SEP}${index}`;\n\t}\n\n\tconst name = typeof data.name === 'string' ? data.name : object.name;\n\tconst layer = typeof data.layer === 'string' ? data.layer : '';\n\tif (name) return `name${SEP}${layer}${SEP}${name}`;\n\n\treturn null;\n}\n\n/** Falls back to the instance uuid when the object has no stable identity, unlike `getStableKey`. */\nexport function getTrackingKey(object: THREE.Object3D): string {\n\treturn getStableKey(object) ?? object.uuid;\n}\n","// ============================================================================\n// Visibility: hidden-set bookkeeping over live THREE objects\n// ============================================================================\n//\n// Hiding an object flips `.visible` on the object *and its whole subtree* (edge overlays and CSS2D\n// labels are children, and three does not propagate `.visible` down for the label renderer), and\n// records the fact in a hidden-set so the UI can render an eye-off state and compute per-layer\n// tri-state.\n//\n// The set is keyed by stable identity (see `identity.ts`), not `uuid`, so hiding survives a solve —\n// a solve regenerates uuids but not the Grasshopper source the key is derived from. Objects with no\n// identifying userData fall back to their uuid, so their hidden state lasts only until the next solve.\n\nimport type * as THREE from 'three';\nimport { getTrackingKey as hiddenKey } from './identity.js';\n\n// Backed by a caller-supplied `Set` so a reactive host can pass a framework-observable set\n// (Svelte's `SvelteSet`) and get re-renders for free.\nexport interface VisibilityState {\n\treadonly hidden: Set<string>;\n\tisHidden(object: THREE.Object3D): boolean;\n\tsetVisible(object: THREE.Object3D, visible: boolean): void;\n\tisLayerHidden(objects: THREE.Object3D[]): boolean;\n\t/** Drives the tri-state eye icon. */\n\tisLayerPartial(objects: THREE.Object3D[]): boolean;\n\ttoggleLayer(objects: THREE.Object3D[]): void;\n\t/** Restores `.visible` on everything the user had hidden before the solve. Call after each solve. */\n\tapplyTo(objects: THREE.Object3D[]): void;\n\treset(): void;\n}\n\nexport function createVisibilityState(hidden: Set<string> = new Set()): VisibilityState {\n\tconst state: VisibilityState = {\n\t\thidden,\n\n\t\tisHidden: (object) => hidden.has(hiddenKey(object)),\n\n\t\tsetVisible(object, visible) {\n\t\t\tobject.visible = visible;\n\t\t\tobject.traverse((child) => {\n\t\t\t\tchild.visible = visible;\n\t\t\t});\n\t\t\tconst key = hiddenKey(object);\n\t\t\tif (visible) hidden.delete(key);\n\t\t\telse hidden.add(key);\n\t\t},\n\n\t\tisLayerHidden: (objects) => objects.length > 0 && objects.every((obj) => state.isHidden(obj)),\n\n\t\tisLayerPartial(objects) {\n\t\t\tconst count = objects.filter((obj) => state.isHidden(obj)).length;\n\t\t\treturn count > 0 && count < objects.length;\n\t\t},\n\n\t\ttoggleLayer(objects) {\n\t\t\t// A partially hidden layer hides the rest — only a fully hidden layer comes back.\n\t\t\tconst show = state.isLayerHidden(objects);\n\t\t\tfor (const obj of objects) state.setVisible(obj, show);\n\t\t},\n\n\t\tapplyTo(objects) {\n\t\t\tfor (const object of objects) {\n\t\t\t\t// Only push objects down. Anything not in the set keeps whatever visibility the render\n\t\t\t\t// layer gave it, so this never fights another feature that hid something for its own\n\t\t\t\t// reasons (isolate mode, section-box culling).\n\t\t\t\tif (!hidden.has(hiddenKey(object))) continue;\n\t\t\t\tobject.visible = false;\n\t\t\t\tobject.traverse((child) => {\n\t\t\t\t\tchild.visible = false;\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\n\t\treset: () => hidden.clear()\n\t};\n\treturn state;\n}\n","// ============================================================================\n// Selection: click / ctrl-click / shift-range over a flat list\n// ============================================================================\n//\n// Standard file-explorer selection semantics. Range selection needs the *flattened, currently\n// visible* order — what the user actually sees after search filtering and layer collapse — which\n// only the caller knows, so it is passed in per click rather than derived here.\n\nexport interface SelectionModifiers {\n\tshiftKey: boolean;\n\t/** Ctrl (Windows/Linux) or Meta (macOS) — the caller normalizes the platform difference. */\n\ttoggleKey: boolean;\n}\n\nexport interface SelectionState {\n\treadonly selected: Set<string>;\n\t/** The click that anchors the next shift-range. */\n\treadonly anchor: string | null;\n\tisSelected(uuid: string): boolean;\n\t/**\n\t * Apply a click.\n\t *\n\t * @param flatOrder The uuids currently visible, in display order — the span a shift-range walks.\n\t */\n\tselect(uuid: string, modifiers: SelectionModifiers, flatOrder: () => string[]): void;\n\tclear(): void;\n\t/**\n\t * Observe anchor moves. The anchor is a scalar, so unlike `selected` it cannot be watched by\n\t * handing in an observable set — a host that renders it mirrors it through this instead.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n}\n\nexport function createSelectionState(selected: Set<string> = new Set()): SelectionState {\n\tlet anchor: string | null = null;\n\tconst anchorListeners = new Set<(anchor: string | null) => void>();\n\n\tconst setAnchor = (next: string | null) => {\n\t\tanchor = next;\n\t\tfor (const listener of anchorListeners) listener(next);\n\t};\n\n\treturn {\n\t\tselected,\n\t\tget anchor() {\n\t\t\treturn anchor;\n\t\t},\n\n\t\tisSelected: (uuid) => selected.has(uuid),\n\n\t\tselect(uuid, modifiers, flatOrder) {\n\t\t\tif (modifiers.shiftKey && anchor) {\n\t\t\t\tconst flat = flatOrder();\n\t\t\t\tconst a = flat.indexOf(anchor);\n\t\t\t\tconst b = flat.indexOf(uuid);\n\t\t\t\tif (a !== -1 && b !== -1) {\n\t\t\t\t\tconst [lo, hi] = a < b ? [a, b] : [b, a];\n\t\t\t\t\t// Ctrl+Shift extends the selection; plain Shift replaces it.\n\t\t\t\t\tif (!modifiers.toggleKey) selected.clear();\n\t\t\t\t\tfor (let i = lo; i <= hi; i++) selected.add(flat[i]);\n\t\t\t\t}\n\t\t\t\t// The anchor stays put, so dragging the shift-click around pivots on the same origin.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (modifiers.toggleKey) {\n\t\t\t\tif (selected.has(uuid)) selected.delete(uuid);\n\t\t\t\telse selected.add(uuid);\n\t\t\t} else {\n\t\t\t\tselected.clear();\n\t\t\t\tselected.add(uuid);\n\t\t\t}\n\t\t\tsetAnchor(uuid);\n\t\t},\n\n\t\tclear() {\n\t\t\tselected.clear();\n\t\t\tsetAnchor(null);\n\t\t},\n\n\t\tonAnchorChange(listener) {\n\t\t\tanchorListeners.add(listener);\n\t\t\treturn () => anchorListeners.delete(listener);\n\t\t}\n\t};\n}\n","// ============================================================================\n// SceneOutliner: the object-list state machine over a live THREE.Scene\n// ============================================================================\n//\n// Everything an outliner panel needs — content filtering, layer grouping, search, collapse,\n// visibility and selection — with no DOM and no framework. A host renders `layerGroups()` and\n// forwards clicks; see `SceneManager.svelte` in `@selvajs/ui` for the Svelte binding.\n\nimport type * as THREE from 'three';\nimport { getSceneObjects } from './objects.js';\nimport { filterLayerGroups, groupByLayer } from './layers.js';\nimport { createVisibilityState, type VisibilityState } from './visibility.js';\nimport { createSelectionState, type SelectionModifiers, type SelectionState } from './selection.js';\n\nexport interface SceneOutlinerOptions {\n\t/**\n\t * Backing sets for the mutable state. Supply framework-observable sets (e.g. Svelte's\n\t * `SvelteSet`) to make a host re-render on mutation; omit for plain `Set`s.\n\t */\n\tsets?: {\n\t\thidden?: Set<string>;\n\t\tselected?: Set<string>;\n\t\tcollapsed?: Set<string>;\n\t};\n}\n\nexport interface SceneOutliner {\n\treadonly visibility: VisibilityState;\n\treadonly selection: SelectionState;\n\treadonly collapsed: Set<string>;\n\n\t/** Free-text search over layer and object names. */\n\tsearchQuery: string;\n\n\t/** Not memoized — recomputes from the scene on every call. */\n\tobjects(): THREE.Object3D[];\n\t/** Content grouped by layer, after the search filter. */\n\tlayerGroups(): Map<string, THREE.Object3D[]>;\n\n\tisCollapsed(layerName: string): boolean;\n\ttoggleCollapsed(layerName: string): void;\n\n\t/**\n\t * Toggle one object's visibility. When the object is part of a multi-selection, the whole\n\t * selection follows it — hiding one of five selected meshes hides all five.\n\t */\n\ttoggleObject(object: THREE.Object3D): void;\n\n\t/** Shift-ranges resolve against `flatVisibleUuids()`, not scene-graph order. */\n\tselect(uuid: string, modifiers: SelectionModifiers): void;\n\n\t/**\n\t * Observe shift-range anchor moves, for hosts that mirror it into their own state.\n\t * Convenience passthrough to `selection.onAnchorChange`.\n\t *\n\t * @returns An unsubscribe function.\n\t */\n\tonAnchorChange(listener: (anchor: string | null) => void): () => void;\n\n\t/**\n\t * The uuids currently visible in the panel, in display order: every object of every\n\t * non-collapsed layer that survived the search filter. This is the span a shift-range walks.\n\t */\n\tflatVisibleUuids(): string[];\n\n\t/**\n\t * Re-apply hidden state to freshly built scene content. **Call after every solve.**\n\t *\n\t * A solve discards all content and rebuilds it, so whatever the user had hidden comes back\n\t * visible unless it is hidden again. Hidden state is keyed by stable identity rather than\n\t * instance uuid, so this restores it (see `identity.ts`). Selection is dropped: it refers to\n\t * object instances that no longer exist, and a persistent selection across solves is not a\n\t * behaviour anyone asked for.\n\t */\n\tapplyTo(): void;\n\n\t/** Drop all hidden and selected state, showing everything. Objects are left as they are. */\n\treset(): void;\n}\n\nexport function createSceneOutliner(\n\tscene: THREE.Scene,\n\toptions: SceneOutlinerOptions = {}\n): SceneOutliner {\n\tconst collapsed = options.sets?.collapsed ?? new Set<string>();\n\tconst visibility = createVisibilityState(options.sets?.hidden);\n\tconst selection = createSelectionState(options.sets?.selected);\n\n\tconst outliner: SceneOutliner = {\n\t\tvisibility,\n\t\tselection,\n\t\tcollapsed,\n\t\tsearchQuery: '',\n\n\t\tobjects: () => getSceneObjects(scene),\n\n\t\tlayerGroups: () =>\n\t\t\tfilterLayerGroups(groupByLayer(getSceneObjects(scene)), outliner.searchQuery),\n\n\t\tisCollapsed: (layerName) => collapsed.has(layerName),\n\n\t\ttoggleCollapsed(layerName) {\n\t\t\tif (collapsed.has(layerName)) collapsed.delete(layerName);\n\t\t\telse collapsed.add(layerName);\n\t\t},\n\n\t\ttoggleObject(object) {\n\t\t\tif (selection.isSelected(object.uuid) && selection.selected.size > 1) {\n\t\t\t\tconst chosen = outliner.objects().filter((o) => selection.isSelected(o.uuid));\n\t\t\t\t// Show the group only once every member is hidden, matching the per-layer rule.\n\t\t\t\tconst allHidden = chosen.every((o) => visibility.isHidden(o));\n\t\t\t\tfor (const o of chosen) visibility.setVisible(o, allHidden);\n\t\t\t} else {\n\t\t\t\tvisibility.setVisible(object, visibility.isHidden(object));\n\t\t\t}\n\t\t},\n\n\t\tselect(uuid, modifiers) {\n\t\t\tselection.select(uuid, modifiers, () => outliner.flatVisibleUuids());\n\t\t},\n\n\t\tonAnchorChange: (listener) => selection.onAnchorChange(listener),\n\n\t\tflatVisibleUuids() {\n\t\t\tconst result: string[] = [];\n\t\t\tfor (const [layerName, objects] of outliner.layerGroups()) {\n\t\t\t\tif (collapsed.has(layerName)) continue;\n\t\t\t\tfor (const obj of objects) result.push(obj.uuid);\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\n\t\tapplyTo() {\n\t\t\tvisibility.applyTo(outliner.objects());\n\t\t\tselection.clear();\n\t\t},\n\n\t\treset() {\n\t\t\tvisibility.reset();\n\t\t\tselection.clear();\n\t\t}\n\t};\n\n\treturn outliner;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/visualization",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.1",
4
4
  "description": "Headless, extensible viewer core for Selva — parse, render and scene layers over Three.js",
5
5
  "author": "VektorNode",
6
6
  "license": "MIT",
@@ -84,18 +84,18 @@
84
84
  },
85
85
  "devDependencies": {
86
86
  "@eslint/js": "^10.0.1",
87
- "@types/node": "^26.1.2",
88
- "@types/three": "^0.185.1",
89
- "eslint": "^10.8.0",
87
+ "@types/node": "^26.2.0",
88
+ "@types/three": "^0.185.4",
89
+ "eslint": "^10.8.1",
90
90
  "eslint-config-prettier": "^10.1.8",
91
- "globals": "^17.5.0",
91
+ "globals": "^17.11.0",
92
92
  "prettier": "^3.9.6",
93
93
  "three": "^0.185.1",
94
94
  "tsdown": "^0.22.14",
95
95
  "typescript": "~6.0.3",
96
- "vite": "^8.1.5",
96
+ "vite": "^8.2.1",
97
97
  "vitest": "^4.1.10",
98
- "@selvajs/config": "0.0.3"
98
+ "@selvajs/config": "0.0.4"
99
99
  },
100
100
  "engines": {
101
101
  "node": ">=24.0.0"